Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions packages/plugin-rsc/src/core/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
50 changes: 34 additions & 16 deletions packages/plugin-rsc/src/core/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import MagicString from 'magic-string'
import type { Plugin } from 'vite'

export default function vitePluginRscCore(): Plugin[] {
Expand All @@ -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 }),
}
}
},
},
Expand Down