diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts
index 5825aa032..eb3b2d119 100644
--- a/packages/compiler-sfc/src/compileScript.ts
+++ b/packages/compiler-sfc/src/compileScript.ts
@@ -984,7 +984,7 @@ export function compileScript(
ctx.s.prependLeft(
startOffset,
`\n${genDefaultAs} /*@__PURE__*/${ctx.helper(
- vapor ? `defineVaporComponent` : `defineComponent`,
+ vapor && !ssr ? `defineVaporComponent` : `defineComponent`,
)}({${def}${runtimeOptions}\n ${
hasAwait ? `async ` : ``
}setup(${args}) {\n${exposeCall}`,
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap
index 4e34c1818..4ea0db55f 100644
--- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap
@@ -113,6 +113,35 @@ export function render(_ctx) {
}"
`;
+exports[`cache multiple access > object property name substring cases 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("
", true)
+
+export function render(_ctx) {
+ const n0 = t0()
+ _renderEffect(() => {
+ const _p = _ctx.p
+ const _p_title = _p.title
+ _setProp(n0, "id", _p_title + _p.titles + _p_title)
+ })
+ return n0
+}"
+`;
+
+exports[`cache multiple access > optional chaining 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("", true)
+
+export function render(_ctx) {
+ const n0 = t0()
+ _renderEffect(() => {
+ const _obj = _ctx.obj
+ _setProp(n0, "id", _obj?.foo + _obj?.bar)
+ })
+ return n0
+}"
+`;
+
exports[`cache multiple access > repeated expression in expressions 1`] = `
"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("")
@@ -180,6 +209,20 @@ export function render(_ctx) {
}"
`;
+exports[`cache multiple access > variable name substring edge cases 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("", true)
+
+export function render(_ctx) {
+ const n0 = t0()
+ _renderEffect(() => {
+ const _title = _ctx.title
+ _setProp(n0, "id", _title + _ctx.titles + _title)
+ })
+ return n0
+}"
+`;
+
exports[`compiler v-bind > .attr modifier 1`] = `
"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("", true)
diff --git a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts
index 60c3ebf0c..e96186c27 100644
--- a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts
+++ b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts
@@ -785,6 +785,25 @@ describe('cache multiple access', () => {
expect(code).contains('_setProp(n0, "id", _obj[1][_ctx.baz] + _obj.bar)')
})
+ test('variable name substring edge cases', () => {
+ const { code } = compileWithVBind(
+ ``,
+ )
+ expect(code).matchSnapshot()
+ expect(code).contains('const _title = _ctx.title')
+ expect(code).contains('_setProp(n0, "id", _title + _ctx.titles + _title)')
+ })
+
+ test('object property name substring cases', () => {
+ const { code } = compileWithVBind(
+ ``,
+ )
+ expect(code).matchSnapshot()
+ expect(code).contains('const _p = _ctx.p')
+ expect(code).contains('const _p_title = _p.title')
+ expect(code).contains('_setProp(n0, "id", _p_title + _p.titles + _p_title)')
+ })
+
test('cache variable used in both property shorthand and normal binding', () => {
const { code } = compileWithVBind(`
@@ -794,6 +813,13 @@ describe('cache multiple access', () => {
expect(code).contains('_setStyle(n0, {color: _color})')
})
+ test('optional chaining', () => {
+ const { code } = compileWithVBind(``)
+ expect(code).matchSnapshot()
+ expect(code).contains('const _obj = _ctx.obj')
+ expect(code).contains('_setProp(n0, "id", _obj?.foo + _obj?.bar)')
+ })
+
test('not cache variable only used in property shorthand', () => {
const { code } = compileWithVBind(`
diff --git a/packages/compiler-vapor/src/generators/expression.ts b/packages/compiler-vapor/src/generators/expression.ts
index 845c8bedd..a8fbc8f83 100644
--- a/packages/compiler-vapor/src/generators/expression.ts
+++ b/packages/compiler-vapor/src/generators/expression.ts
@@ -283,7 +283,13 @@ export function processExpressions(
function analyzeExpressions(expressions: SimpleExpressionNode[]) {
const seenVariable: Record = Object.create(null)
const variableToExpMap = new Map>()
- const expToVariableMap = new Map()
+ const expToVariableMap = new Map<
+ SimpleExpressionNode,
+ Array<{
+ name: string
+ loc?: { start: number; end: number }
+ }>
+ >()
const seenIdentifier = new Set()
const updatedVariable = new Set()
@@ -291,6 +297,7 @@ function analyzeExpressions(expressions: SimpleExpressionNode[]) {
name: string,
exp: SimpleExpressionNode,
isIdentifier: boolean,
+ loc?: { start: number; end: number },
parentStack: Node[] = [],
) => {
if (isIdentifier) seenIdentifier.add(name)
@@ -299,7 +306,11 @@ function analyzeExpressions(expressions: SimpleExpressionNode[]) {
name,
(variableToExpMap.get(name) || new Set()).add(exp),
)
- expToVariableMap.set(exp, (expToVariableMap.get(exp) || []).concat(name))
+
+ const variables = expToVariableMap.get(exp) || []
+ variables.push({ name, loc })
+ expToVariableMap.set(exp, variables)
+
if (
parentStack.some(
p => p.type === 'UpdateExpression' || p.type === 'AssignmentExpression',
@@ -317,12 +328,27 @@ function analyzeExpressions(expressions: SimpleExpressionNode[]) {
walkIdentifiers(exp.ast, (currentNode, parent, parentStack) => {
if (parent && isMemberExpression(parent)) {
- const memberExp = extractMemberExpression(parent, name => {
- registerVariable(name, exp, true)
+ const memberExp = extractMemberExpression(parent, id => {
+ registerVariable(id.name, exp, true, {
+ start: id.start!,
+ end: id.end!,
+ })
})
- registerVariable(memberExp, exp, false, parentStack)
+ registerVariable(
+ memberExp,
+ exp,
+ false,
+ { start: parent.start!, end: parent.end! },
+ parentStack,
+ )
} else if (!parentStack.some(isMemberExpression)) {
- registerVariable(currentNode.name, exp, true, parentStack)
+ registerVariable(
+ currentNode.name,
+ exp,
+ true,
+ { start: currentNode.start!, end: currentNode.end! },
+ parentStack,
+ )
}
})
}
@@ -340,11 +366,22 @@ function processRepeatedVariables(
context: CodegenContext,
seenVariable: Record,
variableToExpMap: Map>,
- expToVariableMap: Map,
+ expToVariableMap: Map<
+ SimpleExpressionNode,
+ Array<{ name: string; loc?: { start: number; end: number } }>
+ >,
seenIdentifier: Set,
updatedVariable: Set,
): DeclarationValue[] {
const declarations: DeclarationValue[] = []
+ const expToReplacementMap = new Map<
+ SimpleExpressionNode,
+ Array<{
+ name: string
+ locs: { start: number; end: number }[]
+ }>
+ >()
+
for (const [name, exps] of variableToExpMap) {
if (updatedVariable.has(name)) continue
if (seenVariable[name] > 1 && exps.size > 0) {
@@ -356,12 +393,20 @@ function processRepeatedVariables(
// e.g., foo[baz] -> foo_baz.
// for identifiers, we don't need to replace the content - they will be
// replaced during context.withId(..., ids)
- const replaceRE = new RegExp(escapeRegExp(name), 'g')
exps.forEach(node => {
- if (node.ast) {
- node.content = node.content.replace(replaceRE, varName)
- // re-parse the expression
- node.ast = parseExp(context, node.content)
+ if (node.ast && varName !== name) {
+ const replacements = expToReplacementMap.get(node) || []
+ replacements.push({
+ name: varName,
+ locs: expToVariableMap.get(node)!.reduce(
+ (locs, v) => {
+ if (v.name === name && v.loc) locs.push(v.loc)
+ return locs
+ },
+ [] as { start: number; end: number }[],
+ ),
+ })
+ expToReplacementMap.set(node, replacements)
}
})
@@ -384,15 +429,35 @@ function processRepeatedVariables(
}
}
+ for (const [exp, replacements] of expToReplacementMap) {
+ replacements
+ .flatMap(({ name, locs }) =>
+ locs.map(({ start, end }) => ({ start, end, name })),
+ )
+ .sort((a, b) => b.end - a.end)
+ .forEach(({ start, end, name }) => {
+ exp.content =
+ exp.content.slice(0, start - 1) + name + exp.content.slice(end - 1)
+ })
+
+ // re-parse the expression
+ exp.ast = parseExp(context, exp.content)
+ }
+
return declarations
}
function shouldDeclareVariable(
name: string,
- expToVariableMap: Map,
+ expToVariableMap: Map<
+ SimpleExpressionNode,
+ Array<{ name: string; loc?: { start: number; end: number } }>
+ >,
exps: Set,
): boolean {
- const vars = Array.from(exps, exp => expToVariableMap.get(exp)!)
+ const vars = Array.from(exps, exp =>
+ expToVariableMap.get(exp)!.map(v => v.name),
+ )
// assume name equals to `foo`
// if each expression only references `foo`, declaration is needed
// to avoid reactivity tracking
@@ -439,12 +504,15 @@ function processRepeatedExpressions(
expressions: SimpleExpressionNode[],
varDeclarations: DeclarationValue[],
updatedVariable: Set,
- expToVariableMap: Map,
+ expToVariableMap: Map<
+ SimpleExpressionNode,
+ Array<{ name: string; loc?: { start: number; end: number } }>
+ >,
): DeclarationValue[] {
const declarations: DeclarationValue[] = []
const seenExp = expressions.reduce(
(acc, exp) => {
- const variables = expToVariableMap.get(exp)
+ const variables = expToVariableMap.get(exp)!.map(v => v.name)
// only handle expressions that are not identifiers
if (
exp.ast &&
@@ -572,12 +640,12 @@ function genVarName(exp: string): string {
function extractMemberExpression(
exp: Node,
- onIdentifier: (name: string) => void,
+ onIdentifier: (id: Identifier) => void,
): string {
if (!exp) return ''
switch (exp.type) {
case 'Identifier': // foo[bar]
- onIdentifier(exp.name)
+ onIdentifier(exp)
return exp.name
case 'StringLiteral': // foo['bar']
return exp.extra ? (exp.extra.raw as string) : exp.value
@@ -588,6 +656,7 @@ function extractMemberExpression(
case 'CallExpression': // foo[bar(baz)]
return `${extractMemberExpression(exp.callee, onIdentifier)}(${exp.arguments.map(arg => extractMemberExpression(arg, onIdentifier)).join(', ')})`
case 'MemberExpression': // foo[bar.baz]
+ case 'OptionalMemberExpression': // foo?.bar
const object = extractMemberExpression(exp.object, onIdentifier)
const prop = exp.computed
? `[${extractMemberExpression(exp.property, onIdentifier)}]`
diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts
index 3365910ce..9d90f7230 100644
--- a/packages/runtime-vapor/src/vdomInterop.ts
+++ b/packages/runtime-vapor/src/vdomInterop.ts
@@ -18,6 +18,7 @@ import {
isEmitListener,
onScopeDispose,
renderSlot,
+ shallowReactive,
shallowRef,
simpleSetCurrentInstance,
} from '@vue/runtime-dom'
@@ -187,7 +188,8 @@ function createVDOMComponent(
// overwrite how the vdom instance handles props
vnode.vi = (instance: ComponentInternalInstance) => {
- instance.props = wrapper.props
+ // ensure props are shallow reactive to align with VDOM behavior.
+ instance.props = shallowReactive(wrapper.props)
const attrs = (instance.attrs = createInternalObject())
for (const key in wrapper.attrs) {