Skip to content

Commit 9909dbd

Browse files
committed
test(plugin-rsc): add unit tests for webpack-require sourcemap fix
Extract patchWebpackRequire() into a standalone exported function so it can be tested directly. Add 6 test cases covering: - bare __webpack_require__ → __vite_rsc_require__ replacement - __webpack_require__.u → ({}).u replacement - both patterns in the same source - no-op when __webpack_require__ is absent - returned sourcemap has non-empty mappings (key regression test) - sourcemap source field is set correctly for offset-changing replacements
1 parent c923953 commit 9909dbd

File tree

2 files changed

+113
-36
lines changed

2 files changed

+113
-36
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { describe, expect, test } from 'vitest'
2+
import { patchWebpackRequire } from './plugin'
3+
4+
describe('patchWebpackRequire', () => {
5+
test('replaces bare __webpack_require__ with __vite_rsc_require__', () => {
6+
const code = 'var x = __webpack_require__(42);'
7+
const result = patchWebpackRequire(code, 'test.js')
8+
expect(result).toBeDefined()
9+
expect(result!.code).toBe('var x = __vite_rsc_require__(42);')
10+
})
11+
12+
test('replaces __webpack_require__.u with ({}).u', () => {
13+
const code = 'var chunkUrl = __webpack_require__.u;'
14+
const result = patchWebpackRequire(code, 'test.js')
15+
expect(result).toBeDefined()
16+
expect(result!.code).toBe('var chunkUrl = ({}).u;')
17+
})
18+
19+
test('handles both patterns in the same source', () => {
20+
const code = [
21+
'var u = __webpack_require__.u;',
22+
'var x = __webpack_require__(1);',
23+
'var y = __webpack_require__(2);',
24+
].join('\n')
25+
const result = patchWebpackRequire(code, 'test.js')
26+
expect(result).toBeDefined()
27+
expect(result!.code).toBe(
28+
[
29+
'var u = ({}).u;',
30+
'var x = __vite_rsc_require__(1);',
31+
'var y = __vite_rsc_require__(2);',
32+
].join('\n'),
33+
)
34+
})
35+
36+
test('returns undefined when no __webpack_require__ present', () => {
37+
const code = 'var x = 1;'
38+
const result = patchWebpackRequire(code, 'test.js')
39+
expect(result).toBeUndefined()
40+
})
41+
42+
test('returns a valid sourcemap with non-empty mappings', () => {
43+
// This is the key regression test: the old code returned `map: null`
44+
// which broke the Rollup sourcemap chain, causing
45+
// "Can't resolve original location of error" warnings.
46+
const code = 'var x = __webpack_require__(42);'
47+
const result = patchWebpackRequire(code, 'test.js')
48+
expect(result).toBeDefined()
49+
expect(result!.map).toBeDefined()
50+
expect(result!.map).not.toBeNull()
51+
// mappings must be a non-empty string (not "" which is an empty map)
52+
expect(result!.map.mappings).toBeTruthy()
53+
expect(typeof result!.map.mappings).toBe('string')
54+
expect(result!.map.mappings.length).toBeGreaterThan(0)
55+
})
56+
57+
test('sourcemap maps positions correctly for offset-changing replacement', () => {
58+
// __webpack_require__ (18 chars) → __vite_rsc_require__ (20 chars)
59+
// This non-same-length replacement is what broke sourcemaps before.
60+
const code = 'call(__webpack_require__)'
61+
const result = patchWebpackRequire(code, 'test.js')
62+
expect(result).toBeDefined()
63+
expect(result!.code).toBe('call(__vite_rsc_require__)')
64+
65+
// Verify source is set correctly
66+
const map = result!.map
67+
expect(map.sources).toContain('test.js')
68+
})
69+
})

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

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,57 @@
11
import MagicString from 'magic-string'
22
import type { Plugin } from 'vite'
33

4+
/**
5+
* Replace `__webpack_require__` references with safe alternatives.
6+
*
7+
* - `__webpack_require__.u` → `({}).u` (avoid import side-effect access)
8+
* - `__webpack_require__` → `__vite_rsc_require__`
9+
*
10+
* Returns `{ code, map }` with a proper sourcemap so the Rollup sourcemap
11+
* chain stays intact, or `undefined` if no replacements were made.
12+
*/
13+
export function patchWebpackRequire(
14+
code: string,
15+
id: string,
16+
): { code: string; map: ReturnType<MagicString['generateMap']> } | undefined {
17+
if (!code.includes('__webpack_require__')) return
18+
19+
const s = new MagicString(code)
20+
21+
// Match `__webpack_require__.u` first (longer pattern), then bare
22+
// `__webpack_require__`, in a single left-to-right pass to avoid
23+
// overlapping overwrites into MagicString.
24+
const re = /__webpack_require__(?:\.u)?/g
25+
let match: RegExpExecArray | null
26+
while ((match = re.exec(code)) !== null) {
27+
const { index } = match
28+
if (match[0] === '__webpack_require__.u') {
29+
// avoid accessing `__webpack_require__` on import side effect
30+
// https://github.com/facebook/react/blob/a9bbe34622885ef5667d33236d580fe7321c0d8b/packages/react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackBrowser.js#L16-L17
31+
s.overwrite(index, index + match[0].length, '({}).u')
32+
} else {
33+
// the existance of `__webpack_require__` global can break some packages
34+
// https://github.com/TooTallNate/node-bindings/blob/c8033dcfc04c34397384e23f7399a30e6c13830d/bindings.js#L90-L94
35+
s.overwrite(index, index + match[0].length, '__vite_rsc_require__')
36+
}
37+
}
38+
39+
if (s.hasChanged()) {
40+
return {
41+
code: s.toString(),
42+
map: s.generateMap({ hires: true, source: id }),
43+
}
44+
}
45+
}
46+
447
export default function vitePluginRscCore(): Plugin[] {
548
return [
649
{
750
name: 'rsc:patch-react-server-dom-webpack',
851
transform: {
952
filter: { code: '__webpack_require__' },
1053
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)
18-
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-
}
39-
}
40-
41-
if (s.hasChanged()) {
42-
return {
43-
code: s.toString(),
44-
map: s.generateMap({ hires: true, source: id }),
45-
}
46-
}
54+
return patchWebpackRequire(code, id)
4755
},
4856
},
4957
},

0 commit comments

Comments
 (0)