Skip to content

Commit 531b91e

Browse files
committed
fix(compiler-sfc): import usage detection for non-inline templates
1 parent 75220c7 commit 531b91e

File tree

5 files changed

+108
-29
lines changed

5 files changed

+108
-29
lines changed

packages/compiler-sfc/__tests__/compileScript/__snapshots__/importUsageCheck.spec.ts.snap

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,21 @@ return { fooBar, get FooBaz() { return FooBaz }, get FooQux() { return FooQux },
4949
})"
5050
`;
5151

52+
exports[`custom template lang 1`] = `
53+
"import { defineComponent as _defineComponent } from 'vue'
54+
import { Thing1, Thing2, Thing3, Thing4, Thing5, Thing6 } from "./types.ts"
55+
56+
export default /*@__PURE__*/_defineComponent({
57+
setup(__props, { expose: __expose }) {
58+
__expose();
59+
60+
61+
return { get Thing2() { return Thing2 }, get Thing3() { return Thing3 }, get Thing4() { return Thing4 }, get Thing5() { return Thing5 }, get Thing6() { return Thing6 } }
62+
}
63+
64+
})"
65+
`;
66+
5267
exports[`directive 1`] = `
5368
"import { defineComponent as _defineComponent } from 'vue'
5469
import { vMyDir } from './x'

packages/compiler-sfc/__tests__/compileScript/importUsageCheck.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,31 @@ test('shorthand binding w/ kebab-case', () => {
265265
)
266266
expect(content).toMatch('return { get fooBar() { return fooBar }')
267267
})
268+
269+
test('custom template lang', () => {
270+
const { content } = compile(
271+
`
272+
<script setup lang="ts">
273+
import { Thing1, Thing2, Thing3, Thing4, Thing5, Thing6 } from "./types.ts"
274+
</script>
275+
<template lang="pug">
276+
h1 Thing1
277+
div {{ Thing2 }}
278+
279+
Thing3 World
280+
281+
div(v-bind:abc='Thing4')
282+
283+
div(v-text='Thing5')
284+
285+
div(ref='Thing6')
286+
</template>
287+
`,
288+
{ templateOptions: { preprocessLang: 'pug' } },
289+
)
290+
// Thing1 is just a string in the template so should not be included
291+
expect(content).toMatch(
292+
'return { get Thing2() { return Thing2 }, get Thing3() { return Thing3 }, get Thing4() { return Thing4 }, get Thing5() { return Thing5 }, get Thing6() { return Thing6 } }',
293+
)
294+
assertCode(content)
295+
})

packages/compiler-sfc/src/compileScript.ts

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
DEFAULT_FILENAME,
1010
type SFCDescriptor,
1111
type SFCScriptBlock,
12+
type SFCTemplateBlock,
1213
} from './parse'
1314
import type { ParserPlugin } from '@babel/parser'
1415
import { generateCodeFrame } from '@vue/shared'
@@ -36,6 +37,7 @@ import {
3637
import { CSS_VARS_HELPER, genCssVarsCode } from './style/cssVars'
3738
import {
3839
type SFCTemplateCompileOptions,
40+
type SFCTemplateCompileResults,
3941
compileTemplate,
4042
} from './compileTemplate'
4143
import { warnOnce } from './warn'
@@ -245,6 +247,8 @@ export function compileScript(
245247
ctx.s.move(start, end, 0)
246248
}
247249

250+
let customTemplateLangCompiledSFC: SFCTemplateCompileResults | undefined
251+
248252
function registerUserImport(
249253
source: string,
250254
local: string,
@@ -260,10 +264,22 @@ export function compileScript(
260264
needTemplateUsageCheck &&
261265
ctx.isTS &&
262266
sfc.template &&
263-
!sfc.template.src &&
264-
!sfc.template.lang
267+
!sfc.template.src
265268
) {
266-
isUsedInTemplate = isImportUsed(local, sfc)
269+
if (!sfc.template.lang) {
270+
isUsedInTemplate = isImportUsed(
271+
local,
272+
sfc.template.content,
273+
sfc.template.ast!,
274+
)
275+
} else {
276+
customTemplateLangCompiledSFC ||= compileSFCTemplate(sfc.template)
277+
isUsedInTemplate = isImportUsed(
278+
local,
279+
sfc.template.content,
280+
customTemplateLangCompiledSFC.ast!,
281+
)
282+
}
267283
}
268284

269285
ctx.userImports[local] = {
@@ -293,6 +309,28 @@ export function compileScript(
293309
})
294310
}
295311

312+
function compileSFCTemplate(
313+
sfcTemplate: SFCTemplateBlock,
314+
): SFCTemplateCompileResults {
315+
return compileTemplate({
316+
filename,
317+
ast: sfcTemplate.ast,
318+
source: sfcTemplate.content,
319+
inMap: sfcTemplate.map,
320+
...options.templateOptions,
321+
id: scopeId,
322+
scoped: sfc.styles.some(s => s.scoped),
323+
isProd: options.isProd,
324+
ssrCssVars: sfc.cssVars,
325+
compilerOptions: {
326+
...(options.templateOptions && options.templateOptions.compilerOptions),
327+
inline: true,
328+
isTS: ctx.isTS,
329+
bindingMetadata: ctx.bindingMetadata,
330+
},
331+
})
332+
}
333+
296334
const scriptAst = ctx.scriptAst
297335
const scriptSetupAst = ctx.scriptSetupAst!
298336

@@ -880,24 +918,9 @@ export function compileScript(
880918
}
881919
// inline render function mode - we are going to compile the template and
882920
// inline it right here
883-
const { code, ast, preamble, tips, errors, map } = compileTemplate({
884-
filename,
885-
ast: sfc.template.ast,
886-
source: sfc.template.content,
887-
inMap: sfc.template.map,
888-
...options.templateOptions,
889-
id: scopeId,
890-
scoped: sfc.styles.some(s => s.scoped),
891-
isProd: options.isProd,
892-
ssrCssVars: sfc.cssVars,
893-
compilerOptions: {
894-
...(options.templateOptions &&
895-
options.templateOptions.compilerOptions),
896-
inline: true,
897-
isTS: ctx.isTS,
898-
bindingMetadata: ctx.bindingMetadata,
899-
},
900-
})
921+
const { code, ast, preamble, tips, errors, map } = compileSFCTemplate(
922+
sfc.template,
923+
)
901924
templateMap = map
902925
if (tips.length) {
903926
tips.forEach(warnOnce)

packages/compiler-sfc/src/parse.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,10 @@ export function hmrShouldReload(
448448
for (const key in prevImports) {
449449
// if an import was previous unused, but now is used, we need to force
450450
// reload so that the script now includes that import.
451-
if (!prevImports[key].isUsedInTemplate && isImportUsed(key, next)) {
451+
if (
452+
!prevImports[key].isUsedInTemplate &&
453+
isImportUsed(key, next.template!.content!, next.template!.ast!)
454+
) {
452455
return true
453456
}
454457
}

packages/compiler-sfc/src/script/importUsageCheck.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { SFCDescriptor } from '../parse'
21
import {
32
type ExpressionNode,
43
NodeTypes,
4+
type RootNode,
55
type SimpleExpressionNode,
66
type TemplateChildNode,
77
parserOptions,
@@ -15,22 +15,28 @@ import { camelize, capitalize, isBuiltInDirective } from '@vue/shared'
1515
* the properties that should be included in the object returned from setup()
1616
* when not using inline mode.
1717
*/
18-
export function isImportUsed(local: string, sfc: SFCDescriptor): boolean {
19-
return resolveTemplateUsedIdentifiers(sfc).has(local)
18+
export function isImportUsed(
19+
local: string,
20+
content: string,
21+
ast: RootNode,
22+
): boolean {
23+
return resolveTemplateUsedIdentifiers(content, ast).has(local)
2024
}
2125

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

24-
function resolveTemplateUsedIdentifiers(sfc: SFCDescriptor): Set<string> {
25-
const { content, ast } = sfc.template!
28+
function resolveTemplateUsedIdentifiers(
29+
content: string,
30+
ast: RootNode,
31+
): Set<string> {
2632
const cached = templateUsageCheckCache.get(content)
2733
if (cached) {
2834
return cached
2935
}
3036

3137
const ids = new Set<string>()
3238

33-
ast!.children.forEach(walk)
39+
ast.children.forEach(walk)
3440

3541
function walk(node: TemplateChildNode) {
3642
switch (node.type) {
@@ -89,6 +95,10 @@ function extractIdentifiers(ids: Set<string>, node: ExpressionNode) {
8995
if (node.ast) {
9096
walkIdentifiers(node.ast, n => ids.add(n.name))
9197
} else if (node.ast === null) {
92-
ids.add((node as SimpleExpressionNode).content)
98+
const content = (node as SimpleExpressionNode).content.replace(
99+
/^_ctx\./,
100+
'',
101+
)
102+
ids.add(content)
93103
}
94104
}

0 commit comments

Comments
 (0)