Skip to content

Commit b620e6c

Browse files
committed
fix(plugin-rsc): use MagicString to preserve sourcemap chain in webpack-require transform
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.
1 parent 377cfda commit b620e6c

File tree

2 files changed

+78
-16
lines changed

2 files changed

+78
-16
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import fs from 'node:fs'
2+
import path from 'node:path'
3+
import { expect, test } from '@playwright/test'
4+
import { x } from 'tinyexec'
5+
6+
test.describe('sourcemap', () => {
7+
const root = 'examples/starter'
8+
9+
test('build --sourcemap produces valid sourcemaps without rsc:patch-react-server-dom-webpack warnings', async () => {
10+
// Clean previous build
11+
fs.rmSync(path.join(root, 'dist'), { recursive: true, force: true })
12+
13+
const result = await x('pnpm', ['build', '--sourcemap'], {
14+
nodeOptions: { cwd: root },
15+
throwOnError: true,
16+
})
17+
expect(result.exitCode).toBe(0)
18+
19+
// The rsc:patch-react-server-dom-webpack plugin replaces
20+
// __webpack_require__ with __vite_rsc_require__ (different lengths).
21+
// With the MagicString fix, this transform preserves the sourcemap
22+
// chain and must not appear in any "Sourcemap" warnings.
23+
const output = result.stdout + result.stderr
24+
expect(output).not.toContain(
25+
'[plugin rsc:patch-react-server-dom-webpack] Sourcemap is likely to be incorrect',
26+
)
27+
28+
// Verify the rsc build output has a valid sourcemap with non-empty mappings.
29+
// The rsc bundle contains the vendored react-server-dom-webpack code
30+
// that goes through the __webpack_require__ transform.
31+
const rscDir = path.join(root, 'dist/rsc')
32+
const mapFiles = fs.readdirSync(rscDir).filter((f) => f.endsWith('.js.map'))
33+
expect(mapFiles.length).toBeGreaterThan(0)
34+
35+
for (const mapFile of mapFiles) {
36+
const map = JSON.parse(
37+
fs.readFileSync(path.join(rscDir, mapFile), 'utf-8'),
38+
)
39+
// Sourcemap must have non-empty mappings
40+
expect(map.mappings).toBeTruthy()
41+
expect(map.mappings.length).toBeGreaterThan(0)
42+
}
43+
})
44+
})

packages/plugin-rsc/src/core/plugin.ts

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import MagicString from 'magic-string'
12
import type { Plugin } from 'vite'
23

34
export default function vitePluginRscCore(): Plugin[] {
@@ -6,25 +7,42 @@ export default function vitePluginRscCore(): Plugin[] {
67
name: 'rsc:patch-react-server-dom-webpack',
78
transform: {
89
filter: { code: '__webpack_require__' },
9-
handler(originalCode, _id, _options) {
10-
let code = originalCode
11-
if (code.includes('__webpack_require__.u')) {
12-
// avoid accessing `__webpack_require__` on import side effect
13-
// https://github.com/facebook/react/blob/a9bbe34622885ef5667d33236d580fe7321c0d8b/packages/react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackBrowser.js#L16-L17
14-
code = code.replaceAll('__webpack_require__.u', '({}).u')
15-
}
10+
handler(code, id, _options) {
11+
if (!code.includes('__webpack_require__')) return
12+
13+
// Use MagicString to perform replacements with a proper sourcemap,
14+
// so the Rollup sourcemap chain stays intact and doesn't emit
15+
// 'Can't resolve original location of error' warnings for every
16+
// file processed by this transform (e.g. all "use client" modules).
17+
const s = new MagicString(code)
1618

17-
// the existance of `__webpack_require__` global can break some packages
18-
// https://github.com/TooTallNate/node-bindings/blob/c8033dcfc04c34397384e23f7399a30e6c13830d/bindings.js#L90-L94
19-
if (code.includes('__webpack_require__')) {
20-
code = code.replaceAll(
21-
'__webpack_require__',
22-
'__vite_rsc_require__',
23-
)
19+
// Match `__webpack_require__.u` first (longer pattern), then bare
20+
// `__webpack_require__`, in a single left-to-right pass to avoid
21+
// overlapping overwrites into MagicString.
22+
const re = /__webpack_require__(?:\.u)?/g
23+
let match: RegExpExecArray | null
24+
while ((match = re.exec(code)) !== null) {
25+
const { index } = match
26+
if (match[0] === '__webpack_require__.u') {
27+
// avoid accessing `__webpack_require__` on import side effect
28+
// https://github.com/facebook/react/blob/a9bbe34622885ef5667d33236d580fe7321c0d8b/packages/react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackBrowser.js#L16-L17
29+
s.overwrite(index, index + match[0].length, '({}).u')
30+
} else {
31+
// the existance of `__webpack_require__` global can break some packages
32+
// https://github.com/TooTallNate/node-bindings/blob/c8033dcfc04c34397384e23f7399a30e6c13830d/bindings.js#L90-L94
33+
s.overwrite(
34+
index,
35+
index + match[0].length,
36+
'__vite_rsc_require__',
37+
)
38+
}
2439
}
2540

26-
if (code !== originalCode) {
27-
return { code, map: null }
41+
if (s.hasChanged()) {
42+
return {
43+
code: s.toString(),
44+
map: s.generateMap({ hires: true, source: id }),
45+
}
2846
}
2947
},
3048
},

0 commit comments

Comments
 (0)