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

@@ -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

View File

@@ -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('')
}
}

View File

@@ -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)