refactor(compiler-sfc): extract more defineProps logic

This commit is contained in:
Evan You 2023-04-11 13:45:45 +08:00
parent fe9760188d
commit d0ac57872c
8 changed files with 578 additions and 429 deletions

View File

@ -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))
)
}

View File

@ -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

View File

@ -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,

View File

@ -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 }`
}

View File

@ -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'
}
}

View File

@ -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

View File

@ -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))
)
)
]
}

View File

@ -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]
}