templateTransformAssetUrl.ts 4.6 KB

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