From 8aa4ea81d6e4d3110aa1619cca594543da4c9b63 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 13 Apr 2023 20:49:16 +0800 Subject: [PATCH] feat(compiler-sfc): support relative imported types in macros --- packages/compiler-core/src/babelUtils.ts | 16 +- .../compileScript/resolveType.spec.ts | 103 ++++- packages/compiler-sfc/src/compileScript.ts | 17 +- packages/compiler-sfc/src/script/context.ts | 81 ++-- .../compiler-sfc/src/script/resolveType.ts | 375 ++++++++++++++---- packages/compiler-sfc/src/script/utils.ts | 32 +- 6 files changed, 472 insertions(+), 152 deletions(-) diff --git a/packages/compiler-core/src/babelUtils.ts b/packages/compiler-core/src/babelUtils.ts index 7d96ec51928..4b8f4182d2c 100644 --- a/packages/compiler-core/src/babelUtils.ts +++ b/packages/compiler-core/src/babelUtils.ts @@ -6,10 +6,7 @@ import type { Function, ObjectProperty, BlockStatement, - Program, - ImportDefaultSpecifier, - ImportNamespaceSpecifier, - ImportSpecifier + Program } from '@babel/types' import { walk } from 'estree-walker' @@ -246,17 +243,6 @@ export const isStaticProperty = (node: Node): node is ObjectProperty => export const isStaticPropertyKey = (node: Node, parent: Node) => isStaticProperty(parent) && parent.key === node -export function getImportedName( - specifier: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier -) { - if (specifier.type === 'ImportSpecifier') - return specifier.imported.type === 'Identifier' - ? specifier.imported.name - : specifier.imported.value - else if (specifier.type === 'ImportNamespaceSpecifier') return '*' - return 'default' -} - /** * Copied from https://github.com/babel/babel/blob/main/packages/babel-types/src/validators/isReferenced.ts * To avoid runtime dependency on @babel/types (which includes process references) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 38bcdf988ee..7f25ae4888d 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -3,6 +3,7 @@ import { parse } from '../../src' import { ScriptCompileContext } from '../../src/script/context' import { inferRuntimeType, + recordImports, resolveTypeElements } from '../../src/script/resolveType' @@ -246,6 +247,85 @@ describe('resolveType', () => { }) }) + describe('external type imports', () => { + test('relative ts', () => { + expect( + resolve( + ` + import { P } from './foo' + import { Y as PP } from './bar' + type Target = P & PP + `, + { + 'foo.ts': 'export type P = { foo: number }', + 'bar.d.ts': 'type X = { bar: string }; export { X as Y }' + } + ).props + ).toStrictEqual({ + foo: ['Number'], + bar: ['String'] + }) + }) + + test('relative vue', () => { + expect( + resolve( + ` + import { P } from './foo.vue' + import { P as PP } from './bar.vue' + type Target = P & PP + `, + { + 'foo.vue': + '', + 'bar.vue': + '' + } + ).props + ).toStrictEqual({ + foo: ['Number'], + bar: ['String'] + }) + }) + + test('relative (chained)', () => { + expect( + resolve( + ` + import { P } from './foo' + type Target = P + `, + { + 'foo.ts': `import type { P as PP } from './nested/bar.vue' + export type P = { foo: number } & PP`, + 'nested/bar.vue': + '' + } + ).props + ).toStrictEqual({ + foo: ['Number'], + bar: ['String'] + }) + }) + + test('relative (chained, re-export)', () => { + expect( + resolve( + ` + import { PP as P } from './foo' + type Target = P + `, + { + 'foo.ts': `export { P as PP } from './bar'`, + 'bar.ts': 'export type P = { bar: string }' + } + ).props + ).toStrictEqual({ + bar: ['String'] + }) + }) + }) + describe('errors', () => { test('error on computed keys', () => { expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow( @@ -255,9 +335,26 @@ describe('resolveType', () => { }) }) -function resolve(code: string) { - const { descriptor } = parse(``) - const ctx = new ScriptCompileContext(descriptor, { id: 'test' }) +function resolve(code: string, files: Record = {}) { + const { descriptor } = parse(``, { + filename: 'Test.vue' + }) + const ctx = new ScriptCompileContext(descriptor, { + id: 'test', + fs: { + fileExists(file) { + return !!files[file] + }, + readFile(file) { + return files[file] + } + } + }) + + // ctx.userImports is collected when calling compileScript(), but we are + // skipping that here, so need to manually register imports + ctx.userImports = recordImports(ctx.scriptSetupAst!.body) as any + const targetDecl = ctx.scriptSetupAst!.body.find( s => s.type === 'TSTypeAliasDeclaration' && s.id.name === 'Target' ) as TSTypeAliasDeclaration diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index c828d77f6d5..593e8e072c6 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -2,8 +2,7 @@ import { BindingTypes, UNREF, isFunctionType, - walkIdentifiers, - getImportedName + walkIdentifiers } from '@vue/compiler-dom' import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse' import { parse as _parse, ParserPlugin } from '@babel/parser' @@ -45,7 +44,12 @@ import { DEFINE_EXPOSE, processDefineExpose } from './script/defineExpose' import { DEFINE_OPTIONS, processDefineOptions } from './script/defineOptions' import { processDefineSlots } from './script/defineSlots' import { DEFINE_MODEL, processDefineModel } from './script/defineModel' -import { isLiteralNode, unwrapTSNode, isCallOf } from './script/utils' +import { + isLiteralNode, + unwrapTSNode, + isCallOf, + getImportedName +} from './script/utils' import { analyzeScriptBindings } from './script/analyzeScriptBindings' import { isImportUsed } from './script/importUsageCheck' import { processAwait } from './script/topLevelAwait' @@ -106,6 +110,13 @@ export interface SFCScriptCompileOptions { * (**Experimental**) Enable macro `defineModel` */ defineModel?: boolean + /** + * + */ + fs?: { + fileExists(file: string): boolean + readFile(file: string): string + } } export interface ImportBinding { diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts index 718f23da5ca..1928ea900fc 100644 --- a/packages/compiler-sfc/src/script/context.ts +++ b/packages/compiler-sfc/src/script/context.ts @@ -1,13 +1,13 @@ import { Node, ObjectPattern, Program } from '@babel/types' import { SFCDescriptor } from '../parse' import { generateCodeFrame } from '@vue/shared' -import { parse as babelParse, ParserOptions, ParserPlugin } from '@babel/parser' +import { parse as babelParse, ParserPlugin } from '@babel/parser' import { ImportBinding, SFCScriptCompileOptions } from '../compileScript' import { PropsDestructureBindings } from './defineProps' import { ModelDecl } from './defineModel' import { BindingMetadata } from '../../../compiler-core/src' import MagicString from 'magic-string' -import { TypeScope } from './resolveType' +import { TypeScope, WithScope } from './resolveType' export class ScriptCompileContext { isJS: boolean @@ -83,31 +83,17 @@ export class ScriptCompileContext { scriptSetupLang === 'tsx' // resolve parser plugins - const plugins: ParserPlugin[] = [] - if (!this.isTS || scriptLang === 'tsx' || scriptSetupLang === 'tsx') { - plugins.push('jsx') - } else { - // If don't match the case of adding jsx, should remove the jsx from the babelParserPlugins - if (options.babelParserPlugins) - options.babelParserPlugins = options.babelParserPlugins.filter( - n => n !== 'jsx' - ) - } - if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins) - if (this.isTS) { - plugins.push('typescript') - if (!plugins.includes('decorators')) { - plugins.push('decorators-legacy') - } - } + const plugins: ParserPlugin[] = resolveParserPlugins( + (scriptLang || scriptSetupLang)!, + options.babelParserPlugins + ) - function parse( - input: string, - options: ParserOptions, - offset: number - ): Program { + function parse(input: string, offset: number): Program { try { - return babelParse(input, options).program + return babelParse(input, { + plugins, + sourceType: 'module' + }).program } catch (e: any) { e.message = `[@vue/compiler-sfc] ${e.message}\n\n${ descriptor.filename @@ -124,23 +110,12 @@ export class ScriptCompileContext { this.descriptor.script && parse( this.descriptor.script.content, - { - plugins, - sourceType: 'module' - }, this.descriptor.script.loc.start.offset ) this.scriptSetupAst = this.descriptor.scriptSetup && - parse( - this.descriptor.scriptSetup!.content, - { - plugins: [...plugins, 'topLevelAwait'], - sourceType: 'module' - }, - this.startOffset! - ) + parse(this.descriptor.scriptSetup!.content, this.startOffset!) } getString(node: Node, scriptSetup = true): string { @@ -150,19 +125,39 @@ export class ScriptCompileContext { return block.content.slice(node.start!, node.end!) } - error( - msg: string, - node: Node, - end: number = node.end! + this.startOffset! - ): never { + error(msg: string, node: Node & WithScope, scope?: TypeScope): never { throw new Error( `[@vue/compiler-sfc] ${msg}\n\n${ this.descriptor.filename }\n${generateCodeFrame( this.descriptor.source, node.start! + this.startOffset!, - end + node.end! + this.startOffset! )}` ) } } + +export function resolveParserPlugins( + lang: string, + userPlugins?: ParserPlugin[] +) { + const plugins: ParserPlugin[] = [] + if (lang === 'jsx' || lang === 'tsx') { + plugins.push('jsx') + } else if (userPlugins) { + // If don't match the case of adding jsx + // should remove the jsx from user options + userPlugins = userPlugins.filter(p => p !== 'jsx') + } + if (lang === 'ts' || lang === 'tsx') { + plugins.push('typescript') + if (!plugins.includes('decorators')) { + plugins.push('decorators-legacy') + } + } + if (userPlugins) { + plugins.push(...userPlugins) + } + return plugins +} diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index f3b20a7ee15..1e82d2c8326 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -1,11 +1,13 @@ import { + Expression, Identifier, - Node as _Node, + Node, Statement, TSCallSignatureDeclaration, TSEnumDeclaration, TSExpressionWithTypeArguments, TSFunctionType, + TSInterfaceDeclaration, TSMappedType, TSMethodSignature, TSModuleBlock, @@ -18,81 +20,108 @@ import { TSTypeReference, TemplateLiteral } from '@babel/types' -import { UNKNOWN_TYPE } from './utils' -import { ScriptCompileContext } from './context' -import { ImportBinding } from '../compileScript' -import { TSInterfaceDeclaration } from '@babel/types' +import { UNKNOWN_TYPE, getId, getImportedName } from './utils' +import { ScriptCompileContext, resolveParserPlugins } from './context' +import { ImportBinding, SFCScriptCompileOptions } from '../compileScript' import { capitalize, hasOwn } from '@vue/shared' -import { Expression } from '@babel/types' +import path from 'path' +import { parse as babelParse } from '@babel/parser' +import { parse } from '../parse' + +type Import = Pick export interface TypeScope { filename: string - imports: Record - types: Record - parent?: TypeScope + source: string + imports: Record + types: Record< + string, + Node & { + // scope types always has ownerScope attached + _ownerScope: TypeScope + } + > + exportedTypes: Record< + string, + Node & { + // scope types always has ownerScope attached + _ownerScope: TypeScope + } + > } -interface WithScope { +export interface WithScope { _ownerScope?: TypeScope } interface ResolvedElements { - props: Record + props: Record< + string, + (TSPropertySignature | TSMethodSignature) & { + // resolved props always has ownerScope attached + _ownerScope: TypeScope + } + > calls?: (TSCallSignatureDeclaration | TSFunctionType)[] } -type Node = _Node & - WithScope & { - _resolvedElements?: ResolvedElements - } - /** * Resolve arbitrary type node to a list of type elements that can be then * mapped to runtime props or emits. */ export function resolveTypeElements( ctx: ScriptCompileContext, - node: Node + node: Node & WithScope & { _resolvedElements?: ResolvedElements }, + scope?: TypeScope ): ResolvedElements { if (node._resolvedElements) { return node._resolvedElements } - return (node._resolvedElements = innerResolveTypeElements(ctx, node)) + return (node._resolvedElements = innerResolveTypeElements( + ctx, + node, + node._ownerScope || scope || ctxToScope(ctx) + )) } function innerResolveTypeElements( ctx: ScriptCompileContext, - node: Node + node: Node, + scope: TypeScope ): ResolvedElements { switch (node.type) { case 'TSTypeLiteral': - return typeElementsToMap(ctx, node.members, node._ownerScope) + return typeElementsToMap(ctx, node.members, scope) case 'TSInterfaceDeclaration': - return resolveInterfaceMembers(ctx, node) + return resolveInterfaceMembers(ctx, node, scope) case 'TSTypeAliasDeclaration': case 'TSParenthesizedType': - return resolveTypeElements(ctx, node.typeAnnotation) + return resolveTypeElements(ctx, node.typeAnnotation, scope) case 'TSFunctionType': { return { props: {}, calls: [node] } } case 'TSUnionType': case 'TSIntersectionType': return mergeElements( - node.types.map(t => resolveTypeElements(ctx, t)), + node.types.map(t => resolveTypeElements(ctx, t, scope)), node.type ) case 'TSMappedType': - return resolveMappedType(ctx, node) + return resolveMappedType(ctx, node, scope) case 'TSIndexedAccessType': { if ( node.indexType.type === 'TSLiteralType' && node.indexType.literal.type === 'StringLiteral' ) { - const resolved = resolveTypeElements(ctx, node.objectType) + const resolved = resolveTypeElements(ctx, node.objectType, scope) const key = node.indexType.literal.value const targetType = resolved.props[key].typeAnnotation if (targetType) { - return resolveTypeElements(ctx, targetType.typeAnnotation) + return resolveTypeElements( + ctx, + targetType.typeAnnotation, + resolved.props[key]._ownerScope + ) } else { break } @@ -105,9 +134,9 @@ function innerResolveTypeElements( } case 'TSExpressionWithTypeArguments': // referenced by interface extends case 'TSTypeReference': { - const resolved = resolveTypeReference(ctx, node) + const resolved = resolveTypeReference(ctx, node, scope) if (resolved) { - return resolveTypeElements(ctx, resolved) + return resolveTypeElements(ctx, resolved, resolved._ownerScope) } else { const typeName = getReferenceName(node) if ( @@ -118,7 +147,7 @@ function innerResolveTypeElements( return resolveBuiltin(ctx, node, typeName as any) } ctx.error( - `Failed to resolved type reference, or unsupported built-in utlility type.`, + `Failed to resolve type reference, or unsupported built-in utlility type.`, node ) } @@ -135,18 +164,13 @@ function typeElementsToMap( const res: ResolvedElements = { props: {} } for (const e of elements) { if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') { - ;(e as Node)._ownerScope = scope - const name = - e.key.type === 'Identifier' - ? e.key.name - : e.key.type === 'StringLiteral' - ? e.key.value - : null + ;(e as WithScope)._ownerScope = scope + const name = getId(e.key) if (name && !e.computed) { - res.props[name] = e + res.props[name] = e as ResolvedElements['props'][string] } else if (e.key.type === 'TemplateLiteral') { for (const key of resolveTemplateKeys(ctx, e.key)) { - res.props[key] = e + res.props[key] = e as ResolvedElements['props'][string] } } else { ctx.error( @@ -172,11 +196,15 @@ function mergeElements( if (!hasOwn(baseProps, key)) { baseProps[key] = props[key] } else { - baseProps[key] = createProperty(baseProps[key].key, { - type, - // @ts-ignore - types: [baseProps[key], props[key]] - }) + baseProps[key] = createProperty( + baseProps[key].key, + { + type, + // @ts-ignore + types: [baseProps[key], props[key]] + }, + baseProps[key]._ownerScope + ) } } if (calls) { @@ -188,8 +216,9 @@ function mergeElements( function createProperty( key: Expression, - typeAnnotation: TSType -): TSPropertySignature { + typeAnnotation: TSType, + scope: TypeScope +): TSPropertySignature & { _ownerScope: TypeScope } { return { type: 'TSPropertySignature', key, @@ -197,18 +226,20 @@ function createProperty( typeAnnotation: { type: 'TSTypeAnnotation', typeAnnotation - } + }, + _ownerScope: scope } } function resolveInterfaceMembers( ctx: ScriptCompileContext, - node: TSInterfaceDeclaration & WithScope + node: TSInterfaceDeclaration & WithScope, + scope: TypeScope ): ResolvedElements { const base = typeElementsToMap(ctx, node.body.body, node._ownerScope) if (node.extends) { for (const ext of node.extends) { - const { props } = resolveTypeElements(ctx, ext) + const { props } = resolveTypeElements(ctx, ext, scope) for (const key in props) { if (!hasOwn(base.props, key)) { base.props[key] = props[key] @@ -221,7 +252,8 @@ function resolveInterfaceMembers( function resolveMappedType( ctx: ScriptCompileContext, - node: TSMappedType + node: TSMappedType, + scope: TypeScope ): ResolvedElements { const res: ResolvedElements = { props: {} } if (!node.typeParameter.constraint) { @@ -234,7 +266,8 @@ function resolveMappedType( type: 'Identifier', name: key }, - node.typeAnnotation! + node.typeAnnotation!, + scope ) } return res @@ -357,32 +390,52 @@ function resolveTypeReference( node: (TSTypeReference | TSExpressionWithTypeArguments) & { _resolvedReference?: Node }, - scope = ctxToScope(ctx) -): Node | undefined { + scope?: TypeScope, + name?: string, + onlyExported = false +): (Node & WithScope) | undefined { if (node._resolvedReference) { return node._resolvedReference } - const name = getReferenceName(node) - return (node._resolvedReference = innerResolveTypeReference(scope, name)) + return (node._resolvedReference = innerResolveTypeReference( + ctx, + scope || ctxToScope(ctx), + name || getReferenceName(node), + node, + onlyExported + )) } function innerResolveTypeReference( + ctx: ScriptCompileContext, scope: TypeScope, - name: string | string[] + name: string | string[], + node: TSTypeReference | TSExpressionWithTypeArguments, + onlyExported: boolean ): Node | undefined { if (typeof name === 'string') { if (scope.imports[name]) { - // TODO external import - } else if (scope.types[name]) { - return scope.types[name] + return resolveTypeFromImport(ctx, scope, scope.imports[name], node) + } else { + const types = onlyExported ? scope.exportedTypes : scope.types + return types[name] } } else { - const ns = innerResolveTypeReference(scope, name[0]) + const ns = innerResolveTypeReference( + ctx, + scope, + name[0], + node, + onlyExported + ) if (ns && ns.type === 'TSModuleDeclaration') { const childScope = moduleDeclToScope(ns, scope) return innerResolveTypeReference( + ctx, childScope, - name.length > 2 ? name.slice(1) : name[name.length - 1] + name.length > 2 ? name.slice(1) : name[name.length - 1], + node, + true ) } } @@ -407,20 +460,125 @@ function qualifiedNameToPath(node: Identifier | TSQualifiedName): string[] { } } +function resolveTypeFromImport( + ctx: ScriptCompileContext, + scope: TypeScope, + { source, imported }: Import, + node: TSTypeReference | TSExpressionWithTypeArguments +): Node | undefined { + const fs = ctx.options.fs + if (!fs) { + ctx.error( + `fs options for compileScript are required for resolving imported types`, + node + ) + } + // TODO (hmr) register dependency file on ctx + const containingFile = scope.filename + if (source.startsWith('.')) { + // relative import - fast path + const filename = path.join(containingFile, '..', source) + const resolved = resolveExt(filename, fs) + if (resolved) { + return resolveTypeReference( + ctx, + node, + fileToScope(ctx, resolved, fs), + imported, + true + ) + } else { + ctx.error(`Failed to resolve import source for type`, node) + } + } else { + // TODO module or aliased import - use full TS resolution + return + } +} + +function resolveExt( + filename: string, + fs: NonNullable +) { + const tryResolve = (filename: string) => { + if (fs.fileExists(filename)) return filename + } + return ( + tryResolve(filename) || + tryResolve(filename + `.ts`) || + tryResolve(filename + `.d.ts`) || + tryResolve(filename + `/index.ts`) || + tryResolve(filename + `/index.d.ts`) + ) +} + +function fileToScope( + ctx: ScriptCompileContext, + filename: string, + fs: NonNullable +): TypeScope { + // TODO cache + const source = fs.readFile(filename) + const body = parseFile(ctx, filename, source) + const scope: TypeScope = { + filename, + source, + types: Object.create(null), + exportedTypes: Object.create(null), + imports: recordImports(body) + } + recordTypes(body, scope) + return scope +} + +function parseFile( + ctx: ScriptCompileContext, + filename: string, + content: string +): Statement[] { + const ext = path.extname(filename) + if (ext === '.ts' || ext === '.tsx') { + return babelParse(content, { + plugins: resolveParserPlugins( + ext.slice(1), + ctx.options.babelParserPlugins + ), + sourceType: 'module' + }).program.body + } else if (ext === '.vue') { + const { + descriptor: { script, scriptSetup } + } = parse(content) + const scriptContent = (script?.content || '') + (scriptSetup?.content || '') + const lang = script?.lang || scriptSetup?.lang + return babelParse(scriptContent, { + plugins: resolveParserPlugins(lang!, ctx.options.babelParserPlugins), + sourceType: 'module' + }).program.body + } + return [] +} + function ctxToScope(ctx: ScriptCompileContext): TypeScope { if (ctx.scope) { return ctx.scope } + const scope: TypeScope = { + filename: ctx.descriptor.filename, + source: ctx.descriptor.source, + imports: Object.create(ctx.userImports), + types: Object.create(null), + exportedTypes: Object.create(null) + } + const body = ctx.scriptAst ? [...ctx.scriptAst.body, ...ctx.scriptSetupAst!.body] : ctx.scriptSetupAst!.body - return (ctx.scope = { - filename: ctx.descriptor.filename, - imports: ctx.userImports, - types: recordTypes(body) - }) + recordTypes(body, scope) + + return (ctx.scope = scope) } function moduleDeclToScope( @@ -430,27 +588,56 @@ function moduleDeclToScope( if (node._resolvedChildScope) { return node._resolvedChildScope } - const types: TypeScope['types'] = Object.create(parent.types) const scope: TypeScope = { - filename: parent.filename, - imports: Object.create(parent.imports), - types: recordTypes((node.body as TSModuleBlock).body, types), - parent - } - for (const key of Object.keys(types)) { - types[key]._ownerScope = scope + ...parent, + types: Object.create(parent.types), + imports: Object.create(parent.imports) } + recordTypes((node.body as TSModuleBlock).body, scope) return (node._resolvedChildScope = scope) } -function recordTypes( - body: Statement[], - types: Record = Object.create(null) -) { - for (const s of body) { - recordType(s, types) +function recordTypes(body: Statement[], scope: TypeScope) { + const { types, exportedTypes, imports } = scope + for (const stmt of body) { + recordType(stmt, types) + } + for (const stmt of body) { + if (stmt.type === 'ExportNamedDeclaration') { + if (stmt.declaration) { + recordType(stmt.declaration, types) + recordType(stmt.declaration, exportedTypes) + } else { + for (const spec of stmt.specifiers) { + if (spec.type === 'ExportSpecifier') { + const local = spec.local.name + const exported = getId(spec.exported) + if (stmt.source) { + // re-export, register an import + export as a type reference + imports[local] = { + source: stmt.source.value, + imported: local + } + exportedTypes[exported] = { + type: 'TSTypeReference', + typeName: { + type: 'Identifier', + name: local + }, + _ownerScope: scope + } + } else if (types[local]) { + // exporting local defined type + exportedTypes[exported] = types[local] + } + } + } + } + } + } + for (const key of Object.keys(types)) { + types[key]._ownerScope = scope } - return types } function recordType(node: Node, types: Record) { @@ -465,12 +652,6 @@ function recordType(node: Node, types: Record) { case 'TSTypeAliasDeclaration': types[node.id.name] = node.typeAnnotation break - case 'ExportNamedDeclaration': { - if (node.declaration) { - recordType(node.declaration, types) - } - break - } case 'VariableDeclaration': { if (node.declare) { for (const decl of node.declarations) { @@ -486,9 +667,29 @@ function recordType(node: Node, types: Record) { } } +export function recordImports(body: Statement[]) { + const imports: TypeScope['imports'] = Object.create(null) + for (const s of body) { + recordImport(s, imports) + } + return imports +} + +function recordImport(node: Node, imports: TypeScope['imports']) { + if (node.type !== 'ImportDeclaration') { + return + } + for (const s of node.specifiers) { + imports[s.local.name] = { + imported: getImportedName(s), + source: node.source.value + } + } +} + export function inferRuntimeType( ctx: ScriptCompileContext, - node: Node, + node: Node & WithScope, scope = node._ownerScope || ctxToScope(ctx) ): string[] { switch (node.type) { diff --git a/packages/compiler-sfc/src/script/utils.ts b/packages/compiler-sfc/src/script/utils.ts index 11bc011820e..780c780e2cc 100644 --- a/packages/compiler-sfc/src/script/utils.ts +++ b/packages/compiler-sfc/src/script/utils.ts @@ -1,4 +1,13 @@ -import { CallExpression, Node } from '@babel/types' +import { + CallExpression, + Expression, + Identifier, + ImportDefaultSpecifier, + ImportNamespaceSpecifier, + ImportSpecifier, + Node, + StringLiteral +} from '@babel/types' import { TS_NODE_TYPES } from '@vue/compiler-dom' export const UNKNOWN_TYPE = 'Unknown' @@ -48,3 +57,24 @@ export function isCallOf( export function toRuntimeTypeString(types: string[]) { return types.length > 1 ? `[${types.join(', ')}]` : types[0] } + +export function getImportedName( + specifier: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier +) { + if (specifier.type === 'ImportSpecifier') + return specifier.imported.type === 'Identifier' + ? specifier.imported.name + : specifier.imported.value + else if (specifier.type === 'ImportNamespaceSpecifier') return '*' + return 'default' +} + +export function getId(node: Identifier | StringLiteral): string +export function getId(node: Expression): string | null +export function getId(node: Expression) { + return node.type === 'Identifier' + ? node.name + : node.type === 'StringLiteral' + ? node.value + : null +}