Skip to content

Commit 4c6078c

Browse files
committed
fix(compiler-core/compiler-sfc): handle destructure assignment expressions
1 parent 4d52421 commit 4c6078c

File tree

5 files changed

+188
-49
lines changed

5 files changed

+188
-49
lines changed

packages/compiler-core/src/transforms/transformExpression.ts

+49-14
Original file line numberDiff line numberDiff line change
@@ -111,22 +111,26 @@ export function processExpression(
111111
const rewriteIdentifier = (raw: string, parent?: Node, id?: Identifier) => {
112112
const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw]
113113
if (inline) {
114+
// x = y
114115
const isAssignmentLVal =
115116
parent && parent.type === 'AssignmentExpression' && parent.left === id
117+
// x++
116118
const isUpdateArg =
117119
parent && parent.type === 'UpdateExpression' && parent.argument === id
118-
// setup inline mode
120+
// ({ x } = y)
121+
const isDestructureAssignment =
122+
parent && isInDestructureAssignment(parent, parentStack)
123+
119124
if (type === BindingTypes.SETUP_CONST) {
120125
return raw
121-
} else if (
122-
type === BindingTypes.SETUP_REF ||
123-
type === BindingTypes.SETUP_MAYBE_REF
124-
) {
126+
} else if (type === BindingTypes.SETUP_REF) {
127+
return `${raw}.value`
128+
} else if (type === BindingTypes.SETUP_MAYBE_REF) {
125129
// const binding that may or may not be ref
126130
// if it's not a ref, then assignments don't make sense -
127131
// so we ignore the non-ref assignment case and generate code
128132
// that assumes the value to be a ref for more efficiency
129-
return isAssignmentLVal || isUpdateArg
133+
return isAssignmentLVal || isUpdateArg || isDestructureAssignment
130134
? `${raw}.value`
131135
: `${context.helperString(UNREF)}(${raw})`
132136
} else if (type === BindingTypes.SETUP_LET) {
@@ -157,6 +161,13 @@ export function processExpression(
157161
return `${context.helperString(IS_REF)}(${raw})${
158162
context.isTS ? ` //@ts-ignore\n` : ``
159163
} ? ${prefix}${raw}.value${postfix} : ${prefix}${raw}${postfix}`
164+
} else if (isDestructureAssignment) {
165+
// TODO
166+
// let binding in a destructure assignment - it's very tricky to
167+
// handle both possible cases here without altering the original
168+
// structure of the code, so we just assume it's not a ref here
169+
// for now
170+
return raw
160171
} else {
161172
return `${context.helperString(UNREF)}(${raw})`
162173
}
@@ -231,22 +242,24 @@ export function processExpression(
231242
const knownIds = Object.create(context.identifiers)
232243
const isDuplicate = (node: Node & PrefixMeta): boolean =>
233244
ids.some(id => id.start === node.start)
245+
const parentStack: Node[] = []
234246

235247
// walk the AST and look for identifiers that need to be prefixed.
236248
;(walk as any)(ast, {
237-
enter(node: Node & PrefixMeta, parent: Node) {
249+
enter(node: Node & PrefixMeta, parent: Node | undefined) {
250+
parent && parentStack.push(parent)
238251
if (node.type === 'Identifier') {
239252
if (!isDuplicate(node)) {
240-
const needPrefix = shouldPrefix(node, parent)
253+
const needPrefix = shouldPrefix(node, parent!, parentStack)
241254
if (!knownIds[node.name] && needPrefix) {
242-
if (isStaticProperty(parent) && parent.shorthand) {
255+
if (isStaticProperty(parent!) && parent.shorthand) {
243256
// property shorthand like { foo }, we need to add the key since
244257
// we rewrite the value
245258
node.prefix = `${node.name}: `
246259
}
247260
node.name = rewriteIdentifier(node.name, parent, node)
248261
ids.push(node)
249-
} else if (!isStaticPropertyKey(node, parent)) {
262+
} else if (!isStaticPropertyKey(node, parent!)) {
250263
// The identifier is considered constant unless it's pointing to a
251264
// scope variable (a v-for alias, or a v-slot prop)
252265
if (!(needPrefix && knownIds[node.name]) && !bailConstant) {
@@ -291,7 +304,8 @@ export function processExpression(
291304
)
292305
}
293306
},
294-
leave(node: Node & PrefixMeta) {
307+
leave(node: Node & PrefixMeta, parent: Node | undefined) {
308+
parent && parentStack.pop()
295309
if (node !== ast.body[0].expression && node.scopeIds) {
296310
node.scopeIds.forEach((id: string) => {
297311
knownIds[id]--
@@ -359,7 +373,7 @@ const isStaticProperty = (node: Node): node is ObjectProperty =>
359373
const isStaticPropertyKey = (node: Node, parent: Node) =>
360374
isStaticProperty(parent) && parent.key === node
361375

362-
function shouldPrefix(id: Identifier, parent: Node) {
376+
function shouldPrefix(id: Identifier, parent: Node, parentStack: Node[]) {
363377
// declaration id
364378
if (
365379
(parent.type === 'VariableDeclarator' ||
@@ -386,8 +400,11 @@ function shouldPrefix(id: Identifier, parent: Node) {
386400
return false
387401
}
388402

389-
// array destructure pattern
390-
if (parent.type === 'ArrayPattern') {
403+
// non-assignment array destructure pattern
404+
if (
405+
parent.type === 'ArrayPattern' &&
406+
!isInDestructureAssignment(parent, parentStack)
407+
) {
391408
return false
392409
}
393410

@@ -419,6 +436,24 @@ function shouldPrefix(id: Identifier, parent: Node) {
419436
return true
420437
}
421438

439+
function isInDestructureAssignment(parent: Node, parentStack: Node[]): boolean {
440+
if (
441+
parent &&
442+
(parent.type === 'ObjectProperty' || parent.type === 'ArrayPattern')
443+
) {
444+
let i = parentStack.length
445+
while (i--) {
446+
const p = parentStack[i]
447+
if (p.type === 'AssignmentExpression') {
448+
return true
449+
} else if (p.type !== 'ObjectProperty' && !p.type.endsWith('Pattern')) {
450+
break
451+
}
452+
}
453+
}
454+
return false
455+
}
456+
422457
function stringifyExpression(exp: ExpressionNode | string): string {
423458
if (isString(exp)) {
424459
return exp

packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap

+47-11
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ return { ref }
117117
`;
118118
119119
exports[`SFC compile <script setup> inlineTemplate mode avoid unref() when necessary 1`] = `
120-
"import { createVNode as _createVNode, unref as _unref, toDisplayString as _toDisplayString, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
120+
"import { createVNode as _createVNode, toDisplayString as _toDisplayString, unref as _unref, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
121121
122122
import { ref } from 'vue'
123123
import Foo from './Foo.vue'
@@ -129,12 +129,14 @@ export default {
129129
130130
const count = ref(0)
131131
const constant = {}
132+
const maybe = foo()
133+
let lett = 1
132134
function fn() {}
133135
134136
return (_ctx, _cache) => {
135137
return (_openBlock(), _createBlock(_Fragment, null, [
136138
_createVNode(Foo),
137-
_createVNode(\\"div\\", { onClick: fn }, _toDisplayString(_unref(count)) + \\" \\" + _toDisplayString(constant) + \\" \\" + _toDisplayString(_unref(other)), 1 /* TEXT */)
139+
_createVNode(\\"div\\", { onClick: fn }, _toDisplayString(count.value) + \\" \\" + _toDisplayString(constant) + \\" \\" + _toDisplayString(_unref(maybe)) + \\" \\" + _toDisplayString(_unref(lett)) + \\" \\" + _toDisplayString(_unref(other)), 1 /* TEXT */)
138140
], 64 /* STABLE_FRAGMENT */))
139141
}
140142
}
@@ -143,7 +145,7 @@ return (_ctx, _cache) => {
143145
`;
144146
145147
exports[`SFC compile <script setup> inlineTemplate mode should work 1`] = `
146-
"import { unref as _unref, toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
148+
"import { toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
147149
148150
const _hoisted_1 = /*#__PURE__*/_createVNode(\\"div\\", null, \\"static\\", -1 /* HOISTED */)
149151
@@ -157,7 +159,7 @@ export default {
157159
158160
return (_ctx, _cache) => {
159161
return (_openBlock(), _createBlock(_Fragment, null, [
160-
_createVNode(\\"div\\", null, _toDisplayString(_unref(count)), 1 /* TEXT */),
162+
_createVNode(\\"div\\", null, _toDisplayString(count.value), 1 /* TEXT */),
161163
_hoisted_1
162164
], 64 /* STABLE_FRAGMENT */))
163165
}
@@ -167,7 +169,7 @@ return (_ctx, _cache) => {
167169
`;
168170
169171
exports[`SFC compile <script setup> inlineTemplate mode template assignment expression codegen 1`] = `
170-
"import { createVNode as _createVNode, isRef as _isRef, unref as _unref, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
172+
"import { createVNode as _createVNode, isRef as _isRef, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
171173
172174
import { ref } from 'vue'
173175
@@ -185,10 +187,42 @@ return (_ctx, _cache) => {
185187
onClick: _cache[1] || (_cache[1] = $event => (count.value = 1))
186188
}),
187189
_createVNode(\\"div\\", {
188-
onClick: _cache[2] || (_cache[2] = $event => (!_isRef(maybe) ? null : maybe.value = _unref(count)))
190+
onClick: _cache[2] || (_cache[2] = $event => (maybe.value = count.value))
191+
}),
192+
_createVNode(\\"div\\", {
193+
onClick: _cache[3] || (_cache[3] = $event => (_isRef(lett) ? lett.value = count.value : lett = count.value))
194+
})
195+
], 64 /* STABLE_FRAGMENT */))
196+
}
197+
}
198+
199+
}"
200+
`;
201+
202+
exports[`SFC compile <script setup> inlineTemplate mode template destructure assignment codegen 1`] = `
203+
"import { createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
204+
205+
import { ref } from 'vue'
206+
207+
export default {
208+
expose: [],
209+
setup(__props) {
210+
211+
const val = {}
212+
const count = ref(0)
213+
const maybe = foo()
214+
let lett = 1
215+
216+
return (_ctx, _cache) => {
217+
return (_openBlock(), _createBlock(_Fragment, null, [
218+
_createVNode(\\"div\\", {
219+
onClick: _cache[1] || (_cache[1] = $event => (({ count: count.value } = val)))
220+
}),
221+
_createVNode(\\"div\\", {
222+
onClick: _cache[2] || (_cache[2] = $event => ([maybe.value] = val))
189223
}),
190224
_createVNode(\\"div\\", {
191-
onClick: _cache[3] || (_cache[3] = $event => (_isRef(lett) ? lett.value = _unref(count) : lett = _unref(count)))
225+
onClick: _cache[3] || (_cache[3] = $event => (({ lett: lett } = val)))
192226
})
193227
], 64 /* STABLE_FRAGMENT */))
194228
}
@@ -219,10 +253,10 @@ return (_ctx, _cache) => {
219253
onClick: _cache[2] || (_cache[2] = $event => (--count.value))
220254
}),
221255
_createVNode(\\"div\\", {
222-
onClick: _cache[3] || (_cache[3] = $event => (!_isRef(maybe) ? null : maybe.value++))
256+
onClick: _cache[3] || (_cache[3] = $event => (maybe.value++))
223257
}),
224258
_createVNode(\\"div\\", {
225-
onClick: _cache[4] || (_cache[4] = $event => (!_isRef(maybe) ? null : --maybe.value))
259+
onClick: _cache[4] || (_cache[4] = $event => (--maybe.value))
226260
}),
227261
_createVNode(\\"div\\", {
228262
onClick: _cache[5] || (_cache[5] = $event => (_isRef(lett) ? lett.value++ : lett++))
@@ -238,7 +272,7 @@ return (_ctx, _cache) => {
238272
`;
239273
240274
exports[`SFC compile <script setup> inlineTemplate mode v-model codegen 1`] = `
241-
"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\\"
275+
"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\\"
242276
243277
import { ref } from 'vue'
244278
@@ -255,7 +289,7 @@ return (_ctx, _cache) => {
255289
_withDirectives(_createVNode(\\"input\\", {
256290
\\"onUpdate:modelValue\\": _cache[1] || (_cache[1] = $event => (count.value = $event))
257291
}, null, 512 /* NEED_PATCH */), [
258-
[_vModelText, _unref(count)]
292+
[_vModelText, count.value]
259293
]),
260294
_withDirectives(_createVNode(\\"input\\", {
261295
\\"onUpdate:modelValue\\": _cache[2] || (_cache[2] = $event => (_isRef(maybe) ? maybe.value = $event : null))
@@ -364,6 +398,8 @@ export default {
364398
a.value = a.value + 1
365399
b.value.count++
366400
b.value.count = b.value.count + 1
401+
;({ a: a.value } = { a: 2 })
402+
;[a.value] = [1]
367403
}
368404
369405
return { a, b, inc }

packages/compiler-sfc/__tests__/compileScript.spec.ts

+48-13
Original file line numberDiff line numberDiff line change
@@ -128,28 +128,34 @@ const bar = 1
128128
import other from './util'
129129
const count = ref(0)
130130
const constant = {}
131+
const maybe = foo()
132+
let lett = 1
131133
function fn() {}
132134
</script>
133135
<template>
134136
<Foo/>
135-
<div @click="fn">{{ count }} {{ constant }} {{ other }}</div>
137+
<div @click="fn">{{ count }} {{ constant }} {{ maybe }} {{ lett }} {{ other }}</div>
136138
</template>
137139
`,
138140
{ inlineTemplate: true }
139141
)
140-
assertCode(content)
141142
// no need to unref vue component import
142143
expect(content).toMatch(`createVNode(Foo)`)
143144
// should unref other imports
144145
expect(content).toMatch(`unref(other)`)
145146
// no need to unref constant literals
146147
expect(content).not.toMatch(`unref(constant)`)
147-
// should unref const w/ call init (e.g. ref())
148-
expect(content).toMatch(`unref(count)`)
148+
// should directly use .value for known refs
149+
expect(content).toMatch(`count.value`)
150+
// should unref() on const bindings that may be refs
151+
expect(content).toMatch(`unref(maybe)`)
152+
// should unref() on let bindings
153+
expect(content).toMatch(`unref(lett)`)
149154
// no need to unref function declarations
150155
expect(content).toMatch(`{ onClick: fn }`)
151156
// no need to mark constant fns in patch flag
152157
expect(content).not.toMatch(`PROPS`)
158+
assertCode(content)
153159
})
154160

155161
test('v-model codegen', () => {
@@ -170,8 +176,9 @@ const bar = 1
170176
)
171177
// known const ref: set value
172178
expect(content).toMatch(`count.value = $event`)
173-
// const but maybe ref: only assign after check
174-
expect(content).toMatch(`_isRef(maybe) ? maybe.value = $event : null`)
179+
// const but maybe ref: also assign .value directly since non-ref
180+
// won't work
181+
expect(content).toMatch(`maybe.value = $event`)
175182
// let: handle both cases
176183
expect(content).toMatch(
177184
`_isRef(lett) ? lett.value = $event : lett = $event`
@@ -198,12 +205,10 @@ const bar = 1
198205
// known const ref: set value
199206
expect(content).toMatch(`count.value = 1`)
200207
// const but maybe ref: only assign after check
201-
expect(content).toMatch(
202-
`!_isRef(maybe) ? null : maybe.value = _unref(count)`
203-
)
208+
expect(content).toMatch(`maybe.value = count.value`)
204209
// let: handle both cases
205210
expect(content).toMatch(
206-
`_isRef(lett) ? lett.value = _unref(count) : lett = _unref(count)`
211+
`_isRef(lett) ? lett.value = count.value : lett = count.value`
207212
)
208213
assertCode(content)
209214
})
@@ -230,14 +235,40 @@ const bar = 1
230235
// known const ref: set value
231236
expect(content).toMatch(`count.value++`)
232237
expect(content).toMatch(`--count.value`)
233-
// const but maybe ref: only assign after check
234-
expect(content).toMatch(`!_isRef(maybe) ? null : maybe.value++`)
235-
expect(content).toMatch(`!_isRef(maybe) ? null : --maybe.value`)
238+
// const but maybe ref (non-ref case ignored)
239+
expect(content).toMatch(`maybe.value++`)
240+
expect(content).toMatch(`--maybe.value`)
236241
// let: handle both cases
237242
expect(content).toMatch(`_isRef(lett) ? lett.value++ : lett++`)
238243
expect(content).toMatch(`_isRef(lett) ? --lett.value : --lett`)
239244
assertCode(content)
240245
})
246+
247+
test('template destructure assignment codegen', () => {
248+
const { content } = compile(
249+
`<script setup>
250+
import { ref } from 'vue'
251+
const val = {}
252+
const count = ref(0)
253+
const maybe = foo()
254+
let lett = 1
255+
</script>
256+
<template>
257+
<div @click="({ count } = val)"/>
258+
<div @click="[maybe] = val"/>
259+
<div @click="({ lett } = val)"/>
260+
</template>
261+
`,
262+
{ inlineTemplate: true }
263+
)
264+
// known const ref: set value
265+
expect(content).toMatch(`({ count: count.value } = val)`)
266+
// const but maybe ref (non-ref case ignored)
267+
expect(content).toMatch(`[maybe.value] = val`)
268+
// let: assumes non-ref
269+
expect(content).toMatch(`{ lett: lett } = val`)
270+
assertCode(content)
271+
})
241272
})
242273

243274
describe('with TypeScript', () => {
@@ -524,12 +555,16 @@ const { props, emit } = defineOptions({
524555
a = a + 1
525556
b.count++
526557
b.count = b.count + 1
558+
;({ a } = { a: 2 })
559+
;[a] = [1]
527560
}
528561
</script>`)
529562
expect(content).toMatch(`a.value++`)
530563
expect(content).toMatch(`a.value = a.value + 1`)
531564
expect(content).toMatch(`b.value.count++`)
532565
expect(content).toMatch(`b.value.count = b.value.count + 1`)
566+
expect(content).toMatch(`;({ a: a.value } = { a: 2 })`)
567+
expect(content).toMatch(`;[a.value] = [1]`)
533568
assertCode(content)
534569
})
535570

0 commit comments

Comments
 (0)