wip: properly handle assignment/update expressions in inline mode

This commit is contained in:
Evan You
2020-11-18 19:38:18 -05:00
parent 4449fc3b9e
commit 8567feb2aa
7 changed files with 421 additions and 107 deletions

View File

@@ -166,8 +166,8 @@ return (_ctx, _cache) => {
}"
`;
exports[`SFC compile <script setup> inlineTemplate mode v-model codegen with unref() 1`] = `
"import { unref as _unref, isRef as _isRef, vModelText as _vModelText, createVNode as _createVNode, withDirectives as _withDirectives, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
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 { ref } from 'vue'
@@ -176,13 +176,98 @@ export default {
setup(__props) {
const count = ref(0)
const maybe = foo()
let lett = 1
return (_ctx, _cache) => {
return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
\\"onUpdate:modelValue\\": _cache[1] || (_cache[1] = $event => (_isRef(count) ? (count.value = $event) : (count = $event)))
}, null, 512 /* NEED_PATCH */)), [
[_vModelText, _unref(count)]
])
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"div\\", {
onClick: _cache[1] || (_cache[1] = $event => (count.value = 1))
}),
_createVNode(\\"div\\", {
onClick: _cache[2] || (_cache[2] = $event => (!_isRef(maybe) ? null : maybe.value = _unref(count)))
}),
_createVNode(\\"div\\", {
onClick: _cache[3] || (_cache[3] = $event => (_isRef(lett) ? lett.value = _unref(count) : lett = _unref(count)))
})
], 64 /* STABLE_FRAGMENT */))
}
}
}"
`;
exports[`SFC compile <script setup> inlineTemplate mode template update expression codegen 1`] = `
"import { createVNode as _createVNode, isRef as _isRef, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
import { ref } from 'vue'
export default {
expose: [],
setup(__props) {
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.value++))
}),
_createVNode(\\"div\\", {
onClick: _cache[2] || (_cache[2] = $event => (--count.value))
}),
_createVNode(\\"div\\", {
onClick: _cache[3] || (_cache[3] = $event => (!_isRef(maybe) ? null : maybe.value++))
}),
_createVNode(\\"div\\", {
onClick: _cache[4] || (_cache[4] = $event => (!_isRef(maybe) ? null : --maybe.value))
}),
_createVNode(\\"div\\", {
onClick: _cache[5] || (_cache[5] = $event => (_isRef(lett) ? lett.value++ : lett++))
}),
_createVNode(\\"div\\", {
onClick: _cache[6] || (_cache[6] = $event => (_isRef(lett) ? --lett.value : --lett))
})
], 64 /* STABLE_FRAGMENT */))
}
}
}"
`;
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 { ref } from 'vue'
export default {
expose: [],
setup(__props) {
const count = ref(0)
const maybe = foo()
let lett = 1
return (_ctx, _cache) => {
return (_openBlock(), _createBlock(_Fragment, null, [
_withDirectives(_createVNode(\\"input\\", {
\\"onUpdate:modelValue\\": _cache[1] || (_cache[1] = $event => (count.value = $event))
}, null, 512 /* NEED_PATCH */), [
[_vModelText, _unref(count)]
]),
_withDirectives(_createVNode(\\"input\\", {
\\"onUpdate:modelValue\\": _cache[2] || (_cache[2] = $event => (_isRef(maybe) ? maybe.value = $event : null))
}, null, 512 /* NEED_PATCH */), [
[_vModelText, _unref(maybe)]
]),
_withDirectives(_createVNode(\\"input\\", {
\\"onUpdate:modelValue\\": _cache[3] || (_cache[3] = $event => (_isRef(lett) ? lett.value = $event : lett = $event))
}, null, 512 /* NEED_PATCH */), [
[_vModelText, _unref(lett)]
])
], 64 /* STABLE_FRAGMENT */))
}
}

View File

@@ -152,18 +152,90 @@ const bar = 1
expect(content).not.toMatch(`PROPS`)
})
test('v-model codegen with unref()', () => {
test('v-model codegen', () => {
const { content } = compile(
`<script setup>
import { ref } from 'vue'
const count = ref(0)
const maybe = foo()
let lett = 1
</script>
<template>
<input v-model="count">
<input v-model="maybe">
<input v-model="lett">
</template>
`,
{ inlineTemplate: true }
)
// 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`)
// let: handle both cases
expect(content).toMatch(
`_isRef(lett) ? lett.value = $event : lett = $event`
)
assertCode(content)
})
test('template assignment expression codegen', () => {
const { content } = compile(
`<script setup>
import { ref } from 'vue'
const count = ref(0)
const maybe = foo()
let lett = 1
</script>
<template>
<div @click="count = 1"/>
<div @click="maybe = count"/>
<div @click="lett = count"/>
</template>
`,
{ inlineTemplate: true }
)
// 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)`
)
// let: handle both cases
expect(content).toMatch(
`_isRef(lett) ? lett.value = _unref(count) : lett = _unref(count)`
)
assertCode(content)
})
test('template update expression codegen', () => {
const { content } = compile(
`<script setup>
import { ref } from 'vue'
const count = ref(0)
const maybe = foo()
let lett = 1
</script>
<template>
<div @click="count++"/>
<div @click="--count"/>
<div @click="maybe++"/>
<div @click="--maybe"/>
<div @click="lett++"/>
<div @click="--lett"/>
</template>
`,
{ inlineTemplate: true }
)
// 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`)
// let: handle both cases
expect(content).toMatch(`_isRef(lett) ? lett.value++ : lett++`)
expect(content).toMatch(`_isRef(lett) ? --lett.value : --lett`)
assertCode(content)
})
})
@@ -381,9 +453,9 @@ const { props, emit } = defineOptions({
expect(content).toMatch(`let d`)
assertCode(content)
expect(bindings).toStrictEqual({
foo: BindingTypes.SETUP_CONST_REF,
a: BindingTypes.SETUP_CONST_REF,
b: BindingTypes.SETUP_CONST_REF,
foo: BindingTypes.SETUP_REF,
a: BindingTypes.SETUP_REF,
b: BindingTypes.SETUP_REF,
c: BindingTypes.SETUP_LET,
d: BindingTypes.SETUP_LET
})
@@ -403,9 +475,9 @@ const { props, emit } = defineOptions({
expect(content).toMatch(`return { a, b, c }`)
assertCode(content)
expect(bindings).toStrictEqual({
a: BindingTypes.SETUP_CONST_REF,
b: BindingTypes.SETUP_CONST_REF,
c: BindingTypes.SETUP_CONST_REF
a: BindingTypes.SETUP_REF,
b: BindingTypes.SETUP_REF,
c: BindingTypes.SETUP_REF
})
})
@@ -495,12 +567,12 @@ const { props, emit } = defineOptions({
)
expect(content).toMatch(`return { n, a, c, d, f, g }`)
expect(bindings).toStrictEqual({
n: BindingTypes.SETUP_CONST_REF,
a: BindingTypes.SETUP_CONST_REF,
c: BindingTypes.SETUP_CONST_REF,
d: BindingTypes.SETUP_CONST_REF,
f: BindingTypes.SETUP_CONST_REF,
g: BindingTypes.SETUP_CONST_REF
n: BindingTypes.SETUP_REF,
a: BindingTypes.SETUP_REF,
c: BindingTypes.SETUP_REF,
d: BindingTypes.SETUP_REF,
f: BindingTypes.SETUP_REF,
g: BindingTypes.SETUP_REF
})
assertCode(content)
})
@@ -519,10 +591,10 @@ const { props, emit } = defineOptions({
expect(content).toMatch(`console.log(n.value, a.value, b.value, c.value)`)
expect(content).toMatch(`return { n, a, b, c }`)
expect(bindings).toStrictEqual({
n: BindingTypes.SETUP_CONST_REF,
a: BindingTypes.SETUP_CONST_REF,
b: BindingTypes.SETUP_CONST_REF,
c: BindingTypes.SETUP_CONST_REF
n: BindingTypes.SETUP_REF,
a: BindingTypes.SETUP_REF,
b: BindingTypes.SETUP_REF,
c: BindingTypes.SETUP_REF
})
assertCode(content)
})
@@ -542,9 +614,9 @@ const { props, emit } = defineOptions({
expect(content).toMatch(`\nconst e = _ref(__e);`)
expect(content).toMatch(`return { b, d, e }`)
expect(bindings).toStrictEqual({
b: BindingTypes.SETUP_CONST_REF,
d: BindingTypes.SETUP_CONST_REF,
e: BindingTypes.SETUP_CONST_REF
b: BindingTypes.SETUP_REF,
d: BindingTypes.SETUP_REF,
e: BindingTypes.SETUP_REF
})
assertCode(content)
})
@@ -728,8 +800,8 @@ describe('SFC analyze <script> bindings', () => {
</script>
`)
expect(bindings).toStrictEqual({
foo: BindingTypes.SETUP_CONST_REF,
bar: BindingTypes.SETUP_CONST_REF
foo: BindingTypes.SETUP_MAYBE_REF,
bar: BindingTypes.SETUP_MAYBE_REF
})
})
@@ -748,8 +820,8 @@ describe('SFC analyze <script> bindings', () => {
</script>
`)
expect(bindings).toStrictEqual({
foo: BindingTypes.SETUP_CONST_REF,
bar: BindingTypes.SETUP_CONST_REF
foo: BindingTypes.SETUP_MAYBE_REF,
bar: BindingTypes.SETUP_MAYBE_REF
})
})
@@ -867,7 +939,7 @@ describe('SFC analyze <script> bindings', () => {
expect(bindings).toStrictEqual({
foo: BindingTypes.OPTIONS,
bar: BindingTypes.PROPS,
baz: BindingTypes.SETUP_CONST_REF,
baz: BindingTypes.SETUP_MAYBE_REF,
qux: BindingTypes.DATA,
quux: BindingTypes.OPTIONS,
quuz: BindingTypes.OPTIONS
@@ -877,15 +949,28 @@ describe('SFC analyze <script> bindings', () => {
it('works for script setup', () => {
const { bindings } = compile(`
<script setup>
import { defineOptions } from 'vue'
import { defineOptions, ref as r } from 'vue'
defineOptions({
props: {
foo: String,
}
})
const a = r(1)
let b = 2
const c = 3
const { d } = someFoo()
let { e } = someBar()
</script>
`)
expect(bindings).toStrictEqual({
r: BindingTypes.SETUP_CONST,
a: BindingTypes.SETUP_REF,
b: BindingTypes.SETUP_LET,
c: BindingTypes.SETUP_CONST,
d: BindingTypes.SETUP_MAYBE_REF,
e: BindingTypes.SETUP_LET,
foo: BindingTypes.PROPS
})
})

View File

@@ -20,7 +20,8 @@ import {
Statement,
Expression,
LabeledStatement,
TSUnionType
TSUnionType,
CallExpression
} from '@babel/types'
import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map'
@@ -159,6 +160,7 @@ export function compileScript(
source: string
}
> = Object.create(null)
const userImportAlias: Record<string, string> = Object.create(null)
const setupBindings: Record<string, BindingTypes> = Object.create(null)
const refBindings: Record<string, BindingTypes> = Object.create(null)
const refIdentifiers: Set<Identifier> = new Set()
@@ -220,12 +222,22 @@ export function compileScript(
)
}
function registerUserImport(
source: string,
local: string,
imported: string | false
) {
if (source === 'vue' && imported) {
userImportAlias[imported] = local
}
userImports[local] = {
imported: imported || null,
source
}
}
function processDefineOptions(node: Node): boolean {
if (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === DEFINE_OPTIONS
) {
if (isCallOf(node, DEFINE_OPTIONS)) {
if (hasOptionsCall) {
error(`duplicate ${DEFINE_OPTIONS}() call`, node)
}
@@ -308,7 +320,7 @@ export function compileScript(
if (id.name[0] === '$') {
error(`ref variable identifiers cannot start with $.`, id)
}
refBindings[id.name] = setupBindings[id.name] = BindingTypes.SETUP_CONST_REF
refBindings[id.name] = setupBindings[id.name] = BindingTypes.SETUP_REF
refIdentifiers.add(id)
}
@@ -409,15 +421,11 @@ export function compileScript(
if (node.type === 'ImportDeclaration') {
// record imports for dedupe
for (const specifier of node.specifiers) {
const name = specifier.local.name
const imported =
specifier.type === 'ImportSpecifier' &&
specifier.imported.type === 'Identifier' &&
specifier.imported.name
userImports[name] = {
imported: imported || null,
source: node.source.value
}
registerUserImport(node.source.value, specifier.local.name, imported)
}
} else if (node.type === 'ExportDefaultDeclaration') {
// export default
@@ -567,10 +575,7 @@ export function compileScript(
error(`different imports aliased to same local name.`, specifier)
}
} else {
userImports[local] = {
imported: imported || null,
source: node.source.value
}
registerUserImport(source, local, imported)
}
}
if (removed === node.specifiers.length) {
@@ -605,7 +610,7 @@ export function compileScript(
node.type === 'ClassDeclaration') &&
!node.declare
) {
walkDeclaration(node, setupBindings)
walkDeclaration(node, setupBindings, userImportAlias)
}
// Type declarations
@@ -783,9 +788,10 @@ export function compileScript(
Object.assign(bindingMetadata, analyzeBindingsFromOptions(optionsArg))
}
for (const [key, { source }] of Object.entries(userImports)) {
bindingMetadata[key] = source.endsWith('.vue')
? BindingTypes.SETUP_CONST
: BindingTypes.SETUP_CONST_REF
bindingMetadata[key] =
source.endsWith('.vue') || source === 'vue'
? BindingTypes.SETUP_CONST
: BindingTypes.SETUP_MAYBE_REF
}
for (const key in setupBindings) {
bindingMetadata[key] = setupBindings[key]
@@ -941,32 +947,34 @@ export function compileScript(
function walkDeclaration(
node: Declaration,
bindings: Record<string, BindingTypes>
bindings: Record<string, BindingTypes>,
userImportAlias: Record<string, string>
) {
if (node.type === 'VariableDeclaration') {
const isConst = node.kind === 'const'
// export const foo = ...
for (const { id, init } of node.declarations) {
const isUseOptionsCall = !!(
isConst &&
init &&
init.type === 'CallExpression' &&
init.callee.type === 'Identifier' &&
init.callee.name === DEFINE_OPTIONS
)
const isUseOptionsCall = !!(isConst && isCallOf(init, DEFINE_OPTIONS))
if (id.type === 'Identifier') {
bindings[id.name] =
let bindingType
if (
// if a declaration is a const literal, we can mark it so that
// the generated render fn code doesn't need to unref() it
isUseOptionsCall ||
(isConst &&
init!.type !== 'Identifier' && // const a = b
init!.type !== 'CallExpression' && // const a = ref()
init!.type !== 'MemberExpression') // const a = b.c
? BindingTypes.SETUP_CONST
: isConst
? BindingTypes.SETUP_CONST_REF
: BindingTypes.SETUP_LET
canNeverBeRef(init!, userImportAlias['reactive'] || 'reactive'))
) {
bindingType = BindingTypes.SETUP_CONST
} else if (isConst) {
if (isCallOf(init, userImportAlias['ref'] || 'ref')) {
bindingType = BindingTypes.SETUP_REF
} else {
bindingType = BindingTypes.SETUP_MAYBE_REF
}
} else {
bindingType = BindingTypes.SETUP_LET
}
bindings[id.name] = bindingType
} else if (id.type === 'ObjectPattern') {
walkObjectPattern(id, bindings, isConst, isUseOptionsCall)
} else if (id.type === 'ArrayPattern') {
@@ -998,7 +1006,7 @@ function walkObjectPattern(
bindings[p.key.name] = isUseOptionsCall
? BindingTypes.SETUP_CONST
: isConst
? BindingTypes.SETUP_CONST_REF
? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET
} else {
walkPattern(p.value, bindings, isConst, isUseOptionsCall)
@@ -1035,7 +1043,7 @@ function walkPattern(
bindings[node.name] = isUseOptionsCall
? BindingTypes.SETUP_CONST
: isConst
? BindingTypes.SETUP_CONST_REF
? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET
} else if (node.type === 'RestElement') {
// argument can only be identifer when destructuring
@@ -1051,7 +1059,7 @@ function walkPattern(
bindings[node.left.name] = isUseOptionsCall
? BindingTypes.SETUP_CONST
: isConst
? BindingTypes.SETUP_CONST_REF
? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET
} else {
walkPattern(node.left, bindings, isConst)
@@ -1419,6 +1427,43 @@ function getObjectOrArrayExpressionKeys(property: ObjectProperty): string[] {
return []
}
function isCallOf(node: Node | null, name: string): node is CallExpression {
return !!(
node &&
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === name
)
}
function canNeverBeRef(node: Node, userReactiveImport: string): boolean {
if (isCallOf(node, userReactiveImport)) {
return true
}
switch (node.type) {
case 'UnaryExpression':
case 'BinaryExpression':
case 'ArrayExpression':
case 'ObjectExpression':
case 'FunctionExpression':
case 'ArrowFunctionExpression':
case 'UpdateExpression':
case 'ClassExpression':
case 'TaggedTemplateExpression':
return true
case 'SequenceExpression':
return canNeverBeRef(
node.expressions[node.expressions.length - 1],
userReactiveImport
)
default:
if (node.type.endsWith('Literal')) {
return true
}
return false
}
}
/**
* Analyze bindings in normal `<script>`
* Note that `compileScriptSetup` already analyzes bindings as part of its
@@ -1495,7 +1540,7 @@ function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
for (const key of getObjectExpressionKeys(bodyItem.argument)) {
bindings[key] =
property.key.name === 'setup'
? BindingTypes.SETUP_CONST_REF
? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.DATA
}
}