templateTransformAssetUrl.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import path from 'path'
  2. import {
  3. ConstantTypes,
  4. createSimpleExpression,
  5. ExpressionNode,
  6. NodeTransform,
  7. NodeTypes,
  8. SimpleExpressionNode,
  9. SourceLocation,
  10. TransformContext
  11. } from '@vue/compiler-core'
  12. import {
  13. isRelativeUrl,
  14. parseUrl,
  15. isExternalUrl,
  16. isDataUrl
  17. } from './templateUtils'
  18. import { isArray } from '@vue/shared'
  19. export interface AssetURLTagConfig {
  20. [name: string]: string[]
  21. }
  22. export interface AssetURLOptions {
  23. /**
  24. * If base is provided, instead of transforming relative asset urls into
  25. * imports, they will be directly rewritten to absolute urls.
  26. */
  27. base?: string | null
  28. /**
  29. * If true, also processes absolute urls.
  30. */
  31. includeAbsolute?: boolean
  32. tags?: AssetURLTagConfig
  33. }
  34. export const defaultAssetUrlOptions: Required<AssetURLOptions> = {
  35. base: null,
  36. includeAbsolute: false,
  37. tags: {
  38. video: ['src', 'poster'],
  39. source: ['src'],
  40. img: ['src'],
  41. image: ['xlink:href', 'href'],
  42. use: ['xlink:href', 'href']
  43. }
  44. }
  45. export const normalizeOptions = (
  46. options: AssetURLOptions | AssetURLTagConfig
  47. ): Required<AssetURLOptions> => {
  48. if (Object.keys(options).some(key => isArray((options as any)[key]))) {
  49. // legacy option format which directly passes in tags config
  50. return {
  51. ...defaultAssetUrlOptions,
  52. tags: options as any
  53. }
  54. }
  55. return {
  56. ...defaultAssetUrlOptions,
  57. ...options
  58. }
  59. }
  60. export const createAssetUrlTransformWithOptions = (
  61. options: Required<AssetURLOptions>
  62. ): NodeTransform => {
  63. return (node, context) =>
  64. (transformAssetUrl as Function)(node, context, options)
  65. }
  66. /**
  67. * A `@vue/compiler-core` plugin that transforms relative asset urls into
  68. * either imports or absolute urls.
  69. *
  70. * ``` js
  71. * // Before
  72. * createVNode('img', { src: './logo.png' })
  73. *
  74. * // After
  75. * import _imports_0 from './logo.png'
  76. * createVNode('img', { src: _imports_0 })
  77. * ```
  78. */
  79. export const transformAssetUrl: NodeTransform = (
  80. node,
  81. context,
  82. options: AssetURLOptions = defaultAssetUrlOptions
  83. ) => {
  84. if (node.type === NodeTypes.ELEMENT) {
  85. if (!node.props.length) {
  86. return
  87. }
  88. const tags = options.tags || defaultAssetUrlOptions.tags
  89. const attrs = tags[node.tag]
  90. const wildCardAttrs = tags['*']
  91. if (!attrs && !wildCardAttrs) {
  92. return
  93. }
  94. const assetAttrs = (attrs || []).concat(wildCardAttrs || [])
  95. node.props.forEach((attr, index) => {
  96. if (
  97. attr.type !== NodeTypes.ATTRIBUTE ||
  98. !assetAttrs.includes(attr.name) ||
  99. !attr.value ||
  100. isExternalUrl(attr.value.content) ||
  101. isDataUrl(attr.value.content) ||
  102. attr.value.content[0] === '#' ||
  103. (!options.includeAbsolute && !isRelativeUrl(attr.value.content))
  104. ) {
  105. return
  106. }
  107. const url = parseUrl(attr.value.content)
  108. if (options.base && attr.value.content[0] === '.') {
  109. // explicit base - directly rewrite relative urls into absolute url
  110. // to avoid generating extra imports
  111. // Allow for full hostnames provided in options.base
  112. const base = parseUrl(options.base)
  113. const protocol = base.protocol || ''
  114. const host = base.host ? protocol + '//' + base.host : ''
  115. const basePath = base.path || '/'
  116. // when packaged in the browser, path will be using the posix-
  117. // only version provided by rollup-plugin-node-builtins.
  118. attr.value.content =
  119. host +
  120. (path.posix || path).join(basePath, url.path + (url.hash || ''))
  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. let name: string
  146. let exp: SimpleExpressionNode
  147. const existingIndex = context.imports.findIndex(i => i.path === path)
  148. if (existingIndex > -1) {
  149. name = `_imports_${existingIndex}`
  150. exp = context.imports[existingIndex].exp as SimpleExpressionNode
  151. } else {
  152. name = `_imports_${context.imports.length}`
  153. exp = createSimpleExpression(
  154. name,
  155. false,
  156. loc,
  157. ConstantTypes.CAN_STRINGIFY
  158. )
  159. context.imports.push({ exp, path })
  160. }
  161. if (!hash) {
  162. return exp
  163. }
  164. const hashExp = `${name} + '${hash}'`
  165. const existingHoistIndex = context.hoists.findIndex(h => {
  166. return (
  167. h &&
  168. h.type === NodeTypes.SIMPLE_EXPRESSION &&
  169. !h.isStatic &&
  170. h.content === hashExp
  171. )
  172. })
  173. if (existingHoistIndex > -1) {
  174. return createSimpleExpression(
  175. `_hoisted_${existingHoistIndex + 1}`,
  176. false,
  177. loc,
  178. ConstantTypes.CAN_STRINGIFY
  179. )
  180. }
  181. return context.hoist(
  182. createSimpleExpression(hashExp, false, loc, ConstantTypes.CAN_STRINGIFY)
  183. )
  184. } else {
  185. return createSimpleExpression(`''`, false, loc, ConstantTypes.CAN_STRINGIFY)
  186. }
  187. }