templateTransformAssetUrl.ts 4.7 KB

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