templateTransformAssetUrl.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. import path from 'path'
  2. import {
  3. createSimpleExpression,
  4. ExpressionNode,
  5. NodeTransform,
  6. NodeTypes,
  7. SourceLocation,
  8. TransformContext
  9. } from '@vue/compiler-core'
  10. import { isRelativeUrl, parseUrl } from './templateUtils'
  11. import { isArray } from '@vue/shared'
  12. export interface AssetURLTagConfig {
  13. [name: string]: string[]
  14. }
  15. export interface AssetURLOptions {
  16. /**
  17. * If base is provided, instead of transforming relative asset urls into
  18. * imports, they will be directly rewritten to absolute urls.
  19. */
  20. base?: string | null
  21. /**
  22. * If true, also processes absolute urls.
  23. */
  24. includeAbsolute?: boolean
  25. tags?: AssetURLTagConfig
  26. }
  27. export const defaultAssetUrlOptions: Required<AssetURLOptions> = {
  28. base: null,
  29. includeAbsolute: false,
  30. tags: {
  31. video: ['src', 'poster'],
  32. source: ['src'],
  33. img: ['src'],
  34. image: ['xlink:href', 'href'],
  35. use: ['xlink:href', 'href']
  36. }
  37. }
  38. export const normalizeOptions = (
  39. options: AssetURLOptions | AssetURLTagConfig
  40. ): Required<AssetURLOptions> => {
  41. if (Object.keys(options).some(key => isArray((options as any)[key]))) {
  42. // legacy option format which directly passes in tags config
  43. return {
  44. ...defaultAssetUrlOptions,
  45. tags: options as any
  46. }
  47. }
  48. return {
  49. ...defaultAssetUrlOptions,
  50. ...options
  51. }
  52. }
  53. export const createAssetUrlTransformWithOptions = (
  54. options: Required<AssetURLOptions>
  55. ): NodeTransform => {
  56. return (node, context) =>
  57. (transformAssetUrl as Function)(node, context, options)
  58. }
  59. /**
  60. * A `@vue/compiler-core` plugin that transforms relative asset urls into
  61. * either imports or absolute urls.
  62. *
  63. * ``` js
  64. * // Before
  65. * createVNode('img', { src: './logo.png' })
  66. *
  67. * // After
  68. * import _imports_0 from './logo.png'
  69. * createVNode('img', { src: _imports_0 })
  70. * ```
  71. */
  72. export const transformAssetUrl: NodeTransform = (
  73. node,
  74. context,
  75. options: AssetURLOptions = defaultAssetUrlOptions
  76. ) => {
  77. if (node.type === NodeTypes.ELEMENT) {
  78. if (!node.props.length) {
  79. return
  80. }
  81. const tags = options.tags || defaultAssetUrlOptions.tags
  82. const attrs = tags[node.tag]
  83. const wildCardAttrs = tags['*']
  84. if (!attrs && !wildCardAttrs) {
  85. return
  86. }
  87. const assetAttrs = (attrs || []).concat(wildCardAttrs || [])
  88. node.props.forEach((attr, index) => {
  89. if (
  90. attr.type !== NodeTypes.ATTRIBUTE ||
  91. !assetAttrs.includes(attr.name) ||
  92. !attr.value ||
  93. (!options.includeAbsolute && !isRelativeUrl(attr.value.content))
  94. ) {
  95. return
  96. }
  97. const url = parseUrl(attr.value.content)
  98. if (options.base) {
  99. // explicit base - directly rewrite the url into absolute url
  100. // does not apply to absolute urls or urls that start with `@`
  101. // since they are aliases
  102. if (
  103. attr.value.content[0] !== '@' &&
  104. isRelativeUrl(attr.value.content)
  105. ) {
  106. // when packaged in the browser, path will be using the posix-
  107. // only version provided by rollup-plugin-node-builtins.
  108. attr.value.content = (path.posix || path).join(
  109. options.base,
  110. url.path + (url.hash || '')
  111. )
  112. }
  113. return
  114. }
  115. // otherwise, transform the url into an import.
  116. // this assumes a bundler will resolve the import into the correct
  117. // absolute url (e.g. webpack file-loader)
  118. const exp = getImportsExpressionExp(url.path, url.hash, attr.loc, context)
  119. node.props[index] = {
  120. type: NodeTypes.DIRECTIVE,
  121. name: 'bind',
  122. arg: createSimpleExpression(attr.name, true, attr.loc),
  123. exp,
  124. modifiers: [],
  125. loc: attr.loc
  126. }
  127. })
  128. }
  129. }
  130. function getImportsExpressionExp(
  131. path: string | null,
  132. hash: string | null,
  133. loc: SourceLocation,
  134. context: TransformContext
  135. ): ExpressionNode {
  136. if (path) {
  137. const importsArray = Array.from(context.imports)
  138. const existing = importsArray.find(i => i.path === path)
  139. if (existing) {
  140. return existing.exp as ExpressionNode
  141. }
  142. const name = `_imports_${importsArray.length}`
  143. const exp = createSimpleExpression(name, false, loc, true)
  144. exp.isRuntimeConstant = true
  145. context.imports.add({ exp, path })
  146. if (hash && path) {
  147. const ret = context.hoist(
  148. createSimpleExpression(`${name} + '${hash}'`, false, loc, true)
  149. )
  150. ret.isRuntimeConstant = true
  151. return ret
  152. } else {
  153. return exp
  154. }
  155. } else {
  156. return createSimpleExpression(`''`, false, loc, true)
  157. }
  158. }