From d0d4d2b067f95ab7bfa69b0bf812eedccf1f3a5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:09:02 +0000 Subject: [PATCH 1/3] Initial plan From 3561fff89e3e317cce98e2b6b3a73ea5939d1c3e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:18:35 +0000 Subject: [PATCH 2/3] fix(rsc): fix false detection of import.meta.viteRsc API inside comments and strings Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- packages/plugin-rsc/package.json | 1 + packages/plugin-rsc/src/plugin.ts | 16 ++- .../src/plugins/strip-literal.test.ts | 125 ++++++++++++++++++ pnpm-lock.yaml | 3 + 4 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 packages/plugin-rsc/src/plugins/strip-literal.test.ts diff --git a/packages/plugin-rsc/package.json b/packages/plugin-rsc/package.json index d04999d32..184be3408 100644 --- a/packages/plugin-rsc/package.json +++ b/packages/plugin-rsc/package.json @@ -44,6 +44,7 @@ "estree-walker": "^3.0.3", "magic-string": "^0.30.19", "periscopic": "^4.0.2", + "strip-literal": "^3.1.0", "turbo-stream": "^3.1.0", "vitefu": "^1.1.1" }, diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 1dd69d51a..8dd2e56ca 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -55,6 +55,7 @@ import { scanBuildStripPlugin } from './plugins/scan' import { validateImportPlugin } from './plugins/validate-import' import { vitePluginFindSourceMapURL } from './plugins/find-source-map-url' import { parseCssVirtual, toCssVirtual, parseIdQuery } from './plugins/shared' +import { stripLiteral } from 'strip-literal' const isRolldownVite = 'rolldownVersion' in vite @@ -702,10 +703,11 @@ export default function vitePluginRsc( if (!code.includes('import.meta.viteRsc.loadModule')) return const { server } = manager const s = new MagicString(code) - for (const match of code.matchAll( + for (const match of stripLiteral(code).matchAll( /import\.meta\.viteRsc\.loadModule\(([\s\S]*?)\)/dg, )) { - const argCode = match[1]!.trim() + const [argStart, argEnd] = match.indices![1]! + const argCode = code.slice(argStart, argEnd).trim() const [environmentName, entryName] = evalValue(`[${argCode}]`) let replacement: string if ( @@ -973,10 +975,11 @@ export default assetsManifest.bootstrapScriptContent; assert(this.environment.name !== 'client') const output = new MagicString(code) - for (const match of code.matchAll( + for (const match of stripLiteral(code).matchAll( /import\s*\.\s*meta\s*\.\s*viteRsc\s*\.\s*loadBootstrapScriptContent\(([\s\S]*?)\)/dg, )) { - const argCode = match[1]!.trim() + const [argStart, argEnd] = match.indices![1]! + const argCode = code.slice(argStart, argEnd).trim() const entryName = evalValue(argCode) assert( entryName, @@ -2087,11 +2090,12 @@ function vitePluginRscCss( const output = new MagicString(code) let importAdded = false - for (const match of code.matchAll( + for (const match of stripLiteral(code).matchAll( /import\.meta\.viteRsc\.loadCss\(([\s\S]*?)\)/dg, )) { const [start, end] = match.indices![0]! - const argCode = match[1]!.trim() + const [argStart, argEnd] = match.indices![1]! + const argCode = code.slice(argStart, argEnd).trim() let importer = id if (argCode) { const argValue = evalValue(argCode) diff --git a/packages/plugin-rsc/src/plugins/strip-literal.test.ts b/packages/plugin-rsc/src/plugins/strip-literal.test.ts new file mode 100644 index 000000000..c3f1c88a7 --- /dev/null +++ b/packages/plugin-rsc/src/plugins/strip-literal.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, it } from 'vitest' +import { stripLiteral } from 'strip-literal' + +describe('stripLiteral for viteRsc API detection', () => { + it('should strip comments with import.meta.viteRsc.loadModule', () => { + const code = ` +// This is a comment with import.meta.viteRsc.loadModule("test") +/* block comment import.meta.viteRsc.loadModule("test") */ +import.meta.viteRsc.loadModule("actual"); +` + const stripped = stripLiteral(code) + const matches = [ + ...stripped.matchAll(/import\.meta\.viteRsc\.loadModule\(([\s\S]*?)\)/dg), + ] + + // Should only match the actual call, not the ones in comments + expect(matches.length).toBe(1) + // Extract argument using indices from original code + const [argStart, argEnd] = matches[0]!.indices![1]! + const argCode = code.slice(argStart, argEnd).trim() + expect(argCode).toBe('"actual"') + }) + + it('should strip strings with import.meta.viteRsc.loadModule', () => { + const code = ` +const x = "string with import.meta.viteRsc.loadModule('test')"; +const y = 'another string import.meta.viteRsc.loadModule("test")'; +import.meta.viteRsc.loadModule("actual"); +` + const stripped = stripLiteral(code) + const matches = [ + ...stripped.matchAll(/import\.meta\.viteRsc\.loadModule\(([\s\S]*?)\)/dg), + ] + + // Should only match the actual call, not the ones in strings + expect(matches.length).toBe(1) + const [argStart, argEnd] = matches[0]!.indices![1]! + const argCode = code.slice(argStart, argEnd).trim() + expect(argCode).toBe('"actual"') + }) + + it('should strip comments with import.meta.viteRsc.loadCss', () => { + const code = ` +// This is a comment with import.meta.viteRsc.loadCss("test") +/* block comment import.meta.viteRsc.loadCss("test") */ +import.meta.viteRsc.loadCss("actual"); +` + const stripped = stripLiteral(code) + const matches = [ + ...stripped.matchAll(/import\.meta\.viteRsc\.loadCss\(([\s\S]*?)\)/dg), + ] + + // Should only match the actual call, not the ones in comments + expect(matches.length).toBe(1) + const [argStart, argEnd] = matches[0]!.indices![1]! + const argCode = code.slice(argStart, argEnd).trim() + expect(argCode).toBe('"actual"') + }) + + it('should strip strings with import.meta.viteRsc.loadCss', () => { + const code = ` +const x = "string with import.meta.viteRsc.loadCss('test')"; +import.meta.viteRsc.loadCss("actual"); +` + const stripped = stripLiteral(code) + const matches = [ + ...stripped.matchAll(/import\.meta\.viteRsc\.loadCss\(([\s\S]*?)\)/dg), + ] + + // Should only match the actual call, not the ones in strings + expect(matches.length).toBe(1) + const [argStart, argEnd] = matches[0]!.indices![1]! + const argCode = code.slice(argStart, argEnd).trim() + expect(argCode).toBe('"actual"') + }) + + it('should strip comments with import.meta.viteRsc.loadBootstrapScriptContent', () => { + const code = ` +// This is a comment with import.meta.viteRsc.loadBootstrapScriptContent("test") +/* block import.meta.viteRsc.loadBootstrapScriptContent("test") */ +import.meta.viteRsc.loadBootstrapScriptContent("actual"); +` + const stripped = stripLiteral(code) + const matches = [ + ...stripped.matchAll( + /import\s*\.\s*meta\s*\.\s*viteRsc\s*\.\s*loadBootstrapScriptContent\(([\s\S]*?)\)/dg, + ), + ] + + // Should only match the actual call, not the ones in comments + expect(matches.length).toBe(1) + const [argStart, argEnd] = matches[0]!.indices![1]! + const argCode = code.slice(argStart, argEnd).trim() + expect(argCode).toBe('"actual"') + }) + + it('should handle mixed comments and strings', () => { + const code = ` +// Comment with import.meta.viteRsc.loadModule("comment") +const x = "string with import.meta.viteRsc.loadModule('string')"; +/* + * Multi-line comment + * import.meta.viteRsc.loadModule("multiline") + */ +import.meta.viteRsc.loadModule("first-actual"); +const y = \`template with import.meta.viteRsc.loadModule('template')\`; +import.meta.viteRsc.loadModule("second-actual"); +` + const stripped = stripLiteral(code) + const matches = [ + ...stripped.matchAll(/import\.meta\.viteRsc\.loadModule\(([\s\S]*?)\)/dg), + ] + + // Should only match the actual calls + expect(matches.length).toBe(2) + + const [argStart1, argEnd1] = matches[0]!.indices![1]! + const argCode1 = code.slice(argStart1, argEnd1).trim() + expect(argCode1).toBe('"first-actual"') + + const [argStart2, argEnd2] = matches[1]!.indices![1]! + const argCode2 = code.slice(argStart2, argEnd2).trim() + expect(argCode2).toBe('"second-actual"') + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db0a7a5c2..ea574171f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -451,6 +451,9 @@ importers: periscopic: specifier: ^4.0.2 version: 4.0.2 + strip-literal: + specifier: ^3.1.0 + version: 3.1.0 turbo-stream: specifier: ^3.1.0 version: 3.1.0 From f13b79af2482e95948eaaff9ce806ceb58220ea3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:43:35 +0000 Subject: [PATCH 3/3] test: remove strip-literal.test.ts Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- .../src/plugins/strip-literal.test.ts | 125 ------------------ 1 file changed, 125 deletions(-) delete mode 100644 packages/plugin-rsc/src/plugins/strip-literal.test.ts diff --git a/packages/plugin-rsc/src/plugins/strip-literal.test.ts b/packages/plugin-rsc/src/plugins/strip-literal.test.ts deleted file mode 100644 index c3f1c88a7..000000000 --- a/packages/plugin-rsc/src/plugins/strip-literal.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { stripLiteral } from 'strip-literal' - -describe('stripLiteral for viteRsc API detection', () => { - it('should strip comments with import.meta.viteRsc.loadModule', () => { - const code = ` -// This is a comment with import.meta.viteRsc.loadModule("test") -/* block comment import.meta.viteRsc.loadModule("test") */ -import.meta.viteRsc.loadModule("actual"); -` - const stripped = stripLiteral(code) - const matches = [ - ...stripped.matchAll(/import\.meta\.viteRsc\.loadModule\(([\s\S]*?)\)/dg), - ] - - // Should only match the actual call, not the ones in comments - expect(matches.length).toBe(1) - // Extract argument using indices from original code - const [argStart, argEnd] = matches[0]!.indices![1]! - const argCode = code.slice(argStart, argEnd).trim() - expect(argCode).toBe('"actual"') - }) - - it('should strip strings with import.meta.viteRsc.loadModule', () => { - const code = ` -const x = "string with import.meta.viteRsc.loadModule('test')"; -const y = 'another string import.meta.viteRsc.loadModule("test")'; -import.meta.viteRsc.loadModule("actual"); -` - const stripped = stripLiteral(code) - const matches = [ - ...stripped.matchAll(/import\.meta\.viteRsc\.loadModule\(([\s\S]*?)\)/dg), - ] - - // Should only match the actual call, not the ones in strings - expect(matches.length).toBe(1) - const [argStart, argEnd] = matches[0]!.indices![1]! - const argCode = code.slice(argStart, argEnd).trim() - expect(argCode).toBe('"actual"') - }) - - it('should strip comments with import.meta.viteRsc.loadCss', () => { - const code = ` -// This is a comment with import.meta.viteRsc.loadCss("test") -/* block comment import.meta.viteRsc.loadCss("test") */ -import.meta.viteRsc.loadCss("actual"); -` - const stripped = stripLiteral(code) - const matches = [ - ...stripped.matchAll(/import\.meta\.viteRsc\.loadCss\(([\s\S]*?)\)/dg), - ] - - // Should only match the actual call, not the ones in comments - expect(matches.length).toBe(1) - const [argStart, argEnd] = matches[0]!.indices![1]! - const argCode = code.slice(argStart, argEnd).trim() - expect(argCode).toBe('"actual"') - }) - - it('should strip strings with import.meta.viteRsc.loadCss', () => { - const code = ` -const x = "string with import.meta.viteRsc.loadCss('test')"; -import.meta.viteRsc.loadCss("actual"); -` - const stripped = stripLiteral(code) - const matches = [ - ...stripped.matchAll(/import\.meta\.viteRsc\.loadCss\(([\s\S]*?)\)/dg), - ] - - // Should only match the actual call, not the ones in strings - expect(matches.length).toBe(1) - const [argStart, argEnd] = matches[0]!.indices![1]! - const argCode = code.slice(argStart, argEnd).trim() - expect(argCode).toBe('"actual"') - }) - - it('should strip comments with import.meta.viteRsc.loadBootstrapScriptContent', () => { - const code = ` -// This is a comment with import.meta.viteRsc.loadBootstrapScriptContent("test") -/* block import.meta.viteRsc.loadBootstrapScriptContent("test") */ -import.meta.viteRsc.loadBootstrapScriptContent("actual"); -` - const stripped = stripLiteral(code) - const matches = [ - ...stripped.matchAll( - /import\s*\.\s*meta\s*\.\s*viteRsc\s*\.\s*loadBootstrapScriptContent\(([\s\S]*?)\)/dg, - ), - ] - - // Should only match the actual call, not the ones in comments - expect(matches.length).toBe(1) - const [argStart, argEnd] = matches[0]!.indices![1]! - const argCode = code.slice(argStart, argEnd).trim() - expect(argCode).toBe('"actual"') - }) - - it('should handle mixed comments and strings', () => { - const code = ` -// Comment with import.meta.viteRsc.loadModule("comment") -const x = "string with import.meta.viteRsc.loadModule('string')"; -/* - * Multi-line comment - * import.meta.viteRsc.loadModule("multiline") - */ -import.meta.viteRsc.loadModule("first-actual"); -const y = \`template with import.meta.viteRsc.loadModule('template')\`; -import.meta.viteRsc.loadModule("second-actual"); -` - const stripped = stripLiteral(code) - const matches = [ - ...stripped.matchAll(/import\.meta\.viteRsc\.loadModule\(([\s\S]*?)\)/dg), - ] - - // Should only match the actual calls - expect(matches.length).toBe(2) - - const [argStart1, argEnd1] = matches[0]!.indices![1]! - const argCode1 = code.slice(argStart1, argEnd1).trim() - expect(argCode1).toBe('"first-actual"') - - const [argStart2, argEnd2] = matches[1]!.indices![1]! - const argCode2 = code.slice(argStart2, argEnd2).trim() - expect(argCode2).toBe('"second-actual"') - }) -})