From: Gianthard-cyh <45843411+Gianthard-cyh@users.noreply.github.com> Date: Mon, 10 Nov 2025 02:30:35 +0000 (+0800) Subject: feat(compiler-vapor): handle asset imports (#13630) X-Git-Tag: v3.6.0-alpha.4~7 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=7d4ab91e9cac5dc177f1a55ac1137246fee9407c;p=thirdparty%2Fvuejs%2Fcore.git feat(compiler-vapor): handle asset imports (#13630) --- diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 9da54790cc..06e963acfd 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -21,6 +21,7 @@ export { type NodeTransform, type StructuralDirectiveTransform, type DirectiveTransform, + type ImportItem, } from './transform' export { generate, diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index 10121fb5d5..b37d665b14 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -77,7 +77,7 @@ export type StructuralDirectiveTransform = ( ) => void | (() => void) export interface ImportItem { - exp: string | ExpressionNode + exp: SimpleExpressionNode path: string } diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileTemplate.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileTemplate.spec.ts.snap index 3cc6c17355..c958c9e031 100644 --- a/packages/compiler-sfc/__tests__/__snapshots__/compileTemplate.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/__snapshots__/compileTemplate.spec.ts.snap @@ -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 }) ]) ] diff --git a/packages/compiler-sfc/src/template/transformSrcset.ts b/packages/compiler-sfc/src/template/transformSrcset.ts index 40fba4882b..a825f02db6 100644 --- a/packages/compiler-sfc/src/template/transformSrcset.ts +++ b/packages/compiler-sfc/src/template/transformSrcset.ts @@ -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 index 0000000000..38b325e178 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/templateTransformAssetUrl.spec.ts.snap @@ -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("", 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("", 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("", 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("", 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("", 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("", 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("") +const t1 = _template("") +const t2 = _template("") +const t3 = _template("") +const t4 = _template("") +const t5 = _template("") + +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("
", 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("") +const t1 = _template("") +const t2 = _template("") + +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("") +const t1 = _template("") +const t2 = _template("") +const t3 = _template("") + +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 index 0000000000..68ed7686e2 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/templateTransformSrcset.spec.ts.snap @@ -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("") +const t1 = _template("") + +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("") +const t1 = _template("") +const t2 = _template("") +const t3 = _template("") +const t4 = _template("") +const t5 = _template("") +const t6 = _template("") +const t7 = _template("") +const t8 = _template("") +const t9 = _template("") +const t10 = _template("") + +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("") +const t1 = _template("") +const t2 = _template("") +const t3 = _template("") +const t4 = _template("") +const t5 = _template("") +const t6 = _template("") +const t7 = _template("") +const t8 = _template("") +const t9 = _template("") +const t10 = _template("") + +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("") +const t1 = _template("") +const t2 = _template("") +const t3 = _template("") +const t4 = _template("") +const t5 = _template("") +const t6 = _template("") +const t7 = _template("") +const t8 = _template("") +const t9 = _template("") +const t10 = _template("") + +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("
", 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 index 0000000000..225d18aded --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/templateTransformAssetUrl.spec.ts @@ -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(` + + + + + + + + `) + + expect(result.code).toMatchSnapshot() + }) + + /** + * vuejs/component-compiler-utils#22 Support uri fragment in transformed require + */ + test('support uri fragment', () => { + const result = compileWithAssetUrls( + '' + + '', + {}, + { + 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('') + + expect(result.code).toMatchSnapshot() + }) + + test('with explicit base', () => { + const { code } = compileWithAssetUrls( + `` + // -> /foo/bar.png + `` + // -> bar.png (untouched) + `` + // -> still converts to import + ``, // -> 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( + `` + + `` + + `` + + ``, + { + includeAbsolute: true, + }, + ) + expect(code).toMatchSnapshot() + }) + + // vitejs/vite#298 + test('should not transform hash fragments', () => { + const { code } = compileWithAssetUrls( + ` + + + + + `, + ) + // 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(``, { + base: 'http://localhost:3000/src/', + }) + + expect(code).toMatchSnapshot() + }) + + test('should allow for full base URLs, without paths', () => { + const { code } = compileWithAssetUrls(``, { + base: 'http://localhost:3000', + }) + + expect(code).toMatchSnapshot() + }) + + test('should allow for full base URLs, without port', () => { + const { code } = compileWithAssetUrls(``, { + base: 'http://localhost', + }) + + expect(code).toMatchSnapshot() + }) + + test('should allow for full base URLs, without protocol', () => { + const { code } = compileWithAssetUrls(``, { + base: '//localhost', + }) + + expect(code).toMatchSnapshot() + }) + + test('transform with stringify', () => { + const { code } = compileWithAssetUrls( + `
` + + `` + + `` + + `` + + `` + + `` + + `
`, + { + includeAbsolute: true, + }, + { + hoistStatic: true, + transformHoist: stringifyStatic, + }, + ) + expect(code).toMatchSnapshot() + }) + + test('transform with stringify with space in absolute filename', () => { + const { code } = compileWithAssetUrls( + `
`, + { + 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( + `
`, + { + 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 index 0000000000..7f249e5c7a --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/templateTransformSrcset.spec.ts @@ -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 = ` + + + + + + + + + + + + +` + +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( + `
${src}
`, + { + includeAbsolute: true, + }, + { + hoistStatic: true, + transformHoist: stringifyStatic, + }, + ).code + expect(code).toMatchSnapshot() + }) + + test('srcset w/ explicit base option', () => { + const code = compileWithSrcset( + ` + + + `, + { base: '/foo/' }, + { hoistStatic: true }, + ).code + expect(code).toMatchSnapshot() + }) +}) diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index 89af789f5f..63b010be97 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -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 +} diff --git a/packages/compiler-vapor/src/generators/template.ts b/packages/compiler-vapor/src/generators/template.ts index adb7cef1b6..d6f16f7014 100644 --- a/packages/compiler-vapor/src/generators/template.ts +++ b/packages/compiler-vapor/src/generators/template.ts @@ -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, @@ -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++ diff --git a/packages/compiler-vapor/src/generators/utils.ts b/packages/compiler-vapor/src/generators/utils.ts index 93e50a244c..e3c361c6fd 100644 --- a/packages/compiler-vapor/src/generators/utils.ts +++ b/packages/compiler-vapor/src/generators/utils.ts @@ -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` : ``) diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 98db54d315..3d311e5cd8 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -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 { 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 } diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index 641d1472ac..ee1c522abd 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -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(