]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-sfc): add transformAssetUrlsBase option
authorEvan You <yyx990803@gmail.com>
Sat, 2 May 2020 18:49:28 +0000 (14:49 -0400)
committerEvan You <yyx990803@gmail.com>
Sat, 2 May 2020 20:58:17 +0000 (16:58 -0400)
packages/compiler-sfc/__tests__/__snapshots__/templateTransformAssetUrl.spec.ts.snap
packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts
packages/compiler-sfc/src/compileTemplate.ts
packages/compiler-sfc/src/templateTransformAssetUrl.ts
packages/compiler-sfc/src/templateUtils.ts
rollup.config.js

index ddf3194276e543e49fb9edd538c7cfcffab3fb45..e997cd09c1b1e58ee42f829a996245dcdc7d4124 100644 (file)
@@ -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 */))
+}"
+`;
index 40531d780b49b95a0ac0930faed798e790ccc254..64a94c6a6e47c093a20daebef0d7d5946aee5031 100644 (file)
@@ -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()
+  })
 })
index 2c50c7f3ed875f54cc6e8a8da778656a4843af17..df0051b0032f5b329dcd94d0791867c221d1e292 100644 (file)
@@ -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, {
index b981bf670711d2dfface7af8d1d91ab982885e11..44ebe36af0a18c493f87b9463552acff76e1314a 100644 (file)
@@ -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
+              }
             }
           })
         })
index 1040571afc95821680e77844bacca5191bea8bba..70a5b26fa608f951bbcae9cf77f01141b739bc4f 100644 (file)
@@ -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 === '~') {
index e282f777ecd29b623dcb651641736b856c84e7cf..ef8d7af169c9e8642cfb60e62dc0d5356add62a1 100644 (file)
@@ -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