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() + }) +}) 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 }), + } } }, },