diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 40deb6723..c828d77f6 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -16,8 +16,7 @@ import { Identifier, ExportSpecifier, Statement, - CallExpression, - TSEnumDeclaration + CallExpression } from '@babel/types' import { walk } from 'estree-walker' import { RawSourceMap } from 'source-map-js' @@ -47,7 +46,6 @@ 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 { inferRuntimeType } from './script/resolveType' import { analyzeScriptBindings } from './script/analyzeScriptBindings' import { isImportUsed } from './script/importUsageCheck' import { processAwait } from './script/topLevelAwait' @@ -169,7 +167,6 @@ export function compileScript( // metadata that needs to be returned // const ctx.bindingMetadata: BindingMetadata = {} - const userImports: Record = Object.create(null) const scriptBindings: Record = Object.create(null) const setupBindings: Record = Object.create(null) @@ -223,7 +220,7 @@ export function compileScript( isUsedInTemplate = isImportUsed(local, sfc) } - userImports[local] = { + ctx.userImports[local] = { isType, imported, local, @@ -303,7 +300,7 @@ export function compileScript( const local = specifier.local.name const imported = getImportedName(specifier) const source = node.source.value - const existing = userImports[local] + const existing = ctx.userImports[local] if ( source === 'vue' && (imported === DEFINE_PROPS || @@ -345,8 +342,8 @@ export function compileScript( // 1.3 resolve possible user import alias of `ref` and `reactive` const vueImportAliases: Record = {} - for (const key in userImports) { - const { source, imported, local } = userImports[key] + for (const key in ctx.userImports) { + const { source, imported, local } = ctx.userImports[key] if (source === 'vue') vueImportAliases[imported] = local } @@ -658,7 +655,6 @@ export function compileScript( node.exportKind === 'type') || (node.type === 'VariableDeclaration' && node.declare) ) { - recordType(node, ctx.declaredTypes) if (node.type !== 'TSEnumDeclaration') { hoistNode(node) } @@ -723,7 +719,7 @@ export function compileScript( Object.assign(ctx.bindingMetadata, analyzeScriptBindings(scriptAst.body)) } for (const [key, { isType, imported, source }] of Object.entries( - userImports + ctx.userImports )) { if (isType) continue ctx.bindingMetadata[key] = @@ -823,8 +819,11 @@ export function compileScript( ...scriptBindings, ...setupBindings } - for (const key in userImports) { - if (!userImports[key].isType && userImports[key].isUsedInTemplate) { + for (const key in ctx.userImports) { + if ( + !ctx.userImports[key].isType && + ctx.userImports[key].isUsedInTemplate + ) { allBindings[key] = true } } @@ -832,8 +831,8 @@ export function compileScript( for (const key in allBindings) { if ( allBindings[key] === true && - userImports[key].source !== 'vue' && - !userImports[key].source.endsWith('.vue') + ctx.userImports[key].source !== 'vue' && + !ctx.userImports[key].source.endsWith('.vue') ) { // generate getter for import bindings // skip vue imports since we know they will never change @@ -1012,7 +1011,7 @@ export function compileScript( return { ...scriptSetup, bindings: ctx.bindingMetadata, - imports: userImports, + imports: ctx.userImports, content: ctx.s.toString(), map: options.sourceMap !== false @@ -1201,38 +1200,6 @@ function walkPattern( } } -function recordType(node: Node, declaredTypes: Record) { - if (node.type === 'TSInterfaceDeclaration') { - declaredTypes[node.id.name] = [`Object`] - } else if (node.type === 'TSTypeAliasDeclaration') { - declaredTypes[node.id.name] = inferRuntimeType( - node.typeAnnotation, - declaredTypes - ) - } else if (node.type === 'ExportNamedDeclaration' && node.declaration) { - recordType(node.declaration, declaredTypes) - } else if (node.type === 'TSEnumDeclaration') { - declaredTypes[node.id.name] = inferEnumType(node) - } -} - -function inferEnumType(node: TSEnumDeclaration): string[] { - const types = new Set() - for (const m of node.members) { - if (m.initializer) { - switch (m.initializer.type) { - case 'StringLiteral': - types.add('String') - break - case 'NumericLiteral': - types.add('Number') - break - } - } - } - return types.size ? [...types] : ['Number'] -} - function canNeverBeRef(node: Node, userReactiveImport?: string): boolean { if (isCallOf(node, userReactiveImport)) { return true diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts index c5e325c74..718f23da5 100644 --- a/packages/compiler-sfc/src/script/context.ts +++ b/packages/compiler-sfc/src/script/context.ts @@ -2,12 +2,12 @@ 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 { SFCScriptCompileOptions } from '../compileScript' -import { PropsDeclType, PropsDestructureBindings } from './defineProps' +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 { EmitsDeclType } from './defineEmits' +import { TypeScope } from './resolveType' export class ScriptCompileContext { isJS: boolean @@ -20,7 +20,9 @@ export class ScriptCompileContext { startOffset = this.descriptor.scriptSetup?.loc.start.offset endOffset = this.descriptor.scriptSetup?.loc.end.offset - declaredTypes: Record = Object.create(null) + // import / type analysis + scope: TypeScope | undefined + userImports: Record = Object.create(null) // macros presence check hasDefinePropsCall = false @@ -35,7 +37,7 @@ export class ScriptCompileContext { // defineProps propsIdentifier: string | undefined propsRuntimeDecl: Node | undefined - propsTypeDecl: PropsDeclType | undefined + propsTypeDecl: Node | undefined propsDestructureDecl: ObjectPattern | undefined propsDestructuredBindings: PropsDestructureBindings = Object.create(null) propsDestructureRestId: string | undefined @@ -43,7 +45,7 @@ export class ScriptCompileContext { // defineEmits emitsRuntimeDecl: Node | undefined - emitsTypeDecl: EmitsDeclType | undefined + emitsTypeDecl: Node | undefined emitIdentifier: string | undefined // defineModel diff --git a/packages/compiler-sfc/src/script/defineEmits.ts b/packages/compiler-sfc/src/script/defineEmits.ts index 0a5262016..0e080b4fe 100644 --- a/packages/compiler-sfc/src/script/defineEmits.ts +++ b/packages/compiler-sfc/src/script/defineEmits.ts @@ -1,22 +1,10 @@ -import { - Identifier, - LVal, - Node, - RestElement, - TSFunctionType, - TSInterfaceBody, - TSTypeLiteral -} from '@babel/types' -import { FromNormalScript, isCallOf } from './utils' +import { Identifier, LVal, Node, RestElement } from '@babel/types' +import { isCallOf } from './utils' import { ScriptCompileContext } from './context' -import { resolveQualifiedType } from './resolveType' +import { resolveTypeElements } from './resolveType' export const DEFINE_EMITS = 'defineEmits' -export type EmitsDeclType = FromNormalScript< - TSFunctionType | TSTypeLiteral | TSInterfaceBody -> - export function processDefineEmits( ctx: ScriptCompileContext, node: Node, @@ -38,21 +26,7 @@ export function processDefineEmits( node ) } - - const emitsTypeDeclRaw = node.typeParameters.params[0] - ctx.emitsTypeDecl = resolveQualifiedType( - ctx, - emitsTypeDeclRaw, - node => node.type === 'TSFunctionType' || node.type === 'TSTypeLiteral' - ) as EmitsDeclType | undefined - - if (!ctx.emitsTypeDecl) { - ctx.error( - `type argument passed to ${DEFINE_EMITS}() must be a function type, ` + - `a literal type with call signatures, or a reference to the above types.`, - emitsTypeDeclRaw - ) - } + ctx.emitsTypeDecl = node.typeParameters.params[0] } if (declId) { @@ -89,36 +63,32 @@ export function genRuntimeEmits(ctx: ScriptCompileContext): string | undefined { function extractRuntimeEmits(ctx: ScriptCompileContext): Set { const emits = new Set() const node = ctx.emitsTypeDecl! - if (node.type === 'TSTypeLiteral' || node.type === 'TSInterfaceBody') { - const members = node.type === 'TSTypeLiteral' ? node.members : node.body - let hasCallSignature = false - let hasProperty = false - for (let t of members) { - if (t.type === 'TSCallSignatureDeclaration') { - extractEventNames(t.parameters[0], emits) - hasCallSignature = true - } - if (t.type === 'TSPropertySignature') { - if (t.key.type === 'Identifier' && !t.computed) { - emits.add(t.key.name) - hasProperty = true - } else if (t.key.type === 'StringLiteral' && !t.computed) { - emits.add(t.key.value) - hasProperty = true - } else { - ctx.error(`defineEmits() type cannot use computed keys.`, t.key) - } - } - } - if (hasCallSignature && hasProperty) { + + if (node.type === 'TSFunctionType') { + extractEventNames(node.parameters[0], emits) + return emits + } + + const elements = resolveTypeElements(ctx, node) + + let hasProperty = false + for (const key in elements) { + emits.add(key) + hasProperty = true + } + + if (elements.__callSignatures) { + if (hasProperty) { ctx.error( `defineEmits() type cannot mixed call signature and property syntax.`, node ) } - } else { - extractEventNames(node.parameters[0], emits) + for (const call of elements.__callSignatures) { + extractEventNames(call.parameters[0], emits) + } } + return emits } diff --git a/packages/compiler-sfc/src/script/defineModel.ts b/packages/compiler-sfc/src/script/defineModel.ts index e2d2317b1..058419670 100644 --- a/packages/compiler-sfc/src/script/defineModel.ts +++ b/packages/compiler-sfc/src/script/defineModel.ts @@ -99,7 +99,7 @@ export function genModelProps(ctx: ScriptCompileContext) { for (const [name, { type, options }] of Object.entries(ctx.modelDecls)) { let skipCheck = false - let runtimeTypes = type && inferRuntimeType(type, ctx.declaredTypes) + let runtimeTypes = type && inferRuntimeType(ctx, type) if (runtimeTypes) { const hasUnknownType = runtimeTypes.includes(UNKNOWN_TYPE) diff --git a/packages/compiler-sfc/src/script/defineProps.ts b/packages/compiler-sfc/src/script/defineProps.ts index 903d038db..bd462a2a8 100644 --- a/packages/compiler-sfc/src/script/defineProps.ts +++ b/packages/compiler-sfc/src/script/defineProps.ts @@ -1,8 +1,6 @@ import { Node, LVal, - TSTypeLiteral, - TSInterfaceBody, ObjectProperty, ObjectMethod, ObjectExpression, @@ -10,9 +8,8 @@ import { } from '@babel/types' import { BindingTypes, isFunctionType } from '@vue/compiler-dom' import { ScriptCompileContext } from './context' -import { inferRuntimeType, resolveQualifiedType } from './resolveType' +import { inferRuntimeType, resolveTypeElements } from './resolveType' import { - FromNormalScript, resolveObjectKey, UNKNOWN_TYPE, concatStrings, @@ -28,8 +25,6 @@ import { processPropsDestructure } from './definePropsDestructure' export const DEFINE_PROPS = 'defineProps' export const WITH_DEFAULTS = 'withDefaults' -export type PropsDeclType = FromNormalScript - export interface PropTypeData { key: string type: string[] @@ -76,20 +71,7 @@ export function processDefineProps( node ) } - - const rawDecl = node.typeParameters.params[0] - ctx.propsTypeDecl = resolveQualifiedType( - ctx, - rawDecl, - node => node.type === 'TSTypeLiteral' - ) as PropsDeclType | undefined - if (!ctx.propsTypeDecl) { - ctx.error( - `type argument passed to ${DEFINE_PROPS}() must be a literal type, ` + - `or a reference to an interface or literal type.`, - rawDecl - ) - } + ctx.propsTypeDecl = node.typeParameters.params[0] } if (declId) { @@ -176,56 +158,19 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined { } function genRuntimePropsFromTypes(ctx: ScriptCompileContext) { + // this is only called if propsTypeDecl exists + const props = resolveRuntimePropsFromType(ctx, ctx.propsTypeDecl!) + if (!props.length) { + return + } + const propStrings: string[] = [] const hasStaticDefaults = hasStaticWithDefaults(ctx) - // this is only called if propsTypeDecl exists - const node = ctx.propsTypeDecl! - const members = node.type === 'TSTypeLiteral' ? node.members : node.body - for (const m of members) { - if ( - (m.type === 'TSPropertySignature' || m.type === 'TSMethodSignature') && - m.key.type === 'Identifier' - ) { - const key = m.key.name - let type: string[] | undefined - let skipCheck = false - if (m.type === 'TSMethodSignature') { - type = ['Function'] - } else if (m.typeAnnotation) { - type = inferRuntimeType( - m.typeAnnotation.typeAnnotation, - ctx.declaredTypes - ) - // skip check for result containing unknown types - if (type.includes(UNKNOWN_TYPE)) { - if (type.includes('Boolean') || type.includes('Function')) { - type = type.filter(t => t !== UNKNOWN_TYPE) - skipCheck = true - } else { - type = ['null'] - } - } - } - - propStrings.push( - genRuntimePropFromType( - ctx, - key, - !m.optional, - type || [`null`], - skipCheck, - hasStaticDefaults - ) - ) - - // register bindings - ctx.bindingMetadata[key] = BindingTypes.PROPS - } - } - - if (!propStrings.length) { - return + for (const prop of props) { + propStrings.push(genRuntimePropFromType(ctx, prop, hasStaticDefaults)) + // register bindings + ctx.bindingMetadata[prop.key] = BindingTypes.PROPS } let propsDecls = `{ @@ -240,12 +185,43 @@ function genRuntimePropsFromTypes(ctx: ScriptCompileContext) { return propsDecls } +function resolveRuntimePropsFromType( + ctx: ScriptCompileContext, + node: Node +): PropTypeData[] { + const props: PropTypeData[] = [] + const elements = resolveTypeElements(ctx, node) + for (const key in elements) { + const e = elements[key] + let type: string[] | undefined + let skipCheck = false + if (e.type === 'TSMethodSignature') { + type = ['Function'] + } else if (e.typeAnnotation) { + type = inferRuntimeType(ctx, e.typeAnnotation.typeAnnotation) + // skip check for result containing unknown types + if (type.includes(UNKNOWN_TYPE)) { + if (type.includes('Boolean') || type.includes('Function')) { + type = type.filter(t => t !== UNKNOWN_TYPE) + skipCheck = true + } else { + type = ['null'] + } + } + } + props.push({ + key, + required: !e.optional, + type: type || [`null`], + skipCheck + }) + } + return props +} + function genRuntimePropFromType( ctx: ScriptCompileContext, - key: string, - required: boolean, - type: string[], - skipCheck: boolean, + { key, required, type, skipCheck }: PropTypeData, hasStaticDefaults: boolean ): string { let defaultString: string | undefined diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 73e76d137..ba4175706 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -1,122 +1,227 @@ import { Node, Statement, - TSInterfaceBody, + TSCallSignatureDeclaration, + TSEnumDeclaration, + TSExpressionWithTypeArguments, + TSFunctionType, + TSMethodSignature, + TSPropertySignature, TSType, - TSTypeElement + TSTypeAnnotation, + TSTypeElement, + TSTypeReference } from '@babel/types' -import { FromNormalScript, UNKNOWN_TYPE } from './utils' +import { UNKNOWN_TYPE } from './utils' import { ScriptCompileContext } from './context' +import { ImportBinding } from '../compileScript' +import { TSInterfaceDeclaration } from '@babel/types' +import { hasOwn } from '@vue/shared' -export function resolveQualifiedType( +export interface TypeScope { + filename: string + body: Statement[] + imports: Record + types: Record +} + +type ResolvedElements = Record< + string, + TSPropertySignature | TSMethodSignature +> & { + __callSignatures?: (TSCallSignatureDeclaration | TSFunctionType)[] +} + +/** + * 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, - qualifier: (node: Node) => boolean -): Node | undefined { - if (qualifier(node)) { - return node + node: Node & { _resolvedElements?: ResolvedElements } +): ResolvedElements { + if (node._resolvedElements) { + return node._resolvedElements } - if (node.type === 'TSTypeReference' && node.typeName.type === 'Identifier') { - const refName = node.typeName.name - const { scriptAst, scriptSetupAst } = ctx - const body = scriptAst - ? [...scriptSetupAst!.body, ...scriptAst.body] - : scriptSetupAst!.body - for (let i = 0; i < body.length; i++) { - const node = body[i] - let qualified = isQualifiedType( - node, - qualifier, - refName - ) as TSInterfaceBody - if (qualified) { - const extendsTypes = resolveExtendsType(body, node, qualifier) - if (extendsTypes.length) { - const bodies: TSTypeElement[] = [...qualified.body] - filterExtendsType(extendsTypes, bodies) - qualified.body = bodies - } - ;(qualified as FromNormalScript).__fromNormalScript = - scriptAst && i >= scriptSetupAst!.body.length - return qualified + return (node._resolvedElements = innerResolveTypeElements(ctx, node)) +} + +function innerResolveTypeElements( + ctx: ScriptCompileContext, + node: Node +): ResolvedElements { + switch (node.type) { + case 'TSTypeLiteral': + return typeElementsToMap(ctx, node.members) + case 'TSInterfaceDeclaration': + return resolveInterfaceMembers(ctx, node) + case 'TSTypeAliasDeclaration': + case 'TSParenthesizedType': + return resolveTypeElements(ctx, node.typeAnnotation) + case 'TSFunctionType': { + const ret: ResolvedElements = {} + addCallSignature(ret, node) + return ret + } + case 'TSExpressionWithTypeArguments': + case 'TSTypeReference': + return resolveTypeElements(ctx, resolveTypeReference(ctx, node)) + } + ctx.error(`Unsupported type in SFC macro: ${node.type}`, node) +} + +function addCallSignature( + elements: ResolvedElements, + node: TSCallSignatureDeclaration | TSFunctionType +) { + if (!elements.__callSignatures) { + Object.defineProperty(elements, '__callSignatures', { + enumerable: false, + value: [node] + }) + } else { + elements.__callSignatures.push(node) + } +} + +function typeElementsToMap( + ctx: ScriptCompileContext, + elements: TSTypeElement[] +): ResolvedElements { + const ret: ResolvedElements = {} + for (const e of elements) { + if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') { + const name = + e.key.type === 'Identifier' + ? e.key.name + : e.key.type === 'StringLiteral' + ? e.key.value + : null + if (name && !e.computed) { + ret[name] = e + } else { + ctx.error( + `computed keys are not supported in types referenced by SFC macros.`, + e + ) } + } else if (e.type === 'TSCallSignatureDeclaration') { + addCallSignature(ret, e) + } + } + return ret +} + +function resolveInterfaceMembers( + ctx: ScriptCompileContext, + node: TSInterfaceDeclaration +): ResolvedElements { + const base = typeElementsToMap(ctx, node.body.body) + if (node.extends) { + for (const ext of node.extends) { + const resolvedExt = resolveTypeElements(ctx, ext) + for (const key in resolvedExt) { + if (!hasOwn(base, key)) { + base[key] = resolvedExt[key] + } + } + } + } + return base +} + +function resolveTypeReference( + ctx: ScriptCompileContext, + node: TSTypeReference | TSExpressionWithTypeArguments, + scope?: TypeScope +): Node +function resolveTypeReference( + ctx: ScriptCompileContext, + node: TSTypeReference | TSExpressionWithTypeArguments, + scope: TypeScope, + bail: false +): Node | undefined +function resolveTypeReference( + ctx: ScriptCompileContext, + node: TSTypeReference | TSExpressionWithTypeArguments, + scope = getRootScope(ctx), + bail = true +): Node | undefined { + const ref = node.type === 'TSTypeReference' ? node.typeName : node.expression + if (ref.type === 'Identifier') { + if (scope.imports[ref.name]) { + // TODO external import + } else if (scope.types[ref.name]) { + return scope.types[ref.name] + } + } else { + // TODO qualified name, e.g. Foo.Bar + // return resolveTypeReference() + } + if (bail) { + ctx.error('Failed to resolve type reference.', node) + } +} + +function getRootScope(ctx: ScriptCompileContext): TypeScope { + if (ctx.scope) { + return ctx.scope + } + + 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), + body + }) +} + +function recordTypes(body: Statement[]) { + const types: Record = Object.create(null) + for (const s of body) { + recordType(s, types) + } + return types +} + +function recordType(node: Node, types: Record) { + switch (node.type) { + case 'TSInterfaceDeclaration': + case 'TSEnumDeclaration': + types[node.id.name] = node + break + case 'TSTypeAliasDeclaration': + types[node.id.name] = node.typeAnnotation + break + case 'ExportNamedDeclaration': { + if (node.exportKind === 'type') { + recordType(node.declaration!, types) + } + break + } + case 'VariableDeclaration': { + if (node.declare) { + for (const decl of node.declarations) { + if (decl.id.type === 'Identifier' && decl.id.typeAnnotation) { + types[decl.id.name] = ( + decl.id.typeAnnotation as TSTypeAnnotation + ).typeAnnotation + } + } + } + break } } } -function isQualifiedType( - node: Node, - qualifier: (node: Node) => boolean, - refName: String -): Node | undefined { - if (node.type === 'TSInterfaceDeclaration' && node.id.name === refName) { - return node.body - } else if ( - node.type === 'TSTypeAliasDeclaration' && - node.id.name === refName && - qualifier(node.typeAnnotation) - ) { - return node.typeAnnotation - } else if (node.type === 'ExportNamedDeclaration' && node.declaration) { - return isQualifiedType(node.declaration, qualifier, refName) - } -} - -function resolveExtendsType( - body: Statement[], - node: Node, - qualifier: (node: Node) => boolean, - cache: Array = [] -): Array { - if (node.type === 'TSInterfaceDeclaration' && node.extends) { - node.extends.forEach(extend => { - if ( - extend.type === 'TSExpressionWithTypeArguments' && - extend.expression.type === 'Identifier' - ) { - for (const node of body) { - const qualified = isQualifiedType( - node, - qualifier, - extend.expression.name - ) - if (qualified) { - cache.push(qualified) - resolveExtendsType(body, node, qualifier, cache) - return cache - } - } - } - }) - } - return cache -} - -// filter all extends types to keep the override declaration -function filterExtendsType(extendsTypes: Node[], bodies: TSTypeElement[]) { - extendsTypes.forEach(extend => { - const body = (extend as TSInterfaceBody).body - body.forEach(newBody => { - if ( - newBody.type === 'TSPropertySignature' && - newBody.key.type === 'Identifier' - ) { - const name = newBody.key.name - const hasOverride = bodies.some( - seenBody => - seenBody.type === 'TSPropertySignature' && - seenBody.key.type === 'Identifier' && - seenBody.key.name === name - ) - if (!hasOverride) bodies.push(newBody) - } - }) - }) -} - export function inferRuntimeType( - node: TSType, - declaredTypes: Record + ctx: ScriptCompileContext, + node: Node, + scope = getRootScope(ctx) ): string[] { switch (node.type) { case 'TSStringKeyword': @@ -129,10 +234,13 @@ export function inferRuntimeType( return ['Object'] case 'TSNullKeyword': return ['null'] - case 'TSTypeLiteral': { + case 'TSTypeLiteral': + case 'TSInterfaceDeclaration': { // TODO (nice to have) generate runtime property validation const types = new Set() - for (const m of node.members) { + const members = + node.type === 'TSTypeLiteral' ? node.members : node.body.body + for (const m of members) { if ( m.type === 'TSCallSignatureDeclaration' || m.type === 'TSConstructSignatureDeclaration' @@ -166,8 +274,9 @@ export function inferRuntimeType( case 'TSTypeReference': if (node.typeName.type === 'Identifier') { - if (declaredTypes[node.typeName.name]) { - return declaredTypes[node.typeName.name] + const resolved = resolveTypeReference(ctx, node, scope, false) + if (resolved) { + return inferRuntimeType(ctx, resolved, scope) } switch (node.typeName.name) { case 'Array': @@ -205,26 +314,21 @@ export function inferRuntimeType( case 'NonNullable': if (node.typeParameters && node.typeParameters.params[0]) { return inferRuntimeType( + ctx, node.typeParameters.params[0], - declaredTypes + scope ).filter(t => t !== 'null') } break case 'Extract': if (node.typeParameters && node.typeParameters.params[1]) { - return inferRuntimeType( - node.typeParameters.params[1], - declaredTypes - ) + return inferRuntimeType(ctx, node.typeParameters.params[1], scope) } break case 'Exclude': case 'OmitThisParameter': if (node.typeParameters && node.typeParameters.params[0]) { - return inferRuntimeType( - node.typeParameters.params[0], - declaredTypes - ) + return inferRuntimeType(ctx, node.typeParameters.params[0], scope) } break } @@ -233,16 +337,19 @@ export function inferRuntimeType( return [UNKNOWN_TYPE] case 'TSParenthesizedType': - return inferRuntimeType(node.typeAnnotation, declaredTypes) + return inferRuntimeType(ctx, node.typeAnnotation, scope) case 'TSUnionType': - return flattenTypes(node.types, declaredTypes) + return flattenTypes(ctx, node.types, scope) case 'TSIntersectionType': { - return flattenTypes(node.types, declaredTypes).filter( + return flattenTypes(ctx, node.types, scope).filter( t => t !== UNKNOWN_TYPE ) } + case 'TSEnumDeclaration': + return inferEnumType(node) + case 'TSSymbolKeyword': return ['Symbol'] @@ -252,14 +359,32 @@ export function inferRuntimeType( } function flattenTypes( + ctx: ScriptCompileContext, types: TSType[], - declaredTypes: Record + scope: TypeScope ): string[] { return [ ...new Set( ([] as string[]).concat( - ...types.map(t => inferRuntimeType(t, declaredTypes)) + ...types.map(t => inferRuntimeType(ctx, t, scope)) ) ) ] } + +function inferEnumType(node: TSEnumDeclaration): string[] { + const types = new Set() + for (const m of node.members) { + if (m.initializer) { + switch (m.initializer.type) { + case 'StringLiteral': + types.add('String') + break + case 'NumericLiteral': + types.add('Number') + break + } + } + } + return types.size ? [...types] : ['Number'] +} diff --git a/packages/compiler-sfc/src/script/utils.ts b/packages/compiler-sfc/src/script/utils.ts index 1f4b13946..11bc01182 100644 --- a/packages/compiler-sfc/src/script/utils.ts +++ b/packages/compiler-sfc/src/script/utils.ts @@ -3,8 +3,6 @@ import { TS_NODE_TYPES } from '@vue/compiler-dom' export const UNKNOWN_TYPE = 'Unknown' -export type FromNormalScript = T & { __fromNormalScript?: boolean | null } - export function resolveObjectKey(node: Node, computed: boolean) { switch (node.type) { case 'StringLiteral':