refactor(compiler-sfc): optimize import alias check for binding analysis

This commit is contained in:
Evan You 2022-11-08 17:47:47 +08:00
parent 8d1f526174
commit 6861d2380b
3 changed files with 127 additions and 124 deletions

View File

@ -44,8 +44,8 @@ return { a }
`; `;
exports[`SFC compile <script setup> <script> after <script setup> the script content not end with \`\\n\` 1`] = ` exports[`SFC compile <script setup> <script> after <script setup> the script content not end with \`\\n\` 1`] = `
"const n = 1 "import { x } from './x'
import { x } from './x' const n = 1
export default { export default {
setup(__props, { expose }) { setup(__props, { expose }) {
@ -78,10 +78,11 @@ return { n, x }
`; `;
exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first 1`] = ` exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first 1`] = `
"export const n = 1 "import { x } from './x'
export const n = 1
const __default__ = {} const __default__ = {}
import { x } from './x'
export default /*#__PURE__*/Object.assign(__default__, { export default /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose }) { setup(__props, { expose }) {
@ -97,12 +98,12 @@ return { n, x }
exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first, lang="ts", script block content export default 1`] = ` exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first, lang="ts", script block content export default 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { x } from './x'
const __default__ = { const __default__ = {
name: \\"test\\" name: \\"test\\"
} }
import { x } from './x'
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
...__default__, ...__default__,
@ -118,13 +119,14 @@ return { x }
`; `;
exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first, named default export 1`] = ` exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first, named default export 1`] = `
"export const n = 1 "import { x } from './x'
export const n = 1
const def = {} const def = {}
const __default__ = def const __default__ = def
import { x } from './x'
export default /*#__PURE__*/Object.assign(__default__, { export default /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose }) { setup(__props, { expose }) {
@ -1260,13 +1262,14 @@ return () => {}
`; `;
exports[`SFC compile <script setup> should expose top level declarations 1`] = ` exports[`SFC compile <script setup> should expose top level declarations 1`] = `
"import { xx } from './x' "import { x } from './x'
import { xx } from './x'
let aa = 1 let aa = 1
const bb = 2 const bb = 2
function cc() {} function cc() {}
class dd {} class dd {}
import { x } from './x'
export default { export default {
setup(__props, { expose }) { setup(__props, { expose }) {

View File

@ -387,8 +387,8 @@ defineExpose({ foo: 123 })
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
_reactive: BindingTypes.SETUP_MAYBE_REF, _reactive: BindingTypes.SETUP_MAYBE_REF,
_ref: BindingTypes.SETUP_MAYBE_REF, _ref: BindingTypes.SETUP_MAYBE_REF,
foo: BindingTypes.SETUP_REF, foo: BindingTypes.SETUP_MAYBE_REF,
bar: BindingTypes.SETUP_REACTIVE_CONST bar: BindingTypes.SETUP_MAYBE_REF
}) })
}) })

View File

@ -898,10 +898,10 @@ export function compileScript(
} }
} }
// 1. process normal <script> first if it exists // 0. parse both <script> and <script setup> blocks
let scriptAst: Program | undefined const scriptAst =
if (script) { script &&
scriptAst = parse( parse(
script.content, script.content,
{ {
plugins, plugins,
@ -910,7 +910,21 @@ export function compileScript(
scriptStartOffset! scriptStartOffset!
) )
// walk import declarations first const scriptSetupAst = parse(
scriptSetup.content,
{
plugins: [
...plugins,
// allow top level await but only inside <script setup>
'topLevelAwait'
],
sourceType: 'module'
},
startOffset
)
// 1.1 walk import delcarations of <script>
if (scriptAst) {
for (const node of scriptAst.body) { for (const node of scriptAst.body) {
if (node.type === 'ImportDeclaration') { if (node.type === 'ImportDeclaration') {
// record imports for dedupe // record imports for dedupe
@ -932,7 +946,88 @@ export function compileScript(
} }
} }
} }
}
// 1.2 walk import declarations of <script setup>
for (const node of scriptSetupAst.body) {
if (node.type === 'ImportDeclaration') {
// import declarations are moved to top
hoistNode(node)
// dedupe imports
let removed = 0
const removeSpecifier = (i: number) => {
const removeLeft = i > removed
removed++
const current = node.specifiers[i]
const next = node.specifiers[i + 1]
s.remove(
removeLeft
? node.specifiers[i - 1].end! + startOffset
: current.start! + startOffset,
next && !removeLeft
? next.start! + startOffset
: current.end! + startOffset
)
}
for (let i = 0; i < node.specifiers.length; i++) {
const specifier = node.specifiers[i]
const local = specifier.local.name
let imported =
specifier.type === 'ImportSpecifier' &&
specifier.imported.type === 'Identifier' &&
specifier.imported.name
if (specifier.type === 'ImportNamespaceSpecifier') {
imported = '*'
}
const source = node.source.value
const existing = userImports[local]
if (
source === 'vue' &&
(imported === DEFINE_PROPS ||
imported === DEFINE_EMITS ||
imported === DEFINE_EXPOSE)
) {
warnOnce(
`\`${imported}\` is a compiler macro and no longer needs to be imported.`
)
removeSpecifier(i)
} else if (existing) {
if (existing.source === source && existing.imported === imported) {
// already imported in <script setup>, dedupe
removeSpecifier(i)
} else {
error(`different imports aliased to same local name.`, specifier)
}
} else {
registerUserImport(
source,
local,
imported,
node.importKind === 'type' ||
(specifier.type === 'ImportSpecifier' &&
specifier.importKind === 'type'),
true,
!options.inlineTemplate
)
}
}
if (node.specifiers.length && removed === node.specifiers.length) {
s.remove(node.start! + startOffset, node.end! + startOffset)
}
}
}
// 1.3 resolve possible user import alias of `ref` and `reactive`
const vueImportAliases: Record<string, string> = {}
for (const key in userImports) {
const { source, imported, local } = userImports[key]
if (source === 'vue') vueImportAliases[imported] = local
}
// 2.1 process normal <script> body
if (script && scriptAst) {
for (const node of scriptAst.body) { for (const node of scriptAst.body) {
if (node.type === 'ExportDefaultDeclaration') { if (node.type === 'ExportDefaultDeclaration') {
// export default // export default
@ -1011,7 +1106,7 @@ export function compileScript(
} }
} }
if (node.declaration) { if (node.declaration) {
walkDeclaration(node.declaration, scriptBindings, userImports) walkDeclaration(node.declaration, scriptBindings, vueImportAliases)
} }
} else if ( } else if (
(node.type === 'VariableDeclaration' || (node.type === 'VariableDeclaration' ||
@ -1020,7 +1115,7 @@ export function compileScript(
node.type === 'TSEnumDeclaration') && node.type === 'TSEnumDeclaration') &&
!node.declare !node.declare
) { ) {
walkDeclaration(node, scriptBindings, userImports) walkDeclaration(node, scriptBindings, vueImportAliases)
} }
} }
@ -1049,94 +1144,8 @@ export function compileScript(
} }
} }
// 2. parse <script setup> and walk over top level statements // 2.2 process <script setup> body
const scriptSetupAst = parse(
scriptSetup.content,
{
plugins: [
...plugins,
// allow top level await but only inside <script setup>
'topLevelAwait'
],
sourceType: 'module'
},
startOffset
)
for (const node of scriptSetupAst.body) { for (const node of scriptSetupAst.body) {
if (node.type === 'ImportDeclaration') {
// import declarations are moved to top
hoistNode(node)
// dedupe imports
let removed = 0
const removeSpecifier = (i: number) => {
const removeLeft = i > removed
removed++
const current = node.specifiers[i]
const next = node.specifiers[i + 1]
s.remove(
removeLeft
? node.specifiers[i - 1].end! + startOffset
: current.start! + startOffset,
next && !removeLeft
? next.start! + startOffset
: current.end! + startOffset
)
}
for (let i = 0; i < node.specifiers.length; i++) {
const specifier = node.specifiers[i]
const local = specifier.local.name
let imported =
specifier.type === 'ImportSpecifier' &&
specifier.imported.type === 'Identifier' &&
specifier.imported.name
if (specifier.type === 'ImportNamespaceSpecifier') {
imported = '*'
}
const source = node.source.value
const existing = userImports[local]
if (
source === 'vue' &&
(imported === DEFINE_PROPS ||
imported === DEFINE_EMITS ||
imported === DEFINE_EXPOSE)
) {
warnOnce(
`\`${imported}\` is a compiler macro and no longer needs to be imported.`
)
removeSpecifier(i)
} else if (existing) {
if (existing.source === source && existing.imported === imported) {
// already imported in <script setup>, dedupe
removeSpecifier(i)
} else {
error(`different imports aliased to same local name.`, specifier)
}
} else {
registerUserImport(
source,
local,
imported,
node.importKind === 'type' ||
(specifier.type === 'ImportSpecifier' &&
specifier.importKind === 'type'),
true,
!options.inlineTemplate
)
}
}
if (node.specifiers.length && removed === node.specifiers.length) {
s.remove(node.start! + startOffset, node.end! + startOffset)
}
}
}
for (const node of scriptSetupAst.body) {
// already processed
if (node.type === 'ImportDeclaration') continue
// (Dropped) `ref: x` bindings // (Dropped) `ref: x` bindings
// TODO remove when out of experimental // TODO remove when out of experimental
if ( if (
@ -1210,7 +1219,7 @@ export function compileScript(
node.type === 'ClassDeclaration') && node.type === 'ClassDeclaration') &&
!node.declare !node.declare
) { ) {
walkDeclaration(node, setupBindings, userImports) walkDeclaration(node, setupBindings, vueImportAliases)
} }
// walk statements & named exports / variable declarations for top level // walk statements & named exports / variable declarations for top level
@ -1665,17 +1674,8 @@ function registerBinding(
function walkDeclaration( function walkDeclaration(
node: Declaration, node: Declaration,
bindings: Record<string, BindingTypes>, bindings: Record<string, BindingTypes>,
userImports: Record<string, ImportBinding> userImportAliases: Record<string, string>
) { ) {
function getUserBinding(name: string) {
const binding = Object.values(userImports).find(
binding => binding.source === 'vue' && binding.imported === name
)
if (binding) return binding.local
else if (!userImports[name]) return name
return undefined
}
if (node.type === 'VariableDeclaration') { if (node.type === 'VariableDeclaration') {
const isConst = node.kind === 'const' const isConst = node.kind === 'const'
// export const foo = ... // export const foo = ...
@ -1689,7 +1689,7 @@ function walkDeclaration(
) )
if (id.type === 'Identifier') { if (id.type === 'Identifier') {
let bindingType let bindingType
const userReactiveBinding = getUserBinding('reactive') const userReactiveBinding = userImportAliases['reactive']
if (isCallOf(init, userReactiveBinding)) { if (isCallOf(init, userReactiveBinding)) {
// treat reactive() calls as let since it's meant to be mutable // treat reactive() calls as let since it's meant to be mutable
bindingType = isConst bindingType = isConst
@ -1705,7 +1705,7 @@ function walkDeclaration(
? BindingTypes.SETUP_REACTIVE_CONST ? BindingTypes.SETUP_REACTIVE_CONST
: BindingTypes.SETUP_CONST : BindingTypes.SETUP_CONST
} else if (isConst) { } else if (isConst) {
if (isCallOf(init, getUserBinding('ref'))) { if (isCallOf(init, userImportAliases['ref'])) {
bindingType = BindingTypes.SETUP_REF bindingType = BindingTypes.SETUP_REF
} else { } else {
bindingType = BindingTypes.SETUP_MAYBE_REF bindingType = BindingTypes.SETUP_MAYBE_REF