mirror of https://github.com/vuejs/core.git
feat(compiler-sfc): enable reactive props destructure by default and deprecate withDefaults() (#7986)
This commit is contained in:
parent
e10a89e608
commit
ba9c2ae247
|
@ -9,7 +9,8 @@ import type {
|
|||
Program,
|
||||
ImportDefaultSpecifier,
|
||||
ImportNamespaceSpecifier,
|
||||
ImportSpecifier
|
||||
ImportSpecifier,
|
||||
CallExpression
|
||||
} from '@babel/types'
|
||||
import { walk } from 'estree-walker'
|
||||
|
||||
|
@ -449,3 +450,18 @@ export function unwrapTSNode(node: Node): Node {
|
|||
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))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`sfc props transform > aliasing 1`] = `
|
||||
"import { toDisplayString as _toDisplayString } from \\"vue\\"
|
||||
|
||||
|
||||
export default {
|
||||
props: ['foo'],
|
||||
setup(__props) {
|
||||
|
||||
|
||||
let x = foo
|
||||
let y = __props.foo
|
||||
|
||||
return (_ctx, _cache) => {
|
||||
return _toDisplayString(__props.foo + __props.foo)
|
||||
}
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`sfc props transform > basic usage 1`] = `
|
||||
"import { toDisplayString as _toDisplayString } from \\"vue\\"
|
||||
|
||||
|
||||
export default {
|
||||
props: ['foo'],
|
||||
setup(__props) {
|
||||
|
||||
|
||||
console.log(__props.foo)
|
||||
|
||||
return (_ctx, _cache) => {
|
||||
return _toDisplayString(__props.foo)
|
||||
}
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`sfc props transform > computed static key 1`] = `
|
||||
"import { toDisplayString as _toDisplayString } from \\"vue\\"
|
||||
|
||||
|
||||
export default {
|
||||
props: ['foo'],
|
||||
setup(__props) {
|
||||
|
||||
|
||||
console.log(__props.foo)
|
||||
|
||||
return (_ctx, _cache) => {
|
||||
return _toDisplayString(__props.foo)
|
||||
}
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`sfc props transform > default values w/ array runtime declaration 1`] = `
|
||||
"import { mergeDefaults as _mergeDefaults } from 'vue'
|
||||
|
||||
export default {
|
||||
props: _mergeDefaults(['foo', 'bar', 'baz'], {
|
||||
foo: 1,
|
||||
bar: () => ({}),
|
||||
func: () => {}, __skip_func: true
|
||||
}),
|
||||
setup(__props) {
|
||||
|
||||
|
||||
|
||||
return () => {}
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`sfc props transform > default values w/ object runtime declaration 1`] = `
|
||||
"import { mergeDefaults as _mergeDefaults } from 'vue'
|
||||
|
||||
export default {
|
||||
props: _mergeDefaults({ foo: Number, bar: Object, func: Function, ext: null }, {
|
||||
foo: 1,
|
||||
bar: () => ({}),
|
||||
func: () => {}, __skip_func: true,
|
||||
ext: x, __skip_ext: true
|
||||
}),
|
||||
setup(__props) {
|
||||
|
||||
|
||||
|
||||
return () => {}
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`sfc props transform > default values w/ type declaration 1`] = `
|
||||
"import { defineComponent as _defineComponent } from 'vue'
|
||||
|
||||
export default /*#__PURE__*/_defineComponent({
|
||||
props: {
|
||||
foo: { type: Number, required: false, default: 1 },
|
||||
bar: { type: Object, required: false, default: () => ({}) },
|
||||
func: { type: Function, required: false, default: () => {} }
|
||||
},
|
||||
setup(__props: any) {
|
||||
|
||||
|
||||
|
||||
return () => {}
|
||||
}
|
||||
|
||||
})"
|
||||
`;
|
||||
|
||||
exports[`sfc props transform > default values w/ type declaration, prod mode 1`] = `
|
||||
"import { defineComponent as _defineComponent } from 'vue'
|
||||
|
||||
export default /*#__PURE__*/_defineComponent({
|
||||
props: {
|
||||
foo: { default: 1 },
|
||||
bar: { default: () => ({}) },
|
||||
baz: null,
|
||||
boola: { type: Boolean },
|
||||
boolb: { type: [Boolean, Number] },
|
||||
func: { type: Function, default: () => {} }
|
||||
},
|
||||
setup(__props: any) {
|
||||
|
||||
|
||||
|
||||
return () => {}
|
||||
}
|
||||
|
||||
})"
|
||||
`;
|
||||
|
||||
exports[`sfc props transform > multiple variable declarations 1`] = `
|
||||
"import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\"
|
||||
|
||||
|
||||
export default {
|
||||
props: ['foo'],
|
||||
setup(__props) {
|
||||
|
||||
const bar = 'fish', hello = 'world'
|
||||
|
||||
return (_ctx, _cache) => {
|
||||
return (_openBlock(), _createElementBlock(\\"div\\", null, _toDisplayString(__props.foo) + \\" \\" + _toDisplayString(hello) + \\" \\" + _toDisplayString(bar), 1 /* TEXT */))
|
||||
}
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`sfc props transform > nested scope 1`] = `
|
||||
"export default {
|
||||
props: ['foo', 'bar'],
|
||||
setup(__props) {
|
||||
|
||||
|
||||
function test(foo) {
|
||||
console.log(foo)
|
||||
console.log(__props.bar)
|
||||
}
|
||||
|
||||
return () => {}
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`sfc props transform > non-identifier prop names 1`] = `
|
||||
"import { toDisplayString as _toDisplayString } from \\"vue\\"
|
||||
|
||||
|
||||
export default {
|
||||
props: { 'foo.bar': Function },
|
||||
setup(__props) {
|
||||
|
||||
|
||||
let x = __props[\\"foo.bar\\"]
|
||||
|
||||
return (_ctx, _cache) => {
|
||||
return _toDisplayString(__props[\\"foo.bar\\"])
|
||||
}
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`sfc props transform > rest spread 1`] = `
|
||||
"import { createPropsRestProxy as _createPropsRestProxy } from 'vue'
|
||||
|
||||
export default {
|
||||
props: ['foo', 'bar', 'baz'],
|
||||
setup(__props) {
|
||||
|
||||
const rest = _createPropsRestProxy(__props, [\\"foo\\",\\"bar\\"]);
|
||||
|
||||
|
||||
|
||||
return () => {}
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
|
@ -6,7 +6,6 @@ describe('sfc props transform', () => {
|
|||
function compile(src: string, options?: Partial<SFCScriptCompileOptions>) {
|
||||
return compileSFCScript(src, {
|
||||
inlineTemplate: true,
|
||||
reactivityTransform: true,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
@ -211,23 +210,6 @@ describe('sfc props transform', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('$$() escape', () => {
|
||||
const { content } = compile(`
|
||||
<script setup>
|
||||
const { foo, bar: baz } = defineProps(['foo'])
|
||||
console.log($$(foo))
|
||||
console.log($$(baz))
|
||||
$$({ foo, baz })
|
||||
</script>
|
||||
`)
|
||||
expect(content).toMatch(`const __props_foo = _toRef(__props, 'foo')`)
|
||||
expect(content).toMatch(`const __props_bar = _toRef(__props, 'bar')`)
|
||||
expect(content).toMatch(`console.log((__props_foo))`)
|
||||
expect(content).toMatch(`console.log((__props_bar))`)
|
||||
expect(content).toMatch(`({ foo: __props_foo, baz: __props_bar })`)
|
||||
assertCode(content)
|
||||
})
|
||||
|
||||
// #6960
|
||||
test('computed static key', () => {
|
||||
const { content, bindings } = compile(`
|
||||
|
@ -292,7 +274,7 @@ describe('sfc props transform', () => {
|
|||
).toThrow(`cannot reference locally declared variables`)
|
||||
})
|
||||
|
||||
test('should error if assignment to constant variable', () => {
|
||||
test('should error if assignment to destructured prop binding', () => {
|
||||
expect(() =>
|
||||
compile(
|
||||
`<script setup>
|
||||
|
@ -300,7 +282,49 @@ describe('sfc props transform', () => {
|
|||
foo = 'bar'
|
||||
</script>`
|
||||
)
|
||||
).toThrow(`Assignment to constant variable.`)
|
||||
).toThrow(`Cannot assign to destructured props`)
|
||||
|
||||
expect(() =>
|
||||
compile(
|
||||
`<script setup>
|
||||
let { foo } = defineProps(['foo'])
|
||||
foo = 'bar'
|
||||
</script>`
|
||||
)
|
||||
).toThrow(`Cannot assign to destructured props`)
|
||||
})
|
||||
|
||||
test('should error when watching destructured prop', () => {
|
||||
expect(() =>
|
||||
compile(
|
||||
`<script setup>
|
||||
import { watch } from 'vue'
|
||||
const { foo } = defineProps(['foo'])
|
||||
watch(foo, () => {})
|
||||
</script>`
|
||||
)
|
||||
).toThrow(`"foo" is a destructured prop and cannot be directly watched.`)
|
||||
|
||||
expect(() =>
|
||||
compile(
|
||||
`<script setup>
|
||||
import { watch as w } from 'vue'
|
||||
const { foo } = defineProps(['foo'])
|
||||
w(foo, () => {})
|
||||
</script>`
|
||||
)
|
||||
).toThrow(`"foo" is a destructured prop and cannot be directly watched.`)
|
||||
})
|
||||
|
||||
// not comprehensive, but should help for most common cases
|
||||
test('should error if default value type does not match declared type', () => {
|
||||
expect(() =>
|
||||
compile(
|
||||
`<script setup lang="ts">
|
||||
const { foo = 'hello' } = defineProps<{ foo?: number }>()
|
||||
</script>`
|
||||
)
|
||||
).toThrow(`Default value of prop "foo" does not match declared type.`)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -11,7 +11,8 @@ import {
|
|||
isFunctionType,
|
||||
walkIdentifiers,
|
||||
getImportedName,
|
||||
unwrapTSNode
|
||||
unwrapTSNode,
|
||||
isCallOf
|
||||
} from '@vue/compiler-dom'
|
||||
import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse'
|
||||
import {
|
||||
|
@ -59,6 +60,7 @@ import { warnOnce } from './warn'
|
|||
import { rewriteDefaultAST } from './rewriteDefault'
|
||||
import { createCache } from './cache'
|
||||
import { shouldTransform, transformAST } from '@vue/reactivity-transform'
|
||||
import { transformDestructuredProps } from './compileScriptPropsDestructure'
|
||||
|
||||
// Special compiler macros
|
||||
const DEFINE_PROPS = 'defineProps'
|
||||
|
@ -132,6 +134,14 @@ export interface ImportBinding {
|
|||
isUsedInTemplate: boolean
|
||||
}
|
||||
|
||||
export type PropsDestructureBindings = Record<
|
||||
string, // public prop key
|
||||
{
|
||||
local: string // local identifier, may be different
|
||||
default?: Expression
|
||||
}
|
||||
>
|
||||
|
||||
type FromNormalScript<T> = T & { __fromNormalScript?: boolean | null }
|
||||
type PropsDeclType = FromNormalScript<TSTypeLiteral | TSInterfaceBody>
|
||||
type EmitsDeclType = FromNormalScript<
|
||||
|
@ -151,7 +161,6 @@ export function compileScript(
|
|||
// feature flags
|
||||
// TODO remove support for deprecated options when out of experimental
|
||||
const enableReactivityTransform = !!options.reactivityTransform
|
||||
const enablePropsTransform = !!options.reactivityTransform
|
||||
const isProd = !!options.isProd
|
||||
const genSourceMap = options.sourceMap !== false
|
||||
const hoistStatic = options.hoistStatic !== false && !script
|
||||
|
@ -310,14 +319,8 @@ export function compileScript(
|
|||
// record declared types for runtime props type generation
|
||||
const declaredTypes: Record<string, string[]> = {}
|
||||
// props destructure data
|
||||
const propsDestructuredBindings: Record<
|
||||
string, // public prop key
|
||||
{
|
||||
local: string // local identifier, may be different
|
||||
default?: Expression
|
||||
isConst: boolean
|
||||
}
|
||||
> = Object.create(null)
|
||||
const propsDestructuredBindings: PropsDestructureBindings =
|
||||
Object.create(null)
|
||||
|
||||
// magic-string state
|
||||
const s = new MagicString(source)
|
||||
|
@ -410,11 +413,7 @@ export function compileScript(
|
|||
}
|
||||
}
|
||||
|
||||
function processDefineProps(
|
||||
node: Node,
|
||||
declId?: LVal,
|
||||
declKind?: VariableDeclaration['kind']
|
||||
): boolean {
|
||||
function processDefineProps(node: Node, declId?: LVal): boolean {
|
||||
if (!isCallOf(node, DEFINE_PROPS)) {
|
||||
return false
|
||||
}
|
||||
|
@ -452,10 +451,9 @@ export function compileScript(
|
|||
}
|
||||
|
||||
if (declId) {
|
||||
const isConst = declKind === 'const'
|
||||
if (enablePropsTransform && declId.type === 'ObjectPattern') {
|
||||
// handle props destructure
|
||||
if (declId.type === 'ObjectPattern') {
|
||||
propsDestructureDecl = declId
|
||||
// props destructure - handle compilation sugar
|
||||
for (const prop of declId.properties) {
|
||||
if (prop.type === 'ObjectProperty') {
|
||||
const propKey = resolveObjectKey(prop.key, prop.computed)
|
||||
|
@ -479,14 +477,12 @@ export function compileScript(
|
|||
// store default value
|
||||
propsDestructuredBindings[propKey] = {
|
||||
local: left.name,
|
||||
default: right,
|
||||
isConst
|
||||
default: right
|
||||
}
|
||||
} else if (prop.value.type === 'Identifier') {
|
||||
// simple destructure
|
||||
propsDestructuredBindings[propKey] = {
|
||||
local: prop.value.name,
|
||||
isConst
|
||||
local: prop.value.name
|
||||
}
|
||||
} else {
|
||||
error(
|
||||
|
@ -515,7 +511,12 @@ export function compileScript(
|
|||
if (!isCallOf(node, WITH_DEFAULTS)) {
|
||||
return false
|
||||
}
|
||||
if (processDefineProps(node.arguments[0], declId, declKind)) {
|
||||
warnOnce(
|
||||
`withDefaults() has been deprecated. ` +
|
||||
`Props destructure is now reactive by default - ` +
|
||||
`use destructure with default values instead.`
|
||||
)
|
||||
if (processDefineProps(node.arguments[0], declId)) {
|
||||
if (propsRuntimeDecl) {
|
||||
error(
|
||||
`${WITH_DEFAULTS} can only be used with type-based ` +
|
||||
|
@ -943,7 +944,23 @@ export function compileScript(
|
|||
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
|
||||
|
@ -951,10 +968,12 @@ export function compileScript(
|
|||
const needSkipFactory =
|
||||
!inferredType &&
|
||||
(isFunctionType(unwrapped) || unwrapped.type === 'Identifier')
|
||||
|
||||
const needFactoryWrap =
|
||||
!needSkipFactory &&
|
||||
!isLiteralNode(unwrapped) &&
|
||||
!inferredType?.includes('Function')
|
||||
|
||||
return {
|
||||
valueString: needFactoryWrap ? `() => (${value})` : value,
|
||||
needSkipFactory
|
||||
|
@ -1220,6 +1239,7 @@ export function compileScript(
|
|||
}
|
||||
|
||||
// apply reactivity transform
|
||||
// TODO remove in 3.4
|
||||
if (enableReactivityTransform && shouldTransform(script.content)) {
|
||||
const { rootRefs, importedHelpers } = transformAST(
|
||||
scriptAst,
|
||||
|
@ -1300,7 +1320,7 @@ export function compileScript(
|
|||
|
||||
// defineProps / defineEmits
|
||||
const isDefineProps =
|
||||
processDefineProps(init, decl.id, node.kind) ||
|
||||
processDefineProps(init, decl.id) ||
|
||||
processWithDefaults(init, decl.id, node.kind)
|
||||
const isDefineEmits = processDefineEmits(init, decl.id)
|
||||
if (isDefineProps || isDefineEmits) {
|
||||
|
@ -1416,19 +1436,30 @@ export function compileScript(
|
|||
}
|
||||
}
|
||||
|
||||
// 3. Apply reactivity transform
|
||||
// 3.1 props destructure transform
|
||||
if (propsDestructureDecl) {
|
||||
transformDestructuredProps(
|
||||
scriptSetupAst,
|
||||
s,
|
||||
startOffset,
|
||||
propsDestructuredBindings,
|
||||
error,
|
||||
vueImportAliases.watch
|
||||
)
|
||||
}
|
||||
|
||||
// 3.2 Apply reactivity transform
|
||||
// TODO remove in 3.4
|
||||
if (
|
||||
(enableReactivityTransform &&
|
||||
enableReactivityTransform &&
|
||||
// normal <script> had ref bindings that maybe used in <script setup>
|
||||
(refBindings || shouldTransform(scriptSetup.content))) ||
|
||||
propsDestructureDecl
|
||||
(refBindings || shouldTransform(scriptSetup.content))
|
||||
) {
|
||||
const { rootRefs, importedHelpers } = transformAST(
|
||||
scriptSetupAst,
|
||||
s,
|
||||
startOffset,
|
||||
refBindings,
|
||||
propsDestructuredBindings
|
||||
refBindings
|
||||
)
|
||||
refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs
|
||||
for (const h of importedHelpers) {
|
||||
|
@ -1444,7 +1475,7 @@ export function compileScript(
|
|||
extractRuntimeEmits(emitsTypeDecl, typeDeclaredEmits)
|
||||
}
|
||||
|
||||
// 5. check useOptions args to make sure it doesn't reference setup scope
|
||||
// 5. check macro args to make sure it doesn't reference setup scope
|
||||
// variables
|
||||
checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
|
||||
checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
|
||||
|
@ -2219,6 +2250,27 @@ 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>
|
||||
|
@ -2275,21 +2327,6 @@ function genRuntimeEmits(emits: Set<string>) {
|
|||
: ``
|
||||
}
|
||||
|
||||
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))
|
||||
)
|
||||
}
|
||||
|
||||
function canNeverBeRef(node: Node, userReactiveImport?: string): boolean {
|
||||
if (isCallOf(node, userReactiveImport)) {
|
||||
return true
|
||||
|
|
|
@ -0,0 +1,236 @@
|
|||
import {
|
||||
Node,
|
||||
Identifier,
|
||||
BlockStatement,
|
||||
Program,
|
||||
VariableDeclaration
|
||||
} from '@babel/types'
|
||||
import MagicString from 'magic-string'
|
||||
import { walk } from 'estree-walker'
|
||||
import {
|
||||
extractIdentifiers,
|
||||
isFunctionType,
|
||||
isInDestructureAssignment,
|
||||
isReferencedIdentifier,
|
||||
isStaticProperty,
|
||||
walkFunctionParams,
|
||||
isCallOf,
|
||||
unwrapTSNode
|
||||
} from '@vue/compiler-core'
|
||||
import { hasOwn, genPropsAccessExp } from '@vue/shared'
|
||||
import { PropsDestructureBindings } from './compileScript'
|
||||
|
||||
/**
|
||||
* true -> prop binding
|
||||
* false -> local binding
|
||||
*/
|
||||
type Scope = Record<string, boolean>
|
||||
|
||||
export function transformDestructuredProps(
|
||||
ast: Program,
|
||||
s: MagicString,
|
||||
offset = 0,
|
||||
knownProps: PropsDestructureBindings,
|
||||
error: (msg: string, node: Node, end?: number) => never,
|
||||
watchMethodName = 'watch'
|
||||
) {
|
||||
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 knownProps) {
|
||||
const { local } = knownProps[key]
|
||||
rootScope[local] = true
|
||||
propsLocalToPublicMap[local] = key
|
||||
}
|
||||
|
||||
function registerLocalBinding(id: Identifier) {
|
||||
excludedIds.add(id)
|
||||
if (currentScope) {
|
||||
currentScope[id.name] = false
|
||||
} else {
|
||||
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(
|
||||
scope: Scope,
|
||||
id: Identifier,
|
||||
parent: Node,
|
||||
parentStack: Node[]
|
||||
): boolean {
|
||||
if (hasOwn(scope, id.name)) {
|
||||
const binding = scope[id.name]
|
||||
|
||||
if (binding) {
|
||||
if (
|
||||
(parent.type === 'AssignmentExpression' && id === parent.left) ||
|
||||
parent.type === 'UpdateExpression'
|
||||
) {
|
||||
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 }
|
||||
s.appendLeft(
|
||||
id.end! + offset,
|
||||
`: ${genPropsAccessExp(propsLocalToPublicMap[id.name])}`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// x --> __props.x
|
||||
s.overwrite(
|
||||
id.start! + offset,
|
||||
id.end! + offset,
|
||||
genPropsAccessExp(propsLocalToPublicMap[id.name])
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// check root scope first
|
||||
walkScope(ast, true)
|
||||
;(walk as any)(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()
|
||||
}
|
||||
|
||||
if (isCallOf(node, watchMethodName)) {
|
||||
const arg = unwrapTSNode(node.arguments[0])
|
||||
if (arg.type === 'Identifier') {
|
||||
error(
|
||||
`"${arg.name}" is a destructured prop and cannot be directly watched. ` +
|
||||
`Use a getter () => ${arg.name} instead.`,
|
||||
arg
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// function scopes
|
||||
if (isFunctionType(node)) {
|
||||
scopeStack.push((currentScope = {}))
|
||||
walkFunctionParams(node, registerLocalBinding)
|
||||
if (node.body.type === 'BlockStatement') {
|
||||
walkScope(node.body)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// catch param
|
||||
if (node.type === 'CatchClause') {
|
||||
scopeStack.push((currentScope = {}))
|
||||
if (node.param && node.param.type === 'Identifier') {
|
||||
registerLocalBinding(node.param)
|
||||
}
|
||||
walkScope(node.body)
|
||||
return
|
||||
}
|
||||
|
||||
// non-function block scopes
|
||||
if (node.type === 'BlockStatement' && !isFunctionType(parent!)) {
|
||||
scopeStack.push((currentScope = {}))
|
||||
walkScope(node)
|
||||
return
|
||||
}
|
||||
|
||||
if (node.type === 'Identifier') {
|
||||
if (
|
||||
isReferencedIdentifier(node, parent!, parentStack) &&
|
||||
!excludedIds.has(node)
|
||||
) {
|
||||
// walk up the scope chain to check if id should be appended .value
|
||||
let i = scopeStack.length
|
||||
while (i--) {
|
||||
if (rewriteId(scopeStack[i], node, parent!, parentStack)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
leave(node: Node, parent?: Node) {
|
||||
parent && parentStack.pop()
|
||||
if (
|
||||
(node.type === 'BlockStatement' && !isFunctionType(parent!)) ||
|
||||
isFunctionType(node)
|
||||
) {
|
||||
scopeStack.pop()
|
||||
currentScope = scopeStack[scopeStack.length - 1] || null
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -216,6 +216,8 @@ type PropsWithDefaults<Base, Defaults> = Base & {
|
|||
*
|
||||
* This is only usable inside `<script setup>`, is compiled away in the output
|
||||
* and should **not** be actually called at runtime.
|
||||
*
|
||||
* @deprecated use reactive props destructure instead.
|
||||
*/
|
||||
export function withDefaults<Props, Defaults extends InferDefaults<Props>>(
|
||||
props: Props,
|
||||
|
|
Loading…
Reference in New Issue