wip: properly handle assignment/update expressions in inline mode
This commit is contained in:
parent
4449fc3b9e
commit
8567feb2aa
@ -83,7 +83,11 @@ export const enum BindingTypes {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@ -91,7 +95,7 @@ export const enum BindingTypes {
|
||||
}
|
||||
|
||||
export interface BindingMetadata {
|
||||
[key: string]: BindingTypes
|
||||
[key: string]: BindingTypes | undefined
|
||||
}
|
||||
|
||||
interface SharedTransformCodegenOptions {
|
||||
|
@ -272,7 +272,8 @@ export function resolveComponentType(
|
||||
}
|
||||
const tagFromSetup =
|
||||
checkType(BindingTypes.SETUP_LET) ||
|
||||
checkType(BindingTypes.SETUP_CONST_REF)
|
||||
checkType(BindingTypes.SETUP_REF) ||
|
||||
checkType(BindingTypes.SETUP_MAYBE_REF)
|
||||
if (tagFromSetup) {
|
||||
return context.inline
|
||||
? // setup scope bindings that may be refs need to be unrefed
|
||||
|
@ -21,14 +21,22 @@ import {
|
||||
isGloballyWhitelisted,
|
||||
makeMap,
|
||||
babelParserDefaultPlugins,
|
||||
hasOwn
|
||||
hasOwn,
|
||||
isString
|
||||
} from '@vue/shared'
|
||||
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 { parse } from '@babel/parser'
|
||||
import { walk } from 'estree-walker'
|
||||
import { UNREF } from '../runtimeHelpers'
|
||||
import { IS_REF, UNREF } from '../runtimeHelpers'
|
||||
import { BindingTypes } from '../options'
|
||||
|
||||
const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
|
||||
@ -100,28 +108,81 @@ export function processExpression(
|
||||
}
|
||||
|
||||
const { inline, bindingMetadata } = context
|
||||
const prefix = (raw: string) => {
|
||||
const rewriteIdentifier = (raw: string, parent?: Node, id?: Identifier) => {
|
||||
const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw]
|
||||
if (inline) {
|
||||
const isAssignmentLVal =
|
||||
parent && parent.type === 'AssignmentExpression' && parent.left === id
|
||||
const isUpdateArg =
|
||||
parent && parent.type === 'UpdateExpression' && parent.argument === id
|
||||
// setup inline mode
|
||||
if (type === BindingTypes.SETUP_CONST) {
|
||||
return raw
|
||||
} else if (type === BindingTypes.SETUP_REF) {
|
||||
return isAssignmentLVal || isUpdateArg
|
||||
? `${raw}.value`
|
||||
: `${context.helperString(UNREF)}(${raw})`
|
||||
} else if (
|
||||
type === BindingTypes.SETUP_CONST_REF ||
|
||||
type === BindingTypes.SETUP_MAYBE_REF ||
|
||||
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})`
|
||||
}
|
||||
} else if (type === BindingTypes.PROPS) {
|
||||
// use __props which is generated by compileScript so in ts mode
|
||||
// it gets correct type
|
||||
return `__props.${raw}`
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
type === BindingTypes.SETUP_LET ||
|
||||
type === BindingTypes.SETUP_CONST ||
|
||||
type === BindingTypes.SETUP_CONST_REF
|
||||
) {
|
||||
if (type && type.startsWith('setup')) {
|
||||
// setup bindings in non-inline mode
|
||||
return `$setup.${raw}`
|
||||
} else if (type) {
|
||||
@ -149,7 +210,7 @@ export function processExpression(
|
||||
!isGloballyWhitelisted(rawExp) &&
|
||||
!isLiteralWhitelisted(rawExp)
|
||||
) {
|
||||
node.content = prefix(rawExp)
|
||||
node.content = rewriteIdentifier(rawExp)
|
||||
} else if (!context.identifiers[rawExp] && !bailConstant) {
|
||||
// mark node constant for hoisting unless it's referring a scope variable
|
||||
node.isConstant = true
|
||||
@ -199,7 +260,7 @@ export function processExpression(
|
||||
// we rewrite the value
|
||||
node.prefix = `${node.name}: `
|
||||
}
|
||||
node.name = prefix(node.name)
|
||||
node.name = rewriteIdentifier(node.name, parent, node)
|
||||
ids.push(node)
|
||||
} else if (!isStaticPropertyKey(node, parent)) {
|
||||
// The identifier is considered constant unless it's pointing to a
|
||||
@ -373,3 +434,15 @@ function shouldPrefix(id: Identifier, parent: Node) {
|
||||
|
||||
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('')
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,8 @@ import {
|
||||
createCompoundExpression,
|
||||
NodeTypes,
|
||||
Property,
|
||||
ElementTypes
|
||||
ElementTypes,
|
||||
ExpressionNode
|
||||
} from '../ast'
|
||||
import { createCompilerError, ErrorCodes } from '../errors'
|
||||
import {
|
||||
@ -14,7 +15,8 @@ import {
|
||||
hasScopeRef,
|
||||
isStaticExp
|
||||
} 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) => {
|
||||
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
|
||||
// _unref(exp)
|
||||
const isUnrefExp =
|
||||
!__BROWSER__ && expString.startsWith(`_${helperNameMap[UNREF]}`)
|
||||
const bindingType = context.bindingMetadata[rawExp]
|
||||
const maybeRef =
|
||||
!__BROWSER__ &&
|
||||
context.inline &&
|
||||
bindingType &&
|
||||
bindingType !== BindingTypes.SETUP_CONST
|
||||
|
||||
if (!isMemberExpression(expString) && !isUnrefExp) {
|
||||
if (!isMemberExpression(expString) && !maybeRef) {
|
||||
context.onError(
|
||||
createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc)
|
||||
)
|
||||
@ -60,25 +66,40 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
|
||||
: createCompoundExpression(['"onUpdate:" + ', arg])
|
||||
: `onUpdate:modelValue`
|
||||
|
||||
const assigmentExp = isUnrefExp
|
||||
? // v-model used on a potentially ref binding in <script setup> inline mode.
|
||||
let assignmentExp: ExpressionNode
|
||||
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.
|
||||
createSimpleExpression(
|
||||
`$event => (${context.helperString(IS_REF)}(${rawExp}) ` +
|
||||
`? (${rawExp}.value = $event) ` +
|
||||
`: ${context.isTS ? `//@ts-ignore\n` : ``}` +
|
||||
`(${rawExp} = $event)` +
|
||||
`)`,
|
||||
false,
|
||||
exp.loc
|
||||
)
|
||||
: createCompoundExpression([`$event => (`, exp, ` = $event)`])
|
||||
const altAssignment =
|
||||
bindingType === BindingTypes.SETUP_LET ? `${rawExp} = $event` : `null`
|
||||
assignmentExp = createCompoundExpression([
|
||||
`${eventArg} => (${context.helperString(IS_REF)}(${rawExp}) ? `,
|
||||
createSimpleExpression(rawExp, false, exp.loc),
|
||||
`.value = $event : ${altAssignment})`
|
||||
])
|
||||
}
|
||||
} else {
|
||||
assignmentExp = createCompoundExpression([
|
||||
`${eventArg} => (`,
|
||||
exp,
|
||||
` = $event)`
|
||||
])
|
||||
}
|
||||
|
||||
const props = [
|
||||
// modelValue: foo
|
||||
createObjectProperty(propName, dir.exp!),
|
||||
// "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)
|
||||
|
@ -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 */)), [
|
||||
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 */))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
})
|
||||
})
|
||||
|
@ -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 processDefineOptions(node: Node): boolean {
|
||||
if (
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === DEFINE_OPTIONS
|
||||
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 (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')
|
||||
bindingMetadata[key] =
|
||||
source.endsWith('.vue') || source === 'vue'
|
||||
? BindingTypes.SETUP_CONST
|
||||
: BindingTypes.SETUP_CONST_REF
|
||||
: 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
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user