Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ return { fooBar, get FooBaz() { return FooBaz }, get FooQux() { return FooQux },
})"
`;

exports[`custom template lang 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
import { Thing1, Thing2, Thing3, Thing4, Thing5, Thing6 } from "./types.ts"

export default /*@__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) {
__expose();


return { get Thing2() { return Thing2 }, get Thing3() { return Thing3 }, get Thing4() { return Thing4 }, get Thing5() { return Thing5 }, get Thing6() { return Thing6 } }
}

})"
`;

exports[`directive 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
import { vMyDir } from './x'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,31 @@ test('shorthand binding w/ kebab-case', () => {
)
expect(content).toMatch('return { get fooBar() { return fooBar }')
})

test('custom template lang', () => {
const { content } = compile(
`
<script setup lang="ts">
import { Thing1, Thing2, Thing3, Thing4, Thing5, Thing6 } from "./types.ts"
</script>
<template lang="pug">
h1 Thing1
div {{ Thing2 }}

Thing3 World

div(v-bind:abc='Thing4')

div(v-text='Thing5')

div(ref='Thing6')
</template>
`,
{ templateOptions: { preprocessLang: 'pug' } },
)
// Thing1 is just a string in the template so should not be included
expect(content).toMatch(
'return { get Thing2() { return Thing2 }, get Thing3() { return Thing3 }, get Thing4() { return Thing4 }, get Thing5() { return Thing5 }, get Thing6() { return Thing6 } }',
)
assertCode(content)
})
65 changes: 44 additions & 21 deletions packages/compiler-sfc/src/compileScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
DEFAULT_FILENAME,
type SFCDescriptor,
type SFCScriptBlock,
type SFCTemplateBlock,
} from './parse'
import type { ParserPlugin } from '@babel/parser'
import { generateCodeFrame } from '@vue/shared'
Expand Down Expand Up @@ -36,6 +37,7 @@ import {
import { CSS_VARS_HELPER, genCssVarsCode } from './style/cssVars'
import {
type SFCTemplateCompileOptions,
type SFCTemplateCompileResults,
compileTemplate,
} from './compileTemplate'
import { warnOnce } from './warn'
Expand Down Expand Up @@ -245,6 +247,8 @@ export function compileScript(
ctx.s.move(start, end, 0)
}

let customTemplateLangCompiledSFC: SFCTemplateCompileResults | undefined

function registerUserImport(
source: string,
local: string,
Expand All @@ -260,10 +264,22 @@ export function compileScript(
needTemplateUsageCheck &&
ctx.isTS &&
sfc.template &&
!sfc.template.src &&
!sfc.template.lang
!sfc.template.src
) {
isUsedInTemplate = isImportUsed(local, sfc)
if (!sfc.template.lang) {
isUsedInTemplate = isImportUsed(
local,
sfc.template.content,
sfc.template.ast!,
)
} else {
customTemplateLangCompiledSFC ||= compileSFCTemplate(sfc.template)
isUsedInTemplate = isImportUsed(
local,
sfc.template.content,
customTemplateLangCompiledSFC.ast!,
)
}
}
Comment on lines +267 to 283
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: using preprocessed AST with raw (un-preprocessed) content can mis-detect template imports

When lang is set (e.g., pug), compileSFCTemplate returns an AST for the preprocessed template, but content passed to isImportUsed is the raw SFC template (pug). This mismatch can lead to false negatives/positives. Also, ast may be undefined on preprocessing failure; defaulting to “used” is safer in dev/non-inline to avoid runtime breakage.

Apply the following:

-      if (!sfc.template.lang) {
-        isUsedInTemplate = isImportUsed(
-          local,
-          sfc.template.content,
-          sfc.template.ast!,
-        )
-      } else {
-        customTemplateLangCompiledSFC ||= compileSFCTemplate(sfc.template)
-        isUsedInTemplate = isImportUsed(
-          local,
-          sfc.template.content,
-          customTemplateLangCompiledSFC.ast!,
-        )
-      }
+      if (!sfc.template.lang) {
+        isUsedInTemplate = sfc.template.ast
+          ? isImportUsed(local, sfc.template.content, sfc.template.ast)
+          : true
+      } else {
+        const compiled =
+          (customTemplateLangCompiledSFC ||= compileSFCTemplate(sfc.template))
+        isUsedInTemplate = compiled.ast
+          ? isImportUsed(local, compiled.source, compiled.ast)
+          : true
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
!sfc.template.src
) {
isUsedInTemplate = isImportUsed(local, sfc)
if (!sfc.template.lang) {
isUsedInTemplate = isImportUsed(
local,
sfc.template.content,
sfc.template.ast!,
)
} else {
customTemplateLangCompiledSFC ||= compileSFCTemplate(sfc.template)
isUsedInTemplate = isImportUsed(
local,
sfc.template.content,
customTemplateLangCompiledSFC.ast!,
)
}
}
!sfc.template.src
) {
if (!sfc.template.lang) {
isUsedInTemplate = sfc.template.ast
? isImportUsed(local, sfc.template.content, sfc.template.ast)
: true
} else {
const compiled =
(customTemplateLangCompiledSFC ||= compileSFCTemplate(sfc.template))
isUsedInTemplate = compiled.ast
? isImportUsed(local, compiled.source, compiled.ast)
: true
}
}
🤖 Prompt for AI Agents
In packages/compiler-sfc/src/compileScript.ts around lines 267 to 283, the code
calls isImportUsed with the raw SFC template content when a custom template lang
is used, but passes the preprocessed AST — this mismatch can mis-detect imports
and also assumes ast always exists; change the call to use the preprocessed
template content together with its AST when lang is set (i.e., use
customTemplateLangCompiledSFC.content and .ast), and if compileSFCTemplate fails
to produce an ast, avoid a false negative by defaulting isUsedInTemplate to true
(at least for dev/non-inline cases) so runtime behavior isn’t broken.


ctx.userImports[local] = {
Expand Down Expand Up @@ -293,6 +309,28 @@ export function compileScript(
})
}

function compileSFCTemplate(
sfcTemplate: SFCTemplateBlock,
): SFCTemplateCompileResults {
return compileTemplate({
filename,
ast: sfcTemplate.ast,
source: sfcTemplate.content,
inMap: sfcTemplate.map,
...options.templateOptions,
id: scopeId,
scoped: sfc.styles.some(s => s.scoped),
isProd: options.isProd,
ssrCssVars: sfc.cssVars,
compilerOptions: {
...(options.templateOptions && options.templateOptions.compilerOptions),
inline: true,
isTS: ctx.isTS,
bindingMetadata: ctx.bindingMetadata,
},
})
}

const scriptAst = ctx.scriptAst
const scriptSetupAst = ctx.scriptSetupAst!

Expand Down Expand Up @@ -880,24 +918,9 @@ export function compileScript(
}
// inline render function mode - we are going to compile the template and
// inline it right here
const { code, ast, preamble, tips, errors, map } = compileTemplate({
filename,
ast: sfc.template.ast,
source: sfc.template.content,
inMap: sfc.template.map,
...options.templateOptions,
id: scopeId,
scoped: sfc.styles.some(s => s.scoped),
isProd: options.isProd,
ssrCssVars: sfc.cssVars,
compilerOptions: {
...(options.templateOptions &&
options.templateOptions.compilerOptions),
inline: true,
isTS: ctx.isTS,
bindingMetadata: ctx.bindingMetadata,
},
})
const { code, ast, preamble, tips, errors, map } = compileSFCTemplate(
sfc.template,
)
templateMap = map
if (tips.length) {
tips.forEach(warnOnce)
Expand Down
5 changes: 4 additions & 1 deletion packages/compiler-sfc/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,10 @@ export function hmrShouldReload(
for (const key in prevImports) {
// if an import was previous unused, but now is used, we need to force
// reload so that the script now includes that import.
if (!prevImports[key].isUsedInTemplate && isImportUsed(key, next)) {
if (
!prevImports[key].isUsedInTemplate &&
isImportUsed(key, next.template!.content!, next.template!.ast!)
) {
Comment on lines +451 to +454
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Potential runtime crash when template/AST is absent

next.template or next.template.ast can be null (e.g., external src templates). The non-null assertions will throw at runtime during HMR checks. Guard before calling isImportUsed.

Apply this diff:

-    if (
-      !prevImports[key].isUsedInTemplate &&
-      isImportUsed(key, next.template!.content!, next.template!.ast!)
-    ) {
+    // skip when there is no in-file template or no parsed AST (e.g. src=)
+    if (!next.template || !next.template.ast) {
+      continue
+    }
+    if (
+      !prevImports[key].isUsedInTemplate &&
+      isImportUsed(key, next.template.content, next.template.ast)
+    ) {
       return true
     }

Optionally short-circuit earlier:

   ) {
     return false
   }
+  if (!next.template || !next.template.ast) {
+    return false
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (
!prevImports[key].isUsedInTemplate &&
isImportUsed(key, next.template!.content!, next.template!.ast!)
) {
// skip when there is no in-file template or no parsed AST (e.g. src=)
if (!next.template || !next.template.ast) {
continue
}
if (
!prevImports[key].isUsedInTemplate &&
isImportUsed(key, next.template.content, next.template.ast)
) {
return true
}
🤖 Prompt for AI Agents
In packages/compiler-sfc/src/parse.ts around lines 451 to 454, the code uses
non-null assertions on next.template and next.template.ast which can be null and
cause runtime crashes; guard these properties before calling isImportUsed by
first checking that next.template and next.template.ast exist (e.g., if
(next.template && next.template.ast) ) and only then call isImportUsed, or
short-circuit earlier so imports marked as unused when the template/AST is
absent; update the conditional to avoid using ! and .! on possibly null values.

return true
}
}
Expand Down
24 changes: 17 additions & 7 deletions packages/compiler-sfc/src/script/importUsageCheck.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { SFCDescriptor } from '../parse'
import {
type ExpressionNode,
NodeTypes,
type RootNode,
type SimpleExpressionNode,
type TemplateChildNode,
parserOptions,
Expand All @@ -15,22 +15,28 @@ import { camelize, capitalize, isBuiltInDirective } from '@vue/shared'
* the properties that should be included in the object returned from setup()
* when not using inline mode.
*/
export function isImportUsed(local: string, sfc: SFCDescriptor): boolean {
return resolveTemplateUsedIdentifiers(sfc).has(local)
export function isImportUsed(
local: string,
content: string,
ast: RootNode,
): boolean {
return resolveTemplateUsedIdentifiers(content, ast).has(local)
}

const templateUsageCheckCache = createCache<Set<string>>()

function resolveTemplateUsedIdentifiers(sfc: SFCDescriptor): Set<string> {
const { content, ast } = sfc.template!
function resolveTemplateUsedIdentifiers(
content: string,
ast: RootNode,
): Set<string> {
const cached = templateUsageCheckCache.get(content)
if (cached) {
return cached
}

const ids = new Set<string>()

ast!.children.forEach(walk)
ast.children.forEach(walk)

function walk(node: TemplateChildNode) {
switch (node.type) {
Expand Down Expand Up @@ -89,6 +95,10 @@ function extractIdentifiers(ids: Set<string>, node: ExpressionNode) {
if (node.ast) {
walkIdentifiers(node.ast, n => ids.add(n.name))
} else if (node.ast === null) {
ids.add((node as SimpleExpressionNode).content)
const content = (node as SimpleExpressionNode).content.replace(
/^_ctx\./,
'',
)
ids.add(content)
}
}
Loading