wip: properly handle assignment/update expressions in inline mode
This commit is contained in:
@@ -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
|
||||
) {
|
||||
return `${context.helperString(UNREF)}(${raw})`
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user