mirror of https://github.com/vuejs/core.git
307 lines
8.4 KiB
TypeScript
307 lines
8.4 KiB
TypeScript
import {
|
|
Node,
|
|
Identifier,
|
|
BlockStatement,
|
|
Program,
|
|
VariableDeclaration,
|
|
ObjectPattern,
|
|
Expression
|
|
} from '@babel/types'
|
|
import { walk } from 'estree-walker'
|
|
import {
|
|
BindingTypes,
|
|
extractIdentifiers,
|
|
isFunctionType,
|
|
isInDestructureAssignment,
|
|
isReferencedIdentifier,
|
|
isStaticProperty,
|
|
walkFunctionParams
|
|
} from '@vue/compiler-dom'
|
|
import { genPropsAccessExp } from '@vue/shared'
|
|
import { isCallOf, resolveObjectKey, unwrapTSNode } from './utils'
|
|
import { ScriptCompileContext } from './context'
|
|
import { DEFINE_PROPS } from './defineProps'
|
|
import { warnOnce } from '../warn'
|
|
|
|
export function processPropsDestructure(
|
|
ctx: ScriptCompileContext,
|
|
declId: ObjectPattern
|
|
) {
|
|
if (!ctx.options.propsDestructure) {
|
|
return
|
|
}
|
|
|
|
warnOnce(
|
|
`This project is using reactive props destructure, which is an experimental ` +
|
|
`feature. It may receive breaking changes or be removed in the future, so ` +
|
|
`use at your own risk.\n` +
|
|
`To stay updated, follow the RFC at https://github.com/vuejs/rfcs/discussions/502.`
|
|
)
|
|
|
|
ctx.propsDestructureDecl = declId
|
|
|
|
const registerBinding = (
|
|
key: string,
|
|
local: string,
|
|
defaultValue?: Expression
|
|
) => {
|
|
ctx.propsDestructuredBindings[key] = { local, default: defaultValue }
|
|
if (local !== key) {
|
|
ctx.bindingMetadata[local] = BindingTypes.PROPS_ALIASED
|
|
;(ctx.bindingMetadata.__propsAliases ||
|
|
(ctx.bindingMetadata.__propsAliases = {}))[local] = key
|
|
}
|
|
}
|
|
|
|
for (const prop of declId.properties) {
|
|
if (prop.type === 'ObjectProperty') {
|
|
const propKey = resolveObjectKey(prop.key, prop.computed)
|
|
|
|
if (!propKey) {
|
|
ctx.error(
|
|
`${DEFINE_PROPS}() destructure cannot use computed key.`,
|
|
prop.key
|
|
)
|
|
}
|
|
|
|
if (prop.value.type === 'AssignmentPattern') {
|
|
// default value { foo = 123 }
|
|
const { left, right } = prop.value
|
|
if (left.type !== 'Identifier') {
|
|
ctx.error(
|
|
`${DEFINE_PROPS}() destructure does not support nested patterns.`,
|
|
left
|
|
)
|
|
}
|
|
registerBinding(propKey, left.name, right)
|
|
} else if (prop.value.type === 'Identifier') {
|
|
// simple destructure
|
|
registerBinding(propKey, prop.value.name)
|
|
} else {
|
|
ctx.error(
|
|
`${DEFINE_PROPS}() destructure does not support nested patterns.`,
|
|
prop.value
|
|
)
|
|
}
|
|
} else {
|
|
// rest spread
|
|
ctx.propsDestructureRestId = (prop.argument as Identifier).name
|
|
// register binding
|
|
ctx.bindingMetadata[ctx.propsDestructureRestId] =
|
|
BindingTypes.SETUP_REACTIVE_CONST
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* true -> prop binding
|
|
* false -> local binding
|
|
*/
|
|
type Scope = Record<string, boolean>
|
|
|
|
export function transformDestructuredProps(
|
|
ctx: ScriptCompileContext,
|
|
vueImportAliases: Record<string, string>
|
|
) {
|
|
if (!ctx.options.propsDestructure) {
|
|
return
|
|
}
|
|
|
|
const rootScope: Scope = {}
|
|
const scopeStack: Scope[] = [rootScope]
|
|
let currentScope: Scope = rootScope
|
|
const excludedIds = new WeakSet<Identifier>()
|
|
const parentStack: Node[] = []
|
|
const propsLocalToPublicMap: Record<string, string> = Object.create(null)
|
|
|
|
for (const key in ctx.propsDestructuredBindings) {
|
|
const { local } = ctx.propsDestructuredBindings[key]
|
|
rootScope[local] = true
|
|
propsLocalToPublicMap[local] = key
|
|
}
|
|
|
|
function pushScope() {
|
|
scopeStack.push((currentScope = Object.create(currentScope)))
|
|
}
|
|
|
|
function popScope() {
|
|
scopeStack.pop()
|
|
currentScope = scopeStack[scopeStack.length - 1] || null
|
|
}
|
|
|
|
function registerLocalBinding(id: Identifier) {
|
|
excludedIds.add(id)
|
|
if (currentScope) {
|
|
currentScope[id.name] = false
|
|
} else {
|
|
ctx.error(
|
|
'registerBinding called without active scope, something is wrong.',
|
|
id
|
|
)
|
|
}
|
|
}
|
|
|
|
function walkScope(node: Program | BlockStatement, isRoot = false) {
|
|
for (const stmt of node.body) {
|
|
if (stmt.type === 'VariableDeclaration') {
|
|
walkVariableDeclaration(stmt, isRoot)
|
|
} else if (
|
|
stmt.type === 'FunctionDeclaration' ||
|
|
stmt.type === 'ClassDeclaration'
|
|
) {
|
|
if (stmt.declare || !stmt.id) continue
|
|
registerLocalBinding(stmt.id)
|
|
} else if (
|
|
(stmt.type === 'ForOfStatement' || stmt.type === 'ForInStatement') &&
|
|
stmt.left.type === 'VariableDeclaration'
|
|
) {
|
|
walkVariableDeclaration(stmt.left)
|
|
} else if (
|
|
stmt.type === 'ExportNamedDeclaration' &&
|
|
stmt.declaration &&
|
|
stmt.declaration.type === 'VariableDeclaration'
|
|
) {
|
|
walkVariableDeclaration(stmt.declaration, isRoot)
|
|
} else if (
|
|
stmt.type === 'LabeledStatement' &&
|
|
stmt.body.type === 'VariableDeclaration'
|
|
) {
|
|
walkVariableDeclaration(stmt.body, isRoot)
|
|
}
|
|
}
|
|
}
|
|
|
|
function walkVariableDeclaration(stmt: VariableDeclaration, isRoot = false) {
|
|
if (stmt.declare) {
|
|
return
|
|
}
|
|
for (const decl of stmt.declarations) {
|
|
const isDefineProps =
|
|
isRoot && decl.init && isCallOf(unwrapTSNode(decl.init), 'defineProps')
|
|
for (const id of extractIdentifiers(decl.id)) {
|
|
if (isDefineProps) {
|
|
// for defineProps destructure, only exclude them since they
|
|
// are already passed in as knownProps
|
|
excludedIds.add(id)
|
|
} else {
|
|
registerLocalBinding(id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function rewriteId(id: Identifier, parent: Node, parentStack: Node[]) {
|
|
if (
|
|
(parent.type === 'AssignmentExpression' && id === parent.left) ||
|
|
parent.type === 'UpdateExpression'
|
|
) {
|
|
ctx.error(`Cannot assign to destructured props as they are readonly.`, id)
|
|
}
|
|
|
|
if (isStaticProperty(parent) && parent.shorthand) {
|
|
// let binding used in a property shorthand
|
|
// skip for destructure patterns
|
|
if (
|
|
!(parent as any).inPattern ||
|
|
isInDestructureAssignment(parent, parentStack)
|
|
) {
|
|
// { prop } -> { prop: __props.prop }
|
|
ctx.s.appendLeft(
|
|
id.end! + ctx.startOffset!,
|
|
`: ${genPropsAccessExp(propsLocalToPublicMap[id.name])}`
|
|
)
|
|
}
|
|
} else {
|
|
// x --> __props.x
|
|
ctx.s.overwrite(
|
|
id.start! + ctx.startOffset!,
|
|
id.end! + ctx.startOffset!,
|
|
genPropsAccessExp(propsLocalToPublicMap[id.name])
|
|
)
|
|
}
|
|
}
|
|
|
|
function checkUsage(node: Node, method: string, alias = method) {
|
|
if (isCallOf(node, alias)) {
|
|
const arg = unwrapTSNode(node.arguments[0])
|
|
if (arg.type === 'Identifier' && currentScope[arg.name]) {
|
|
ctx.error(
|
|
`"${arg.name}" is a destructured prop and should not be passed directly to ${method}(). ` +
|
|
`Pass a getter () => ${arg.name} instead.`,
|
|
arg
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// check root scope first
|
|
const ast = ctx.scriptSetupAst!
|
|
walkScope(ast, true)
|
|
walk(ast, {
|
|
enter(node: Node, parent?: Node) {
|
|
parent && parentStack.push(parent)
|
|
|
|
// skip type nodes
|
|
if (
|
|
parent &&
|
|
parent.type.startsWith('TS') &&
|
|
parent.type !== 'TSAsExpression' &&
|
|
parent.type !== 'TSNonNullExpression' &&
|
|
parent.type !== 'TSTypeAssertion'
|
|
) {
|
|
return this.skip()
|
|
}
|
|
|
|
checkUsage(node, 'watch', vueImportAliases.watch)
|
|
checkUsage(node, 'toRef', vueImportAliases.toRef)
|
|
|
|
// function scopes
|
|
if (isFunctionType(node)) {
|
|
pushScope()
|
|
walkFunctionParams(node, registerLocalBinding)
|
|
if (node.body.type === 'BlockStatement') {
|
|
walkScope(node.body)
|
|
}
|
|
return
|
|
}
|
|
|
|
// catch param
|
|
if (node.type === 'CatchClause') {
|
|
pushScope()
|
|
if (node.param && node.param.type === 'Identifier') {
|
|
registerLocalBinding(node.param)
|
|
}
|
|
walkScope(node.body)
|
|
return
|
|
}
|
|
|
|
// non-function block scopes
|
|
if (node.type === 'BlockStatement' && !isFunctionType(parent!)) {
|
|
pushScope()
|
|
walkScope(node)
|
|
return
|
|
}
|
|
|
|
if (node.type === 'Identifier') {
|
|
if (
|
|
isReferencedIdentifier(node, parent!, parentStack) &&
|
|
!excludedIds.has(node)
|
|
) {
|
|
if (currentScope[node.name]) {
|
|
rewriteId(node, parent!, parentStack)
|
|
}
|
|
}
|
|
}
|
|
},
|
|
leave(node: Node, parent?: Node) {
|
|
parent && parentStack.pop()
|
|
if (
|
|
(node.type === 'BlockStatement' && !isFunctionType(parent!)) ||
|
|
isFunctionType(node)
|
|
) {
|
|
popScope()
|
|
}
|
|
}
|
|
})
|
|
}
|