mirror of https://github.com/vuejs/core.git
refactor(compiler-sfc): extract more defineProps logic
This commit is contained in:
parent
fe9760188d
commit
d0ac57872c
|
@ -9,8 +9,7 @@ import type {
|
|||
Program,
|
||||
ImportDefaultSpecifier,
|
||||
ImportNamespaceSpecifier,
|
||||
ImportSpecifier,
|
||||
CallExpression
|
||||
ImportSpecifier
|
||||
} from '@babel/types'
|
||||
import { walk } from 'estree-walker'
|
||||
|
||||
|
@ -443,25 +442,3 @@ export const TS_NODE_TYPES = [
|
|||
'TSInstantiationExpression', // foo<string>
|
||||
'TSSatisfiesExpression' // foo satisfies T
|
||||
]
|
||||
export function unwrapTSNode(node: Node): Node {
|
||||
if (TS_NODE_TYPES.includes(node.type)) {
|
||||
return unwrapTSNode((node as any).expression)
|
||||
} else {
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
export function isCallOf(
|
||||
node: Node | null | undefined,
|
||||
test: string | ((id: string) => boolean) | null | undefined
|
||||
): node is CallExpression {
|
||||
return !!(
|
||||
node &&
|
||||
test &&
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
(typeof test === 'string'
|
||||
? node.callee.name === test
|
||||
: test(node.callee.name))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -10,9 +10,7 @@ import {
|
|||
SimpleExpressionNode,
|
||||
isFunctionType,
|
||||
walkIdentifiers,
|
||||
getImportedName,
|
||||
unwrapTSNode,
|
||||
isCallOf
|
||||
getImportedName
|
||||
} from '@vue/compiler-dom'
|
||||
import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse'
|
||||
import { parse as _parse, parseExpression, ParserPlugin } from '@babel/parser'
|
||||
|
@ -36,7 +34,6 @@ import {
|
|||
TSInterfaceBody,
|
||||
TSTypeElement,
|
||||
AwaitExpression,
|
||||
ObjectMethod,
|
||||
LVal,
|
||||
Expression,
|
||||
TSEnumDeclaration
|
||||
|
@ -54,13 +51,22 @@ import { rewriteDefaultAST } from './rewriteDefault'
|
|||
import { createCache } from './cache'
|
||||
import { shouldTransform, transformAST } from '@vue/reactivity-transform'
|
||||
import { transformDestructuredProps } from './script/definePropsDestructure'
|
||||
import { resolveObjectKey, FromNormalScript } from './script/utils'
|
||||
import { ScriptCompileContext } from './script/context'
|
||||
import {
|
||||
processDefineProps,
|
||||
DEFINE_PROPS,
|
||||
WITH_DEFAULTS
|
||||
WITH_DEFAULTS,
|
||||
extractRuntimeProps
|
||||
} from './script/defineProps'
|
||||
import {
|
||||
resolveObjectKey,
|
||||
FromNormalScript,
|
||||
UNKNOWN_TYPE,
|
||||
isLiteralNode,
|
||||
unwrapTSNode,
|
||||
isCallOf
|
||||
} from './script/utils'
|
||||
import { genRuntimeProps } from './script/defineProps'
|
||||
|
||||
// Special compiler macros
|
||||
const DEFINE_EMITS = 'defineEmits'
|
||||
|
@ -151,11 +157,6 @@ export type PropsDestructureBindings = Record<
|
|||
type EmitsDeclType = FromNormalScript<
|
||||
TSFunctionType | TSTypeLiteral | TSInterfaceBody
|
||||
>
|
||||
interface ModelDecl {
|
||||
type: TSType | undefined
|
||||
options: string | undefined
|
||||
identifier: string | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile `<script setup>`
|
||||
|
@ -279,7 +280,6 @@ export function compileScript(
|
|||
|
||||
// metadata that needs to be returned
|
||||
const bindingMetadata: BindingMetadata = {}
|
||||
const helperImports: Set<string> = new Set()
|
||||
const userImports: Record<string, ImportBinding> = Object.create(null)
|
||||
const scriptBindings: Record<string, BindingTypes> = Object.create(null)
|
||||
const setupBindings: Record<string, BindingTypes> = Object.create(null)
|
||||
|
@ -290,14 +290,13 @@ export function compileScript(
|
|||
let emitsTypeDecl: EmitsDeclType | undefined
|
||||
let emitIdentifier: string | undefined
|
||||
let optionsRuntimeDecl: Node | undefined
|
||||
let modelDecls: Record<string, ModelDecl> = {}
|
||||
let hasAwait = false
|
||||
let hasInlinedSsrRenderFn = false
|
||||
// props/emits declared via types
|
||||
const typeDeclaredProps: Record<string, PropTypeData> = {}
|
||||
// const typeDeclaredProps: Record<string, PropTypeData> = {}
|
||||
const typeDeclaredEmits: Set<string> = new Set()
|
||||
// record declared types for runtime props type generation
|
||||
const declaredTypes: Record<string, string[]> = {}
|
||||
// const declaredTypes: Record<string, string[]> = {}
|
||||
|
||||
// magic-string state
|
||||
const s = new MagicString(source)
|
||||
|
@ -306,11 +305,6 @@ export function compileScript(
|
|||
const scriptStartOffset = script && script.loc.start.offset
|
||||
const scriptEndOffset = script && script.loc.end.offset
|
||||
|
||||
function helper(key: string): string {
|
||||
helperImports.add(key)
|
||||
return `_${key}`
|
||||
}
|
||||
|
||||
function error(
|
||||
msg: string,
|
||||
node: Node,
|
||||
|
@ -435,7 +429,7 @@ export function compileScript(
|
|||
s.overwrite(
|
||||
startOffset + node.start!,
|
||||
startOffset + node.end!,
|
||||
`${helper('useSlots')}()`
|
||||
`${ctx.helper('useSlots')}()`
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -461,7 +455,7 @@ export function compileScript(
|
|||
options = arg0
|
||||
}
|
||||
|
||||
if (modelDecls[modelName]) {
|
||||
if (ctx.modelDecls[modelName]) {
|
||||
error(`duplicate model name ${JSON.stringify(modelName)}`, node)
|
||||
}
|
||||
|
||||
|
@ -469,7 +463,7 @@ export function compileScript(
|
|||
? s.slice(startOffset + options.start!, startOffset + options.end!)
|
||||
: undefined
|
||||
|
||||
modelDecls[modelName] = {
|
||||
ctx.modelDecls[modelName] = {
|
||||
type,
|
||||
options: optionsString,
|
||||
identifier:
|
||||
|
@ -507,7 +501,7 @@ export function compileScript(
|
|||
s.overwrite(
|
||||
startOffset + node.start!,
|
||||
startOffset + node.end!,
|
||||
`${helper('useModel')}(__props, ${JSON.stringify(modelName)}${
|
||||
`${ctx.helper('useModel')}(__props, ${JSON.stringify(modelName)}${
|
||||
runtimeOptions ? `, ${runtimeOptions}` : ``
|
||||
})`
|
||||
)
|
||||
|
@ -753,7 +747,7 @@ export function compileScript(
|
|||
s.overwrite(
|
||||
node.start! + startOffset,
|
||||
argumentStart + startOffset,
|
||||
`${needSemi ? `;` : ``}(\n ([__temp,__restore] = ${helper(
|
||||
`${needSemi ? `;` : ``}(\n ([__temp,__restore] = ${ctx.helper(
|
||||
`withAsyncContext`
|
||||
)}(${containsNestedAwait ? `async ` : ``}() => `
|
||||
)
|
||||
|
@ -765,242 +759,6 @@ export function compileScript(
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* check defaults. If the default object is an object literal with only
|
||||
* static properties, we can directly generate more optimized default
|
||||
* declarations. Otherwise we will have to fallback to runtime merging.
|
||||
*/
|
||||
function hasStaticWithDefaults() {
|
||||
return (
|
||||
ctx.propsRuntimeDefaults &&
|
||||
ctx.propsRuntimeDefaults.type === 'ObjectExpression' &&
|
||||
ctx.propsRuntimeDefaults.properties.every(
|
||||
node =>
|
||||
node.type !== 'SpreadElement' &&
|
||||
(!node.computed || node.key.type.endsWith('Literal'))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function concatStrings(strs: Array<string | null | undefined | false>) {
|
||||
return strs.filter((s): s is string => !!s).join(', ')
|
||||
}
|
||||
|
||||
function genRuntimeProps() {
|
||||
function genPropsFromTS() {
|
||||
const keys = Object.keys(typeDeclaredProps)
|
||||
if (!keys.length) return
|
||||
|
||||
const hasStaticDefaults = hasStaticWithDefaults()
|
||||
const scriptSetupSource = scriptSetup!.content
|
||||
let propsDecls = `{
|
||||
${keys
|
||||
.map(key => {
|
||||
let defaultString: string | undefined
|
||||
const destructured = genDestructuredDefaultValue(
|
||||
key,
|
||||
typeDeclaredProps[key].type
|
||||
)
|
||||
if (destructured) {
|
||||
defaultString = `default: ${destructured.valueString}${
|
||||
destructured.needSkipFactory ? `, skipFactory: true` : ``
|
||||
}`
|
||||
} else if (hasStaticDefaults) {
|
||||
const prop = (
|
||||
ctx.propsRuntimeDefaults as ObjectExpression
|
||||
).properties.find(node => {
|
||||
if (node.type === 'SpreadElement') return false
|
||||
return resolveObjectKey(node.key, node.computed) === key
|
||||
}) as ObjectProperty | ObjectMethod
|
||||
if (prop) {
|
||||
if (prop.type === 'ObjectProperty') {
|
||||
// prop has corresponding static default value
|
||||
defaultString = `default: ${scriptSetupSource.slice(
|
||||
prop.value.start!,
|
||||
prop.value.end!
|
||||
)}`
|
||||
} else {
|
||||
defaultString = `${prop.async ? 'async ' : ''}${
|
||||
prop.kind !== 'method' ? `${prop.kind} ` : ''
|
||||
}default() ${scriptSetupSource.slice(
|
||||
prop.body.start!,
|
||||
prop.body.end!
|
||||
)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { type, required, skipCheck } = typeDeclaredProps[key]
|
||||
if (!isProd) {
|
||||
return `${key}: { ${concatStrings([
|
||||
`type: ${toRuntimeTypeString(type)}`,
|
||||
`required: ${required}`,
|
||||
skipCheck && 'skipCheck: true',
|
||||
defaultString
|
||||
])} }`
|
||||
} else if (
|
||||
type.some(
|
||||
el =>
|
||||
el === 'Boolean' ||
|
||||
((!hasStaticDefaults || defaultString) && el === 'Function')
|
||||
)
|
||||
) {
|
||||
// #4783 for boolean, should keep the type
|
||||
// #7111 for function, if default value exists or it's not static, should keep it
|
||||
// in production
|
||||
return `${key}: { ${concatStrings([
|
||||
`type: ${toRuntimeTypeString(type)}`,
|
||||
defaultString
|
||||
])} }`
|
||||
} else {
|
||||
// production: checks are useless
|
||||
return `${key}: ${defaultString ? `{ ${defaultString} }` : `{}`}`
|
||||
}
|
||||
})
|
||||
.join(',\n ')}\n }`
|
||||
|
||||
if (ctx.propsRuntimeDefaults && !hasStaticDefaults) {
|
||||
propsDecls = `${helper('mergeDefaults')}(${propsDecls}, ${source.slice(
|
||||
ctx.propsRuntimeDefaults.start! + startOffset,
|
||||
ctx.propsRuntimeDefaults.end! + startOffset
|
||||
)})`
|
||||
}
|
||||
|
||||
return propsDecls
|
||||
}
|
||||
|
||||
function genModels() {
|
||||
if (!ctx.hasDefineModelCall) return
|
||||
|
||||
let modelPropsDecl = ''
|
||||
for (const [name, { type, options }] of Object.entries(modelDecls)) {
|
||||
let skipCheck = false
|
||||
|
||||
let runtimeTypes = type && inferRuntimeType(type, declaredTypes)
|
||||
if (runtimeTypes) {
|
||||
const hasUnknownType = runtimeTypes.includes(UNKNOWN_TYPE)
|
||||
|
||||
runtimeTypes = runtimeTypes.filter(el => {
|
||||
if (el === UNKNOWN_TYPE) return false
|
||||
return isProd
|
||||
? el === 'Boolean' || (el === 'Function' && options)
|
||||
: true
|
||||
})
|
||||
skipCheck = !isProd && hasUnknownType && runtimeTypes.length > 0
|
||||
}
|
||||
|
||||
let runtimeType =
|
||||
(runtimeTypes &&
|
||||
runtimeTypes.length > 0 &&
|
||||
toRuntimeTypeString(runtimeTypes)) ||
|
||||
undefined
|
||||
|
||||
const codegenOptions = concatStrings([
|
||||
runtimeType && `type: ${runtimeType}`,
|
||||
skipCheck && 'skipCheck: true'
|
||||
])
|
||||
|
||||
let decl: string
|
||||
if (runtimeType && options) {
|
||||
decl = ctx.isTS
|
||||
? `{ ${codegenOptions}, ...${options} }`
|
||||
: `Object.assign({ ${codegenOptions} }, ${options})`
|
||||
} else {
|
||||
decl = options || (runtimeType ? `{ ${codegenOptions} }` : '{}')
|
||||
}
|
||||
modelPropsDecl += `\n ${JSON.stringify(name)}: ${decl},`
|
||||
}
|
||||
return `{${modelPropsDecl}\n }`
|
||||
}
|
||||
|
||||
let propsDecls: undefined | string
|
||||
if (ctx.propsRuntimeDecl) {
|
||||
propsDecls = scriptSetup!.content
|
||||
.slice(ctx.propsRuntimeDecl.start!, ctx.propsRuntimeDecl.end!)
|
||||
.trim()
|
||||
if (ctx.propsDestructureDecl) {
|
||||
const defaults: string[] = []
|
||||
for (const key in ctx.propsDestructuredBindings) {
|
||||
const d = genDestructuredDefaultValue(key)
|
||||
if (d)
|
||||
defaults.push(
|
||||
`${key}: ${d.valueString}${
|
||||
d.needSkipFactory ? `, __skip_${key}: true` : ``
|
||||
}`
|
||||
)
|
||||
}
|
||||
if (defaults.length) {
|
||||
propsDecls = `${helper(
|
||||
`mergeDefaults`
|
||||
)}(${propsDecls}, {\n ${defaults.join(',\n ')}\n})`
|
||||
}
|
||||
}
|
||||
} else if (ctx.propsTypeDecl) {
|
||||
propsDecls = genPropsFromTS()
|
||||
}
|
||||
|
||||
const modelsDecls = genModels()
|
||||
|
||||
if (propsDecls && modelsDecls) {
|
||||
return `${helper('mergeModels')}(${propsDecls}, ${modelsDecls})`
|
||||
} else {
|
||||
return modelsDecls || propsDecls
|
||||
}
|
||||
}
|
||||
|
||||
function genDestructuredDefaultValue(
|
||||
key: string,
|
||||
inferredType?: string[]
|
||||
):
|
||||
| {
|
||||
valueString: string
|
||||
needSkipFactory: boolean
|
||||
}
|
||||
| undefined {
|
||||
const destructured = ctx.propsDestructuredBindings[key]
|
||||
const defaultVal = destructured && destructured.default
|
||||
if (defaultVal) {
|
||||
const value = scriptSetup!.content.slice(
|
||||
defaultVal.start!,
|
||||
defaultVal.end!
|
||||
)
|
||||
|
||||
const unwrapped = unwrapTSNode(defaultVal)
|
||||
|
||||
if (
|
||||
inferredType &&
|
||||
inferredType.length &&
|
||||
!inferredType.includes(UNKNOWN_TYPE)
|
||||
) {
|
||||
const valueType = inferValueType(unwrapped)
|
||||
if (valueType && !inferredType.includes(valueType)) {
|
||||
error(
|
||||
`Default value of prop "${key}" does not match declared type.`,
|
||||
unwrapped
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// If the default value is a function or is an identifier referencing
|
||||
// external value, skip factory wrap. This is needed when using
|
||||
// destructure w/ runtime declaration since we cannot safely infer
|
||||
// whether tje expected runtime prop type is `Function`.
|
||||
const needSkipFactory =
|
||||
!inferredType &&
|
||||
(isFunctionType(unwrapped) || unwrapped.type === 'Identifier')
|
||||
|
||||
const needFactoryWrap =
|
||||
!needSkipFactory &&
|
||||
!isLiteralNode(unwrapped) &&
|
||||
!inferredType?.includes('Function')
|
||||
|
||||
return {
|
||||
valueString: needFactoryWrap ? `() => (${value})` : value,
|
||||
needSkipFactory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function genRuntimeEmits() {
|
||||
function genEmitsFromTS() {
|
||||
return typeDeclaredEmits.size
|
||||
|
@ -1019,41 +777,16 @@ export function compileScript(
|
|||
emitsDecl = genEmitsFromTS()
|
||||
}
|
||||
if (ctx.hasDefineModelCall) {
|
||||
let modelEmitsDecl = `[${Object.keys(modelDecls)
|
||||
let modelEmitsDecl = `[${Object.keys(ctx.modelDecls)
|
||||
.map(n => JSON.stringify(`update:${n}`))
|
||||
.join(', ')}]`
|
||||
emitsDecl = emitsDecl
|
||||
? `${helper('mergeModels')}(${emitsDecl}, ${modelEmitsDecl})`
|
||||
? `${ctx.helper('mergeModels')}(${emitsDecl}, ${modelEmitsDecl})`
|
||||
: modelEmitsDecl
|
||||
}
|
||||
return emitsDecl
|
||||
}
|
||||
|
||||
// 0. parse both <script> and <script setup> blocks
|
||||
// const scriptAst =
|
||||
// script &&
|
||||
// parse(
|
||||
// script.content,
|
||||
// {
|
||||
// plugins,
|
||||
// sourceType: 'module'
|
||||
// },
|
||||
// scriptStartOffset!
|
||||
// )
|
||||
|
||||
// const scriptSetupAst = parse(
|
||||
// scriptSetup.content,
|
||||
// {
|
||||
// plugins: [
|
||||
// ...plugins,
|
||||
// // allow top level await but only inside <script setup>
|
||||
// 'topLevelAwait'
|
||||
// ],
|
||||
// sourceType: 'module'
|
||||
// },
|
||||
// startOffset
|
||||
// )
|
||||
|
||||
const scriptAst = ctx.scriptAst
|
||||
const scriptSetupAst = ctx.scriptSetupAst!
|
||||
|
||||
|
@ -1267,7 +1000,7 @@ export function compileScript(
|
|||
)
|
||||
refBindings = rootRefs
|
||||
for (const h of importedHelpers) {
|
||||
helperImports.add(h)
|
||||
ctx.helperImports.add(h)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1458,7 +1191,7 @@ export function compileScript(
|
|||
node.exportKind === 'type') ||
|
||||
(node.type === 'VariableDeclaration' && node.declare)
|
||||
) {
|
||||
recordType(node, declaredTypes)
|
||||
recordType(node, ctx.declaredTypes)
|
||||
if (node.type !== 'TSEnumDeclaration') {
|
||||
hoistNode(node)
|
||||
}
|
||||
|
@ -1493,14 +1226,12 @@ export function compileScript(
|
|||
)
|
||||
refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs
|
||||
for (const h of importedHelpers) {
|
||||
helperImports.add(h)
|
||||
ctx.helperImports.add(h)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. extract runtime props/emits code from setup context type
|
||||
if (ctx.propsTypeDecl) {
|
||||
extractRuntimeProps(ctx.propsTypeDecl, typeDeclaredProps, declaredTypes)
|
||||
}
|
||||
extractRuntimeProps(ctx)
|
||||
if (emitsTypeDecl) {
|
||||
extractRuntimeEmits(emitsTypeDecl, typeDeclaredEmits, error)
|
||||
}
|
||||
|
@ -1541,10 +1272,10 @@ export function compileScript(
|
|||
bindingMetadata[key] = BindingTypes.PROPS
|
||||
}
|
||||
}
|
||||
for (const key in typeDeclaredProps) {
|
||||
for (const key in ctx.typeDeclaredProps) {
|
||||
bindingMetadata[key] = BindingTypes.PROPS
|
||||
}
|
||||
for (const key in modelDecls) {
|
||||
for (const key in ctx.modelDecls) {
|
||||
bindingMetadata[key] = BindingTypes.PROPS
|
||||
}
|
||||
// props aliases
|
||||
|
@ -1592,8 +1323,8 @@ export function compileScript(
|
|||
// no need to do this when targeting SSR
|
||||
!(options.inlineTemplate && options.templateOptions?.ssr)
|
||||
) {
|
||||
helperImports.add(CSS_VARS_HELPER)
|
||||
helperImports.add('unref')
|
||||
ctx.helperImports.add(CSS_VARS_HELPER)
|
||||
ctx.helperImports.add('unref')
|
||||
s.prependLeft(
|
||||
startOffset,
|
||||
`\n${genCssVarsCode(cssVars, bindingMetadata, scopeId, isProd)}\n`
|
||||
|
@ -1617,7 +1348,7 @@ export function compileScript(
|
|||
if (ctx.propsDestructureRestId) {
|
||||
s.prependLeft(
|
||||
startOffset,
|
||||
`\nconst ${ctx.propsDestructureRestId} = ${helper(
|
||||
`\nconst ${ctx.propsDestructureRestId} = ${ctx.helper(
|
||||
`createPropsRestProxy`
|
||||
)}(__props, ${JSON.stringify(
|
||||
Object.keys(ctx.propsDestructuredBindings)
|
||||
|
@ -1734,7 +1465,7 @@ export function compileScript(
|
|||
// as this may get injected by the render function preamble OR the
|
||||
// css vars codegen
|
||||
if (ast && ast.helpers.has(UNREF)) {
|
||||
helperImports.delete('unref')
|
||||
ctx.helperImports.delete('unref')
|
||||
}
|
||||
returned = code
|
||||
} else {
|
||||
|
@ -1768,7 +1499,7 @@ export function compileScript(
|
|||
runtimeOptions += `\n __ssrInlineRender: true,`
|
||||
}
|
||||
|
||||
const propsDecl = genRuntimeProps()
|
||||
const propsDecl = genRuntimeProps(ctx)
|
||||
if (propsDecl) runtimeOptions += `\n props: ${propsDecl},`
|
||||
|
||||
const emitsDecl = genRuntimeEmits()
|
||||
|
@ -1797,7 +1528,7 @@ export function compileScript(
|
|||
(definedOptions ? `\n ...${definedOptions},` : '')
|
||||
s.prependLeft(
|
||||
startOffset,
|
||||
`\n${genDefaultAs} /*#__PURE__*/${helper(
|
||||
`\n${genDefaultAs} /*#__PURE__*/${ctx.helper(
|
||||
`defineComponent`
|
||||
)}({${def}${runtimeOptions}\n ${
|
||||
hasAwait ? `async ` : ``
|
||||
|
@ -1827,9 +1558,9 @@ export function compileScript(
|
|||
}
|
||||
|
||||
// 12. finalize Vue helper imports
|
||||
if (helperImports.size > 0) {
|
||||
if (ctx.helperImports.size > 0) {
|
||||
s.prepend(
|
||||
`import { ${[...helperImports]
|
||||
`import { ${[...ctx.helperImports]
|
||||
.map(h => `${h} as _${h}`)
|
||||
.join(', ')} } from 'vue'\n`
|
||||
)
|
||||
|
@ -2028,13 +1759,6 @@ function walkPattern(
|
|||
}
|
||||
}
|
||||
|
||||
interface PropTypeData {
|
||||
key: string
|
||||
type: string[]
|
||||
required: boolean
|
||||
skipCheck: boolean
|
||||
}
|
||||
|
||||
function recordType(node: Node, declaredTypes: Record<string, string[]>) {
|
||||
if (node.type === 'TSInterfaceDeclaration') {
|
||||
declaredTypes[node.id.name] = [`Object`]
|
||||
|
@ -2050,45 +1774,6 @@ function recordType(node: Node, declaredTypes: Record<string, string[]>) {
|
|||
}
|
||||
}
|
||||
|
||||
function extractRuntimeProps(
|
||||
node: TSTypeLiteral | TSInterfaceBody,
|
||||
props: Record<string, PropTypeData>,
|
||||
declaredTypes: Record<string, string[]>
|
||||
) {
|
||||
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'
|
||||
) {
|
||||
let type: string[] | undefined
|
||||
let skipCheck = false
|
||||
if (m.type === 'TSMethodSignature') {
|
||||
type = ['Function']
|
||||
} else if (m.typeAnnotation) {
|
||||
type = inferRuntimeType(m.typeAnnotation.typeAnnotation, 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']
|
||||
}
|
||||
}
|
||||
}
|
||||
props[m.key.name] = {
|
||||
key: m.key.name,
|
||||
required: !m.optional,
|
||||
type: type || [`null`],
|
||||
skipCheck
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const UNKNOWN_TYPE = 'Unknown'
|
||||
|
||||
function inferRuntimeType(
|
||||
node: TSType,
|
||||
declaredTypes: Record<string, string[]>
|
||||
|
@ -2239,10 +1924,6 @@ function flattenTypes(
|
|||
]
|
||||
}
|
||||
|
||||
function toRuntimeTypeString(types: string[]) {
|
||||
return types.length > 1 ? `[${types.join(', ')}]` : types[0]
|
||||
}
|
||||
|
||||
function inferEnumType(node: TSEnumDeclaration): string[] {
|
||||
const types = new Set<string>()
|
||||
for (const m of node.members) {
|
||||
|
@ -2260,27 +1941,6 @@ function inferEnumType(node: TSEnumDeclaration): string[] {
|
|||
return types.size ? [...types] : ['Number']
|
||||
}
|
||||
|
||||
// non-comprehensive, best-effort type infernece for a runtime value
|
||||
// this is used to catch default value / type declaration mismatches
|
||||
// when using props destructure.
|
||||
function inferValueType(node: Node): string | undefined {
|
||||
switch (node.type) {
|
||||
case 'StringLiteral':
|
||||
return 'String'
|
||||
case 'NumericLiteral':
|
||||
return 'Number'
|
||||
case 'BooleanLiteral':
|
||||
return 'Boolean'
|
||||
case 'ObjectExpression':
|
||||
return 'Object'
|
||||
case 'ArrayExpression':
|
||||
return 'Array'
|
||||
case 'FunctionExpression':
|
||||
case 'ArrowFunctionExpression':
|
||||
return 'Function'
|
||||
}
|
||||
}
|
||||
|
||||
function extractRuntimeEmits(
|
||||
node: TSFunctionType | TSTypeLiteral | TSInterfaceBody,
|
||||
emits: Set<string>,
|
||||
|
@ -2414,10 +2074,6 @@ function isStaticNode(node: Node): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
function isLiteralNode(node: Node) {
|
||||
return node.type.endsWith('Literal')
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze bindings in normal `<script>`
|
||||
* Note that `compileScriptSetup` already analyzes bindings as part of its
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
import { Expression, Node, ObjectPattern, Program } from '@babel/types'
|
||||
import { Node, ObjectPattern, Program } from '@babel/types'
|
||||
import { SFCDescriptor } from '../parse'
|
||||
import { generateCodeFrame } from '@vue/shared'
|
||||
import { PropsDeclType } from './defineProps'
|
||||
import { parse as babelParse, ParserOptions, ParserPlugin } from '@babel/parser'
|
||||
import { SFCScriptCompileOptions } from '../compileScript'
|
||||
import MagicString from 'magic-string'
|
||||
|
||||
export type PropsDestructureBindings = Record<
|
||||
string, // public prop key
|
||||
{
|
||||
local: string // local identifier, may be different
|
||||
default?: Expression
|
||||
}
|
||||
>
|
||||
import {
|
||||
PropsDeclType,
|
||||
PropTypeData,
|
||||
PropsDestructureBindings
|
||||
} from './defineProps'
|
||||
import { ModelDecl } from './defineModel'
|
||||
|
||||
export class ScriptCompileContext {
|
||||
isJS: boolean
|
||||
|
@ -21,14 +17,20 @@ export class ScriptCompileContext {
|
|||
scriptAst: Program | null
|
||||
scriptSetupAst: Program | null
|
||||
|
||||
s = new MagicString(this.descriptor.source)
|
||||
|
||||
// s = new MagicString(this.descriptor.source)
|
||||
startOffset = this.descriptor.scriptSetup?.loc.start.offset
|
||||
endOffset = this.descriptor.scriptSetup?.loc.end.offset
|
||||
|
||||
scriptStartOffset = this.descriptor.script?.loc.start.offset
|
||||
scriptEndOffset = this.descriptor.script?.loc.end.offset
|
||||
|
||||
helperImports: Set<string> = new Set()
|
||||
helper(key: string): string {
|
||||
this.helperImports.add(key)
|
||||
return `_${key}`
|
||||
}
|
||||
|
||||
declaredTypes: Record<string, string[]> = Object.create(null)
|
||||
|
||||
// macros presence check
|
||||
hasDefinePropsCall = false
|
||||
hasDefineEmitCall = false
|
||||
|
@ -47,6 +49,10 @@ export class ScriptCompileContext {
|
|||
propsDestructuredBindings: PropsDestructureBindings = Object.create(null)
|
||||
propsDestructureRestId: string | undefined
|
||||
propsRuntimeDefaults: Node | undefined
|
||||
typeDeclaredProps: Record<string, PropTypeData> = {}
|
||||
|
||||
// defineModel
|
||||
modelDecls: Record<string, ModelDecl> = {}
|
||||
|
||||
constructor(
|
||||
public descriptor: SFCDescriptor,
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import { TSType } from '@babel/types'
|
||||
import { ScriptCompileContext } from './context'
|
||||
import { inferRuntimeType } from './resolveType'
|
||||
import { UNKNOWN_TYPE, concatStrings, toRuntimeTypeString } from './utils'
|
||||
|
||||
export interface ModelDecl {
|
||||
type: TSType | undefined
|
||||
options: string | undefined
|
||||
identifier: string | undefined
|
||||
}
|
||||
|
||||
export function genModels(ctx: ScriptCompileContext) {
|
||||
if (!ctx.hasDefineModelCall) return
|
||||
|
||||
const isProd = !!ctx.options.isProd
|
||||
let modelPropsDecl = ''
|
||||
for (const [name, { type, options }] of Object.entries(ctx.modelDecls)) {
|
||||
let skipCheck = false
|
||||
|
||||
let runtimeTypes = type && inferRuntimeType(type, ctx.declaredTypes)
|
||||
if (runtimeTypes) {
|
||||
const hasUnknownType = runtimeTypes.includes(UNKNOWN_TYPE)
|
||||
|
||||
runtimeTypes = runtimeTypes.filter(el => {
|
||||
if (el === UNKNOWN_TYPE) return false
|
||||
return isProd
|
||||
? el === 'Boolean' || (el === 'Function' && options)
|
||||
: true
|
||||
})
|
||||
skipCheck = !isProd && hasUnknownType && runtimeTypes.length > 0
|
||||
}
|
||||
|
||||
let runtimeType =
|
||||
(runtimeTypes &&
|
||||
runtimeTypes.length > 0 &&
|
||||
toRuntimeTypeString(runtimeTypes)) ||
|
||||
undefined
|
||||
|
||||
const codegenOptions = concatStrings([
|
||||
runtimeType && `type: ${runtimeType}`,
|
||||
skipCheck && 'skipCheck: true'
|
||||
])
|
||||
|
||||
let decl: string
|
||||
if (runtimeType && options) {
|
||||
decl = ctx.isTS
|
||||
? `{ ${codegenOptions}, ...${options} }`
|
||||
: `Object.assign({ ${codegenOptions} }, ${options})`
|
||||
} else {
|
||||
decl = options || (runtimeType ? `{ ${codegenOptions} }` : '{}')
|
||||
}
|
||||
modelPropsDecl += `\n ${JSON.stringify(name)}: ${decl},`
|
||||
}
|
||||
return `{${modelPropsDecl}\n }`
|
||||
}
|
|
@ -3,20 +3,47 @@ import {
|
|||
LVal,
|
||||
Identifier,
|
||||
TSTypeLiteral,
|
||||
TSInterfaceBody
|
||||
TSInterfaceBody,
|
||||
ObjectProperty,
|
||||
ObjectMethod,
|
||||
ObjectExpression,
|
||||
Expression
|
||||
} from '@babel/types'
|
||||
import { isCallOf } from '@vue/compiler-dom'
|
||||
import { isFunctionType } from '@vue/compiler-dom'
|
||||
import { ScriptCompileContext } from './context'
|
||||
import { resolveObjectKey } from './utils'
|
||||
import { resolveQualifiedType } from './resolveType'
|
||||
import { inferRuntimeType, resolveQualifiedType } from './resolveType'
|
||||
import {
|
||||
FromNormalScript,
|
||||
resolveObjectKey,
|
||||
UNKNOWN_TYPE,
|
||||
concatStrings,
|
||||
isLiteralNode,
|
||||
isCallOf,
|
||||
unwrapTSNode,
|
||||
toRuntimeTypeString
|
||||
} from './utils'
|
||||
import { genModels } from './defineModel'
|
||||
|
||||
export const DEFINE_PROPS = 'defineProps'
|
||||
export const WITH_DEFAULTS = 'withDefaults'
|
||||
|
||||
export type PropsDeclType = (TSTypeLiteral | TSInterfaceBody) & {
|
||||
__fromNormalScript?: boolean | null
|
||||
export type PropsDeclType = FromNormalScript<TSTypeLiteral | TSInterfaceBody>
|
||||
|
||||
export interface PropTypeData {
|
||||
key: string
|
||||
type: string[]
|
||||
required: boolean
|
||||
skipCheck: boolean
|
||||
}
|
||||
|
||||
export type PropsDestructureBindings = Record<
|
||||
string, // public prop key
|
||||
{
|
||||
local: string // local identifier, may be different
|
||||
default?: Expression
|
||||
}
|
||||
>
|
||||
|
||||
export function processDefineProps(
|
||||
ctx: ScriptCompileContext,
|
||||
node: Node,
|
||||
|
@ -146,3 +173,238 @@ function processWithDefaults(
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function extractRuntimeProps(ctx: ScriptCompileContext) {
|
||||
const node = ctx.propsTypeDecl
|
||||
if (!node) return
|
||||
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'
|
||||
) {
|
||||
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']
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.typeDeclaredProps[m.key.name] = {
|
||||
key: m.key.name,
|
||||
required: !m.optional,
|
||||
type: type || [`null`],
|
||||
skipCheck
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
|
||||
let propsDecls: undefined | string
|
||||
if (ctx.propsRuntimeDecl) {
|
||||
propsDecls = ctx.getString(ctx.propsRuntimeDecl).trim()
|
||||
if (ctx.propsDestructureDecl) {
|
||||
const defaults: string[] = []
|
||||
for (const key in ctx.propsDestructuredBindings) {
|
||||
const d = genDestructuredDefaultValue(ctx, key)
|
||||
if (d)
|
||||
defaults.push(
|
||||
`${key}: ${d.valueString}${
|
||||
d.needSkipFactory ? `, __skip_${key}: true` : ``
|
||||
}`
|
||||
)
|
||||
}
|
||||
if (defaults.length) {
|
||||
propsDecls = `${ctx.helper(
|
||||
`mergeDefaults`
|
||||
)}(${propsDecls}, {\n ${defaults.join(',\n ')}\n})`
|
||||
}
|
||||
}
|
||||
} else if (ctx.propsTypeDecl) {
|
||||
propsDecls = genPropsFromTS(ctx)
|
||||
}
|
||||
|
||||
const modelsDecls = genModels(ctx)
|
||||
|
||||
if (propsDecls && modelsDecls) {
|
||||
return `${ctx.helper('mergeModels')}(${propsDecls}, ${modelsDecls})`
|
||||
} else {
|
||||
return modelsDecls || propsDecls
|
||||
}
|
||||
}
|
||||
|
||||
function genPropsFromTS(ctx: ScriptCompileContext) {
|
||||
const keys = Object.keys(ctx.typeDeclaredProps)
|
||||
if (!keys.length) return
|
||||
|
||||
const hasStaticDefaults = hasStaticWithDefaults(ctx)
|
||||
let propsDecls = `{
|
||||
${keys
|
||||
.map(key => {
|
||||
let defaultString: string | undefined
|
||||
const destructured = genDestructuredDefaultValue(
|
||||
ctx,
|
||||
key,
|
||||
ctx.typeDeclaredProps[key].type
|
||||
)
|
||||
if (destructured) {
|
||||
defaultString = `default: ${destructured.valueString}${
|
||||
destructured.needSkipFactory ? `, skipFactory: true` : ``
|
||||
}`
|
||||
} else if (hasStaticDefaults) {
|
||||
const prop = (
|
||||
ctx.propsRuntimeDefaults as ObjectExpression
|
||||
).properties.find(node => {
|
||||
if (node.type === 'SpreadElement') return false
|
||||
return resolveObjectKey(node.key, node.computed) === key
|
||||
}) as ObjectProperty | ObjectMethod
|
||||
if (prop) {
|
||||
if (prop.type === 'ObjectProperty') {
|
||||
// prop has corresponding static default value
|
||||
defaultString = `default: ${ctx.getString(prop.value)}`
|
||||
} else {
|
||||
defaultString = `${prop.async ? 'async ' : ''}${
|
||||
prop.kind !== 'method' ? `${prop.kind} ` : ''
|
||||
}default() ${ctx.getString(prop.body)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { type, required, skipCheck } = ctx.typeDeclaredProps[key]
|
||||
if (!ctx.options.isProd) {
|
||||
return `${key}: { ${concatStrings([
|
||||
`type: ${toRuntimeTypeString(type)}`,
|
||||
`required: ${required}`,
|
||||
skipCheck && 'skipCheck: true',
|
||||
defaultString
|
||||
])} }`
|
||||
} else if (
|
||||
type.some(
|
||||
el =>
|
||||
el === 'Boolean' ||
|
||||
((!hasStaticDefaults || defaultString) && el === 'Function')
|
||||
)
|
||||
) {
|
||||
// #4783 for boolean, should keep the type
|
||||
// #7111 for function, if default value exists or it's not static, should keep it
|
||||
// in production
|
||||
return `${key}: { ${concatStrings([
|
||||
`type: ${toRuntimeTypeString(type)}`,
|
||||
defaultString
|
||||
])} }`
|
||||
} else {
|
||||
// production: checks are useless
|
||||
return `${key}: ${defaultString ? `{ ${defaultString} }` : `{}`}`
|
||||
}
|
||||
})
|
||||
.join(',\n ')}\n }`
|
||||
|
||||
if (ctx.propsRuntimeDefaults && !hasStaticDefaults) {
|
||||
propsDecls = `${ctx.helper('mergeDefaults')}(${propsDecls}, ${ctx.getString(
|
||||
ctx.propsRuntimeDefaults
|
||||
)})`
|
||||
}
|
||||
|
||||
return propsDecls
|
||||
}
|
||||
|
||||
/**
|
||||
* check defaults. If the default object is an object literal with only
|
||||
* static properties, we can directly generate more optimized default
|
||||
* declarations. Otherwise we will have to fallback to runtime merging.
|
||||
*/
|
||||
function hasStaticWithDefaults(ctx: ScriptCompileContext) {
|
||||
return (
|
||||
ctx.propsRuntimeDefaults &&
|
||||
ctx.propsRuntimeDefaults.type === 'ObjectExpression' &&
|
||||
ctx.propsRuntimeDefaults.properties.every(
|
||||
node =>
|
||||
node.type !== 'SpreadElement' &&
|
||||
(!node.computed || node.key.type.endsWith('Literal'))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function genDestructuredDefaultValue(
|
||||
ctx: ScriptCompileContext,
|
||||
key: string,
|
||||
inferredType?: string[]
|
||||
):
|
||||
| {
|
||||
valueString: string
|
||||
needSkipFactory: boolean
|
||||
}
|
||||
| undefined {
|
||||
const destructured = ctx.propsDestructuredBindings[key]
|
||||
const defaultVal = destructured && destructured.default
|
||||
if (defaultVal) {
|
||||
const value = ctx.getString(defaultVal)
|
||||
const unwrapped = unwrapTSNode(defaultVal)
|
||||
|
||||
if (
|
||||
inferredType &&
|
||||
inferredType.length &&
|
||||
!inferredType.includes(UNKNOWN_TYPE)
|
||||
) {
|
||||
const valueType = inferValueType(unwrapped)
|
||||
if (valueType && !inferredType.includes(valueType)) {
|
||||
ctx.error(
|
||||
`Default value of prop "${key}" does not match declared type.`,
|
||||
unwrapped
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// If the default value is a function or is an identifier referencing
|
||||
// external value, skip factory wrap. This is needed when using
|
||||
// destructure w/ runtime declaration since we cannot safely infer
|
||||
// whether tje expected runtime prop type is `Function`.
|
||||
const needSkipFactory =
|
||||
!inferredType &&
|
||||
(isFunctionType(unwrapped) || unwrapped.type === 'Identifier')
|
||||
|
||||
const needFactoryWrap =
|
||||
!needSkipFactory &&
|
||||
!isLiteralNode(unwrapped) &&
|
||||
!inferredType?.includes('Function')
|
||||
|
||||
return {
|
||||
valueString: needFactoryWrap ? `() => (${value})` : value,
|
||||
needSkipFactory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// non-comprehensive, best-effort type infernece for a runtime value
|
||||
// this is used to catch default value / type declaration mismatches
|
||||
// when using props destructure.
|
||||
function inferValueType(node: Node): string | undefined {
|
||||
switch (node.type) {
|
||||
case 'StringLiteral':
|
||||
return 'String'
|
||||
case 'NumericLiteral':
|
||||
return 'Number'
|
||||
case 'BooleanLiteral':
|
||||
return 'Boolean'
|
||||
case 'ObjectExpression':
|
||||
return 'Object'
|
||||
case 'ArrayExpression':
|
||||
return 'Array'
|
||||
case 'FunctionExpression':
|
||||
case 'ArrowFunctionExpression':
|
||||
return 'Function'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,12 +13,11 @@ import {
|
|||
isInDestructureAssignment,
|
||||
isReferencedIdentifier,
|
||||
isStaticProperty,
|
||||
walkFunctionParams,
|
||||
isCallOf,
|
||||
unwrapTSNode
|
||||
walkFunctionParams
|
||||
} from '@vue/compiler-core'
|
||||
import { genPropsAccessExp } from '@vue/shared'
|
||||
import { PropsDestructureBindings } from '../compileScript'
|
||||
import { isCallOf, unwrapTSNode } from './utils'
|
||||
|
||||
/**
|
||||
* true -> prop binding
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import { Node, Statement, TSInterfaceBody, TSTypeElement } from '@babel/types'
|
||||
import { FromNormalScript } from './utils'
|
||||
import {
|
||||
Node,
|
||||
Statement,
|
||||
TSInterfaceBody,
|
||||
TSType,
|
||||
TSTypeElement
|
||||
} from '@babel/types'
|
||||
import { FromNormalScript, UNKNOWN_TYPE } from './utils'
|
||||
import { ScriptCompileContext } from './context'
|
||||
|
||||
/**
|
||||
|
@ -112,3 +118,153 @@ function filterExtendsType(extendsTypes: Node[], bodies: TSTypeElement[]) {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function inferRuntimeType(
|
||||
node: TSType,
|
||||
declaredTypes: Record<string, string[]>
|
||||
): string[] {
|
||||
switch (node.type) {
|
||||
case 'TSStringKeyword':
|
||||
return ['String']
|
||||
case 'TSNumberKeyword':
|
||||
return ['Number']
|
||||
case 'TSBooleanKeyword':
|
||||
return ['Boolean']
|
||||
case 'TSObjectKeyword':
|
||||
return ['Object']
|
||||
case 'TSNullKeyword':
|
||||
return ['null']
|
||||
case 'TSTypeLiteral': {
|
||||
// TODO (nice to have) generate runtime property validation
|
||||
const types = new Set<string>()
|
||||
for (const m of node.members) {
|
||||
if (
|
||||
m.type === 'TSCallSignatureDeclaration' ||
|
||||
m.type === 'TSConstructSignatureDeclaration'
|
||||
) {
|
||||
types.add('Function')
|
||||
} else {
|
||||
types.add('Object')
|
||||
}
|
||||
}
|
||||
return types.size ? Array.from(types) : ['Object']
|
||||
}
|
||||
case 'TSFunctionType':
|
||||
return ['Function']
|
||||
case 'TSArrayType':
|
||||
case 'TSTupleType':
|
||||
// TODO (nice to have) generate runtime element type/length checks
|
||||
return ['Array']
|
||||
|
||||
case 'TSLiteralType':
|
||||
switch (node.literal.type) {
|
||||
case 'StringLiteral':
|
||||
return ['String']
|
||||
case 'BooleanLiteral':
|
||||
return ['Boolean']
|
||||
case 'NumericLiteral':
|
||||
case 'BigIntLiteral':
|
||||
return ['Number']
|
||||
default:
|
||||
return [UNKNOWN_TYPE]
|
||||
}
|
||||
|
||||
case 'TSTypeReference':
|
||||
if (node.typeName.type === 'Identifier') {
|
||||
if (declaredTypes[node.typeName.name]) {
|
||||
return declaredTypes[node.typeName.name]
|
||||
}
|
||||
switch (node.typeName.name) {
|
||||
case 'Array':
|
||||
case 'Function':
|
||||
case 'Object':
|
||||
case 'Set':
|
||||
case 'Map':
|
||||
case 'WeakSet':
|
||||
case 'WeakMap':
|
||||
case 'Date':
|
||||
case 'Promise':
|
||||
return [node.typeName.name]
|
||||
|
||||
// TS built-in utility types
|
||||
// https://www.typescriptlang.org/docs/handbook/utility-types.html
|
||||
case 'Partial':
|
||||
case 'Required':
|
||||
case 'Readonly':
|
||||
case 'Record':
|
||||
case 'Pick':
|
||||
case 'Omit':
|
||||
case 'InstanceType':
|
||||
return ['Object']
|
||||
|
||||
case 'Uppercase':
|
||||
case 'Lowercase':
|
||||
case 'Capitalize':
|
||||
case 'Uncapitalize':
|
||||
return ['String']
|
||||
|
||||
case 'Parameters':
|
||||
case 'ConstructorParameters':
|
||||
return ['Array']
|
||||
|
||||
case 'NonNullable':
|
||||
if (node.typeParameters && node.typeParameters.params[0]) {
|
||||
return inferRuntimeType(
|
||||
node.typeParameters.params[0],
|
||||
declaredTypes
|
||||
).filter(t => t !== 'null')
|
||||
}
|
||||
break
|
||||
case 'Extract':
|
||||
if (node.typeParameters && node.typeParameters.params[1]) {
|
||||
return inferRuntimeType(
|
||||
node.typeParameters.params[1],
|
||||
declaredTypes
|
||||
)
|
||||
}
|
||||
break
|
||||
case 'Exclude':
|
||||
case 'OmitThisParameter':
|
||||
if (node.typeParameters && node.typeParameters.params[0]) {
|
||||
return inferRuntimeType(
|
||||
node.typeParameters.params[0],
|
||||
declaredTypes
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
// cannot infer, fallback to UNKNOWN: ThisParameterType
|
||||
return [UNKNOWN_TYPE]
|
||||
|
||||
case 'TSParenthesizedType':
|
||||
return inferRuntimeType(node.typeAnnotation, declaredTypes)
|
||||
|
||||
case 'TSUnionType':
|
||||
return flattenTypes(node.types, declaredTypes)
|
||||
case 'TSIntersectionType': {
|
||||
return flattenTypes(node.types, declaredTypes).filter(
|
||||
t => t !== UNKNOWN_TYPE
|
||||
)
|
||||
}
|
||||
|
||||
case 'TSSymbolKeyword':
|
||||
return ['Symbol']
|
||||
|
||||
default:
|
||||
return [UNKNOWN_TYPE] // no runtime check
|
||||
}
|
||||
}
|
||||
|
||||
function flattenTypes(
|
||||
types: TSType[],
|
||||
declaredTypes: Record<string, string[]>
|
||||
): string[] {
|
||||
return [
|
||||
...new Set(
|
||||
([] as string[]).concat(
|
||||
...types.map(t => inferRuntimeType(t, declaredTypes))
|
||||
)
|
||||
)
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import { Node } from '@babel/types'
|
||||
import { CallExpression, Node } from '@babel/types'
|
||||
import { TS_NODE_TYPES } from '@vue/compiler-dom'
|
||||
|
||||
export const UNKNOWN_TYPE = 'Unknown'
|
||||
|
||||
export type FromNormalScript<T> = T & { __fromNormalScript?: boolean | null }
|
||||
|
||||
|
@ -12,3 +15,38 @@ export function resolveObjectKey(node: Node, computed: boolean) {
|
|||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function concatStrings(strs: Array<string | null | undefined | false>) {
|
||||
return strs.filter((s): s is string => !!s).join(', ')
|
||||
}
|
||||
|
||||
export function isLiteralNode(node: Node) {
|
||||
return node.type.endsWith('Literal')
|
||||
}
|
||||
|
||||
export function unwrapTSNode(node: Node): Node {
|
||||
if (TS_NODE_TYPES.includes(node.type)) {
|
||||
return unwrapTSNode((node as any).expression)
|
||||
} else {
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
export function isCallOf(
|
||||
node: Node | null | undefined,
|
||||
test: string | ((id: string) => boolean) | null | undefined
|
||||
): node is CallExpression {
|
||||
return !!(
|
||||
node &&
|
||||
test &&
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
(typeof test === 'string'
|
||||
? node.callee.name === test
|
||||
: test(node.callee.name))
|
||||
)
|
||||
}
|
||||
|
||||
export function toRuntimeTypeString(types: string[]) {
|
||||
return types.length > 1 ? `[${types.join(', ')}]` : types[0]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue