Răsfoiți Sursa

feat(compiler-sfc): add transformAssetUrlsBase option

Evan You 6 ani în urmă
părinte
comite
36972c20b5

+ 13 - 0
packages/compiler-sfc/__tests__/__snapshots__/templateTransformAssetUrl.spec.ts.snap

@@ -36,3 +36,16 @@ export function render(_ctx, _cache) {
   ], 64 /* STABLE_FRAGMENT */))
 }"
 `;
+
+exports[`compiler sfc: transform asset url with explicit base 1`] = `
+"import { createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
+
+export function render(_ctx, _cache) {
+  return (_openBlock(), _createBlock(_Fragment, null, [
+    _createVNode(\\"img\\", { src: \\"/foo/bar.png\\" }),
+    _createVNode(\\"img\\", { src: \\"/foo/bar.png\\" }),
+    _createVNode(\\"img\\", { src: \\"bar.png\\" }),
+    _createVNode(\\"img\\", { src: \\"@theme/bar.png\\" })
+  ], 64 /* STABLE_FRAGMENT */))
+}"
+`;

+ 26 - 1
packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts

@@ -1,5 +1,8 @@
 import { generate, baseParse, transform } from '@vue/compiler-core'
-import { transformAssetUrl } from '../src/templateTransformAssetUrl'
+import {
+  transformAssetUrl,
+  createAssetUrlTransformWithOptions
+} from '../src/templateTransformAssetUrl'
 import { transformElement } from '../../compiler-core/src/transforms/transformElement'
 import { transformBind } from '../../compiler-core/src/transforms/vBind'
 
@@ -46,4 +49,26 @@ describe('compiler sfc: transform asset url', () => {
 
     expect(result.code).toMatchSnapshot()
   })
+
+  test('with explicit base', () => {
+    const ast = baseParse(
+      `<img src="./bar.png"></img>` + // -> /foo/bar.png
+      `<img src="~bar.png"></img>` + // -> /foo/bar.png
+      `<img src="bar.png"></img>` + // -> bar.png (untouched)
+        `<img src="@theme/bar.png"></img>` // -> @theme/bar.png (untouched)
+    )
+    transform(ast, {
+      nodeTransforms: [
+        createAssetUrlTransformWithOptions({
+          base: '/foo'
+        }),
+        transformElement
+      ],
+      directiveTransforms: {
+        bind: transformBind
+      }
+    })
+    const { code } = generate(ast, { mode: 'module' })
+    expect(code).toMatchSnapshot()
+  })
 })

+ 29 - 8
packages/compiler-sfc/src/compileTemplate.ts

@@ -40,8 +40,23 @@ export interface SFCTemplateCompileOptions {
   compilerOptions?: CompilerOptions
   preprocessLang?: string
   preprocessOptions?: any
+  /**
+   * In some cases, compiler-sfc may not be inside the project root (e.g. when
+   * linked or globally installed). In such cases a custom `require` can be
+   * passed to correctly resolve the preprocessors.
+   */
   preprocessCustomRequire?: (id: string) => any
+  /**
+   * Configure what tags/attributes to trasnform into relative asset url imports
+   * in the form of `{ [tag: string]: string[] }`, or disable the transform with
+   * `false`.
+   */
   transformAssetUrls?: AssetURLOptions | boolean
+  /**
+   * If base is provided, instead of transforming relative asset urls into
+   * imports, they will be directly rewritten to absolute urls.
+   */
+  transformAssetUrlsBase?: string
 }
 
 function preprocess(
@@ -129,18 +144,24 @@ function doCompileTemplate({
   ssr = false,
   compiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM,
   compilerOptions = {},
-  transformAssetUrls
+  transformAssetUrls,
+  transformAssetUrlsBase
 }: SFCTemplateCompileOptions): SFCTemplateCompileResults {
   const errors: CompilerError[] = []
 
   let nodeTransforms: NodeTransform[] = []
-  if (isObject(transformAssetUrls)) {
-    nodeTransforms = [
-      createAssetUrlTransformWithOptions(transformAssetUrls),
-      transformSrcset
-    ]
-  } else if (transformAssetUrls !== false) {
-    nodeTransforms = [transformAssetUrl, transformSrcset]
+  if (transformAssetUrls !== false) {
+    if (transformAssetUrlsBase || isObject(transformAssetUrls)) {
+      nodeTransforms = [
+        createAssetUrlTransformWithOptions({
+          base: transformAssetUrlsBase,
+          tags: isObject(transformAssetUrls) ? transformAssetUrls : undefined
+        }),
+        transformSrcset
+      ]
+    } else {
+      nodeTransforms = [transformAssetUrl, transformSrcset]
+    }
   }
 
   let { code, map } = compiler.compile(source, {

+ 73 - 29
packages/compiler-sfc/src/templateTransformAssetUrl.ts

@@ -1,3 +1,4 @@
+import path from 'path'
 import {
   createSimpleExpression,
   ExpressionNode,
@@ -12,54 +13,97 @@ export interface AssetURLOptions {
   [name: string]: string[]
 }
 
-const defaultOptions: AssetURLOptions = {
-  video: ['src', 'poster'],
-  source: ['src'],
-  img: ['src'],
-  image: ['xlink:href', 'href'],
-  use: ['xlink:href', 'href']
+export interface NormlaizedAssetURLOptions {
+  base?: string | null
+  tags?: AssetURLOptions
+}
+
+const defaultAssetUrlOptions: Required<NormlaizedAssetURLOptions> = {
+  base: null,
+  tags: {
+    video: ['src', 'poster'],
+    source: ['src'],
+    img: ['src'],
+    image: ['xlink:href', 'href'],
+    use: ['xlink:href', 'href']
+  }
 }
 
 export const createAssetUrlTransformWithOptions = (
-  options: AssetURLOptions
+  options: NormlaizedAssetURLOptions
 ): NodeTransform => {
   const mergedOptions = {
-    ...defaultOptions,
+    ...defaultAssetUrlOptions,
     ...options
   }
   return (node, context) =>
     (transformAssetUrl as Function)(node, context, mergedOptions)
 }
 
+/**
+ * A `@vue/compiler-core` plugin that transforms relative asset urls into
+ * either imports or absolute urls.
+ *
+ * ``` js
+ * // Before
+ * createVNode('img', { src: './logo.png' })
+ *
+ * // After
+ * import _imports_0 from './logo.png'
+ * createVNode('img', { src: _imports_0 })
+ * ```
+ */
 export const transformAssetUrl: NodeTransform = (
   node,
   context,
-  options: AssetURLOptions = defaultOptions
+  options: NormlaizedAssetURLOptions = defaultAssetUrlOptions
 ) => {
   if (node.type === NodeTypes.ELEMENT) {
-    for (const tag in options) {
+    const tags = options.tags || defaultAssetUrlOptions.tags
+    for (const tag in tags) {
       if ((tag === '*' || node.tag === tag) && node.props.length) {
-        const attributes = options[tag]
-        attributes.forEach(item => {
+        const attributes = tags[tag]
+        attributes.forEach(name => {
           node.props.forEach((attr, index) => {
-            if (attr.type !== NodeTypes.ATTRIBUTE) return
-            if (attr.name !== item) return
-            if (!attr.value) return
-            if (!isRelativeUrl(attr.value.content)) return
+            if (
+              attr.type !== NodeTypes.ATTRIBUTE ||
+              attr.name !== name ||
+              !attr.value ||
+              !isRelativeUrl(attr.value.content)
+            ) {
+              return
+            }
             const url = parseUrl(attr.value.content)
-            const exp = getImportsExpressionExp(
-              url.path,
-              url.hash,
-              attr.loc,
-              context
-            )
-            node.props[index] = {
-              type: NodeTypes.DIRECTIVE,
-              name: 'bind',
-              arg: createSimpleExpression(item, true, attr.loc),
-              exp,
-              modifiers: [],
-              loc: attr.loc
+            if (options.base) {
+              // explicit base - directly rewrite the url into absolute url
+              // does not apply to url that starts with `@` since they are
+              // aliases
+              if (attr.value.content[0] !== '@') {
+                // when packaged in the browser, path will be using the posix-
+                // only version provided by rollup-plugin-node-builtins.
+                attr.value.content = (path.posix || path).join(
+                  options.base,
+                  url.path + (url.hash || '')
+                )
+              }
+            } else {
+              // otherwise, transform the url into an import.
+              // this assumes a bundler will resolve the import into the correct
+              // absolute url (e.g. webpack file-loader)
+              const exp = getImportsExpressionExp(
+                url.path,
+                url.hash,
+                attr.loc,
+                context
+              )
+              node.props[index] = {
+                type: NodeTypes.DIRECTIVE,
+                name: 'bind',
+                arg: createSimpleExpression(name, true, attr.loc),
+                exp,
+                modifiers: [],
+                loc: attr.loc
+              }
             }
           })
         })

+ 3 - 2
packages/compiler-sfc/src/templateUtils.ts

@@ -6,8 +6,9 @@ export function isRelativeUrl(url: string): boolean {
   return firstChar === '.' || firstChar === '~' || firstChar === '@'
 }
 
-// We need an extra transform context API for injecting arbitrary import
-// statements.
+/**
+ * Parses string url into URL object.
+ */
 export function parseUrl(url: string): UrlWithStringQuery {
   const firstChar = url.charAt(0)
   if (firstChar === '~') {

+ 1 - 1
rollup.config.js

@@ -129,7 +129,7 @@ function createConfig(format, output, plugins = []) {
         [
           ...Object.keys(pkg.dependencies || {}),
           ...Object.keys(pkg.peerDependencies || {}),
-          'url' // for @vue/compiler-sfc
+          ...['path', 'url'] // for @vue/compiler-sfc
         ]
 
   // the browser builds of @vue/compiler-sfc requires postcss to be available