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 6 commits into
base: minor
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
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
25 changes: 25 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,28 @@ 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: 'foo/',
includeAbsolute: true,
},
})
return code
}

test('from public directory', () => {
const code = compileWithAssets(
`<img src="/vite.svg" class="logo" alt="Vite logo" />`,
)
expect(code).matchSnapshot()
expect(code).contains(`import _imports_0 from '/vite.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
}
21 changes: 20 additions & 1 deletion packages/compiler-vapor/src/generators/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export function genExpression(
assignment?: string,
): CodeFragment[] {
const { content, ast, isStatic, loc } = node
const imports = context.ir.node.imports.map(
i => (i.exp as SimpleExpressionNode).content || i.exp,
)

if (isStatic) {
return [[JSON.stringify(content), NewlineType.None, loc]]
Expand All @@ -44,7 +47,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 @@ -129,6 +132,13 @@ function genIdentifier(
const { options, helper, identifiers } = context
const { inline, bindingMetadata } = options
let name: string | undefined = raw
const imports = context.ir.node.imports.map(
i => (i.exp as SimpleExpressionNode).content || i.exp,
)

if (imports.includes(raw)) {
return [[raw, NewlineType.None, loc]]
}

const idMap = identifiers[raw]
if (idMap && idMap.length) {
Expand Down Expand Up @@ -249,6 +259,15 @@ export function processExpressions(
expressions: SimpleExpressionNode[],
shouldDeclare: boolean,
): DeclarationResult {
// filter out asset import expressions
const imports = context.ir.node.imports
const importVariables = imports.map(
i => (i.exp as SimpleExpressionNode).content,
)
expressions = expressions.filter(
exp => !importVariables.includes(exp.content),
)

// analyze variables
const {
seenVariable,
Expand Down
8 changes: 8 additions & 0 deletions packages/compiler-vapor/src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type CompilerCompatOptions,
type ElementNode,
ElementTypes,
type ExpressionNode,
NodeTypes,
type RootNode,
type SimpleExpressionNode,
Expand Down Expand Up @@ -60,6 +61,10 @@ export type StructuralDirectiveTransform = (
) => void | (() => void)

export type TransformOptions = HackOptions<BaseTransformOptions>
export interface ImportItem {
exp: string | ExpressionNode
path: string
}

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

transformNode(context)

ir.node.imports = context.imports

return ir
}

Expand Down