templateTransformAssetUrl.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  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, isExternalUrl } 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. isExternalUrl(attr.value.content) ||
  94. attr.value.content[0] === '#' ||
  95. (!options.includeAbsolute && !isRelativeUrl(attr.value.content))
  96. ) {
  97. return
  98. }
  99. const url = parseUrl(attr.value.content)
  100. if (options.base) {
  101. // explicit base - directly rewrite the url into absolute url
  102. // does not apply to absolute urls or urls that start with `@`
  103. // since they are aliases
  104. if (
  105. attr.value.content[0] !== '@' &&
  106. isRelativeUrl(attr.value.content)
  107. ) {
  108. // when packaged in the browser, path will be using the posix-
  109. // only version provided by rollup-plugin-node-builtins.
  110. attr.value.content = (path.posix || path).join(
  111. options.base,
  112. url.path + (url.hash || '')
  113. )
  114. }
  115. return
  116. }
  117. // otherwise, transform the url into an import.
  118. // this assumes a bundler will resolve the import into the correct
  119. // absolute url (e.g. webpack file-loader)
  120. const exp = getImportsExpressionExp(url.path, url.hash, attr.loc, context)
  121. node.props[index] = {
  122. type: NodeTypes.DIRECTIVE,
  123. name: 'bind',
  124. arg: createSimpleExpression(attr.name, true, attr.loc),
  125. exp,
  126. modifiers: [],
  127. loc: attr.loc
  128. }
  129. })
  130. }
  131. }
  132. function getImportsExpressionExp(
  133. path: string | null,
  134. hash: string | null,
  135. loc: SourceLocation,
  136. context: TransformContext
  137. ): ExpressionNode {
  138. if (path) {
  139. const importsArray = Array.from(context.imports)
  140. const existing = importsArray.find(i => i.path === path)
  141. if (existing) {
  142. return existing.exp as ExpressionNode
  143. }
  144. const name = `_imports_${importsArray.length}`
  145. const exp = createSimpleExpression(name, false, loc, true)
  146. exp.isRuntimeConstant = true
  147. context.imports.add({ exp, path })
  148. if (hash && path) {
  149. const ret = context.hoist(
  150. createSimpleExpression(`${name} + '${hash}'`, false, loc, true)
  151. )
  152. ret.isRuntimeConstant = true
  153. return ret
  154. } else {
  155. return exp
  156. }
  157. } else {
  158. return createSimpleExpression(`''`, false, loc, true)
  159. }
  160. }