Skip to content

Commit f0ca233

Browse files
committed
refactor(compiler-sfc): improve script setup import expose heuristics
1 parent 5a3ccfd commit f0ca233

File tree

5 files changed

+83
-19
lines changed

5 files changed

+83
-19
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,15 +206,15 @@ return { x }
206206
207207
exports[`SFC compile <script setup> imports imports not used in <template> should not be exposed 1`] = `
208208
"import { defineComponent as _defineComponent } from 'vue'
209-
import { FooBar, FooBaz, FooQux, vMyDir, x, y } from './x'
209+
import { FooBar, FooBaz, FooQux, vMyDir, x, y, z } from './x'
210210
211211
export default _defineComponent({
212212
setup(__props, { expose }) {
213213
expose()
214214
215215
const fooBar: FooBar = 1
216216
217-
return { fooBar, FooBaz, FooQux, vMyDir, x }
217+
return { fooBar, FooBaz, FooQux, vMyDir, x, z }
218218
}
219219
220220
})"

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,20 +213,23 @@ defineExpose({ foo: 123 })
213213
test('imports not used in <template> should not be exposed', () => {
214214
const { content } = compile(`
215215
<script setup lang="ts">
216-
import { FooBar, FooBaz, FooQux, vMyDir, x, y } from './x'
216+
import { FooBar, FooBaz, FooQux, vMyDir, x, y, z } from './x'
217217
const fooBar: FooBar = 1
218218
</script>
219219
<template>
220220
<FooBaz v-my-dir>{{ x }} {{ yy }}</FooBaz>
221221
<foo-qux/>
222+
<div :id="z + 'y'">FooBar</div>
222223
</template>
223224
`)
224225
assertCode(content)
226+
// FooBar: should not be matched by plain text
225227
// FooBaz: used as PascalCase component
226228
// FooQux: used as kebab-case component
227229
// vMyDir: used as directive v-my-dir
228230
// x: used in interpolation
229-
expect(content).toMatch(`return { fooBar, FooBaz, FooQux, vMyDir, x }`)
231+
// y: should not be matched by {{ yy }} or 'y' in binding exps
232+
expect(content).toMatch(`return { fooBar, FooBaz, FooQux, vMyDir, x, z }`)
230233
})
231234
})
232235

packages/compiler-sfc/src/cache.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export function createCache<T>(size = 500) {
2+
return __GLOBAL__ || __ESM_BROWSER__
3+
? new Map<string, T>()
4+
: (new (require('lru-cache'))(size) as Map<string, T>)
5+
}

packages/compiler-sfc/src/compileScript.ts

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@ import MagicString from 'magic-string'
22
import {
33
BindingMetadata,
44
BindingTypes,
5+
createRoot,
56
locStub,
6-
UNREF
7-
} from '@vue/compiler-core'
7+
NodeTypes,
8+
transform,
9+
parserOptions,
10+
UNREF,
11+
SimpleExpressionNode
12+
} from '@vue/compiler-dom'
813
import {
914
ScriptSetupTextRanges,
1015
SFCDescriptor,
@@ -14,8 +19,10 @@ import {
1419
import { parse as _parse, ParserOptions, ParserPlugin } from '@babel/parser'
1520
import {
1621
babelParserDefaultPlugins,
22+
camelize,
23+
capitalize,
1724
generateCodeFrame,
18-
hyphenate
25+
makeMap
1926
} from '@vue/shared'
2027
import {
2128
Node,
@@ -49,6 +56,7 @@ import {
4956
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
5057
import { warnExperimental, warnOnce } from './warn'
5158
import { rewriteDefault } from './rewriteDefault'
59+
import { createCache } from './cache'
5260

5361
// Special compiler macros
5462
const DEFINE_PROPS = 'defineProps'
@@ -61,6 +69,10 @@ const $COMPUTED = `$computed`
6169
const $FROM_REFS = `$fromRefs`
6270
const $RAW = `$raw`
6371

72+
const isBuiltInDir = makeMap(
73+
`once,memo,if,else,else-if,slot,text,html,on,bind,model,show,cloak,is`
74+
)
75+
6476
export interface SFCScriptCompileOptions {
6577
/**
6678
* Scope ID for prefixing injected CSS varialbes.
@@ -319,10 +331,10 @@ export function compileScript(
319331
}
320332

321333
let isUsedInTemplate = true
322-
if (sfc.template && !sfc.template.src) {
323-
isUsedInTemplate = new RegExp(
324-
`\\b(?:${local}|${hyphenate(local)})\\b`
325-
).test(sfc.template.content)
334+
if (isTS && sfc.template && !sfc.template.src) {
335+
isUsedInTemplate = new RegExp(`\\b${local}\\b`).test(
336+
resolveTemplateUsageCheckString(sfc)
337+
)
326338
}
327339

328340
userImports[local] = {
@@ -2154,3 +2166,53 @@ function toTextRange(node: Node): TextRange {
21542166
end: node.end!
21552167
}
21562168
}
2169+
2170+
const templateUsageCheckCache = createCache<string>()
2171+
2172+
function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
2173+
const { content, ast } = sfc.template!
2174+
const cached = templateUsageCheckCache.get(content)
2175+
if (cached) {
2176+
return cached
2177+
}
2178+
2179+
let code = ''
2180+
transform(createRoot([ast]), {
2181+
nodeTransforms: [
2182+
node => {
2183+
if (node.type === NodeTypes.ELEMENT) {
2184+
if (
2185+
!parserOptions.isNativeTag!(node.tag) &&
2186+
!parserOptions.isBuiltInComponent!(node.tag)
2187+
) {
2188+
code += `,${capitalize(camelize(node.tag))}`
2189+
}
2190+
for (let i = 0; i < node.props.length; i++) {
2191+
const prop = node.props[i]
2192+
if (prop.type === NodeTypes.DIRECTIVE) {
2193+
if (!isBuiltInDir(prop.name)) {
2194+
code += `,v${capitalize(camelize(prop.name))}`
2195+
}
2196+
if (prop.exp) {
2197+
code += `,${stripStrings(
2198+
(prop.exp as SimpleExpressionNode).content
2199+
)}`
2200+
}
2201+
}
2202+
}
2203+
} else if (node.type === NodeTypes.INTERPOLATION) {
2204+
code += `,${stripStrings(
2205+
(node.content as SimpleExpressionNode).content
2206+
)}`
2207+
}
2208+
}
2209+
]
2210+
})
2211+
2212+
templateUsageCheckCache.set(content, code)
2213+
return code
2214+
}
2215+
2216+
function stripStrings(exp: string) {
2217+
return exp.replace(/'[^']+'|"[^"]+"|`[^`]+`/g, '')
2218+
}

packages/compiler-sfc/src/parse.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { TemplateCompiler } from './compileTemplate'
1212
import { Statement } from '@babel/types'
1313
import { parseCssVars } from './cssVars'
1414
import { warnExperimental } from './warn'
15+
import { createCache } from './cache'
1516

1617
export interface SFCParseOptions {
1718
filename?: string
@@ -89,14 +90,7 @@ export interface SFCParseResult {
8990
errors: (CompilerError | SyntaxError)[]
9091
}
9192

92-
const SFC_CACHE_MAX_SIZE = 500
93-
const sourceToSFC =
94-
__GLOBAL__ || __ESM_BROWSER__
95-
? new Map<string, SFCParseResult>()
96-
: (new (require('lru-cache'))(SFC_CACHE_MAX_SIZE) as Map<
97-
string,
98-
SFCParseResult
99-
>)
93+
const sourceToSFC = createCache<SFCParseResult>()
10094

10195
export function parse(
10296
source: string,

0 commit comments

Comments
 (0)