mirror of https://github.com/vuejs/core.git
176 lines
5.0 KiB
TypeScript
176 lines
5.0 KiB
TypeScript
import type { LVal, Node, TSType } from '@babel/types'
|
|
import type { ScriptCompileContext } from './context'
|
|
import { inferRuntimeType } from './resolveType'
|
|
import {
|
|
UNKNOWN_TYPE,
|
|
concatStrings,
|
|
isCallOf,
|
|
toRuntimeTypeString,
|
|
} from './utils'
|
|
import { BindingTypes, unwrapTSNode } from '@vue/compiler-dom'
|
|
|
|
export const DEFINE_MODEL = 'defineModel'
|
|
|
|
export interface ModelDecl {
|
|
type: TSType | undefined
|
|
options: string | undefined
|
|
identifier: string | undefined
|
|
runtimeOptionNodes: Node[]
|
|
}
|
|
|
|
export function processDefineModel(
|
|
ctx: ScriptCompileContext,
|
|
node: Node,
|
|
declId?: LVal,
|
|
): boolean {
|
|
if (!isCallOf(node, DEFINE_MODEL)) {
|
|
return false
|
|
}
|
|
|
|
ctx.hasDefineModelCall = true
|
|
|
|
const type =
|
|
(node.typeParameters && node.typeParameters.params[0]) || undefined
|
|
let modelName: string
|
|
let options: Node | undefined
|
|
const arg0 = node.arguments[0] && unwrapTSNode(node.arguments[0])
|
|
const hasName = arg0 && arg0.type === 'StringLiteral'
|
|
if (hasName) {
|
|
modelName = arg0.value
|
|
options = node.arguments[1]
|
|
} else {
|
|
modelName = 'modelValue'
|
|
options = arg0
|
|
}
|
|
|
|
if (ctx.modelDecls[modelName]) {
|
|
ctx.error(`duplicate model name ${JSON.stringify(modelName)}`, node)
|
|
}
|
|
|
|
let optionsString = options && ctx.getString(options)
|
|
let optionsRemoved = !options
|
|
const runtimeOptionNodes: Node[] = []
|
|
|
|
if (
|
|
options &&
|
|
options.type === 'ObjectExpression' &&
|
|
!options.properties.some(p => p.type === 'SpreadElement' || p.computed)
|
|
) {
|
|
let removed = 0
|
|
for (let i = options.properties.length - 1; i >= 0; i--) {
|
|
const p = options.properties[i]
|
|
const next = options.properties[i + 1]
|
|
const start = p.start!
|
|
const end = next ? next.start! : options.end! - 1
|
|
if (
|
|
(p.type === 'ObjectProperty' || p.type === 'ObjectMethod') &&
|
|
((p.key.type === 'Identifier' &&
|
|
(p.key.name === 'get' || p.key.name === 'set')) ||
|
|
(p.key.type === 'StringLiteral' &&
|
|
(p.key.value === 'get' || p.key.value === 'set')))
|
|
) {
|
|
// remove runtime-only options from prop options to avoid duplicates
|
|
optionsString =
|
|
optionsString.slice(0, start - options.start!) +
|
|
optionsString.slice(end - options.start!)
|
|
} else {
|
|
// remove prop options from runtime options
|
|
removed++
|
|
ctx.s.remove(ctx.startOffset! + start, ctx.startOffset! + end)
|
|
// record prop options for invalid scope var reference check
|
|
runtimeOptionNodes.push(p)
|
|
}
|
|
}
|
|
if (removed === options.properties.length) {
|
|
optionsRemoved = true
|
|
ctx.s.remove(
|
|
ctx.startOffset! + (hasName ? arg0.end! : options.start!),
|
|
ctx.startOffset! + options.end!,
|
|
)
|
|
}
|
|
}
|
|
|
|
ctx.modelDecls[modelName] = {
|
|
type,
|
|
options: optionsString,
|
|
runtimeOptionNodes,
|
|
identifier:
|
|
declId && declId.type === 'Identifier' ? declId.name : undefined,
|
|
}
|
|
// register binding type
|
|
ctx.bindingMetadata[modelName] = BindingTypes.PROPS
|
|
|
|
// defineModel -> useModel
|
|
ctx.s.overwrite(
|
|
ctx.startOffset! + node.callee.start!,
|
|
ctx.startOffset! + node.callee.end!,
|
|
ctx.helper('useModel'),
|
|
)
|
|
// inject arguments
|
|
ctx.s.appendLeft(
|
|
ctx.startOffset! +
|
|
(node.arguments.length ? node.arguments[0].start! : node.end! - 1),
|
|
`__props, ` +
|
|
(hasName
|
|
? ``
|
|
: `${JSON.stringify(modelName)}${optionsRemoved ? `` : `, `}`),
|
|
)
|
|
|
|
return true
|
|
}
|
|
|
|
export function genModelProps(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(ctx, type)
|
|
if (runtimeTypes) {
|
|
const hasBoolean = runtimeTypes.includes('Boolean')
|
|
const hasUnknownType = runtimeTypes.includes(UNKNOWN_TYPE)
|
|
|
|
if (isProd || hasUnknownType) {
|
|
runtimeTypes = runtimeTypes.filter(
|
|
t =>
|
|
t === 'Boolean' ||
|
|
(hasBoolean && t === 'String') ||
|
|
(t === 'Function' && options),
|
|
)
|
|
|
|
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},`
|
|
|
|
// also generate modifiers prop
|
|
const modifierPropName = JSON.stringify(
|
|
name === 'modelValue' ? `modelModifiers` : `${name}Modifiers`,
|
|
)
|
|
modelPropsDecl += `\n ${modifierPropName}: {},`
|
|
}
|
|
return `{${modelPropsDecl}\n }`
|
|
}
|