From eb9c5b294c8279bcda5d0f473cf2364b4b5108d6 Mon Sep 17 00:00:00 2001 From: ZHAO Jinxiang Date: Mon, 21 Feb 2022 16:48:25 +0800 Subject: [PATCH] feat: use @babel/traverse to get identifiers --- src/core/babel.ts | 2 + src/core/identifiers.ts | 110 ++++++++++++++++-------------- src/core/parseSFC.ts | 8 +-- src/core/transformScriptSetup.ts | 113 +++++++++++++++++-------------- test/identifiers.test.ts | 2 +- 5 files changed, 124 insertions(+), 111 deletions(-) diff --git a/src/core/babel.ts b/src/core/babel.ts index 0de545f..60009bc 100644 --- a/src/core/babel.ts +++ b/src/core/babel.ts @@ -1,7 +1,9 @@ import * as babel from '@babel/core' import { parse, parseExpression } from '@babel/parser' import g from '@babel/generator' +import * as babel_traverse from '@babel/traverse' export const t: typeof babel['types'] = ((babel as any).default || babel).types export const generate: typeof g = ((g as any).default || g) +export const traverse = ((babel_traverse as any)?.default?.default as null) ?? babel_traverse?.default ?? babel_traverse export { parseExpression, parse } diff --git a/src/core/identifiers.ts b/src/core/identifiers.ts index b5b23fa..33db0e5 100644 --- a/src/core/identifiers.ts +++ b/src/core/identifiers.ts @@ -1,59 +1,41 @@ -import type { Expression, Node, PrivateName, SpreadElement, Statement, TSType } from '@babel/types' +import type { + Expression, + File, + PrivateName, + SpreadElement, + Statement, + TSType, +} from '@babel/types' +import type { ParseResult } from '@babel/parser' +import { t, traverse } from './babel' -export function getIdentifierDeclarations(nodes: Statement[], identifiers = new Set()) { - for (let node of nodes) { - if (node.type === 'ExportNamedDeclaration') { - node = node.declaration! - if (!node) - continue - } - if (node.type === 'ImportDeclaration') { - for (const specifier of node.specifiers) - identifiers.add(specifier.local.name) - } - else if (node.type === 'VariableDeclaration') { - function handleVariableId(node: Node) { - if (node.type === 'Identifier') { - identifiers.add(node.name) - } - else if (node.type === 'ObjectPattern') { - for (const property of node.properties) { - if (property.type === 'ObjectProperty') - handleVariableId(property.value) - else if (property.type === 'RestElement' && property.argument.type === 'Identifier') - identifiers.add(property.argument.name) - } - } - else if (node.type === 'ArrayPattern') { - for (const element of node.elements) { - if (element?.type === 'Identifier') - identifiers.add(element.name) - else if (element?.type === 'RestElement' && element.argument.type === 'Identifier') - identifiers.add(element.argument.name) - else if (element?.type === 'ObjectPattern' || element?.type === 'ArrayPattern') - handleVariableId(element) - } - } +export function getIdentifierDeclarations(nodes: Statement[]) { + let result!: Set + let programScopeUid: number + traverse(t.file(t.program(nodes)), { + Program(path) { + result = new Set(Object.keys(path.scope.bindings)) + programScopeUid = (path.scope as any).uid + }, + // FIXME: babel bug, temporary add TSEnumDeclaration and TSModuleDeclaration logic + TSEnumDeclaration(path) { + if ((path.scope as any).uid === programScopeUid) + result.add(path.node.id.name) + }, + TSModuleDeclaration(path) { + if ((path.scope as any).uid === programScopeUid) { + const id = path.node.id + if (id.type === 'Identifier') + result.add(id.name) } - - for (const declarator of node.declarations) - handleVariableId(declarator.id) - } - else if (node.type === 'FunctionDeclaration' || node.type === 'ClassDeclaration') { - if (node.id) - identifiers.add(node.id.name) - } - else if (node.type === 'TSEnumDeclaration') { - if (node.id) - identifiers.add(node.id.name) - } - // else { - // console.log(node) - // } - } - return identifiers + }, + }) + return Array.from(result) } +/** + * @deprecated use `getFileGlobals` instead + */ export function getIdentifierUsages(node?: Expression | TSType | SpreadElement | PrivateName | Statement | null, identifiers = new Set()) { if (!node) return identifiers @@ -124,3 +106,27 @@ export function getIdentifierUsages(node?: Expression | TSType | SpreadElement | // } return identifiers } + +export function getFileGlobals(result: ParseResult) { + let globals!: Set + let programScopeUid: number + traverse(result, { + Program(path) { + globals = new Set(Object.keys((path.scope as any).globals)) + programScopeUid = (path.scope as any).uid + }, + // FIXME: babel bug, temporary add TSEnumDeclaration and TSModuleDeclaration logic + TSEnumDeclaration(path) { + if ((path.scope as any).uid === programScopeUid) + globals.delete(path.node.id.name) + }, + TSModuleDeclaration(path) { + if ((path.scope as any).uid === programScopeUid) { + const id = path.node.id + if (id.type === 'Identifier') + globals.delete(id.name) + } + }, + }) + return Array.from(globals) +} diff --git a/src/core/parseSFC.ts b/src/core/parseSFC.ts index 6bad635..8d931e5 100644 --- a/src/core/parseSFC.ts +++ b/src/core/parseSFC.ts @@ -19,7 +19,7 @@ import type { ScriptSetupTransformOptions, ScriptTagMeta, } from '../types' -import { getIdentifierUsages } from './identifiers' +import { getFileGlobals } from './identifiers' import { parse } from './babel' import { exhaustiveCheckReturnUndefined, pascalize } from './utils' @@ -137,12 +137,8 @@ function getDirectiveNames(node: TemplateChildNode): string[] { } function getFreeVariablesForText(input: string): string[] { - const identifiers = new Set() const inputWithPrefix = input.trimStart()[0] === '{' ? `(${input})` : input - - const nodes = parse(inputWithPrefix).program.body - nodes.forEach(node => getIdentifierUsages(node, identifiers)) - return [...identifiers.values()] + return getFileGlobals(parse(inputWithPrefix)) } function getFreeVariablesForPropsNode( diff --git a/src/core/transformScriptSetup.ts b/src/core/transformScriptSetup.ts index dce7205..bc8e4b5 100644 --- a/src/core/transformScriptSetup.ts +++ b/src/core/transformScriptSetup.ts @@ -1,23 +1,31 @@ import { capitalize } from '@vue/shared' import type { Node, ObjectExpression, Statement } from '@babel/types' -import { partition } from '@antfu/utils' +import { notNullish, partition, uniq } from '@antfu/utils' import type { ParsedSFC, ScriptSetupTransformOptions } from '../types' import { applyMacros } from './macros' import { getIdentifierDeclarations } from './identifiers' import { generate, t } from './babel' -import { isNotNil, pascalize } from './utils' +import { pascalize } from './utils' -function isAsyncImport(node: any) { - if (node.type === 'VariableDeclaration') { +function isAsyncImport(node: Statement) { + if (t.isVariableDeclaration(node)) { const declaration = node.declarations[0] - return declaration?.init?.callee?.name === 'defineAsyncComponent' + return ( + declaration !== undefined + && t.isCallExpression(declaration.init) + && t.isIdentifier(declaration.init.callee) + && declaration.init.callee.name === 'defineAsyncComponent' + ) } return false } -export function transformScriptSetup(sfc: ParsedSFC, options?: ScriptSetupTransformOptions) { +export function transformScriptSetup( + sfc: ParsedSFC, + options?: ScriptSetupTransformOptions, +) { const { scriptSetup, script, template } = sfc const { nodes: body, props, expose } = applyMacros(scriptSetup.ast.body) @@ -26,16 +34,17 @@ export function transformScriptSetup(sfc: ParsedSFC, options?: ScriptSetupTransf body, n => isAsyncImport(n) - || n.type === 'ImportDeclaration' - || n.type === 'ExportNamedDeclaration' - || n.type.startsWith('TS'), + || t.isImportDeclaration(n) + || t.isExportNamedDeclaration(n) + || n.type.startsWith('TS'), ) // get all identifiers in `