fix(compiler-core/compiler-sfc): handle destructure assignment expressions
This commit is contained in:
parent
4d5242128e
commit
4c6078ce25
@ -111,22 +111,26 @@ export function processExpression(
|
|||||||
const rewriteIdentifier = (raw: string, parent?: Node, id?: Identifier) => {
|
const rewriteIdentifier = (raw: string, parent?: Node, id?: Identifier) => {
|
||||||
const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw]
|
const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw]
|
||||||
if (inline) {
|
if (inline) {
|
||||||
|
// x = y
|
||||||
const isAssignmentLVal =
|
const isAssignmentLVal =
|
||||||
parent && parent.type === 'AssignmentExpression' && parent.left === id
|
parent && parent.type === 'AssignmentExpression' && parent.left === id
|
||||||
|
// x++
|
||||||
const isUpdateArg =
|
const isUpdateArg =
|
||||||
parent && parent.type === 'UpdateExpression' && parent.argument === id
|
parent && parent.type === 'UpdateExpression' && parent.argument === id
|
||||||
// setup inline mode
|
// ({ x } = y)
|
||||||
|
const isDestructureAssignment =
|
||||||
|
parent && isInDestructureAssignment(parent, parentStack)
|
||||||
|
|
||||||
if (type === BindingTypes.SETUP_CONST) {
|
if (type === BindingTypes.SETUP_CONST) {
|
||||||
return raw
|
return raw
|
||||||
} else if (
|
} else if (type === BindingTypes.SETUP_REF) {
|
||||||
type === BindingTypes.SETUP_REF ||
|
return `${raw}.value`
|
||||||
type === BindingTypes.SETUP_MAYBE_REF
|
} else if (type === BindingTypes.SETUP_MAYBE_REF) {
|
||||||
) {
|
|
||||||
// const binding that may or may not be ref
|
// const binding that may or may not be ref
|
||||||
// if it's not a ref, then assignments don't make sense -
|
// if it's not a ref, then assignments don't make sense -
|
||||||
// so we ignore the non-ref assignment case and generate code
|
// so we ignore the non-ref assignment case and generate code
|
||||||
// that assumes the value to be a ref for more efficiency
|
// that assumes the value to be a ref for more efficiency
|
||||||
return isAssignmentLVal || isUpdateArg
|
return isAssignmentLVal || isUpdateArg || isDestructureAssignment
|
||||||
? `${raw}.value`
|
? `${raw}.value`
|
||||||
: `${context.helperString(UNREF)}(${raw})`
|
: `${context.helperString(UNREF)}(${raw})`
|
||||||
} else if (type === BindingTypes.SETUP_LET) {
|
} else if (type === BindingTypes.SETUP_LET) {
|
||||||
@ -157,6 +161,13 @@ export function processExpression(
|
|||||||
return `${context.helperString(IS_REF)}(${raw})${
|
return `${context.helperString(IS_REF)}(${raw})${
|
||||||
context.isTS ? ` //@ts-ignore\n` : ``
|
context.isTS ? ` //@ts-ignore\n` : ``
|
||||||
} ? ${prefix}${raw}.value${postfix} : ${prefix}${raw}${postfix}`
|
} ? ${prefix}${raw}.value${postfix} : ${prefix}${raw}${postfix}`
|
||||||
|
} else if (isDestructureAssignment) {
|
||||||
|
// TODO
|
||||||
|
// let binding in a destructure assignment - it's very tricky to
|
||||||
|
// handle both possible cases here without altering the original
|
||||||
|
// structure of the code, so we just assume it's not a ref here
|
||||||
|
// for now
|
||||||
|
return raw
|
||||||
} else {
|
} else {
|
||||||
return `${context.helperString(UNREF)}(${raw})`
|
return `${context.helperString(UNREF)}(${raw})`
|
||||||
}
|
}
|
||||||
@ -231,22 +242,24 @@ export function processExpression(
|
|||||||
const knownIds = Object.create(context.identifiers)
|
const knownIds = Object.create(context.identifiers)
|
||||||
const isDuplicate = (node: Node & PrefixMeta): boolean =>
|
const isDuplicate = (node: Node & PrefixMeta): boolean =>
|
||||||
ids.some(id => id.start === node.start)
|
ids.some(id => id.start === node.start)
|
||||||
|
const parentStack: Node[] = []
|
||||||
|
|
||||||
// walk the AST and look for identifiers that need to be prefixed.
|
// walk the AST and look for identifiers that need to be prefixed.
|
||||||
;(walk as any)(ast, {
|
;(walk as any)(ast, {
|
||||||
enter(node: Node & PrefixMeta, parent: Node) {
|
enter(node: Node & PrefixMeta, parent: Node | undefined) {
|
||||||
|
parent && parentStack.push(parent)
|
||||||
if (node.type === 'Identifier') {
|
if (node.type === 'Identifier') {
|
||||||
if (!isDuplicate(node)) {
|
if (!isDuplicate(node)) {
|
||||||
const needPrefix = shouldPrefix(node, parent)
|
const needPrefix = shouldPrefix(node, parent!, parentStack)
|
||||||
if (!knownIds[node.name] && needPrefix) {
|
if (!knownIds[node.name] && needPrefix) {
|
||||||
if (isStaticProperty(parent) && parent.shorthand) {
|
if (isStaticProperty(parent!) && parent.shorthand) {
|
||||||
// property shorthand like { foo }, we need to add the key since
|
// property shorthand like { foo }, we need to add the key since
|
||||||
// we rewrite the value
|
// we rewrite the value
|
||||||
node.prefix = `${node.name}: `
|
node.prefix = `${node.name}: `
|
||||||
}
|
}
|
||||||
node.name = rewriteIdentifier(node.name, parent, node)
|
node.name = rewriteIdentifier(node.name, parent, node)
|
||||||
ids.push(node)
|
ids.push(node)
|
||||||
} else if (!isStaticPropertyKey(node, parent)) {
|
} else if (!isStaticPropertyKey(node, parent!)) {
|
||||||
// The identifier is considered constant unless it's pointing to a
|
// The identifier is considered constant unless it's pointing to a
|
||||||
// scope variable (a v-for alias, or a v-slot prop)
|
// scope variable (a v-for alias, or a v-slot prop)
|
||||||
if (!(needPrefix && knownIds[node.name]) && !bailConstant) {
|
if (!(needPrefix && knownIds[node.name]) && !bailConstant) {
|
||||||
@ -291,7 +304,8 @@ export function processExpression(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
leave(node: Node & PrefixMeta) {
|
leave(node: Node & PrefixMeta, parent: Node | undefined) {
|
||||||
|
parent && parentStack.pop()
|
||||||
if (node !== ast.body[0].expression && node.scopeIds) {
|
if (node !== ast.body[0].expression && node.scopeIds) {
|
||||||
node.scopeIds.forEach((id: string) => {
|
node.scopeIds.forEach((id: string) => {
|
||||||
knownIds[id]--
|
knownIds[id]--
|
||||||
@ -359,7 +373,7 @@ const isStaticProperty = (node: Node): node is ObjectProperty =>
|
|||||||
const isStaticPropertyKey = (node: Node, parent: Node) =>
|
const isStaticPropertyKey = (node: Node, parent: Node) =>
|
||||||
isStaticProperty(parent) && parent.key === node
|
isStaticProperty(parent) && parent.key === node
|
||||||
|
|
||||||
function shouldPrefix(id: Identifier, parent: Node) {
|
function shouldPrefix(id: Identifier, parent: Node, parentStack: Node[]) {
|
||||||
// declaration id
|
// declaration id
|
||||||
if (
|
if (
|
||||||
(parent.type === 'VariableDeclarator' ||
|
(parent.type === 'VariableDeclarator' ||
|
||||||
@ -386,8 +400,11 @@ function shouldPrefix(id: Identifier, parent: Node) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// array destructure pattern
|
// non-assignment array destructure pattern
|
||||||
if (parent.type === 'ArrayPattern') {
|
if (
|
||||||
|
parent.type === 'ArrayPattern' &&
|
||||||
|
!isInDestructureAssignment(parent, parentStack)
|
||||||
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -419,6 +436,24 @@ function shouldPrefix(id: Identifier, parent: Node) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isInDestructureAssignment(parent: Node, parentStack: Node[]): boolean {
|
||||||
|
if (
|
||||||
|
parent &&
|
||||||
|
(parent.type === 'ObjectProperty' || parent.type === 'ArrayPattern')
|
||||||
|
) {
|
||||||
|
let i = parentStack.length
|
||||||
|
while (i--) {
|
||||||
|
const p = parentStack[i]
|
||||||
|
if (p.type === 'AssignmentExpression') {
|
||||||
|
return true
|
||||||
|
} else if (p.type !== 'ObjectProperty' && !p.type.endsWith('Pattern')) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
function stringifyExpression(exp: ExpressionNode | string): string {
|
function stringifyExpression(exp: ExpressionNode | string): string {
|
||||||
if (isString(exp)) {
|
if (isString(exp)) {
|
||||||
return exp
|
return exp
|
||||||
|
@ -117,7 +117,7 @@ return { ref }
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`SFC compile <script setup> inlineTemplate mode avoid unref() when necessary 1`] = `
|
exports[`SFC compile <script setup> inlineTemplate mode avoid unref() when necessary 1`] = `
|
||||||
"import { createVNode as _createVNode, unref as _unref, toDisplayString as _toDisplayString, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
|
"import { createVNode as _createVNode, toDisplayString as _toDisplayString, unref as _unref, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
|
||||||
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import Foo from './Foo.vue'
|
import Foo from './Foo.vue'
|
||||||
@ -129,12 +129,14 @@ export default {
|
|||||||
|
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
const constant = {}
|
const constant = {}
|
||||||
|
const maybe = foo()
|
||||||
|
let lett = 1
|
||||||
function fn() {}
|
function fn() {}
|
||||||
|
|
||||||
return (_ctx, _cache) => {
|
return (_ctx, _cache) => {
|
||||||
return (_openBlock(), _createBlock(_Fragment, null, [
|
return (_openBlock(), _createBlock(_Fragment, null, [
|
||||||
_createVNode(Foo),
|
_createVNode(Foo),
|
||||||
_createVNode(\\"div\\", { onClick: fn }, _toDisplayString(_unref(count)) + \\" \\" + _toDisplayString(constant) + \\" \\" + _toDisplayString(_unref(other)), 1 /* TEXT */)
|
_createVNode(\\"div\\", { onClick: fn }, _toDisplayString(count.value) + \\" \\" + _toDisplayString(constant) + \\" \\" + _toDisplayString(_unref(maybe)) + \\" \\" + _toDisplayString(_unref(lett)) + \\" \\" + _toDisplayString(_unref(other)), 1 /* TEXT */)
|
||||||
], 64 /* STABLE_FRAGMENT */))
|
], 64 /* STABLE_FRAGMENT */))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,7 +145,7 @@ return (_ctx, _cache) => {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`SFC compile <script setup> inlineTemplate mode should work 1`] = `
|
exports[`SFC compile <script setup> inlineTemplate mode should work 1`] = `
|
||||||
"import { unref as _unref, toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
|
"import { toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
|
||||||
|
|
||||||
const _hoisted_1 = /*#__PURE__*/_createVNode(\\"div\\", null, \\"static\\", -1 /* HOISTED */)
|
const _hoisted_1 = /*#__PURE__*/_createVNode(\\"div\\", null, \\"static\\", -1 /* HOISTED */)
|
||||||
|
|
||||||
@ -157,7 +159,7 @@ export default {
|
|||||||
|
|
||||||
return (_ctx, _cache) => {
|
return (_ctx, _cache) => {
|
||||||
return (_openBlock(), _createBlock(_Fragment, null, [
|
return (_openBlock(), _createBlock(_Fragment, null, [
|
||||||
_createVNode(\\"div\\", null, _toDisplayString(_unref(count)), 1 /* TEXT */),
|
_createVNode(\\"div\\", null, _toDisplayString(count.value), 1 /* TEXT */),
|
||||||
_hoisted_1
|
_hoisted_1
|
||||||
], 64 /* STABLE_FRAGMENT */))
|
], 64 /* STABLE_FRAGMENT */))
|
||||||
}
|
}
|
||||||
@ -167,7 +169,7 @@ return (_ctx, _cache) => {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`SFC compile <script setup> inlineTemplate mode template assignment expression codegen 1`] = `
|
exports[`SFC compile <script setup> inlineTemplate mode template assignment expression codegen 1`] = `
|
||||||
"import { createVNode as _createVNode, isRef as _isRef, unref as _unref, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
|
"import { createVNode as _createVNode, isRef as _isRef, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
|
||||||
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
@ -185,10 +187,42 @@ return (_ctx, _cache) => {
|
|||||||
onClick: _cache[1] || (_cache[1] = $event => (count.value = 1))
|
onClick: _cache[1] || (_cache[1] = $event => (count.value = 1))
|
||||||
}),
|
}),
|
||||||
_createVNode(\\"div\\", {
|
_createVNode(\\"div\\", {
|
||||||
onClick: _cache[2] || (_cache[2] = $event => (!_isRef(maybe) ? null : maybe.value = _unref(count)))
|
onClick: _cache[2] || (_cache[2] = $event => (maybe.value = count.value))
|
||||||
}),
|
}),
|
||||||
_createVNode(\\"div\\", {
|
_createVNode(\\"div\\", {
|
||||||
onClick: _cache[3] || (_cache[3] = $event => (_isRef(lett) ? lett.value = _unref(count) : lett = _unref(count)))
|
onClick: _cache[3] || (_cache[3] = $event => (_isRef(lett) ? lett.value = count.value : lett = count.value))
|
||||||
|
})
|
||||||
|
], 64 /* STABLE_FRAGMENT */))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`SFC compile <script setup> inlineTemplate mode template destructure assignment codegen 1`] = `
|
||||||
|
"import { createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
|
||||||
|
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
expose: [],
|
||||||
|
setup(__props) {
|
||||||
|
|
||||||
|
const val = {}
|
||||||
|
const count = ref(0)
|
||||||
|
const maybe = foo()
|
||||||
|
let lett = 1
|
||||||
|
|
||||||
|
return (_ctx, _cache) => {
|
||||||
|
return (_openBlock(), _createBlock(_Fragment, null, [
|
||||||
|
_createVNode(\\"div\\", {
|
||||||
|
onClick: _cache[1] || (_cache[1] = $event => (({ count: count.value } = val)))
|
||||||
|
}),
|
||||||
|
_createVNode(\\"div\\", {
|
||||||
|
onClick: _cache[2] || (_cache[2] = $event => ([maybe.value] = val))
|
||||||
|
}),
|
||||||
|
_createVNode(\\"div\\", {
|
||||||
|
onClick: _cache[3] || (_cache[3] = $event => (({ lett: lett } = val)))
|
||||||
})
|
})
|
||||||
], 64 /* STABLE_FRAGMENT */))
|
], 64 /* STABLE_FRAGMENT */))
|
||||||
}
|
}
|
||||||
@ -219,10 +253,10 @@ return (_ctx, _cache) => {
|
|||||||
onClick: _cache[2] || (_cache[2] = $event => (--count.value))
|
onClick: _cache[2] || (_cache[2] = $event => (--count.value))
|
||||||
}),
|
}),
|
||||||
_createVNode(\\"div\\", {
|
_createVNode(\\"div\\", {
|
||||||
onClick: _cache[3] || (_cache[3] = $event => (!_isRef(maybe) ? null : maybe.value++))
|
onClick: _cache[3] || (_cache[3] = $event => (maybe.value++))
|
||||||
}),
|
}),
|
||||||
_createVNode(\\"div\\", {
|
_createVNode(\\"div\\", {
|
||||||
onClick: _cache[4] || (_cache[4] = $event => (!_isRef(maybe) ? null : --maybe.value))
|
onClick: _cache[4] || (_cache[4] = $event => (--maybe.value))
|
||||||
}),
|
}),
|
||||||
_createVNode(\\"div\\", {
|
_createVNode(\\"div\\", {
|
||||||
onClick: _cache[5] || (_cache[5] = $event => (_isRef(lett) ? lett.value++ : lett++))
|
onClick: _cache[5] || (_cache[5] = $event => (_isRef(lett) ? lett.value++ : lett++))
|
||||||
@ -238,7 +272,7 @@ return (_ctx, _cache) => {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`SFC compile <script setup> inlineTemplate mode v-model codegen 1`] = `
|
exports[`SFC compile <script setup> inlineTemplate mode v-model codegen 1`] = `
|
||||||
"import { unref as _unref, vModelText as _vModelText, createVNode as _createVNode, withDirectives as _withDirectives, isRef as _isRef, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
|
"import { vModelText as _vModelText, createVNode as _createVNode, withDirectives as _withDirectives, unref as _unref, isRef as _isRef, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
|
||||||
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
@ -255,7 +289,7 @@ return (_ctx, _cache) => {
|
|||||||
_withDirectives(_createVNode(\\"input\\", {
|
_withDirectives(_createVNode(\\"input\\", {
|
||||||
\\"onUpdate:modelValue\\": _cache[1] || (_cache[1] = $event => (count.value = $event))
|
\\"onUpdate:modelValue\\": _cache[1] || (_cache[1] = $event => (count.value = $event))
|
||||||
}, null, 512 /* NEED_PATCH */), [
|
}, null, 512 /* NEED_PATCH */), [
|
||||||
[_vModelText, _unref(count)]
|
[_vModelText, count.value]
|
||||||
]),
|
]),
|
||||||
_withDirectives(_createVNode(\\"input\\", {
|
_withDirectives(_createVNode(\\"input\\", {
|
||||||
\\"onUpdate:modelValue\\": _cache[2] || (_cache[2] = $event => (_isRef(maybe) ? maybe.value = $event : null))
|
\\"onUpdate:modelValue\\": _cache[2] || (_cache[2] = $event => (_isRef(maybe) ? maybe.value = $event : null))
|
||||||
@ -364,6 +398,8 @@ export default {
|
|||||||
a.value = a.value + 1
|
a.value = a.value + 1
|
||||||
b.value.count++
|
b.value.count++
|
||||||
b.value.count = b.value.count + 1
|
b.value.count = b.value.count + 1
|
||||||
|
;({ a: a.value } = { a: 2 })
|
||||||
|
;[a.value] = [1]
|
||||||
}
|
}
|
||||||
|
|
||||||
return { a, b, inc }
|
return { a, b, inc }
|
||||||
|
@ -128,28 +128,34 @@ const bar = 1
|
|||||||
import other from './util'
|
import other from './util'
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
const constant = {}
|
const constant = {}
|
||||||
|
const maybe = foo()
|
||||||
|
let lett = 1
|
||||||
function fn() {}
|
function fn() {}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Foo/>
|
<Foo/>
|
||||||
<div @click="fn">{{ count }} {{ constant }} {{ other }}</div>
|
<div @click="fn">{{ count }} {{ constant }} {{ maybe }} {{ lett }} {{ other }}</div>
|
||||||
</template>
|
</template>
|
||||||
`,
|
`,
|
||||||
{ inlineTemplate: true }
|
{ inlineTemplate: true }
|
||||||
)
|
)
|
||||||
assertCode(content)
|
|
||||||
// no need to unref vue component import
|
// no need to unref vue component import
|
||||||
expect(content).toMatch(`createVNode(Foo)`)
|
expect(content).toMatch(`createVNode(Foo)`)
|
||||||
// should unref other imports
|
// should unref other imports
|
||||||
expect(content).toMatch(`unref(other)`)
|
expect(content).toMatch(`unref(other)`)
|
||||||
// no need to unref constant literals
|
// no need to unref constant literals
|
||||||
expect(content).not.toMatch(`unref(constant)`)
|
expect(content).not.toMatch(`unref(constant)`)
|
||||||
// should unref const w/ call init (e.g. ref())
|
// should directly use .value for known refs
|
||||||
expect(content).toMatch(`unref(count)`)
|
expect(content).toMatch(`count.value`)
|
||||||
|
// should unref() on const bindings that may be refs
|
||||||
|
expect(content).toMatch(`unref(maybe)`)
|
||||||
|
// should unref() on let bindings
|
||||||
|
expect(content).toMatch(`unref(lett)`)
|
||||||
// no need to unref function declarations
|
// no need to unref function declarations
|
||||||
expect(content).toMatch(`{ onClick: fn }`)
|
expect(content).toMatch(`{ onClick: fn }`)
|
||||||
// no need to mark constant fns in patch flag
|
// no need to mark constant fns in patch flag
|
||||||
expect(content).not.toMatch(`PROPS`)
|
expect(content).not.toMatch(`PROPS`)
|
||||||
|
assertCode(content)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('v-model codegen', () => {
|
test('v-model codegen', () => {
|
||||||
@ -170,8 +176,9 @@ const bar = 1
|
|||||||
)
|
)
|
||||||
// known const ref: set value
|
// known const ref: set value
|
||||||
expect(content).toMatch(`count.value = $event`)
|
expect(content).toMatch(`count.value = $event`)
|
||||||
// const but maybe ref: only assign after check
|
// const but maybe ref: also assign .value directly since non-ref
|
||||||
expect(content).toMatch(`_isRef(maybe) ? maybe.value = $event : null`)
|
// won't work
|
||||||
|
expect(content).toMatch(`maybe.value = $event`)
|
||||||
// let: handle both cases
|
// let: handle both cases
|
||||||
expect(content).toMatch(
|
expect(content).toMatch(
|
||||||
`_isRef(lett) ? lett.value = $event : lett = $event`
|
`_isRef(lett) ? lett.value = $event : lett = $event`
|
||||||
@ -198,12 +205,10 @@ const bar = 1
|
|||||||
// known const ref: set value
|
// known const ref: set value
|
||||||
expect(content).toMatch(`count.value = 1`)
|
expect(content).toMatch(`count.value = 1`)
|
||||||
// const but maybe ref: only assign after check
|
// const but maybe ref: only assign after check
|
||||||
expect(content).toMatch(
|
expect(content).toMatch(`maybe.value = count.value`)
|
||||||
`!_isRef(maybe) ? null : maybe.value = _unref(count)`
|
|
||||||
)
|
|
||||||
// let: handle both cases
|
// let: handle both cases
|
||||||
expect(content).toMatch(
|
expect(content).toMatch(
|
||||||
`_isRef(lett) ? lett.value = _unref(count) : lett = _unref(count)`
|
`_isRef(lett) ? lett.value = count.value : lett = count.value`
|
||||||
)
|
)
|
||||||
assertCode(content)
|
assertCode(content)
|
||||||
})
|
})
|
||||||
@ -230,14 +235,40 @@ const bar = 1
|
|||||||
// known const ref: set value
|
// known const ref: set value
|
||||||
expect(content).toMatch(`count.value++`)
|
expect(content).toMatch(`count.value++`)
|
||||||
expect(content).toMatch(`--count.value`)
|
expect(content).toMatch(`--count.value`)
|
||||||
// const but maybe ref: only assign after check
|
// const but maybe ref (non-ref case ignored)
|
||||||
expect(content).toMatch(`!_isRef(maybe) ? null : maybe.value++`)
|
expect(content).toMatch(`maybe.value++`)
|
||||||
expect(content).toMatch(`!_isRef(maybe) ? null : --maybe.value`)
|
expect(content).toMatch(`--maybe.value`)
|
||||||
// let: handle both cases
|
// let: handle both cases
|
||||||
expect(content).toMatch(`_isRef(lett) ? lett.value++ : lett++`)
|
expect(content).toMatch(`_isRef(lett) ? lett.value++ : lett++`)
|
||||||
expect(content).toMatch(`_isRef(lett) ? --lett.value : --lett`)
|
expect(content).toMatch(`_isRef(lett) ? --lett.value : --lett`)
|
||||||
assertCode(content)
|
assertCode(content)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('template destructure assignment codegen', () => {
|
||||||
|
const { content } = compile(
|
||||||
|
`<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
const val = {}
|
||||||
|
const count = ref(0)
|
||||||
|
const maybe = foo()
|
||||||
|
let lett = 1
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div @click="({ count } = val)"/>
|
||||||
|
<div @click="[maybe] = val"/>
|
||||||
|
<div @click="({ lett } = val)"/>
|
||||||
|
</template>
|
||||||
|
`,
|
||||||
|
{ inlineTemplate: true }
|
||||||
|
)
|
||||||
|
// known const ref: set value
|
||||||
|
expect(content).toMatch(`({ count: count.value } = val)`)
|
||||||
|
// const but maybe ref (non-ref case ignored)
|
||||||
|
expect(content).toMatch(`[maybe.value] = val`)
|
||||||
|
// let: assumes non-ref
|
||||||
|
expect(content).toMatch(`{ lett: lett } = val`)
|
||||||
|
assertCode(content)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with TypeScript', () => {
|
describe('with TypeScript', () => {
|
||||||
@ -524,12 +555,16 @@ const { props, emit } = defineOptions({
|
|||||||
a = a + 1
|
a = a + 1
|
||||||
b.count++
|
b.count++
|
||||||
b.count = b.count + 1
|
b.count = b.count + 1
|
||||||
|
;({ a } = { a: 2 })
|
||||||
|
;[a] = [1]
|
||||||
}
|
}
|
||||||
</script>`)
|
</script>`)
|
||||||
expect(content).toMatch(`a.value++`)
|
expect(content).toMatch(`a.value++`)
|
||||||
expect(content).toMatch(`a.value = a.value + 1`)
|
expect(content).toMatch(`a.value = a.value + 1`)
|
||||||
expect(content).toMatch(`b.value.count++`)
|
expect(content).toMatch(`b.value.count++`)
|
||||||
expect(content).toMatch(`b.value.count = b.value.count + 1`)
|
expect(content).toMatch(`b.value.count = b.value.count + 1`)
|
||||||
|
expect(content).toMatch(`;({ a: a.value } = { a: 2 })`)
|
||||||
|
expect(content).toMatch(`;[a.value] = [1]`)
|
||||||
assertCode(content)
|
assertCode(content)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -670,7 +670,10 @@ export function compileScript(
|
|||||||
// let binding used in a property shorthand
|
// let binding used in a property shorthand
|
||||||
// { foo } -> { foo: foo.value }
|
// { foo } -> { foo: foo.value }
|
||||||
// skip for destructure patterns
|
// skip for destructure patterns
|
||||||
if (!(parent as any).inPattern) {
|
if (
|
||||||
|
!(parent as any).inPattern ||
|
||||||
|
isInDestructureAssignment(parent, parentStack)
|
||||||
|
) {
|
||||||
s.appendLeft(id.end! + startOffset, `: ${id.name}.value`)
|
s.appendLeft(id.end! + startOffset, `: ${id.name}.value`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1258,6 +1261,8 @@ function genRuntimeEmits(emits: Set<string>) {
|
|||||||
: ``
|
: ``
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parentStack: Node[] = []
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Walk an AST and find identifiers that are variable references.
|
* Walk an AST and find identifiers that are variable references.
|
||||||
* This is largely the same logic with `transformExpressions` in compiler-core
|
* This is largely the same logic with `transformExpressions` in compiler-core
|
||||||
@ -1270,10 +1275,11 @@ function walkIdentifiers(
|
|||||||
) {
|
) {
|
||||||
const knownIds: Record<string, number> = Object.create(null)
|
const knownIds: Record<string, number> = Object.create(null)
|
||||||
;(walk as any)(root, {
|
;(walk as any)(root, {
|
||||||
enter(node: Node & { scopeIds?: Set<string> }, parent: Node) {
|
enter(node: Node & { scopeIds?: Set<string> }, parent: Node | undefined) {
|
||||||
|
parent && parentStack.push(parent)
|
||||||
if (node.type === 'Identifier') {
|
if (node.type === 'Identifier') {
|
||||||
if (!knownIds[node.name] && isRefIdentifier(node, parent)) {
|
if (!knownIds[node.name] && isRefIdentifier(node, parent!)) {
|
||||||
onIdentifier(node, parent)
|
onIdentifier(node, parent!)
|
||||||
}
|
}
|
||||||
} else if (isFunction(node)) {
|
} else if (isFunction(node)) {
|
||||||
// walk function expressions and add its arguments to known identifiers
|
// walk function expressions and add its arguments to known identifiers
|
||||||
@ -1309,13 +1315,14 @@ function walkIdentifiers(
|
|||||||
)
|
)
|
||||||
} else if (
|
} else if (
|
||||||
node.type === 'ObjectProperty' &&
|
node.type === 'ObjectProperty' &&
|
||||||
parent.type === 'ObjectPattern'
|
parent!.type === 'ObjectPattern'
|
||||||
) {
|
) {
|
||||||
// mark property in destructure pattern
|
// mark property in destructure pattern
|
||||||
;(node as any).inPattern = true
|
;(node as any).inPattern = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
leave(node: Node & { scopeIds?: Set<string> }) {
|
leave(node: Node & { scopeIds?: Set<string> }, parent: Node | undefined) {
|
||||||
|
parent && parentStack.pop()
|
||||||
if (node.scopeIds) {
|
if (node.scopeIds) {
|
||||||
node.scopeIds.forEach((id: string) => {
|
node.scopeIds.forEach((id: string) => {
|
||||||
knownIds[id]--
|
knownIds[id]--
|
||||||
@ -1355,8 +1362,11 @@ function isRefIdentifier(id: Identifier, parent: Node) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// array destructure pattern
|
// non-assignment array destructure pattern
|
||||||
if (parent.type === 'ArrayPattern') {
|
if (
|
||||||
|
parent.type === 'ArrayPattern' &&
|
||||||
|
!isInDestructureAssignment(parent, parentStack)
|
||||||
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1464,6 +1474,27 @@ function canNeverBeRef(node: Node, userReactiveImport: string): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isInDestructureAssignment(parent: Node, parentStack: Node[]): boolean {
|
||||||
|
if (
|
||||||
|
parent &&
|
||||||
|
(parent.type === 'ObjectProperty' || parent.type === 'ArrayPattern')
|
||||||
|
) {
|
||||||
|
let i = parentStack.length
|
||||||
|
while (i--) {
|
||||||
|
const p = parentStack[i]
|
||||||
|
if (p.type === 'AssignmentExpression') {
|
||||||
|
const root = parentStack[0]
|
||||||
|
// if this is a ref: destructure, it should be treated like a
|
||||||
|
// variable decalration!
|
||||||
|
return !(root.type === 'LabeledStatement' && root.label.name === 'ref')
|
||||||
|
} else if (p.type !== 'ObjectProperty' && !p.type.endsWith('Pattern')) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Analyze bindings in normal `<script>`
|
* Analyze bindings in normal `<script>`
|
||||||
* Note that `compileScriptSetup` already analyzes bindings as part of its
|
* Note that `compileScriptSetup` already analyzes bindings as part of its
|
||||||
|
@ -14,9 +14,11 @@ export const compilerOptions: CompilerOptions = reactive({
|
|||||||
inline: false,
|
inline: false,
|
||||||
ssrCssVars: `{ color }`,
|
ssrCssVars: `{ color }`,
|
||||||
bindingMetadata: {
|
bindingMetadata: {
|
||||||
TestComponent: BindingTypes.SETUP,
|
TestComponent: BindingTypes.SETUP_CONST,
|
||||||
foo: BindingTypes.SETUP,
|
setupRef: BindingTypes.SETUP_REF,
|
||||||
bar: BindingTypes.PROPS
|
setupLet: BindingTypes.SETUP_LET,
|
||||||
|
setupMaybeRef: BindingTypes.SETUP_MAYBE_REF,
|
||||||
|
setupProp: BindingTypes.PROPS
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user