Skip to content

fix(compiler-vapor): fix asset import from public directory #13630

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: minor
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/compiler-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export {
type NodeTransform,
type StructuralDirectiveTransform,
type DirectiveTransform,
type ImportItem,
} from './transform'
export {
generate,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`compile > asset imports > from public directory 1`] = `
"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
import _imports_0 from '/vite.svg';
Copy link
Member

@edison1105 edison1105 Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import _imports_0 from '/vite.svg';
const t0 = _template(`<img src="${_imports_0}">`, true)

It should be done like this to avoid renderEffect and setProp.

Copy link
Author

@Gianthard-cyh Gianthard-cyh Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi! Thanks for the suggestion — I’ve tried to move _imports_0 into the template string as recommended to avoid renderEffect and setProp.

export function genTemplates(
templates: string[],
rootIndex: number | undefined,
{ helper }: CodegenContext,
): string {
return templates
.map(
(template, i) =>
`const t${i} = ${helper('template')}(${JSON.stringify(
template,
)}${i === rootIndex ? ', true' : ''})\n`,
)
.join('')
}

However, the genTemplates function still uses JSON.stringify(), which wraps the entire template in double quotes and escapes inner quotes. That prevents me from generating template literals like:

const t0 = _template(`<img src="${_imports_0}">`, true)

I also tried string concatenation:

template += ` ${key.content}=""+${values[0].content}+""`

But this results in a normal string with escaped quotes.

const t0 = _template("<img src=\"\"+_imports_0+\"\">")

Let me know if you have any thoughts or preferred direction on this — happy to collaborate on the final approach!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be easier to implement using string concatenation. For example, like this:

  • Wrap the variable with special characters
template += ` ${key.content}="$\{${values[0].content}\}$"`
  • In genTemplates, replace the variable with string concatenation
${JSON.stringify(template).replace(/\${_imports_(\d+)}\$/g,'"+ _imports_$1 +"')

const t0 = _template("<img class=\\"logo\\" alt=\\"Vite logo\\">", true)
export function render(_ctx) {
const n0 = t0()
_renderEffect(() => _setProp(n0, "src", _imports_0))
return n0
}"
`;
exports[`compile > bindings 1`] = `
"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<div> </div>", true)
Expand Down
43 changes: 43 additions & 0 deletions packages/compiler-vapor/__tests__/compile.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BindingTypes, type RootNode } from '@vue/compiler-dom'
import { type CompilerOptions, compile as _compile } from '../src'
import { compileTemplate } from '@vue/compiler-sfc'

// TODO This is a temporary test case for initial implementation.
// Remove it once we have more comprehensive tests.
Expand Down Expand Up @@ -262,4 +263,46 @@ describe('compile', () => {
)
})
})

describe('asset imports', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new test file should be created to test assetUrl and srcset. For example:

  • templateTransformAssetUrl.spec.ts
  • templateTransformSrcset.spec.ts

Try to port the relevant test cases from the existing templateTransformAssetUrl.spec.ts and templateTransformSrcset.spec.ts.

const compileWithAssets = (template: string) => {
const { code } = compileTemplate({
vapor: true,
id: 'test',
filename: 'test.vue',
source: template,
transformAssetUrls: {
base: 'base/',
includeAbsolute: true,
},
})
return code
}

test('from public directory', () => {
const code = compileWithAssets(`<img src="/foo.svg" />`)
expect(code).matchSnapshot()
expect(code).contains(`import _imports_0 from '/foo.svg';`)
})

test(`multiple public assets`, () => {
const code = compileWithAssets(
`<img src="/foo.svg" />
<img src="/bar.svg" />`,
)
expect(code).matchSnapshot()
expect(code).contains(`import _imports_0 from '/foo.svg';`)
expect(code).contains(`import _imports_1 from '/bar.svg';`)
})

test(`hybrid assets`, () => {
const code = compileWithAssets(
`<img src="/foo.svg" />
<img src="./bar.svg" />`,
)
expect(code).matchSnapshot()
expect(code).contains(`import _imports_0 from '/foo.svg';`)
expect(code).contains(`src=\\"base/bar.svg\\"`)
})
})
})
13 changes: 12 additions & 1 deletion packages/compiler-vapor/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,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
Expand Down Expand Up @@ -178,3 +178,14 @@ function genHelperImports({ helpers, helper, options }: CodegenContext) {
}
return imports
}

function genAssetImports({ ir, helper, options }: CodegenContext) {
const assetImports = ir.node.imports
let imports = ''
for (const assetImport of assetImports) {
const exp = assetImport.exp as SimpleExpressionNode
const name = exp.content
imports += `import ${name} from '${assetImport.path}';\n`
}
return imports
}
9 changes: 7 additions & 2 deletions packages/compiler-vapor/src/generators/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from '@vue/compiler-dom'
import type { Identifier, Node } from '@babel/types'
import type { CodegenContext } from '../generate'
import { isConstantExpression } from '../utils'
import { getAssetImports, isConstantExpression } from '../utils'
import { type CodeFragment, NEWLINE, buildCodeFragment } from './utils'
import { type ParserOptions, parseExpression } from '@babel/parser'

Expand All @@ -29,6 +29,7 @@ export function genExpression(
assignment?: string,
): CodeFragment[] {
const { content, ast, isStatic, loc } = node
const imports = getAssetImports(context.ir)

if (isStatic) {
return [[JSON.stringify(content), NewlineType.None, loc]]
Expand All @@ -44,7 +45,7 @@ export function genExpression(
}

// the expression is a simple identifier
if (ast === null) {
if (ast === null || imports.includes(content)) {
return genIdentifier(content, context, loc, assignment)
}

Expand Down Expand Up @@ -249,6 +250,10 @@ export function processExpressions(
expressions: SimpleExpressionNode[],
shouldDeclare: boolean,
): DeclarationResult {
// filter out asset import expressions
const imports = getAssetImports(context.ir)
expressions = expressions.filter(exp => !imports.includes(exp.content))

// analyze variables
const {
seenVariable,
Expand Down
17 changes: 11 additions & 6 deletions packages/compiler-vapor/src/generators/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@ import { genDirectivesForElement } from './directive'
import { genOperationWithInsertionState } from './operation'
import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils'

function escapeTemplate(str: string): string {
return str
.replace(/\\/g, '\\\\') // 转义反斜杠
.replace(/`/g, '\\`') // 转义反引号
// 不转义 `${`,保留插值
}

export function genTemplates(
templates: string[],
rootIndex: number | undefined,
{ helper }: CodegenContext,
): string {
return templates
.map(
(template, i) =>
`const t${i} = ${helper('template')}(${JSON.stringify(
template,
)}${i === rootIndex ? ', true' : ''})\n`,
)
.map((template, i) => {
const escaped = escapeTemplate(template)
return `const t${i} = ${helper('template')}(\`${escaped}\`${i === rootIndex ? ', true' : ''})\n`
})
.join('')
}

Expand Down
5 changes: 4 additions & 1 deletion packages/compiler-vapor/src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,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,
Expand Down Expand Up @@ -60,7 +61,6 @@ export type StructuralDirectiveTransform = (
) => void | (() => void)

export type TransformOptions = HackOptions<BaseTransformOptions>

export class TransformContext<T extends AllNode = AllNode> {
selfName: string | null = null
parent: TransformContext<RootNode | ElementNode> | null = null
Expand All @@ -75,6 +75,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
Expand Down Expand Up @@ -225,6 +226,8 @@ export function transform(

transformNode(context)

ir.node.imports = context.imports

return ir
}

Expand Down
7 changes: 5 additions & 2 deletions packages/compiler-vapor/src/transforms/transformElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
type VaporDirectiveNode,
} from '../ir'
import { EMPTY_EXPRESSION } from './utils'
import { findProp } from '../utils'
import { findProp, getAssetImports } from '../utils'

export const isReservedProp: (key: string) => boolean = /*#__PURE__*/ makeMap(
// the leading comma is intentional so empty string "" is also included
Expand Down Expand Up @@ -223,7 +223,10 @@ function transformNativeElement(
} else {
for (const prop of propsResult[1]) {
const { key, values } = prop
if (key.isStatic && values.length === 1 && values[0].isStatic) {
const imports = getAssetImports(context)
if (imports.includes(values[0].content)) {
template += ` ${key.content}=""+${values[0].content}+""`
} else if (key.isStatic && values.length === 1 && values[0].isStatic) {
template += ` ${key.content}`
if (values[0].content) template += `="${values[0].content}"`
} else {
Expand Down
13 changes: 12 additions & 1 deletion packages/compiler-vapor/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import {
isConstantNode,
isLiteralWhitelisted,
} from '@vue/compiler-dom'
import type { VaporDirectiveNode } from './ir'
import type { RootIRNode, VaporDirectiveNode } from './ir'
import { EMPTY_EXPRESSION } from './transforms/utils'
import { TransformContext } from './transform'

export const findProp = _findProp as (
node: ElementNode,
Expand Down Expand Up @@ -88,3 +89,13 @@ export function getLiteralExpressionValue(
}
return exp.isStatic ? exp.content : null
}

export function getAssetImports(context: TransformContext): string[]
export function getAssetImports(ir: RootIRNode): string[]
export function getAssetImports(ctx: TransformContext | RootIRNode): string[] {
const imports =
ctx instanceof TransformContext ? ctx.imports : ctx.node.imports
return imports.map(i =>
typeof i === 'string' ? i : (i.exp as SimpleExpressionNode).content,
)
}