From b620e6cb0fba504b61636506e7cf175e8cb73300 Mon Sep 17 00:00:00 2001 From: likai Date: Wed, 25 Feb 2026 16:45:50 +0900 Subject: [PATCH 1/2] fix(plugin-rsc): use MagicString to preserve sourcemap chain in webpack-require transform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rsc:patch-react-server-dom-webpack transform replaces __webpack_require__ (18 chars) with __vite_rsc_require__ (20 chars) — a non-same-length substitution — and previously returned map: null, which broke the Rollup sourcemap chain. Replace the manual replaceAll + map: null with MagicString, which generates a correct hires sourcemap reflecting the actual offset changes. Added e2e test that builds the starter example with --sourcemap and verifies no sourcemap warnings are emitted for this transform. --- packages/plugin-rsc/e2e/sourcemap.test.ts | 44 ++++++++++++++++++++ packages/plugin-rsc/src/core/plugin.ts | 50 +++++++++++++++-------- 2 files changed, 78 insertions(+), 16 deletions(-) create mode 100644 packages/plugin-rsc/e2e/sourcemap.test.ts diff --git a/packages/plugin-rsc/e2e/sourcemap.test.ts b/packages/plugin-rsc/e2e/sourcemap.test.ts new file mode 100644 index 000000000..bc929c73a --- /dev/null +++ b/packages/plugin-rsc/e2e/sourcemap.test.ts @@ -0,0 +1,44 @@ +import fs from 'node:fs' +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { x } from 'tinyexec' + +test.describe('sourcemap', () => { + const root = 'examples/starter' + + test('build --sourcemap produces valid sourcemaps without rsc:patch-react-server-dom-webpack warnings', async () => { + // Clean previous build + fs.rmSync(path.join(root, 'dist'), { recursive: true, force: true }) + + const result = await x('pnpm', ['build', '--sourcemap'], { + nodeOptions: { cwd: root }, + throwOnError: true, + }) + expect(result.exitCode).toBe(0) + + // The rsc:patch-react-server-dom-webpack plugin replaces + // __webpack_require__ with __vite_rsc_require__ (different lengths). + // With the MagicString fix, this transform preserves the sourcemap + // chain and must not appear in any "Sourcemap" warnings. + const output = result.stdout + result.stderr + expect(output).not.toContain( + '[plugin rsc:patch-react-server-dom-webpack] Sourcemap is likely to be incorrect', + ) + + // Verify the rsc build output has a valid sourcemap with non-empty mappings. + // The rsc bundle contains the vendored react-server-dom-webpack code + // that goes through the __webpack_require__ transform. + const rscDir = path.join(root, 'dist/rsc') + const mapFiles = fs.readdirSync(rscDir).filter((f) => f.endsWith('.js.map')) + expect(mapFiles.length).toBeGreaterThan(0) + + for (const mapFile of mapFiles) { + const map = JSON.parse( + fs.readFileSync(path.join(rscDir, mapFile), 'utf-8'), + ) + // Sourcemap must have non-empty mappings + expect(map.mappings).toBeTruthy() + expect(map.mappings.length).toBeGreaterThan(0) + } + }) +}) diff --git a/packages/plugin-rsc/src/core/plugin.ts b/packages/plugin-rsc/src/core/plugin.ts index b7a408b76..57a4ec6a1 100644 --- a/packages/plugin-rsc/src/core/plugin.ts +++ b/packages/plugin-rsc/src/core/plugin.ts @@ -1,3 +1,4 @@ +import MagicString from 'magic-string' import type { Plugin } from 'vite' export default function vitePluginRscCore(): Plugin[] { @@ -6,25 +7,42 @@ export default function vitePluginRscCore(): Plugin[] { name: 'rsc:patch-react-server-dom-webpack', transform: { filter: { code: '__webpack_require__' }, - handler(originalCode, _id, _options) { - let code = originalCode - if (code.includes('__webpack_require__.u')) { - // avoid accessing `__webpack_require__` on import side effect - // https://github.com/facebook/react/blob/a9bbe34622885ef5667d33236d580fe7321c0d8b/packages/react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackBrowser.js#L16-L17 - code = code.replaceAll('__webpack_require__.u', '({}).u') - } + handler(code, id, _options) { + if (!code.includes('__webpack_require__')) return + + // Use MagicString to perform replacements with a proper sourcemap, + // so the Rollup sourcemap chain stays intact and doesn't emit + // 'Can't resolve original location of error' warnings for every + // file processed by this transform (e.g. all "use client" modules). + const s = new MagicString(code) - // the existance of `__webpack_require__` global can break some packages - // https://github.com/TooTallNate/node-bindings/blob/c8033dcfc04c34397384e23f7399a30e6c13830d/bindings.js#L90-L94 - if (code.includes('__webpack_require__')) { - code = code.replaceAll( - '__webpack_require__', - '__vite_rsc_require__', - ) + // Match `__webpack_require__.u` first (longer pattern), then bare + // `__webpack_require__`, in a single left-to-right pass to avoid + // overlapping overwrites into MagicString. + const re = /__webpack_require__(?:\.u)?/g + let match: RegExpExecArray | null + while ((match = re.exec(code)) !== null) { + const { index } = match + if (match[0] === '__webpack_require__.u') { + // avoid accessing `__webpack_require__` on import side effect + // https://github.com/facebook/react/blob/a9bbe34622885ef5667d33236d580fe7321c0d8b/packages/react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackBrowser.js#L16-L17 + s.overwrite(index, index + match[0].length, '({}).u') + } else { + // the existance of `__webpack_require__` global can break some packages + // https://github.com/TooTallNate/node-bindings/blob/c8033dcfc04c34397384e23f7399a30e6c13830d/bindings.js#L90-L94 + s.overwrite( + index, + index + match[0].length, + '__vite_rsc_require__', + ) + } } - if (code !== originalCode) { - return { code, map: null } + if (s.hasChanged()) { + return { + code: s.toString(), + map: s.generateMap({ hires: true, source: id }), + } } }, }, From 4a6e90329efc42fd5f1c8051ab4b004f22e67706 Mon Sep 17 00:00:00 2001 From: likai Date: Thu, 26 Feb 2026 10:57:29 +0900 Subject: [PATCH 2/2] test: replace e2e test with vitest that fails on main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous e2e test didn't fail on main because `map: null` doesn't trigger Vite's 'Sourcemap is likely to be incorrect' warning — it silently breaks the sourcemap chain. The new vitest directly verifies the transform returns a valid sourcemap (not null) with non-empty mappings, which fails on main (3 failures) and passes with the fix. --- packages/plugin-rsc/e2e/sourcemap.test.ts | 44 -------------- packages/plugin-rsc/src/core/plugin.test.ts | 67 +++++++++++++++++++++ 2 files changed, 67 insertions(+), 44 deletions(-) delete mode 100644 packages/plugin-rsc/e2e/sourcemap.test.ts create mode 100644 packages/plugin-rsc/src/core/plugin.test.ts diff --git a/packages/plugin-rsc/e2e/sourcemap.test.ts b/packages/plugin-rsc/e2e/sourcemap.test.ts deleted file mode 100644 index bc929c73a..000000000 --- a/packages/plugin-rsc/e2e/sourcemap.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import { expect, test } from '@playwright/test' -import { x } from 'tinyexec' - -test.describe('sourcemap', () => { - const root = 'examples/starter' - - test('build --sourcemap produces valid sourcemaps without rsc:patch-react-server-dom-webpack warnings', async () => { - // Clean previous build - fs.rmSync(path.join(root, 'dist'), { recursive: true, force: true }) - - const result = await x('pnpm', ['build', '--sourcemap'], { - nodeOptions: { cwd: root }, - throwOnError: true, - }) - expect(result.exitCode).toBe(0) - - // The rsc:patch-react-server-dom-webpack plugin replaces - // __webpack_require__ with __vite_rsc_require__ (different lengths). - // With the MagicString fix, this transform preserves the sourcemap - // chain and must not appear in any "Sourcemap" warnings. - const output = result.stdout + result.stderr - expect(output).not.toContain( - '[plugin rsc:patch-react-server-dom-webpack] Sourcemap is likely to be incorrect', - ) - - // Verify the rsc build output has a valid sourcemap with non-empty mappings. - // The rsc bundle contains the vendored react-server-dom-webpack code - // that goes through the __webpack_require__ transform. - const rscDir = path.join(root, 'dist/rsc') - const mapFiles = fs.readdirSync(rscDir).filter((f) => f.endsWith('.js.map')) - expect(mapFiles.length).toBeGreaterThan(0) - - for (const mapFile of mapFiles) { - const map = JSON.parse( - fs.readFileSync(path.join(rscDir, mapFile), 'utf-8'), - ) - // Sourcemap must have non-empty mappings - expect(map.mappings).toBeTruthy() - expect(map.mappings.length).toBeGreaterThan(0) - } - }) -}) diff --git a/packages/plugin-rsc/src/core/plugin.test.ts b/packages/plugin-rsc/src/core/plugin.test.ts new file mode 100644 index 000000000..7adbd4524 --- /dev/null +++ b/packages/plugin-rsc/src/core/plugin.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from 'vitest' +import vitePluginRscCore from './plugin' + +describe('rsc:patch-react-server-dom-webpack', () => { + function getHandler() { + const plugins = vitePluginRscCore() + const plugin = plugins.find( + (p) => p.name === 'rsc:patch-react-server-dom-webpack', + )! + return (plugin.transform as { handler: (...args: any[]) => any }).handler + } + + it('preserves sourcemap chain when replacing __webpack_require__', () => { + const handler = getHandler() + const code = 'const x = __webpack_require__("test");\n' + const result = handler(code, '/test.js') + + expect(result).toBeDefined() + expect(result.code).toContain('__vite_rsc_require__') + expect(result.code).not.toContain('__webpack_require__') + + // The transform MUST return a valid sourcemap (not null) to keep the + // Rollup sourcemap chain intact. Returning `map: null` silently breaks + // downstream error-location resolution, causing Rollup to emit + // "Can't resolve original location of error" for every module that + // passes through this transform. + expect(result.map).not.toBeNull() + expect(result.map).toHaveProperty('mappings') + expect(result.map.mappings).toBeTruthy() + }) + + it('preserves sourcemap chain when replacing __webpack_require__.u', () => { + const handler = getHandler() + const code = 'const u = __webpack_require__.u;\n' + const result = handler(code, '/test.js') + + expect(result).toBeDefined() + expect(result.code).toContain('({}).u') + expect(result.map).not.toBeNull() + expect(result.map).toHaveProperty('mappings') + expect(result.map.mappings).toBeTruthy() + }) + + it('handles both patterns in the same source', () => { + const handler = getHandler() + const code = [ + 'const u = __webpack_require__.u;', + 'const x = __webpack_require__("test");', + '', + ].join('\n') + const result = handler(code, '/test.js') + + expect(result).toBeDefined() + expect(result.code).toContain('({}).u') + expect(result.code).toContain('__vite_rsc_require__') + expect(result.code).not.toContain('__webpack_require__') + expect(result.map).not.toBeNull() + expect(result.map).toHaveProperty('mappings') + expect(result.map.mappings).toBeTruthy() + }) + + it('returns undefined when no __webpack_require__ is present', () => { + const handler = getHandler() + const result = handler('const x = 1;\n', '/test.js') + expect(result).toBeUndefined() + }) +})