From 48a1370405abfcb1258e4e4cf7ea247cfba5a5a9 Mon Sep 17 00:00:00 2001 From: edison Date: Thu, 26 Jun 2025 14:44:39 +0800 Subject: [PATCH 1/5] fix(compiler-vapor): properly cache variable with optional chaining (#13519) --- .../transforms/__snapshots__/vBind.spec.ts.snap | 14 ++++++++++++++ .../__tests__/transforms/vBind.spec.ts | 7 +++++++ .../compiler-vapor/src/generators/expression.ts | 1 + 3 files changed, 22 insertions(+) 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..62a68ca42 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,20 @@ export function render(_ctx) { }" `; +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("
") diff --git a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts index 60c3ebf0c..c062c96ba 100644 --- a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts @@ -794,6 +794,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..e2c3c0e17 100644 --- a/packages/compiler-vapor/src/generators/expression.ts +++ b/packages/compiler-vapor/src/generators/expression.ts @@ -588,6 +588,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)}]` From 2074d66c909bcfcc1423c52ec09469b0c7a0d47c Mon Sep 17 00:00:00 2001 From: edison Date: Thu, 26 Jun 2025 14:46:21 +0800 Subject: [PATCH 2/5] fix(compiler-sfc): always use defineComponent in SSR mode (#13525) --- packages/compiler-sfc/src/compileScript.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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}`, From 280829bf73d58d6b17218b3df9b5e38ee8b7f720 Mon Sep 17 00:00:00 2001 From: edison Date: Thu, 26 Jun 2025 15:34:47 +0800 Subject: [PATCH 3/5] fix(runtime-vapor): ensure props are shallow reactive in VDOM component (#13502) --- packages/runtime-vapor/src/vdomInterop.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index b916a2c8e..e277024d7 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -16,6 +16,7 @@ import { isEmitListener, onScopeDispose, renderSlot, + shallowReactive, shallowRef, simpleSetCurrentInstance, } from '@vue/runtime-dom' @@ -163,7 +164,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) { From 66f16ee5db347a572d4d8437c3ec8aa3e3a84e32 Mon Sep 17 00:00:00 2001 From: edison Date: Thu, 26 Jun 2025 15:35:27 +0800 Subject: [PATCH 4/5] fix(compiler-vapor): properly handle static ref in inline mode (#13257) --- .../transformTemplateRef.spec.ts.snap | 9 ++ .../transforms/transformTemplateRef.spec.ts | 11 +++ .../src/generators/templateRef.ts | 20 ++++- pnpm-lock.yaml | 88 +++++++++---------- 4 files changed, 83 insertions(+), 45 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap index cb520a4b2..2d64e1ffe 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap @@ -63,6 +63,15 @@ export function render(_ctx) { }" `; +exports[`compiler: template ref transform > static ref (inline mode) 1`] = ` +" + const _setTemplateRef = _createTemplateRefSetter() + const n0 = t0() + _setTemplateRef(n0, foo) + return n0 +" +`; + exports[`compiler: template ref transform > static ref 1`] = ` "import { createTemplateRefSetter as _createTemplateRefSetter, template as _template } from 'vue'; const t0 = _template("
", true) diff --git a/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts index b6bc479a0..2c883d10c 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts @@ -1,3 +1,4 @@ +import { BindingTypes } from '@vue/compiler-dom' import { DynamicFlag, type ForIRNode, @@ -48,6 +49,16 @@ describe('compiler: template ref transform', () => { expect(code).contains('_setTemplateRef(n0, "foo")') }) + test('static ref (inline mode)', () => { + const { code } = compileWithTransformRef(`
`, { + inline: true, + bindingMetadata: { foo: BindingTypes.SETUP_REF }, + }) + expect(code).matchSnapshot() + // pass the actual ref + expect(code).contains('_setTemplateRef(n0, foo)') + }) + test('dynamic ref', () => { const { ir, code } = compileWithTransformRef(`
`) diff --git a/packages/compiler-vapor/src/generators/templateRef.ts b/packages/compiler-vapor/src/generators/templateRef.ts index a4d6d546e..af8facc57 100644 --- a/packages/compiler-vapor/src/generators/templateRef.ts +++ b/packages/compiler-vapor/src/generators/templateRef.ts @@ -2,6 +2,7 @@ import { genExpression } from './expression' import type { CodegenContext } from '../generate' import type { DeclareOldRefIRNode, SetTemplateRefIRNode } from '../ir' import { type CodeFragment, NEWLINE, genCall } from './utils' +import { BindingTypes, type SimpleExpressionNode } from '@vue/compiler-dom' export const setTemplateRefIdent = `_setTemplateRef` @@ -15,7 +16,7 @@ export function genSetTemplateRef( ...genCall( setTemplateRefIdent, // will be generated in root scope `n${oper.element}`, - genExpression(oper.value, context), + genRefValue(oper.value, context), oper.effect ? `r${oper.element}` : oper.refFor ? 'void 0' : undefined, oper.refFor && 'true', ), @@ -25,3 +26,20 @@ export function genSetTemplateRef( export function genDeclareOldRef(oper: DeclareOldRefIRNode): CodeFragment[] { return [NEWLINE, `let r${oper.id}`] } + +function genRefValue(value: SimpleExpressionNode, context: CodegenContext) { + // in inline mode there is no setupState object, so we can't use string + // keys to set the ref. Instead, we need to transform it to pass the + // actual ref instead. + if (!__BROWSER__ && value && context.options.inline) { + const binding = context.options.bindingMetadata[value.content] + if ( + binding === BindingTypes.SETUP_LET || + binding === BindingTypes.SETUP_REF || + binding === BindingTypes.SETUP_MAYBE_REF + ) { + return [value.content] + } + } + return genExpression(value, context) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8c6e9bb4..52d16261e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,7 +55,7 @@ importers: version: 5.0.4(rollup@4.44.0) '@swc/core': specifier: ^1.11.24 - version: 1.12.3 + version: 1.12.4 '@types/hash-sum': specifier: ^1.0.2 version: 1.0.2 @@ -1288,68 +1288,68 @@ packages: cpu: [x64] os: [win32] - '@swc/core-darwin-arm64@1.12.3': - resolution: {integrity: sha512-QCV9vQ/s27AMxm8j8MTDL/nDoiEMrANiENRrWnb0Fxvz/O39CajPVShp/W7HlOkzt1GYtUXPdQJpSKylugfrWw==} + '@swc/core-darwin-arm64@1.12.4': + resolution: {integrity: sha512-HihKfeitjZU2ab94Zf893sxzFryLKX0TweGsNXXOLNtkSMLw50auuYfpRM0BOL9/uXXtuCWgRIF6P030SAX5xQ==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.12.3': - resolution: {integrity: sha512-LylCMfzGhdvl5tyKaTT9ePetHUX7wSsST7hxWiHzS+cUMj7FnhcfdEr6kcNVT7y1RJn3fCvuv7T98ZB+T2q3HA==} + '@swc/core-darwin-x64@1.12.4': + resolution: {integrity: sha512-meYCXHyYb6RDdu2N5PNAf0EelyxPBFhRcVo4kBFLuvuNb0m6EUg///VWy8MUMXq9/s9uzGS9kJVXXdRdr/d6FA==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.12.3': - resolution: {integrity: sha512-DQODb7S+q+pwQY41Azcavwb2rb4rGxP70niScRDxB9X68hHOM9D0w9fxzC+Nr3AHcPSmVJUYUIiq5h38O5hVgQ==} + '@swc/core-linux-arm-gnueabihf@1.12.4': + resolution: {integrity: sha512-szfDbf7mE8V64of0q/LSqbk+em+T+TD3uqnH40Z7Qu/aL8vi5CHgyLjWG2SLkLLpyjgkAUF6AKrupgnBYcC2NA==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.12.3': - resolution: {integrity: sha512-nTxtJSq78AjeaQBueYImoFBs5j7qXbgOxtirpyt8jE29NQBd0VFzDzRBhkr6I9jq0hNiChgMkqBN4eUkEQjytg==} + '@swc/core-linux-arm64-gnu@1.12.4': + resolution: {integrity: sha512-n0IY76w+Scx8m3HIVRvLkoResuwsQgjDfAk9bxn99dq4leQO+mE0fkPl0Yw/1BIsPh+kxGfopIJH9zsZ1Z2YrA==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.12.3': - resolution: {integrity: sha512-lBGvC5UgPSxqLr/y1NZxQhyRQ7nXy3/Ec1Z47YNXtqtpKiG1EcOGPyS0UZgwiYQkXqq8NBFMHnyHmpKnXTvRDA==} + '@swc/core-linux-arm64-musl@1.12.4': + resolution: {integrity: sha512-wE5jmFi5cEQyLy8WmCWmNwfKETrnzy2D8YNi/xpYWpLPWqPhcelpa6tswkfYlbsMmmOh7hQNoTba1QdGu0jvHQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.12.3': - resolution: {integrity: sha512-61wZ8hwxNYzBY9MCWB50v90ICzdIhOuPk1O1qXswz9AXw5O6iQStEBHQ1rozPkfQ/rmhepk0pOf/6LCwssJOwg==} + '@swc/core-linux-x64-gnu@1.12.4': + resolution: {integrity: sha512-6S50Xd/7ePjEwrXyHMxpKTZ+KBrgUwMA8hQPbArUOwH4S5vHBr51heL0iXbUkppn1bkSr0J0IbOove5hzn+iqQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.12.3': - resolution: {integrity: sha512-NNeBiTpCgWt80vumTKVoaj6Fa/ZjUcaNQNM7np3PIgB8EbuXfyztboV7vUxpkmD/lUgsk8GlEFYViHvo6VMefQ==} + '@swc/core-linux-x64-musl@1.12.4': + resolution: {integrity: sha512-hbYRyaHhC13vYKuGG5BrAG5fjjWEQFfQetuFp/4QKEoXDzdnabJoixxWTQACDL3m0JW32nJ+gUzsYIPtFYkwXg==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.12.3': - resolution: {integrity: sha512-fxraM7exaPb1/W0CoHW45EFNOQUQh0nonBEcNFm2iv095mziBwttyxZyQBoDkQocpkd5NtsZw3xW5FTBPnn+Vw==} + '@swc/core-win32-arm64-msvc@1.12.4': + resolution: {integrity: sha512-e6EbfjPL8GA/bb1lc9Omtxjlz+1ThTsAuBsy4Q3Kpbuh6B3jclg8KzxU/6t91v23wG593mieTyR5f3Pr7X3AWw==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.12.3': - resolution: {integrity: sha512-FFIhMPXIDjRcewomwbYGPvem7Fj76AsuzbRahnAyp+OzJwrrtxVmra/kyUCfj4kix7vdGByY0WvVfiVCf5b7Mg==} + '@swc/core-win32-ia32-msvc@1.12.4': + resolution: {integrity: sha512-RG2FzmllBTUf4EksANlIvLckcBrLZEA0t13LIa6L213UZKQfEuDNHezqESgoVhJMg2S/tWauitATOCFgZNSmjg==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.12.3': - resolution: {integrity: sha512-Sf4iSg+IYT5AzFSDDmii08DfeKcvtkVxIuo+uS8BJMbiLjFNjgMkkVlBthknGyJcSK15ncg9248XjnM4jU8DZA==} + '@swc/core-win32-x64-msvc@1.12.4': + resolution: {integrity: sha512-oRHKnZlR83zaMeVUCmHENa4j5uNRAWbmEpjYbzRcfC45LPFNWKGWGAGERLx0u87XMUtTGqnVYxnBTHN/rzDHOw==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.12.3': - resolution: {integrity: sha512-c4NeXW8P3gPqcFwtm+4aH+F2Cj5KJLMiLaKhSj3mpv19glq+jmekomdktAw/VHyjsXlsmouOeNWrk8rVlkCRsg==} + '@swc/core@1.12.4': + resolution: {integrity: sha512-hn30ebV4njAn0NAUM+3a0qCF+MJgqTNSrfA/hUAbC6TVjOQy2OYGQwkUvCu/V7S2+rZxrUsTpKOnZ7qqECZV9Q==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -4482,51 +4482,51 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.44.0': optional: true - '@swc/core-darwin-arm64@1.12.3': + '@swc/core-darwin-arm64@1.12.4': optional: true - '@swc/core-darwin-x64@1.12.3': + '@swc/core-darwin-x64@1.12.4': optional: true - '@swc/core-linux-arm-gnueabihf@1.12.3': + '@swc/core-linux-arm-gnueabihf@1.12.4': optional: true - '@swc/core-linux-arm64-gnu@1.12.3': + '@swc/core-linux-arm64-gnu@1.12.4': optional: true - '@swc/core-linux-arm64-musl@1.12.3': + '@swc/core-linux-arm64-musl@1.12.4': optional: true - '@swc/core-linux-x64-gnu@1.12.3': + '@swc/core-linux-x64-gnu@1.12.4': optional: true - '@swc/core-linux-x64-musl@1.12.3': + '@swc/core-linux-x64-musl@1.12.4': optional: true - '@swc/core-win32-arm64-msvc@1.12.3': + '@swc/core-win32-arm64-msvc@1.12.4': optional: true - '@swc/core-win32-ia32-msvc@1.12.3': + '@swc/core-win32-ia32-msvc@1.12.4': optional: true - '@swc/core-win32-x64-msvc@1.12.3': + '@swc/core-win32-x64-msvc@1.12.4': optional: true - '@swc/core@1.12.3': + '@swc/core@1.12.4': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.23 optionalDependencies: - '@swc/core-darwin-arm64': 1.12.3 - '@swc/core-darwin-x64': 1.12.3 - '@swc/core-linux-arm-gnueabihf': 1.12.3 - '@swc/core-linux-arm64-gnu': 1.12.3 - '@swc/core-linux-arm64-musl': 1.12.3 - '@swc/core-linux-x64-gnu': 1.12.3 - '@swc/core-linux-x64-musl': 1.12.3 - '@swc/core-win32-arm64-msvc': 1.12.3 - '@swc/core-win32-ia32-msvc': 1.12.3 - '@swc/core-win32-x64-msvc': 1.12.3 + '@swc/core-darwin-arm64': 1.12.4 + '@swc/core-darwin-x64': 1.12.4 + '@swc/core-linux-arm-gnueabihf': 1.12.4 + '@swc/core-linux-arm64-gnu': 1.12.4 + '@swc/core-linux-arm64-musl': 1.12.4 + '@swc/core-linux-x64-gnu': 1.12.4 + '@swc/core-linux-x64-musl': 1.12.4 + '@swc/core-win32-arm64-msvc': 1.12.4 + '@swc/core-win32-ia32-msvc': 1.12.4 + '@swc/core-win32-x64-msvc': 1.12.4 '@swc/counter@0.1.3': {} From bb4ae25793961728a069e77f8a16a963091d8cbe Mon Sep 17 00:00:00 2001 From: edison Date: Thu, 26 Jun 2025 15:41:25 +0800 Subject: [PATCH 5/5] fix(compiler-vapor): handle variable name substring edge cases (#13520) --- .../__snapshots__/vBind.spec.ts.snap | 29 +++++ .../__tests__/transforms/vBind.spec.ts | 19 ++++ .../src/generators/expression.ts | 104 +++++++++++++++--- 3 files changed, 134 insertions(+), 18 deletions(-) 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 62a68ca42..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,21 @@ 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) @@ -194,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 c062c96ba..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(`
diff --git a/packages/compiler-vapor/src/generators/expression.ts b/packages/compiler-vapor/src/generators/expression.ts index e2c3c0e17..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