vue3-core/packages/compiler-sfc/src/script/definePropsDestructure.ts

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