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'
TSInterfaceBody,
TSTypeElement,
AwaitExpression,
- ObjectMethod,
LVal,
Expression,
TSEnumDeclaration
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'
type EmitsDeclType = FromNormalScript<
TSFunctionType | TSTypeLiteral | TSInterfaceBody
>
-interface ModelDecl {
- type: TSType | undefined
- options: string | undefined
- identifier: string | undefined
-}
/**
* Compile `<script setup>`
// 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)
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)
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,
s.overwrite(
startOffset + node.start!,
startOffset + node.end!,
- `${helper('useSlots')}()`
+ `${ctx.helper('useSlots')}()`
)
}
options = arg0
}
- if (modelDecls[modelName]) {
+ if (ctx.modelDecls[modelName]) {
error(`duplicate model name ${JSON.stringify(modelName)}`, node)
}
? s.slice(startOffset + options.start!, startOffset + options.end!)
: undefined
- modelDecls[modelName] = {
+ ctx.modelDecls[modelName] = {
type,
options: optionsString,
identifier:
s.overwrite(
startOffset + node.start!,
startOffset + node.end!,
- `${helper('useModel')}(__props, ${JSON.stringify(modelName)}${
+ `${ctx.helper('useModel')}(__props, ${JSON.stringify(modelName)}${
runtimeOptions ? `, ${runtimeOptions}` : ``
})`
)
s.overwrite(
node.start! + startOffset,
argumentStart + startOffset,
- `${needSemi ? `;` : ``}(\n ([__temp,__restore] = ${helper(
+ `${needSemi ? `;` : ``}(\n ([__temp,__restore] = ${ctx.helper(
`withAsyncContext`
)}(${containsNestedAwait ? `async ` : ``}() => `
)
)
}
- /**
- * 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
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!
)
refBindings = rootRefs
for (const h of importedHelpers) {
- helperImports.add(h)
+ ctx.helperImports.add(h)
}
}
node.exportKind === 'type') ||
(node.type === 'VariableDeclaration' && node.declare)
) {
- recordType(node, declaredTypes)
+ recordType(node, ctx.declaredTypes)
if (node.type !== 'TSEnumDeclaration') {
hoistNode(node)
}
)
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)
}
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
// 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`
if (ctx.propsDestructureRestId) {
s.prependLeft(
startOffset,
- `\nconst ${ctx.propsDestructureRestId} = ${helper(
+ `\nconst ${ctx.propsDestructureRestId} = ${ctx.helper(
`createPropsRestProxy`
)}(__props, ${JSON.stringify(
Object.keys(ctx.propsDestructuredBindings)
// 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 {
runtimeOptions += `\n __ssrInlineRender: true,`
}
- const propsDecl = genRuntimeProps()
+ const propsDecl = genRuntimeProps(ctx)
if (propsDecl) runtimeOptions += `\n props: ${propsDecl},`
const emitsDecl = genRuntimeEmits()
(definedOptions ? `\n ...${definedOptions},` : '')
s.prependLeft(
startOffset,
- `\n${genDefaultAs} /*#__PURE__*/${helper(
+ `\n${genDefaultAs} /*#__PURE__*/${ctx.helper(
`defineComponent`
)}({${def}${runtimeOptions}\n ${
hasAwait ? `async ` : ``
}
// 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`
)
}
}
-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`]
}
}
-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[]>
]
}
-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) {
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>,
}
}
-function isLiteralNode(node: Node) {
- return node.type.endsWith('Literal')
-}
-
/**
* Analyze bindings in normal `<script>`
* Note that `compileScriptSetup` already analyzes bindings as part of its
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,
}
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'
+ }
+}