Skip to content

Commit 6861d23

Browse files
committed
refactor(compiler-sfc): optimize import alias check for binding analysis
1 parent 8d1f526 commit 6861d23

File tree

3 files changed

+127
-124
lines changed

3 files changed

+127
-124
lines changed

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

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ return { a }
4444
`;
4545

4646
exports[`SFC compile <script setup> <script> after <script setup> the script content not end with \`\\n\` 1`] = `
47-
"const n = 1
48-
import { x } from './x'
49-
47+
"import { x } from './x'
48+
const n = 1
49+
5050
export default {
5151
setup(__props, { expose }) {
5252
expose();
@@ -78,11 +78,12 @@ return { n, x }
7878
`;
7979

8080
exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first 1`] = `
81-
"export const n = 1
82-
const __default__ = {}
81+
"import { x } from './x'
8382

84-
import { x } from './x'
83+
export const n = 1
84+
const __default__ = {}
8585

86+
8687
export default /*#__PURE__*/Object.assign(__default__, {
8788
setup(__props, { expose }) {
8889
expose();
@@ -97,13 +98,13 @@ return { n, x }
9798

9899
exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first, lang="ts", script block content export default 1`] = `
99100
"import { defineComponent as _defineComponent } from 'vue'
100-
101+
import { x } from './x'
102+
101103
const __default__ = {
102104
name: \\"test\\"
103105
}
104106

105-
import { x } from './x'
106-
107+
107108
export default /*#__PURE__*/_defineComponent({
108109
...__default__,
109110
setup(__props, { expose }) {
@@ -118,14 +119,15 @@ return { x }
118119
`;
119120

120121
exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first, named default export 1`] = `
121-
"export const n = 1
122+
"import { x } from './x'
123+
124+
export const n = 1
122125
const def = {}
123126

124127

125128
const __default__ = def
126129

127-
import { x } from './x'
128-
130+
129131
export default /*#__PURE__*/Object.assign(__default__, {
130132
setup(__props, { expose }) {
131133
expose();
@@ -1260,14 +1262,15 @@ return () => {}
12601262
`;
12611263

12621264
exports[`SFC compile <script setup> should expose top level declarations 1`] = `
1263-
"import { xx } from './x'
1265+
"import { x } from './x'
1266+
1267+
import { xx } from './x'
12641268
let aa = 1
12651269
const bb = 2
12661270
function cc() {}
12671271
class dd {}
12681272

1269-
import { x } from './x'
1270-
1273+
12711274
export default {
12721275
setup(__props, { expose }) {
12731276
expose();

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,8 +387,8 @@ defineExpose({ foo: 123 })
387387
expect(bindings).toStrictEqual({
388388
_reactive: BindingTypes.SETUP_MAYBE_REF,
389389
_ref: BindingTypes.SETUP_MAYBE_REF,
390-
foo: BindingTypes.SETUP_REF,
391-
bar: BindingTypes.SETUP_REACTIVE_CONST
390+
foo: BindingTypes.SETUP_MAYBE_REF,
391+
bar: BindingTypes.SETUP_MAYBE_REF
392392
})
393393
})
394394

packages/compiler-sfc/src/compileScript.ts

Lines changed: 107 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -898,10 +898,10 @@ export function compileScript(
898898
}
899899
}
900900

901-
// 1. process normal <script> first if it exists
902-
let scriptAst: Program | undefined
903-
if (script) {
904-
scriptAst = parse(
901+
// 0. parse both <script> and <script setup> blocks
902+
const scriptAst =
903+
script &&
904+
parse(
905905
script.content,
906906
{
907907
plugins,
@@ -910,7 +910,21 @@ export function compileScript(
910910
scriptStartOffset!
911911
)
912912

913-
// walk import declarations first
913+
const scriptSetupAst = parse(
914+
scriptSetup.content,
915+
{
916+
plugins: [
917+
...plugins,
918+
// allow top level await but only inside <script setup>
919+
'topLevelAwait'
920+
],
921+
sourceType: 'module'
922+
},
923+
startOffset
924+
)
925+
926+
// 1.1 walk import delcarations of <script>
927+
if (scriptAst) {
914928
for (const node of scriptAst.body) {
915929
if (node.type === 'ImportDeclaration') {
916930
// record imports for dedupe
@@ -932,7 +946,88 @@ export function compileScript(
932946
}
933947
}
934948
}
949+
}
950+
951+
// 1.2 walk import declarations of <script setup>
952+
for (const node of scriptSetupAst.body) {
953+
if (node.type === 'ImportDeclaration') {
954+
// import declarations are moved to top
955+
hoistNode(node)
956+
957+
// dedupe imports
958+
let removed = 0
959+
const removeSpecifier = (i: number) => {
960+
const removeLeft = i > removed
961+
removed++
962+
const current = node.specifiers[i]
963+
const next = node.specifiers[i + 1]
964+
s.remove(
965+
removeLeft
966+
? node.specifiers[i - 1].end! + startOffset
967+
: current.start! + startOffset,
968+
next && !removeLeft
969+
? next.start! + startOffset
970+
: current.end! + startOffset
971+
)
972+
}
973+
974+
for (let i = 0; i < node.specifiers.length; i++) {
975+
const specifier = node.specifiers[i]
976+
const local = specifier.local.name
977+
let imported =
978+
specifier.type === 'ImportSpecifier' &&
979+
specifier.imported.type === 'Identifier' &&
980+
specifier.imported.name
981+
if (specifier.type === 'ImportNamespaceSpecifier') {
982+
imported = '*'
983+
}
984+
const source = node.source.value
985+
const existing = userImports[local]
986+
if (
987+
source === 'vue' &&
988+
(imported === DEFINE_PROPS ||
989+
imported === DEFINE_EMITS ||
990+
imported === DEFINE_EXPOSE)
991+
) {
992+
warnOnce(
993+
`\`${imported}\` is a compiler macro and no longer needs to be imported.`
994+
)
995+
removeSpecifier(i)
996+
} else if (existing) {
997+
if (existing.source === source && existing.imported === imported) {
998+
// already imported in <script setup>, dedupe
999+
removeSpecifier(i)
1000+
} else {
1001+
error(`different imports aliased to same local name.`, specifier)
1002+
}
1003+
} else {
1004+
registerUserImport(
1005+
source,
1006+
local,
1007+
imported,
1008+
node.importKind === 'type' ||
1009+
(specifier.type === 'ImportSpecifier' &&
1010+
specifier.importKind === 'type'),
1011+
true,
1012+
!options.inlineTemplate
1013+
)
1014+
}
1015+
}
1016+
if (node.specifiers.length && removed === node.specifiers.length) {
1017+
s.remove(node.start! + startOffset, node.end! + startOffset)
1018+
}
1019+
}
1020+
}
1021+
1022+
// 1.3 resolve possible user import alias of `ref` and `reactive`
1023+
const vueImportAliases: Record<string, string> = {}
1024+
for (const key in userImports) {
1025+
const { source, imported, local } = userImports[key]
1026+
if (source === 'vue') vueImportAliases[imported] = local
1027+
}
9351028

1029+
// 2.1 process normal <script> body
1030+
if (script && scriptAst) {
9361031
for (const node of scriptAst.body) {
9371032
if (node.type === 'ExportDefaultDeclaration') {
9381033
// export default
@@ -1011,7 +1106,7 @@ export function compileScript(
10111106
}
10121107
}
10131108
if (node.declaration) {
1014-
walkDeclaration(node.declaration, scriptBindings, userImports)
1109+
walkDeclaration(node.declaration, scriptBindings, vueImportAliases)
10151110
}
10161111
} else if (
10171112
(node.type === 'VariableDeclaration' ||
@@ -1020,7 +1115,7 @@ export function compileScript(
10201115
node.type === 'TSEnumDeclaration') &&
10211116
!node.declare
10221117
) {
1023-
walkDeclaration(node, scriptBindings, userImports)
1118+
walkDeclaration(node, scriptBindings, vueImportAliases)
10241119
}
10251120
}
10261121

@@ -1049,94 +1144,8 @@ export function compileScript(
10491144
}
10501145
}
10511146

1052-
// 2. parse <script setup> and walk over top level statements
1053-
const scriptSetupAst = parse(
1054-
scriptSetup.content,
1055-
{
1056-
plugins: [
1057-
...plugins,
1058-
// allow top level await but only inside <script setup>
1059-
'topLevelAwait'
1060-
],
1061-
sourceType: 'module'
1062-
},
1063-
startOffset
1064-
)
1065-
1147+
// 2.2 process <script setup> body
10661148
for (const node of scriptSetupAst.body) {
1067-
if (node.type === 'ImportDeclaration') {
1068-
// import declarations are moved to top
1069-
hoistNode(node)
1070-
1071-
// dedupe imports
1072-
let removed = 0
1073-
const removeSpecifier = (i: number) => {
1074-
const removeLeft = i > removed
1075-
removed++
1076-
const current = node.specifiers[i]
1077-
const next = node.specifiers[i + 1]
1078-
s.remove(
1079-
removeLeft
1080-
? node.specifiers[i - 1].end! + startOffset
1081-
: current.start! + startOffset,
1082-
next && !removeLeft
1083-
? next.start! + startOffset
1084-
: current.end! + startOffset
1085-
)
1086-
}
1087-
1088-
for (let i = 0; i < node.specifiers.length; i++) {
1089-
const specifier = node.specifiers[i]
1090-
const local = specifier.local.name
1091-
let imported =
1092-
specifier.type === 'ImportSpecifier' &&
1093-
specifier.imported.type === 'Identifier' &&
1094-
specifier.imported.name
1095-
if (specifier.type === 'ImportNamespaceSpecifier') {
1096-
imported = '*'
1097-
}
1098-
const source = node.source.value
1099-
const existing = userImports[local]
1100-
if (
1101-
source === 'vue' &&
1102-
(imported === DEFINE_PROPS ||
1103-
imported === DEFINE_EMITS ||
1104-
imported === DEFINE_EXPOSE)
1105-
) {
1106-
warnOnce(
1107-
`\`${imported}\` is a compiler macro and no longer needs to be imported.`
1108-
)
1109-
removeSpecifier(i)
1110-
} else if (existing) {
1111-
if (existing.source === source && existing.imported === imported) {
1112-
// already imported in <script setup>, dedupe
1113-
removeSpecifier(i)
1114-
} else {
1115-
error(`different imports aliased to same local name.`, specifier)
1116-
}
1117-
} else {
1118-
registerUserImport(
1119-
source,
1120-
local,
1121-
imported,
1122-
node.importKind === 'type' ||
1123-
(specifier.type === 'ImportSpecifier' &&
1124-
specifier.importKind === 'type'),
1125-
true,
1126-
!options.inlineTemplate
1127-
)
1128-
}
1129-
}
1130-
if (node.specifiers.length && removed === node.specifiers.length) {
1131-
s.remove(node.start! + startOffset, node.end! + startOffset)
1132-
}
1133-
}
1134-
}
1135-
1136-
for (const node of scriptSetupAst.body) {
1137-
// already processed
1138-
if (node.type === 'ImportDeclaration') continue
1139-
11401149
// (Dropped) `ref: x` bindings
11411150
// TODO remove when out of experimental
11421151
if (
@@ -1210,7 +1219,7 @@ export function compileScript(
12101219
node.type === 'ClassDeclaration') &&
12111220
!node.declare
12121221
) {
1213-
walkDeclaration(node, setupBindings, userImports)
1222+
walkDeclaration(node, setupBindings, vueImportAliases)
12141223
}
12151224

12161225
// walk statements & named exports / variable declarations for top level
@@ -1665,17 +1674,8 @@ function registerBinding(
16651674
function walkDeclaration(
16661675
node: Declaration,
16671676
bindings: Record<string, BindingTypes>,
1668-
userImports: Record<string, ImportBinding>
1677+
userImportAliases: Record<string, string>
16691678
) {
1670-
function getUserBinding(name: string) {
1671-
const binding = Object.values(userImports).find(
1672-
binding => binding.source === 'vue' && binding.imported === name
1673-
)
1674-
if (binding) return binding.local
1675-
else if (!userImports[name]) return name
1676-
return undefined
1677-
}
1678-
16791679
if (node.type === 'VariableDeclaration') {
16801680
const isConst = node.kind === 'const'
16811681
// export const foo = ...
@@ -1689,7 +1689,7 @@ function walkDeclaration(
16891689
)
16901690
if (id.type === 'Identifier') {
16911691
let bindingType
1692-
const userReactiveBinding = getUserBinding('reactive')
1692+
const userReactiveBinding = userImportAliases['reactive']
16931693
if (isCallOf(init, userReactiveBinding)) {
16941694
// treat reactive() calls as let since it's meant to be mutable
16951695
bindingType = isConst
@@ -1705,7 +1705,7 @@ function walkDeclaration(
17051705
? BindingTypes.SETUP_REACTIVE_CONST
17061706
: BindingTypes.SETUP_CONST
17071707
} else if (isConst) {
1708-
if (isCallOf(init, getUserBinding('ref'))) {
1708+
if (isCallOf(init, userImportAliases['ref'])) {
17091709
bindingType = BindingTypes.SETUP_REF
17101710
} else {
17111711
bindingType = BindingTypes.SETUP_MAYBE_REF

0 commit comments

Comments
 (0)