const rewriteIdentifier = (raw: string, parent?: Node, id?: Identifier) => {
const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw]
if (inline) {
+ // x = y
const isAssignmentLVal =
parent && parent.type === 'AssignmentExpression' && parent.left === id
+ // x++
const isUpdateArg =
parent && parent.type === 'UpdateExpression' && parent.argument === id
- // setup inline mode
+ // ({ x } = y)
+ const isDestructureAssignment =
+ parent && isInDestructureAssignment(parent, parentStack)
+
if (type === BindingTypes.SETUP_CONST) {
return raw
- } else if (
- type === BindingTypes.SETUP_REF ||
- type === BindingTypes.SETUP_MAYBE_REF
- ) {
+ } else if (type === BindingTypes.SETUP_REF) {
+ return `${raw}.value`
+ } else if (type === BindingTypes.SETUP_MAYBE_REF) {
// const binding that may or may not be ref
// if it's not a ref, then assignments don't make sense -
// so we ignore the non-ref assignment case and generate code
// that assumes the value to be a ref for more efficiency
- return isAssignmentLVal || isUpdateArg
+ return isAssignmentLVal || isUpdateArg || isDestructureAssignment
? `${raw}.value`
: `${context.helperString(UNREF)}(${raw})`
} else if (type === BindingTypes.SETUP_LET) {
return `${context.helperString(IS_REF)}(${raw})${
context.isTS ? ` //@ts-ignore\n` : ``
} ? ${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 {
return `${context.helperString(UNREF)}(${raw})`
}
const knownIds = Object.create(context.identifiers)
const isDuplicate = (node: Node & PrefixMeta): boolean =>
ids.some(id => id.start === node.start)
+ const parentStack: Node[] = []
// walk the AST and look for identifiers that need to be prefixed.
;(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 (!isDuplicate(node)) {
- const needPrefix = shouldPrefix(node, parent)
+ const needPrefix = shouldPrefix(node, parent!, parentStack)
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
// we rewrite the value
node.prefix = `${node.name}: `
}
node.name = rewriteIdentifier(node.name, parent, 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
// scope variable (a v-for alias, or a v-slot prop)
if (!(needPrefix && knownIds[node.name]) && !bailConstant) {
)
}
},
- leave(node: Node & PrefixMeta) {
+ leave(node: Node & PrefixMeta, parent: Node | undefined) {
+ parent && parentStack.pop()
if (node !== ast.body[0].expression && node.scopeIds) {
node.scopeIds.forEach((id: string) => {
knownIds[id]--
const isStaticPropertyKey = (node: Node, parent: Node) =>
isStaticProperty(parent) && parent.key === node
-function shouldPrefix(id: Identifier, parent: Node) {
+function shouldPrefix(id: Identifier, parent: Node, parentStack: Node[]) {
// declaration id
if (
(parent.type === 'VariableDeclarator' ||
return false
}
- // array destructure pattern
- if (parent.type === 'ArrayPattern') {
+ // non-assignment array destructure pattern
+ if (
+ parent.type === 'ArrayPattern' &&
+ !isInDestructureAssignment(parent, parentStack)
+ ) {
return false
}
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 {
if (isString(exp)) {
return exp
`;
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 Foo from './Foo.vue'
const count = ref(0)
const constant = {}
+ const maybe = foo()
+ let lett = 1
function fn() {}
return (_ctx, _cache) => {
return (_openBlock(), _createBlock(_Fragment, null, [
_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 */))
}
}
`;
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 */)
return (_ctx, _cache) => {
return (_openBlock(), _createBlock(_Fragment, null, [
- _createVNode(\\"div\\", null, _toDisplayString(_unref(count)), 1 /* TEXT */),
+ _createVNode(\\"div\\", null, _toDisplayString(count.value), 1 /* TEXT */),
_hoisted_1
], 64 /* STABLE_FRAGMENT */))
}
`;
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'
onClick: _cache[1] || (_cache[1] = $event => (count.value = 1))
}),
_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\\", {
+ 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 => (_isRef(lett) ? lett.value = _unref(count) : lett = _unref(count)))
+ onClick: _cache[3] || (_cache[3] = $event => (({ lett: lett } = val)))
})
], 64 /* STABLE_FRAGMENT */))
}
onClick: _cache[2] || (_cache[2] = $event => (--count.value))
}),
_createVNode(\\"div\\", {
- onClick: _cache[3] || (_cache[3] = $event => (!_isRef(maybe) ? null : maybe.value++))
+ onClick: _cache[3] || (_cache[3] = $event => (maybe.value++))
}),
_createVNode(\\"div\\", {
- onClick: _cache[4] || (_cache[4] = $event => (!_isRef(maybe) ? null : --maybe.value))
+ onClick: _cache[4] || (_cache[4] = $event => (--maybe.value))
}),
_createVNode(\\"div\\", {
onClick: _cache[5] || (_cache[5] = $event => (_isRef(lett) ? lett.value++ : lett++))
`;
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'
_withDirectives(_createVNode(\\"input\\", {
\\"onUpdate:modelValue\\": _cache[1] || (_cache[1] = $event => (count.value = $event))
}, null, 512 /* NEED_PATCH */), [
- [_vModelText, _unref(count)]
+ [_vModelText, count.value]
]),
_withDirectives(_createVNode(\\"input\\", {
\\"onUpdate:modelValue\\": _cache[2] || (_cache[2] = $event => (_isRef(maybe) ? maybe.value = $event : null))
a.value = a.value + 1
b.value.count++
b.value.count = b.value.count + 1
+ ;({ a: a.value } = { a: 2 })
+ ;[a.value] = [1]
}
return { a, b, inc }
import other from './util'
const count = ref(0)
const constant = {}
+ const maybe = foo()
+ let lett = 1
function fn() {}
</script>
<template>
<Foo/>
- <div @click="fn">{{ count }} {{ constant }} {{ other }}</div>
+ <div @click="fn">{{ count }} {{ constant }} {{ maybe }} {{ lett }} {{ other }}</div>
</template>
`,
{ inlineTemplate: true }
)
- assertCode(content)
// no need to unref vue component import
expect(content).toMatch(`createVNode(Foo)`)
// should unref other imports
expect(content).toMatch(`unref(other)`)
// no need to unref constant literals
expect(content).not.toMatch(`unref(constant)`)
- // should unref const w/ call init (e.g. ref())
- expect(content).toMatch(`unref(count)`)
+ // should directly use .value for known refs
+ 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
expect(content).toMatch(`{ onClick: fn }`)
// no need to mark constant fns in patch flag
expect(content).not.toMatch(`PROPS`)
+ assertCode(content)
})
test('v-model codegen', () => {
)
// known const ref: set value
expect(content).toMatch(`count.value = $event`)
- // const but maybe ref: only assign after check
- expect(content).toMatch(`_isRef(maybe) ? maybe.value = $event : null`)
+ // const but maybe ref: also assign .value directly since non-ref
+ // won't work
+ expect(content).toMatch(`maybe.value = $event`)
// let: handle both cases
expect(content).toMatch(
`_isRef(lett) ? lett.value = $event : lett = $event`
// known const ref: set value
expect(content).toMatch(`count.value = 1`)
// const but maybe ref: only assign after check
- expect(content).toMatch(
- `!_isRef(maybe) ? null : maybe.value = _unref(count)`
- )
+ expect(content).toMatch(`maybe.value = count.value`)
// let: handle both cases
expect(content).toMatch(
- `_isRef(lett) ? lett.value = _unref(count) : lett = _unref(count)`
+ `_isRef(lett) ? lett.value = count.value : lett = count.value`
)
assertCode(content)
})
// known const ref: set value
expect(content).toMatch(`count.value++`)
expect(content).toMatch(`--count.value`)
- // const but maybe ref: only assign after check
- expect(content).toMatch(`!_isRef(maybe) ? null : maybe.value++`)
- expect(content).toMatch(`!_isRef(maybe) ? null : --maybe.value`)
+ // const but maybe ref (non-ref case ignored)
+ expect(content).toMatch(`maybe.value++`)
+ expect(content).toMatch(`--maybe.value`)
// let: handle both cases
expect(content).toMatch(`_isRef(lett) ? lett.value++ : lett++`)
expect(content).toMatch(`_isRef(lett) ? --lett.value : --lett`)
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', () => {
a = a + 1
b.count++
b.count = b.count + 1
+ ;({ a } = { a: 2 })
+ ;[a] = [1]
}
</script>`)
expect(content).toMatch(`a.value++`)
expect(content).toMatch(`a.value = a.value + 1`)
expect(content).toMatch(`b.value.count++`)
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)
})
// let binding used in a property shorthand
// { foo } -> { foo: foo.value }
// 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`)
}
} else {
: ``
}
+const parentStack: Node[] = []
+
/**
* Walk an AST and find identifiers that are variable references.
* This is largely the same logic with `transformExpressions` in compiler-core
) {
const knownIds: Record<string, number> = Object.create(null)
;(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 (!knownIds[node.name] && isRefIdentifier(node, parent)) {
- onIdentifier(node, parent)
+ if (!knownIds[node.name] && isRefIdentifier(node, parent!)) {
+ onIdentifier(node, parent!)
}
} else if (isFunction(node)) {
// walk function expressions and add its arguments to known identifiers
)
} else if (
node.type === 'ObjectProperty' &&
- parent.type === 'ObjectPattern'
+ parent!.type === 'ObjectPattern'
) {
// mark property in destructure pattern
;(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) {
node.scopeIds.forEach((id: string) => {
knownIds[id]--
return false
}
- // array destructure pattern
- if (parent.type === 'ArrayPattern') {
+ // non-assignment array destructure pattern
+ if (
+ parent.type === 'ArrayPattern' &&
+ !isInDestructureAssignment(parent, parentStack)
+ ) {
return false
}
}
}
+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>`
* Note that `compileScriptSetup` already analyzes bindings as part of its
inline: false,
ssrCssVars: `{ color }`,
bindingMetadata: {
- TestComponent: BindingTypes.SETUP,
- foo: BindingTypes.SETUP,
- bar: BindingTypes.PROPS
+ TestComponent: BindingTypes.SETUP_CONST,
+ setupRef: BindingTypes.SETUP_REF,
+ setupLet: BindingTypes.SETUP_LET,
+ setupMaybeRef: BindingTypes.SETUP_MAYBE_REF,
+ setupProp: BindingTypes.PROPS
}
})