mirror of https://github.com/vuejs/core.git
refactor(compiler-sfc): rework macro type resolution
This commit is contained in:
parent
ae5a9323b7
commit
b2cdb2645f
|
@ -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<string, ImportBinding> = Object.create(null)
|
||||
const scriptBindings: Record<string, BindingTypes> = Object.create(null)
|
||||
const setupBindings: Record<string, BindingTypes> = 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<string, string> = {}
|
||||
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<string, string[]>) {
|
||||
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<string>()
|
||||
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
|
||||
|
|
|
@ -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<string, string[]> = Object.create(null)
|
||||
// import / type analysis
|
||||
scope: TypeScope | undefined
|
||||
userImports: Record<string, ImportBinding> = 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
|
||||
|
|
|
@ -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<string> {
|
||||
const emits = new Set<string>()
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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<TSTypeLiteral | TSInterfaceBody>
|
||||
|
||||
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
|
||||
|
|
|
@ -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<string, ImportBinding>
|
||||
types: Record<string, Node>
|
||||
}
|
||||
|
||||
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<Node>).__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<string, Node> = Object.create(null)
|
||||
for (const s of body) {
|
||||
recordType(s, types)
|
||||
}
|
||||
return types
|
||||
}
|
||||
|
||||
function recordType(node: Node, types: Record<string, Node>) {
|
||||
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<Node> = []
|
||||
): Array<Node> {
|
||||
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<string, string[]>
|
||||
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<string>()
|
||||
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<string, string[]>
|
||||
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<string>()
|
||||
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']
|
||||
}
|
||||
|
|
|
@ -3,8 +3,6 @@ import { TS_NODE_TYPES } from '@vue/compiler-dom'
|
|||
|
||||
export const UNKNOWN_TYPE = 'Unknown'
|
||||
|
||||
export type FromNormalScript<T> = T & { __fromNormalScript?: boolean | null }
|
||||
|
||||
export function resolveObjectKey(node: Node, computed: boolean) {
|
||||
switch (node.type) {
|
||||
case 'StringLiteral':
|
||||
|
|
Loading…
Reference in New Issue