]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-vapor): handle asset imports (#13630)
authorGianthard-cyh <45843411+Gianthard-cyh@users.noreply.github.com>
Mon, 10 Nov 2025 02:30:35 +0000 (10:30 +0800)
committerGitHub <noreply@github.com>
Mon, 10 Nov 2025 02:30:35 +0000 (10:30 +0800)
13 files changed:
packages/compiler-core/src/index.ts
packages/compiler-core/src/transform.ts
packages/compiler-sfc/__tests__/__snapshots__/compileTemplate.spec.ts.snap
packages/compiler-sfc/src/template/transformSrcset.ts
packages/compiler-vapor/__tests__/transforms/__snapshots__/templateTransformAssetUrl.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/templateTransformSrcset.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/templateTransformAssetUrl.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/templateTransformSrcset.spec.ts [new file with mode: 0644]
packages/compiler-vapor/src/generate.ts
packages/compiler-vapor/src/generators/template.ts
packages/compiler-vapor/src/generators/utils.ts
packages/compiler-vapor/src/transform.ts
packages/compiler-vapor/src/transforms/transformElement.ts

index 9da54790cc04abb6f9221f9194c5d8b5263fc790..06e963acfd618255866da2b9b30a64b23f43b253 100644 (file)
@@ -21,6 +21,7 @@ export {
   type NodeTransform,
   type StructuralDirectiveTransform,
   type DirectiveTransform,
+  type ImportItem,
 } from './transform'
 export {
   generate,
index 10121fb5d5cb1fcfdb36e04d2371fea430bcd0ec..b37d665b145edb8d9b3cdb2620e9676f861dcfba 100644 (file)
@@ -77,7 +77,7 @@ export type StructuralDirectiveTransform = (
 ) => void | (() => void)
 
 export interface ImportItem {
-  exp: string | ExpressionNode
+  exp: SimpleExpressionNode
   path: string
 }
 
index 3cc6c173557e7dc84dfc5a91a9f52a1415acb4fc..c958c9e031ea90fff341eaecc7a4bf40957c8bae 100644 (file)
@@ -65,9 +65,7 @@ export function ssrRender(_ctx, _push, _parent, _attrs) {
       } else {
         return [
           _createVNode("picture", null, [
-            _createVNode("source", {
-              srcset: _imports_1
-            }),
+            _createVNode("source", { srcset: _imports_1 }),
             _createVNode("img", { src: _imports_1 })
           ])
         ]
index 40fba4882b8d695ced25c54dcd8f88079733887b..a825f02db64da7b593009267f0edbe2b6a98cf25 100644 (file)
@@ -1,11 +1,8 @@
 import path from 'path'
 import {
   ConstantTypes,
-  type ExpressionNode,
   type NodeTransform,
   NodeTypes,
-  type SimpleExpressionNode,
-  createCompoundExpression,
   createSimpleExpression,
 } from '@vue/compiler-core'
 import {
@@ -106,55 +103,52 @@ export const transformSrcset: NodeTransform = (
             }
           }
 
-          const compoundExpression = createCompoundExpression([], attr.loc)
+          let content = ''
           imageCandidates.forEach(({ url, descriptor }, index) => {
             if (shouldProcessUrl(url)) {
               const { path } = parseUrl(url)
-              let exp: SimpleExpressionNode
               if (path) {
+                let exp = ''
                 const existingImportsIndex = context.imports.findIndex(
                   i => i.path === path,
                 )
                 if (existingImportsIndex > -1) {
-                  exp = createSimpleExpression(
-                    `_imports_${existingImportsIndex}`,
-                    false,
-                    attr.loc,
-                    ConstantTypes.CAN_STRINGIFY,
-                  )
+                  exp = `_imports_${existingImportsIndex}`
                 } else {
-                  exp = createSimpleExpression(
-                    `_imports_${context.imports.length}`,
-                    false,
-                    attr.loc,
-                    ConstantTypes.CAN_STRINGIFY,
-                  )
-                  context.imports.push({ exp, path })
+                  exp = `_imports_${context.imports.length}`
+                  context.imports.push({
+                    exp: createSimpleExpression(
+                      exp,
+                      false,
+                      attr.loc,
+                      ConstantTypes.CAN_STRINGIFY,
+                    ),
+                    path,
+                  })
                 }
-                compoundExpression.children.push(exp)
+                content += exp
               }
             } else {
-              const exp = createSimpleExpression(
-                `"${url}"`,
-                false,
-                attr.loc,
-                ConstantTypes.CAN_STRINGIFY,
-              )
-              compoundExpression.children.push(exp)
+              content += `"${url}"`
             }
             const isNotLast = imageCandidates.length - 1 > index
-            if (descriptor && isNotLast) {
-              compoundExpression.children.push(` + ' ${descriptor}, ' + `)
-            } else if (descriptor) {
-              compoundExpression.children.push(` + ' ${descriptor}'`)
+            if (descriptor) {
+              content += ` + ' ${descriptor}${isNotLast ? ', ' : ''}'${
+                isNotLast ? ' + ' : ''
+              }`
             } else if (isNotLast) {
-              compoundExpression.children.push(` + ', ' + `)
+              content += ` + ', ' + `
             }
           })
 
-          let exp: ExpressionNode = compoundExpression
+          let exp = createSimpleExpression(
+            content,
+            false,
+            attr.loc,
+            ConstantTypes.CAN_STRINGIFY,
+          )
           if (context.hoistStatic) {
-            exp = context.hoist(compoundExpression)
+            exp = context.hoist(exp)
             exp.constType = ConstantTypes.CAN_STRINGIFY
           }
 
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/templateTransformAssetUrl.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/templateTransformAssetUrl.spec.ts.snap
new file mode 100644 (file)
index 0000000..38b325e
--- /dev/null
@@ -0,0 +1,134 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler sfc: transform asset url > should allow for full base URLs, with paths 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<img src=\\"http://localhost:3000/src/logo.png\\">", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler sfc: transform asset url > should allow for full base URLs, without paths 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<img src=\\"http://localhost:3000/logo.png\\">", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler sfc: transform asset url > should allow for full base URLs, without port 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<img src=\\"http://localhost/logo.png\\">", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler sfc: transform asset url > should allow for full base URLs, without protocol 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<img src=\\"//localhost/logo.png\\">", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler sfc: transform asset url > support uri fragment 1`] = `
+"import { template as _template } from 'vue';
+import _imports_0 from '@svg/file.svg';
+const t0 = _template("<use href=\\"" + _imports_0 + '#fragment' + "\\"></use>", false, 1)
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t0()
+  return [n0, n1]
+}"
+`;
+
+exports[`compiler sfc: transform asset url > support uri is empty 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<use href=\\"\\"></use>", true, 1)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler sfc: transform asset url > transform assetUrls 1`] = `
+"import { template as _template } from 'vue';
+import _imports_0 from './logo.png';
+import _imports_1 from 'fixtures/logo.png';
+import _imports_2 from '/fixtures/logo.png';
+const t0 = _template("<img src=\\"" + _imports_0 + "\\">")
+const t1 = _template("<img src=\\"" + _imports_1 + "\\">")
+const t2 = _template("<img src=\\"http://example.com/fixtures/logo.png\\">")
+const t3 = _template("<img src=\\"//example.com/fixtures/logo.png\\">")
+const t4 = _template("<img src=\\"" + _imports_2 + "\\">")
+const t5 = _template("<img src=\\"data:image/png;base64,i\\">")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t1()
+  const n2 = t1()
+  const n3 = t2()
+  const n4 = t3()
+  const n5 = t4()
+  const n6 = t5()
+  return [n0, n1, n2, n3, n4, n5, n6]
+}"
+`;
+
+exports[`compiler sfc: transform asset url > transform with stringify 1`] = `
+"import { template as _template } from 'vue';
+import _imports_0 from './bar.png';
+import _imports_1 from '/bar.png';
+const t0 = _template("<div><img src=\\"" + _imports_0 + "\\"><img src=\\"" + _imports_1 + "\\"><img src=\\"https://foo.bar/baz.png\\"><img src=\\"//foo.bar/baz.png\\"><img src=\\"" + _imports_0 + "\\"></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler sfc: transform asset url > with explicit base 1`] = `
+"import { template as _template } from 'vue';
+import _imports_0 from 'bar.png';
+import _imports_1 from '@theme/bar.png';
+const t0 = _template("<img src=\\"/foo/bar.png\\">")
+const t1 = _template("<img src=\\"" + _imports_0 + "\\">")
+const t2 = _template("<img src=\\"" + _imports_1 + "\\">")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t1()
+  const n2 = t1()
+  const n3 = t2()
+  return [n0, n1, n2, n3]
+}"
+`;
+
+exports[`compiler sfc: transform asset url > with includeAbsolute: true 1`] = `
+"import { template as _template } from 'vue';
+import _imports_0 from './bar.png';
+import _imports_1 from '/bar.png';
+const t0 = _template("<img src=\\"" + _imports_0 + "\\">")
+const t1 = _template("<img src=\\"" + _imports_1 + "\\">")
+const t2 = _template("<img src=\\"https://foo.bar/baz.png\\">")
+const t3 = _template("<img src=\\"//foo.bar/baz.png\\">")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t1()
+  const n2 = t2()
+  const n3 = t3()
+  return [n0, n1, n2, n3]
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/templateTransformSrcset.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/templateTransformSrcset.spec.ts.snap
new file mode 100644 (file)
index 0000000..68ed768
--- /dev/null
@@ -0,0 +1,126 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler sfc: transform srcset > srcset w/ explicit base option 1`] = `
+"import { template as _template } from 'vue';
+import _imports_0 from '@/logo.png';
+import _imports_1 from '/foo/logo.png';
+const t0 = _template("<img srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x' + "\\">")
+const t1 = _template("<img srcset=\\"" + _imports_0 + ' 1x, ' + _imports_1 + ' 2x' + "\\">")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t1()
+  return [n0, n1]
+}"
+`;
+
+exports[`compiler sfc: transform srcset > transform srcset 1`] = `
+"import { template as _template } from 'vue';
+import _imports_0 from './logo.png';
+import _imports_1 from '/logo.png';
+const t0 = _template("<img src=\\"" + _imports_0 + "\\" srcset>")
+const t1 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + "\\">")
+const t2 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x' + "\\">")
+const t3 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x' + "\\">")
+const t4 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x, ' + _imports_0 + "\\">")
+const t5 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' + "\\">")
+const t6 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' + "\\">")
+const t7 = _template("<img src=\\"" + _imports_1 + "\\" srcset=\\"" + _imports_1 + ', ' + _imports_1 + ' 2x' + "\\">")
+const t8 = _template("<img src=\\"https://example.com/logo.png\\" srcset=\\"https://example.com/logo.png, https://example.com/logo.png 2x\\">")
+const t9 = _template("<img src=\\"" + _imports_1 + "\\" srcset=\\"" + _imports_1 + ', ' + _imports_0 + ' 2x' + "\\">")
+const t10 = _template("<img src=\\"data:image/png;base64,i\\" srcset=\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\">")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t1()
+  const n2 = t2()
+  const n3 = t2()
+  const n4 = t3()
+  const n5 = t4()
+  const n6 = t5()
+  const n7 = t6()
+  const n8 = t7()
+  const n9 = t8()
+  const n10 = t9()
+  const n11 = t10()
+  return [n0, n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11]
+}"
+`;
+
+exports[`compiler sfc: transform srcset > transform srcset w/ base 1`] = `
+"import { template as _template } from 'vue';
+import _imports_0 from '/logo.png';
+import _imports_1 from '/foo/logo.png';
+const t0 = _template("<img src=\\"/foo/logo.png\\" srcset>")
+const t1 = _template("<img src=\\"/foo/logo.png\\" srcset=\\"/foo/logo.png\\">")
+const t2 = _template("<img src=\\"/foo/logo.png\\" srcset=\\"/foo/logo.png 2x\\">")
+const t3 = _template("<img src=\\"/foo/logo.png\\" srcset=\\"/foo/logo.png, /foo/logo.png 2x\\">")
+const t4 = _template("<img src=\\"/foo/logo.png\\" srcset=\\"/foo/logo.png 2x, /foo/logo.png\\">")
+const t5 = _template("<img src=\\"/foo/logo.png\\" srcset=\\"/foo/logo.png 2x, /foo/logo.png 3x\\">")
+const t6 = _template("<img src=\\"/foo/logo.png\\" srcset=\\"/foo/logo.png, /foo/logo.png 2x, /foo/logo.png 3x\\">")
+const t7 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x' + "\\">")
+const t8 = _template("<img src=\\"https://example.com/logo.png\\" srcset=\\"https://example.com/logo.png, https://example.com/logo.png 2x\\">")
+const t9 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ', ' + _imports_1 + ' 2x' + "\\">")
+const t10 = _template("<img src=\\"data:image/png;base64,i\\" srcset=\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\">")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t1()
+  const n2 = t2()
+  const n3 = t2()
+  const n4 = t3()
+  const n5 = t4()
+  const n6 = t5()
+  const n7 = t6()
+  const n8 = t7()
+  const n9 = t8()
+  const n10 = t9()
+  const n11 = t10()
+  return [n0, n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11]
+}"
+`;
+
+exports[`compiler sfc: transform srcset > transform srcset w/ includeAbsolute: true 1`] = `
+"import { template as _template } from 'vue';
+import _imports_0 from './logo.png';
+import _imports_1 from '/logo.png';
+const t0 = _template("<img src=\\"" + _imports_0 + "\\" srcset>")
+const t1 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + "\\">")
+const t2 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x' + "\\">")
+const t3 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x' + "\\">")
+const t4 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x, ' + _imports_0 + "\\">")
+const t5 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' + "\\">")
+const t6 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' + "\\">")
+const t7 = _template("<img src=\\"" + _imports_1 + "\\" srcset=\\"" + _imports_1 + ', ' + _imports_1 + ' 2x' + "\\">")
+const t8 = _template("<img src=\\"https://example.com/logo.png\\" srcset=\\"https://example.com/logo.png, https://example.com/logo.png 2x\\">")
+const t9 = _template("<img src=\\"" + _imports_1 + "\\" srcset=\\"" + _imports_1 + ', ' + _imports_0 + ' 2x' + "\\">")
+const t10 = _template("<img src=\\"data:image/png;base64,i\\" srcset=\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\">")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t1()
+  const n2 = t2()
+  const n3 = t2()
+  const n4 = t3()
+  const n5 = t4()
+  const n6 = t5()
+  const n7 = t6()
+  const n8 = t7()
+  const n9 = t8()
+  const n10 = t9()
+  const n11 = t10()
+  return [n0, n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11]
+}"
+`;
+
+exports[`compiler sfc: transform srcset > transform srcset w/ stringify 1`] = `
+"import { template as _template } from 'vue';
+import _imports_0 from './logo.png';
+import _imports_1 from '/logo.png';
+const t0 = _template("<div><img src=\\"" + _imports_0 + "\\" srcset><img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + "\\"><img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x' + "\\"><img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x' + "\\"><img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x' + "\\"><img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x, ' + _imports_0 + "\\"><img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' + "\\"><img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' + "\\"><img src=\\"" + _imports_1 + "\\" srcset=\\"" + _imports_1 + ', ' + _imports_1 + ' 2x' + "\\"><img src=\\"https://example.com/logo.png\\" srcset=\\"https://example.com/logo.png, https://example.com/logo.png 2x\\"><img src=\\"" + _imports_1 + "\\" srcset=\\"" + _imports_1 + ', ' + _imports_0 + ' 2x' + "\\"><img src=\\"data:image/png;base64,i\\" srcset=\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\"></div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/templateTransformAssetUrl.spec.ts b/packages/compiler-vapor/__tests__/transforms/templateTransformAssetUrl.spec.ts
new file mode 100644 (file)
index 0000000..225d18a
--- /dev/null
@@ -0,0 +1,183 @@
+import type { TransformOptions } from '@vue/compiler-core'
+import type { AssetURLOptions } from '../../../compiler-sfc/src/template/transformAssetUrl'
+import { stringifyStatic } from '../../../compiler-dom/src/transforms/stringifyStatic'
+import { compileTemplate } from '../../../compiler-sfc/src'
+
+function compileWithAssetUrls(
+  template: string,
+  options?: AssetURLOptions,
+  transformOptions?: TransformOptions,
+) {
+  return compileTemplate({
+    vapor: true,
+    id: 'test',
+    filename: 'test.vue',
+    source: template,
+    transformAssetUrls: {
+      includeAbsolute: true,
+      ...options,
+    },
+  })
+}
+
+describe('compiler sfc: transform asset url', () => {
+  test('transform assetUrls', () => {
+    const result = compileWithAssetUrls(`
+                       <img src="./logo.png"/>
+                       <img src="~fixtures/logo.png"/>
+                       <img src="~/fixtures/logo.png"/>
+                       <img src="http://example.com/fixtures/logo.png"/>
+                       <img src="//example.com/fixtures/logo.png"/>
+                       <img src="/fixtures/logo.png"/>
+                       <img src="data:image/png;base64,i"/>
+               `)
+
+    expect(result.code).toMatchSnapshot()
+  })
+
+  /**
+   * vuejs/component-compiler-utils#22 Support uri fragment in transformed require
+   */
+  test('support uri fragment', () => {
+    const result = compileWithAssetUrls(
+      '<use href="~@svg/file.svg#fragment"></use>' +
+        '<use href="~@svg/file.svg#fragment"></use>',
+      {},
+      {
+        hoistStatic: true,
+      },
+    )
+    expect(result.code).toMatchSnapshot()
+  })
+
+  /**
+   * vuejs/component-compiler-utils#22 Support uri fragment in transformed require
+   */
+  test('support uri is empty', () => {
+    const result = compileWithAssetUrls('<use href="~"></use>')
+
+    expect(result.code).toMatchSnapshot()
+  })
+
+  test('with explicit base', () => {
+    const { code } = compileWithAssetUrls(
+      `<img src="./bar.png"></img>` + // -> /foo/bar.png
+        `<img src="bar.png"></img>` + // -> bar.png (untouched)
+        `<img src="~bar.png"></img>` + // -> still converts to import
+        `<img src="@theme/bar.png"></img>`, // -> still converts to import
+      {
+        base: '/foo',
+      },
+    )
+    expect(code).toMatch(`import _imports_0 from 'bar.png'`)
+    expect(code).toMatch(`import _imports_1 from '@theme/bar.png'`)
+    expect(code).toMatchSnapshot()
+  })
+
+  test('with includeAbsolute: true', () => {
+    const { code } = compileWithAssetUrls(
+      `<img src="./bar.png"/>` +
+        `<img src="/bar.png"/>` +
+        `<img src="https://foo.bar/baz.png"/>` +
+        `<img src="//foo.bar/baz.png"/>`,
+      {
+        includeAbsolute: true,
+      },
+    )
+    expect(code).toMatchSnapshot()
+  })
+
+  // vitejs/vite#298
+  test('should not transform hash fragments', () => {
+    const { code } = compileWithAssetUrls(
+      `<svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+        <defs>
+          <circle id="myCircle" cx="0" cy="0" r="5" />
+        </defs>
+        <use x="5" y="5" xlink:href="#myCircle" />
+      </svg>`,
+    )
+    // should not remove it
+    expect(code).toMatch(`xlink:href=\\"#myCircle\\"`) // compiled to template string, not object, so remove quotes
+  })
+
+  test('should allow for full base URLs, with paths', () => {
+    const { code } = compileWithAssetUrls(`<img src="./logo.png" />`, {
+      base: 'http://localhost:3000/src/',
+    })
+
+    expect(code).toMatchSnapshot()
+  })
+
+  test('should allow for full base URLs, without paths', () => {
+    const { code } = compileWithAssetUrls(`<img src="./logo.png" />`, {
+      base: 'http://localhost:3000',
+    })
+
+    expect(code).toMatchSnapshot()
+  })
+
+  test('should allow for full base URLs, without port', () => {
+    const { code } = compileWithAssetUrls(`<img src="./logo.png" />`, {
+      base: 'http://localhost',
+    })
+
+    expect(code).toMatchSnapshot()
+  })
+
+  test('should allow for full base URLs, without protocol', () => {
+    const { code } = compileWithAssetUrls(`<img src="./logo.png" />`, {
+      base: '//localhost',
+    })
+
+    expect(code).toMatchSnapshot()
+  })
+
+  test('transform with stringify', () => {
+    const { code } = compileWithAssetUrls(
+      `<div>` +
+        `<img src="./bar.png"/>` +
+        `<img src="/bar.png"/>` +
+        `<img src="https://foo.bar/baz.png"/>` +
+        `<img src="//foo.bar/baz.png"/>` +
+        `<img src="./bar.png"/>` +
+        `</div>`,
+      {
+        includeAbsolute: true,
+      },
+      {
+        hoistStatic: true,
+        transformHoist: stringifyStatic,
+      },
+    )
+    expect(code).toMatchSnapshot()
+  })
+
+  test('transform with stringify with space in absolute filename', () => {
+    const { code } = compileWithAssetUrls(
+      `<div><img src="/foo bar.png"/></div>`,
+      {
+        includeAbsolute: true,
+      },
+      {
+        hoistStatic: true,
+        transformHoist: stringifyStatic,
+      },
+    )
+    expect(code).toContain(`import _imports_0 from '/foo bar.png'`)
+  })
+
+  test('transform with stringify with space in relative filename', () => {
+    const { code } = compileWithAssetUrls(
+      `<div><img src="./foo bar.png"/></div>`,
+      {
+        includeAbsolute: true,
+      },
+      {
+        hoistStatic: true,
+        transformHoist: stringifyStatic,
+      },
+    )
+    expect(code).toContain(`import _imports_0 from './foo bar.png'`)
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/templateTransformSrcset.spec.ts b/packages/compiler-vapor/__tests__/transforms/templateTransformSrcset.spec.ts
new file mode 100644 (file)
index 0000000..7f249e5
--- /dev/null
@@ -0,0 +1,84 @@
+import type { TransformOptions } from '@vue/compiler-core'
+import type { AssetURLOptions } from '../../../compiler-sfc/src/template/transformAssetUrl'
+import { compileTemplate } from '../../../compiler-sfc/src/compileTemplate'
+import { stringifyStatic } from '../../../compiler-dom/src/transforms/stringifyStatic'
+
+function compileWithSrcset(
+  template: string,
+  options?: AssetURLOptions,
+  transformOptions?: TransformOptions,
+) {
+  return compileTemplate({
+    vapor: true,
+    id: 'test',
+    filename: 'test.vue',
+    source: template,
+    transformAssetUrls: {
+      includeAbsolute: true,
+      ...options,
+    },
+  })
+}
+
+const src = `
+<img src="./logo.png" srcset=""/>
+<img src="./logo.png" srcset="./logo.png"/>
+<img src="./logo.png" srcset="./logo.png 2x"/>
+<img src="./logo.png" srcset="./logo.png 2x"/>
+<img src="./logo.png" srcset="./logo.png, ./logo.png 2x"/>
+<img src="./logo.png" srcset="./logo.png 2x, ./logo.png"/>
+<img src="./logo.png" srcset="./logo.png 2x, ./logo.png 3x"/>
+<img src="./logo.png" srcset="./logo.png, ./logo.png 2x, ./logo.png 3x"/>
+<img src="/logo.png" srcset="/logo.png, /logo.png 2x"/>
+<img src="https://example.com/logo.png" srcset="https://example.com/logo.png, https://example.com/logo.png 2x"/>
+<img src="/logo.png" srcset="/logo.png, ./logo.png 2x"/>
+<img src="data:image/png;base64,i" srcset="data:image/png;base64,i 1x, data:image/png;base64,i 2x"/>
+`
+
+describe('compiler sfc: transform srcset', () => {
+  test('transform srcset', () => {
+    expect(compileWithSrcset(src).code).toMatchSnapshot()
+  })
+
+  test('transform srcset w/ base', () => {
+    expect(
+      compileWithSrcset(src, {
+        base: '/foo',
+      }).code,
+    ).toMatchSnapshot()
+  })
+
+  test('transform srcset w/ includeAbsolute: true', () => {
+    expect(
+      compileWithSrcset(src, {
+        includeAbsolute: true,
+      }).code,
+    ).toMatchSnapshot()
+  })
+
+  test('transform srcset w/ stringify', () => {
+    const code = compileWithSrcset(
+      `<div>${src}</div>`,
+      {
+        includeAbsolute: true,
+      },
+      {
+        hoistStatic: true,
+        transformHoist: stringifyStatic,
+      },
+    ).code
+    expect(code).toMatchSnapshot()
+  })
+
+  test('srcset w/ explicit base option', () => {
+    const code = compileWithSrcset(
+      `
+      <img srcset="@/logo.png, @/logo.png 2x"/>
+      <img srcset="@/logo.png 1x, ./logo.png 2x"/>
+    `,
+      { base: '/foo/' },
+      { hoistStatic: true },
+    ).code
+    expect(code).toMatchSnapshot()
+  })
+})
index 89af789f5f9336d4cfced58e8162300d6e75f653..63b010be974f474492783d7cac7ee6b9d0a849e3 100644 (file)
@@ -210,7 +210,7 @@ export function generate(
 
   const delegates = genDelegates(context)
   const templates = genTemplates(ir.template, ir.rootTemplateIndex, context)
-  const imports = genHelperImports(context)
+  const imports = genHelperImports(context) + genAssetImports(context)
   const preamble = imports + templates + delegates
 
   const newlineCount = [...preamble].filter(c => c === '\n').length
@@ -250,3 +250,14 @@ function genHelperImports({ helpers, options }: CodegenContext) {
   }
   return imports
 }
+
+function genAssetImports({ ir }: CodegenContext) {
+  const assetImports = ir.node.imports
+  let imports = ''
+  for (const assetImport of assetImports) {
+    const exp = assetImport.exp
+    const name = exp.content
+    imports += `import ${name} from '${assetImport.path}';\n`
+  }
+  return imports
+}
index adb7cef1b623624773001b91fcfcbc9a5d64eb61..d6f16f7014f2f2864ce1d8166b5096068a319ea3 100644 (file)
@@ -6,7 +6,13 @@ import {
 } from '../ir'
 import { genDirectivesForElement } from './directive'
 import { genOperationWithInsertionState } from './operation'
-import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils'
+import {
+  type CodeFragment,
+  IMPORT_EXPR_RE,
+  NEWLINE,
+  buildCodeFragment,
+  genCall,
+} from './utils'
 
 export function genTemplates(
   templates: Map<string, number>,
@@ -19,6 +25,10 @@ export function genTemplates(
     result.push(
       `const ${context.tName(i)} = ${context.helper('template')}(${JSON.stringify(
         template,
+      ).replace(
+        // replace import expressions with string concatenation
+        IMPORT_EXPR_RE,
+        `" + $1 + "`,
       )}${i === rootIndex ? ', true' : ns ? ', false' : ''}${ns ? `, ${ns}` : ''})\n`,
     )
     i++
index 93e50a244c1f48731b80c9738d50a2ba8ffb237a..e3c361c6fd78697e22e0c14f52bea17295b5c724 100644 (file)
@@ -10,6 +10,13 @@ import {
 import { isArray, isString } from '@vue/shared'
 import type { CodegenContext } from '../generate'
 
+export const IMPORT_EXP_START = '__IMPORT_EXP_START__'
+export const IMPORT_EXP_END = '__IMPORT_EXP_END__'
+export const IMPORT_EXPR_RE: RegExp = new RegExp(
+  `${IMPORT_EXP_START}(.*?)${IMPORT_EXP_END}`,
+  'g',
+)
+
 export const NEWLINE: unique symbol = Symbol(__DEV__ ? `newline` : ``)
 /** increase offset but don't push actual code */
 export const LF: unique symbol = Symbol(__DEV__ ? `line feed` : ``)
index 98db54d3153ab7655262dc28d169cac0e75fe10e..3d311e5cd8ac396efea89e201dd20d3f2e523983 100644 (file)
@@ -30,6 +30,7 @@ import {
 } from './ir'
 import { isConstantExpression, isStaticExpression } from './utils'
 import { newBlock, newDynamic } from './transforms/utils'
+import type { ImportItem } from '@vue/compiler-core'
 
 export type NodeTransform = (
   node: RootNode | TemplateChildNode,
@@ -79,6 +80,7 @@ export class TransformContext<T extends AllNode = AllNode> {
   template: string = ''
   childrenTemplate: (string | null)[] = []
   dynamic: IRDynamicInfo = this.ir.block.dynamic
+  imports: ImportItem[] = []
 
   inVOnce: boolean = false
   inVFor: number = 0
@@ -259,6 +261,8 @@ export function transform(
 
   transformNode(context)
 
+  ir.node.imports = context.imports
+
   return ir
 }
 
index 641d1472acd38acd67c2de6de54d39b46a808e1e..ee1c522abd7c3f3dd2bceaf35d05c132d77abca6 100644 (file)
@@ -37,6 +37,7 @@ import {
 } from '../ir'
 import { EMPTY_EXPRESSION } from './utils'
 import { findProp, isBuiltInComponent } from '../utils'
+import { IMPORT_EXP_END, IMPORT_EXP_START } from '../generators/utils'
 
 export const isReservedProp: (key: string) => boolean = /*#__PURE__*/ makeMap(
   // the leading comma is intentional so empty string "" is also included
@@ -233,14 +234,24 @@ function transformNativeElement(
   } else {
     for (const prop of propsResult[1]) {
       const { key, values } = prop
+      // handling asset imports
       if (
+        context.imports.some(imported =>
+          values[0].content.includes(imported.exp.content),
+        )
+      ) {
+        // add start and end markers to the import expression, so it can be replaced
+        // with string concatenation in the generator, see genTemplates
+        template += ` ${key.content}="${IMPORT_EXP_START}${values[0].content}${IMPORT_EXP_END}"`
+      } else if (
         key.isStatic &&
         values.length === 1 &&
-        values[0].isStatic &&
+        (values[0].isStatic || values[0].content === "''") &&
         !dynamicKeys.includes(key.content)
       ) {
         template += ` ${key.content}`
-        if (values[0].content) template += `="${values[0].content}"`
+        if (values[0].content)
+          template += `="${values[0].content === "''" ? '' : values[0].content}"`
       } else {
         dynamicProps.push(key.content)
         context.registerEffect(