11import fs from 'node:fs'
2+ import path from 'node:path'
3+ import { build } from 'esbuild'
24import { defineConfig } from 'tsdown'
35
46export default defineConfig ( {
@@ -31,7 +33,7 @@ export default defineConfig({
3133 plugins : [
3234 {
3335 name : 'vendor-react-server-dom' ,
34- buildStart ( ) {
36+ async buildStart ( ) {
3537 fs . rmSync ( './dist/vendor/' , { recursive : true , force : true } )
3638 fs . mkdirSync ( './dist/vendor' , { recursive : true } )
3739 fs . cpSync (
@@ -43,7 +45,131 @@ export default defineConfig({
4345 recursive : true ,
4446 force : true ,
4547 } )
48+ // Convert CJS entry files to ESM so pure ESM runtimes (Cloudflare
49+ // Workers, Deno Deploy) don't fail with "require is not defined".
50+ await convertVendorToEsm ( './dist/vendor/react-server-dom' )
4651 } ,
4752 } ,
4853 ] ,
4954} ) as any
55+
56+ const EXTERNALS = [
57+ 'react' ,
58+ 'react-dom' ,
59+ 'react/jsx-runtime' ,
60+ 'react/jsx-dev-runtime' ,
61+ ]
62+
63+ // Convert CJS entry files in the vendor directory to ESM in-place using esbuild.
64+ async function convertVendorToEsm ( vendorDir : string ) {
65+ const entries = fs
66+ . readdirSync ( vendorDir )
67+ . filter ( ( f ) => f . endsWith ( '.js' ) )
68+ . filter ( ( f ) => {
69+ const content = fs . readFileSync ( path . join ( vendorDir , f ) , 'utf-8' )
70+ return content . includes ( 'require(' ) || content . includes ( 'exports.' )
71+ } )
72+
73+ for ( const entry of entries ) {
74+ const entryPath = path . join ( vendorDir , entry )
75+ const content = fs . readFileSync ( entryPath , 'utf-8' )
76+
77+ let result
78+ try {
79+ result = await build ( {
80+ entryPoints : [ entryPath ] ,
81+ bundle : true ,
82+ format : 'esm' ,
83+ write : false ,
84+ platform : 'neutral' ,
85+ external : [
86+ ...EXTERNALS ,
87+ 'node:*' ,
88+ 'util' ,
89+ 'crypto' ,
90+ 'stream' ,
91+ 'async_hooks' ,
92+ ] ,
93+ define : { 'process.env.NODE_ENV' : '"production"' } ,
94+ sourcemap : false ,
95+ logLevel : 'silent' ,
96+ } )
97+ } catch {
98+ continue
99+ }
100+
101+ let code = result . outputFiles [ 0 ] ! . text
102+
103+ // esbuild wraps CJS externals as __require("react") inside __commonJS
104+ // wrappers instead of lifting them to top-level imports. Replace with ESM.
105+ const externalRequires = new Map < string , string > ( )
106+ code = code . replace ( / _ _ r e q u i r e \( " ( [ ^ " ] + ) " \) / g, ( match , specifier ) => {
107+ if ( ! EXTERNALS . includes ( specifier ) ) return match
108+ const varName =
109+ externalRequires . get ( specifier ) ??
110+ `__ext_${ specifier . replace ( / [ ^ a - z A - Z 0 - 9 ] / g, '_' ) } `
111+ externalRequires . set ( specifier , varName )
112+ return varName
113+ } )
114+ if ( externalRequires . size > 0 ) {
115+ const imports = Array . from ( externalRequires . entries ( ) )
116+ . map ( ( [ spec , varName ] ) => `import * as ${ varName } from "${ spec } ";` )
117+ . join ( '\n' )
118+ code = imports + '\n' + code
119+ }
120+
121+ // Remove the __require shim (references `require` which doesn't exist in ESM runtimes)
122+ if ( ! code . includes ( '__require(' ) ) {
123+ const lines = code . split ( '\n' )
124+ const startIdx = lines . findIndex ( ( l ) => l . startsWith ( 'var __require =' ) )
125+ if ( startIdx >= 0 ) {
126+ let endIdx = startIdx
127+ for ( let i = startIdx ; i < lines . length ; i ++ ) {
128+ if ( lines [ i ] ! . startsWith ( '});' ) ) {
129+ endIdx = i
130+ break
131+ }
132+ }
133+ lines . splice ( startIdx , endIdx - startIdx + 1 )
134+ code = lines . join ( '\n' )
135+ }
136+ }
137+
138+ // esbuild only generates `export default`. Add named exports by scanning the CJS source.
139+ const namedExports = extractCjsExportNames ( content , entryPath )
140+ if ( namedExports . length > 0 ) {
141+ code = code . replace (
142+ / ^ e x p o r t d e f a u l t ( .+ ) ; $ / m,
143+ [
144+ `var __cjs_default__ = $1;` ,
145+ `export default __cjs_default__;` ,
146+ `export var { ${ namedExports . join ( ', ' ) } } = __cjs_default__;` ,
147+ ] . join ( '\n' ) ,
148+ )
149+ }
150+
151+ fs . writeFileSync ( entryPath , code )
152+ }
153+
154+ fs . rmSync ( path . join ( vendorDir , 'cjs' ) , { recursive : true , force : true } )
155+ }
156+
157+ function extractCjsExportNames ( content : string , filePath : string ) : string [ ] {
158+ const names = new Set < string > ( )
159+ for ( const m of content . matchAll ( / e x p o r t s \. ( \w + ) \s * = / g) ) {
160+ if ( m [ 1 ] !== '__esModule' ) names . add ( m [ 1 ] ! )
161+ }
162+ const requireMatch = content . match (
163+ / r e q u i r e \( [ ' " ] ( \. \/ c j s \/ [ ^ ' " ] + \. p r o d u c t i o n [ ^ ' " ] * ) [ ' " ] \) / ,
164+ )
165+ if ( requireMatch ) {
166+ try {
167+ const cjsPath = path . resolve ( path . dirname ( filePath ) , requireMatch [ 1 ] ! )
168+ const cjsContent = fs . readFileSync ( cjsPath , 'utf-8' )
169+ for ( const m of cjsContent . matchAll ( / e x p o r t s \. ( \w + ) \s * = / g) ) {
170+ if ( m [ 1 ] !== '__esModule' ) names . add ( m [ 1 ] ! )
171+ }
172+ } catch { }
173+ }
174+ return Array . from ( names )
175+ }
0 commit comments