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

@ -83,7 +83,11 @@ export const enum BindingTypes {
/** /**
* a const binding that may be a ref. * a const binding that may be a ref.
*/ */
SETUP_CONST_REF = 'setup-const-ref', SETUP_MAYBE_REF = 'setup-maybe-ref',
/**
* bindings that are guaranteed to be refs
*/
SETUP_REF = 'setup-ref',
/** /**
* declared by other options, e.g. computed, inject * declared by other options, e.g. computed, inject
*/ */
@ -91,7 +95,7 @@ export const enum BindingTypes {
} }
export interface BindingMetadata { export interface BindingMetadata {
[key: string]: BindingTypes [key: string]: BindingTypes | undefined
} }
interface SharedTransformCodegenOptions { interface SharedTransformCodegenOptions {

View File

@ -272,7 +272,8 @@ export function resolveComponentType(
} }
const tagFromSetup = const tagFromSetup =
checkType(BindingTypes.SETUP_LET) || checkType(BindingTypes.SETUP_LET) ||
checkType(BindingTypes.SETUP_CONST_REF) checkType(BindingTypes.SETUP_REF) ||
checkType(BindingTypes.SETUP_MAYBE_REF)
if (tagFromSetup) { if (tagFromSetup) {
return context.inline return context.inline
? // setup scope bindings that may be refs need to be unrefed ? // setup scope bindings that may be refs need to be unrefed

View File

@ -21,14 +21,22 @@ import {
isGloballyWhitelisted, isGloballyWhitelisted,
makeMap, makeMap,
babelParserDefaultPlugins, babelParserDefaultPlugins,
hasOwn hasOwn,
isString
} from '@vue/shared' } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { Node, Function, Identifier, ObjectProperty } from '@babel/types' import {
Node,
Function,
Identifier,
ObjectProperty,
AssignmentExpression,
UpdateExpression
} from '@babel/types'
import { validateBrowserExpression } from '../validateExpression' import { validateBrowserExpression } from '../validateExpression'
import { parse } from '@babel/parser' import { parse } from '@babel/parser'
import { walk } from 'estree-walker' import { walk } from 'estree-walker'
import { UNREF } from '../runtimeHelpers' import { IS_REF, UNREF } from '../runtimeHelpers'
import { BindingTypes } from '../options' import { BindingTypes } from '../options'
const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this') const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
@ -100,28 +108,81 @@ export function processExpression(
} }
const { inline, bindingMetadata } = context const { inline, bindingMetadata } = context
const prefix = (raw: string) => { 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) {
const isAssignmentLVal =
parent && parent.type === 'AssignmentExpression' && parent.left === id
const isUpdateArg =
parent && parent.type === 'UpdateExpression' && parent.argument === id
// setup inline mode // setup inline mode
if (type === BindingTypes.SETUP_CONST) { if (type === BindingTypes.SETUP_CONST) {
return raw return raw
} else if (type === BindingTypes.SETUP_REF) {
return isAssignmentLVal || isUpdateArg
? `${raw}.value`
: `${context.helperString(UNREF)}(${raw})`
} else if ( } else if (
type === BindingTypes.SETUP_CONST_REF || type === BindingTypes.SETUP_MAYBE_REF ||
type === BindingTypes.SETUP_LET type === BindingTypes.SETUP_LET
) { ) {
if (isAssignmentLVal) {
if (type === BindingTypes.SETUP_MAYBE_REF) {
// const binding that may or may not be ref
// if it's not a ref, then the assignment doesn't make sense so
// just no-op it
// x = y ---> !isRef(x) ? null : x.value = y
return `!${context.helperString(
IS_REF
)}(${raw}) ? null : ${raw}.value`
} else {
// let binding.
// this is a bit more tricky as we need to cover the case where
// let is a local non-ref value, and we need to replicate the
// right hand side value.
// x = y --> isRef(x) ? x.value = y : x = y
const rVal = (parent as AssignmentExpression).right
const rExp = rawExp.slice(rVal.start! - 1, rVal.end! - 1)
const rExpString = stringifyExpression(
processExpression(createSimpleExpression(rExp, false), context)
)
return `${context.helperString(IS_REF)}(${raw})${
context.isTS ? ` //@ts-ignore\n` : ``
} ? ${raw}.value = ${rExpString} : ${raw}`
}
} else if (isUpdateArg) {
// make id replace parent in the code range so the raw update operator
// is removed
id!.start = parent!.start
id!.end = parent!.end
const { prefix: isPrefix, operator } = parent as UpdateExpression
const prefix = isPrefix ? operator : ``
const postfix = isPrefix ? `` : operator
if (type === BindingTypes.SETUP_MAYBE_REF) {
// const binding that may or may not be ref
// if it's not a ref, then the assignment doesn't make sense so
// just no-op it
// x++ ---> !isRef(x) ? null : x.value++
return `!${context.helperString(
IS_REF
)}(${raw}) ? null : ${prefix}${raw}.value${postfix}`
} else {
// let binding.
// x++ --> isRef(a) ? a.value++ : a++
return `${context.helperString(IS_REF)}(${raw})${
context.isTS ? ` //@ts-ignore\n` : ``
} ? ${prefix}${raw}.value${postfix} : ${prefix}${raw}${postfix}`
}
} else {
return `${context.helperString(UNREF)}(${raw})` return `${context.helperString(UNREF)}(${raw})`
}
} else if (type === BindingTypes.PROPS) { } else if (type === BindingTypes.PROPS) {
// use __props which is generated by compileScript so in ts mode // use __props which is generated by compileScript so in ts mode
// it gets correct type // it gets correct type
return `__props.${raw}` return `__props.${raw}`
} }
} else { } else {
if ( if (type && type.startsWith('setup')) {
type === BindingTypes.SETUP_LET ||
type === BindingTypes.SETUP_CONST ||
type === BindingTypes.SETUP_CONST_REF
) {
// setup bindings in non-inline mode // setup bindings in non-inline mode
return `$setup.${raw}` return `$setup.${raw}`
} else if (type) { } else if (type) {
@ -149,7 +210,7 @@ export function processExpression(
!isGloballyWhitelisted(rawExp) && !isGloballyWhitelisted(rawExp) &&
!isLiteralWhitelisted(rawExp) !isLiteralWhitelisted(rawExp)
) { ) {
node.content = prefix(rawExp) node.content = rewriteIdentifier(rawExp)
} else if (!context.identifiers[rawExp] && !bailConstant) { } else if (!context.identifiers[rawExp] && !bailConstant) {
// mark node constant for hoisting unless it's referring a scope variable // mark node constant for hoisting unless it's referring a scope variable
node.isConstant = true node.isConstant = true
@ -199,7 +260,7 @@ export function processExpression(
// we rewrite the value // we rewrite the value
node.prefix = `${node.name}: ` node.prefix = `${node.name}: `
} }
node.name = prefix(node.name) 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
@ -373,3 +434,15 @@ function shouldPrefix(id: Identifier, parent: Node) {
return true return true
} }
function stringifyExpression(exp: ExpressionNode | string): string {
if (isString(exp)) {
return exp
} else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
return exp.content
} else {
return (exp.children as (ExpressionNode | string)[])
.map(stringifyExpression)
.join('')
}
}

View File

@ -5,7 +5,8 @@ import {
createCompoundExpression, createCompoundExpression,
NodeTypes, NodeTypes,
Property, Property,
ElementTypes ElementTypes,
ExpressionNode
} from '../ast' } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { import {
@ -14,7 +15,8 @@ import {
hasScopeRef, hasScopeRef,
isStaticExp isStaticExp
} from '../utils' } from '../utils'
import { helperNameMap, IS_REF, UNREF } from '../runtimeHelpers' import { IS_REF } from '../runtimeHelpers'
import { BindingTypes } from '../options'
export const transformModel: DirectiveTransform = (dir, node, context) => { export const transformModel: DirectiveTransform = (dir, node, context) => {
const { exp, arg } = dir const { exp, arg } = dir
@ -31,10 +33,14 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
// im SFC <script setup> inline mode, the exp may have been transformed into // im SFC <script setup> inline mode, the exp may have been transformed into
// _unref(exp) // _unref(exp)
const isUnrefExp = const bindingType = context.bindingMetadata[rawExp]
!__BROWSER__ && expString.startsWith(`_${helperNameMap[UNREF]}`) const maybeRef =
!__BROWSER__ &&
context.inline &&
bindingType &&
bindingType !== BindingTypes.SETUP_CONST
if (!isMemberExpression(expString) && !isUnrefExp) { if (!isMemberExpression(expString) && !maybeRef) {
context.onError( context.onError(
createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc) createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc)
) )
@ -60,25 +66,40 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
: createCompoundExpression(['"onUpdate:" + ', arg]) : createCompoundExpression(['"onUpdate:" + ', arg])
: `onUpdate:modelValue` : `onUpdate:modelValue`
const assigmentExp = isUnrefExp let assignmentExp: ExpressionNode
? // v-model used on a potentially ref binding in <script setup> inline mode. const eventArg = context.isTS ? `($event: any)` : `$event`
if (maybeRef) {
if (bindingType === BindingTypes.SETUP_REF) {
// v-model used on known ref.
assignmentExp = createCompoundExpression([
`${eventArg} => (`,
createSimpleExpression(rawExp, false, exp.loc),
`.value = $event)`
])
} else {
// v-model used on a potentially ref binding in <script setup> inline mode.
// the assignment needs to check whether the binding is actually a ref. // the assignment needs to check whether the binding is actually a ref.
createSimpleExpression( const altAssignment =
`$event => (${context.helperString(IS_REF)}(${rawExp}) ` + bindingType === BindingTypes.SETUP_LET ? `${rawExp} = $event` : `null`
`? (${rawExp}.value = $event) ` + assignmentExp = createCompoundExpression([
`: ${context.isTS ? `//@ts-ignore\n` : ``}` + `${eventArg} => (${context.helperString(IS_REF)}(${rawExp}) ? `,
`(${rawExp} = $event)` + createSimpleExpression(rawExp, false, exp.loc),
`)`, `.value = $event : ${altAssignment})`
false, ])
exp.loc }
) } else {
: createCompoundExpression([`$event => (`, exp, ` = $event)`]) assignmentExp = createCompoundExpression([
`${eventArg} => (`,
exp,
` = $event)`
])
}
const props = [ const props = [
// modelValue: foo // modelValue: foo
createObjectProperty(propName, dir.exp!), createObjectProperty(propName, dir.exp!),
// "onUpdate:modelValue": $event => (foo = $event) // "onUpdate:modelValue": $event => (foo = $event)
createObjectProperty(eventName, assigmentExp) createObjectProperty(eventName, assignmentExp)
] ]
// cache v-model handler if applicable (when it doesn't refer any scope vars) // cache v-model handler if applicable (when it doesn't refer any scope vars)

View File

@ -166,8 +166,8 @@ return (_ctx, _cache) => {
}" }"
`; `;
exports[`SFC compile <script setup> inlineTemplate mode v-model codegen with unref() 1`] = ` exports[`SFC compile <script setup> inlineTemplate mode template assignment expression codegen 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\\" "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' import { ref } from 'vue'
@ -176,13 +176,98 @@ export default {
setup(__props) { setup(__props) {
const count = ref(0) const count = ref(0)
const maybe = foo()
let lett = 1
return (_ctx, _cache) => { return (_ctx, _cache) => {
return _withDirectives((_openBlock(), _createBlock(\\"input\\", { return (_openBlock(), _createBlock(_Fragment, null, [
\\"onUpdate:modelValue\\": _cache[1] || (_cache[1] = $event => (_isRef(count) ? (count.value = $event) : (count = $event))) _createVNode(\\"div\\", {
}, null, 512 /* NEED_PATCH */)), [ 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)] [_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`) expect(content).not.toMatch(`PROPS`)
}) })
test('v-model codegen with unref()', () => { test('v-model codegen', () => {
const { content } = compile( const { content } = compile(
`<script setup> `<script setup>
import { ref } from 'vue' import { ref } from 'vue'
const count = ref(0) const count = ref(0)
const maybe = foo()
let lett = 1
</script> </script>
<template> <template>
<input v-model="count"> <input v-model="count">
<input v-model="maybe">
<input v-model="lett">
</template> </template>
`, `,
{ inlineTemplate: true } { 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) assertCode(content)
}) })
}) })
@ -381,9 +453,9 @@ const { props, emit } = defineOptions({
expect(content).toMatch(`let d`) expect(content).toMatch(`let d`)
assertCode(content) assertCode(content)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
foo: BindingTypes.SETUP_CONST_REF, foo: BindingTypes.SETUP_REF,
a: BindingTypes.SETUP_CONST_REF, a: BindingTypes.SETUP_REF,
b: BindingTypes.SETUP_CONST_REF, b: BindingTypes.SETUP_REF,
c: BindingTypes.SETUP_LET, c: BindingTypes.SETUP_LET,
d: BindingTypes.SETUP_LET d: BindingTypes.SETUP_LET
}) })
@ -403,9 +475,9 @@ const { props, emit } = defineOptions({
expect(content).toMatch(`return { a, b, c }`) expect(content).toMatch(`return { a, b, c }`)
assertCode(content) assertCode(content)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
a: BindingTypes.SETUP_CONST_REF, a: BindingTypes.SETUP_REF,
b: BindingTypes.SETUP_CONST_REF, b: BindingTypes.SETUP_REF,
c: BindingTypes.SETUP_CONST_REF c: BindingTypes.SETUP_REF
}) })
}) })
@ -495,12 +567,12 @@ const { props, emit } = defineOptions({
) )
expect(content).toMatch(`return { n, a, c, d, f, g }`) expect(content).toMatch(`return { n, a, c, d, f, g }`)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
n: BindingTypes.SETUP_CONST_REF, n: BindingTypes.SETUP_REF,
a: BindingTypes.SETUP_CONST_REF, a: BindingTypes.SETUP_REF,
c: BindingTypes.SETUP_CONST_REF, c: BindingTypes.SETUP_REF,
d: BindingTypes.SETUP_CONST_REF, d: BindingTypes.SETUP_REF,
f: BindingTypes.SETUP_CONST_REF, f: BindingTypes.SETUP_REF,
g: BindingTypes.SETUP_CONST_REF g: BindingTypes.SETUP_REF
}) })
assertCode(content) 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(`console.log(n.value, a.value, b.value, c.value)`)
expect(content).toMatch(`return { n, a, b, c }`) expect(content).toMatch(`return { n, a, b, c }`)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
n: BindingTypes.SETUP_CONST_REF, n: BindingTypes.SETUP_REF,
a: BindingTypes.SETUP_CONST_REF, a: BindingTypes.SETUP_REF,
b: BindingTypes.SETUP_CONST_REF, b: BindingTypes.SETUP_REF,
c: BindingTypes.SETUP_CONST_REF c: BindingTypes.SETUP_REF
}) })
assertCode(content) assertCode(content)
}) })
@ -542,9 +614,9 @@ const { props, emit } = defineOptions({
expect(content).toMatch(`\nconst e = _ref(__e);`) expect(content).toMatch(`\nconst e = _ref(__e);`)
expect(content).toMatch(`return { b, d, e }`) expect(content).toMatch(`return { b, d, e }`)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
b: BindingTypes.SETUP_CONST_REF, b: BindingTypes.SETUP_REF,
d: BindingTypes.SETUP_CONST_REF, d: BindingTypes.SETUP_REF,
e: BindingTypes.SETUP_CONST_REF e: BindingTypes.SETUP_REF
}) })
assertCode(content) assertCode(content)
}) })
@ -728,8 +800,8 @@ describe('SFC analyze <script> bindings', () => {
</script> </script>
`) `)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
foo: BindingTypes.SETUP_CONST_REF, foo: BindingTypes.SETUP_MAYBE_REF,
bar: BindingTypes.SETUP_CONST_REF bar: BindingTypes.SETUP_MAYBE_REF
}) })
}) })
@ -748,8 +820,8 @@ describe('SFC analyze <script> bindings', () => {
</script> </script>
`) `)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
foo: BindingTypes.SETUP_CONST_REF, foo: BindingTypes.SETUP_MAYBE_REF,
bar: BindingTypes.SETUP_CONST_REF bar: BindingTypes.SETUP_MAYBE_REF
}) })
}) })
@ -867,7 +939,7 @@ describe('SFC analyze <script> bindings', () => {
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
foo: BindingTypes.OPTIONS, foo: BindingTypes.OPTIONS,
bar: BindingTypes.PROPS, bar: BindingTypes.PROPS,
baz: BindingTypes.SETUP_CONST_REF, baz: BindingTypes.SETUP_MAYBE_REF,
qux: BindingTypes.DATA, qux: BindingTypes.DATA,
quux: BindingTypes.OPTIONS, quux: BindingTypes.OPTIONS,
quuz: BindingTypes.OPTIONS quuz: BindingTypes.OPTIONS
@ -877,15 +949,28 @@ describe('SFC analyze <script> bindings', () => {
it('works for script setup', () => { it('works for script setup', () => {
const { bindings } = compile(` const { bindings } = compile(`
<script setup> <script setup>
import { defineOptions } from 'vue' import { defineOptions, ref as r } from 'vue'
defineOptions({ defineOptions({
props: { props: {
foo: String, foo: String,
} }
}) })
const a = r(1)
let b = 2
const c = 3
const { d } = someFoo()
let { e } = someBar()
</script> </script>
`) `)
expect(bindings).toStrictEqual({ 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 foo: BindingTypes.PROPS
}) })
}) })

View File

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