11import { readFileSync , statSync , writeFileSync } from "node:fs" ;
22import { join } from "node:path" ;
33
4+ import * as ts from "ts-morph" ;
5+
46import { Config } from "../../../config.js" ;
7+ import { tsParseFile } from "../../utils/index.js" ;
58
69export function patchWranglerDeps ( config : Config ) {
710 console . log ( "# patchWranglerDeps" ) ;
@@ -23,6 +26,8 @@ export function patchWranglerDeps(config: Config) {
2326
2427 writeFileSync ( pagesRuntimeFile , patchedPagesRuntime ) ;
2528
29+ patchRequireReactDomServerEdge ( config ) ;
30+
2631 // Patch .next/standalone/node_modules/next/dist/server/lib/trace/tracer.js
2732 //
2833 // Remove the need for an alias in wrangler.toml:
@@ -67,3 +72,120 @@ function getDistPath(config: Config): string {
6772
6873 throw new Error ( "Unexpected error: unable to detect the node_modules/next/dist directory" ) ;
6974}
75+
76+ /**
77+ * `react-dom` v>=19 has a `server.edge` export: https://github.com/facebook/react/blob/a160102f3/packages/react-dom/package.json#L79
78+ * but version of `react-dom` <= 18 do not have this export but have a `server.browser` export instead: https://github.com/facebook/react/blob/8a015b68/packages/react-dom/package.json#L49
79+ *
80+ * Next.js also try-catches importing the `server.edge` export:
81+ * https://github.com/vercel/next.js/blob/6784575/packages/next/src/server/ReactDOMServerPages.js
82+ *
83+ * The issue here is that in the `.next/standalone/node_modules/next/dist/compiled/next-server/pages.runtime.prod.js`
84+ * file for whatever reason there is a non `try-catch`ed require for the `server.edge` export
85+ *
86+ * This functions fixes this issue by wrapping the require in a try-catch block in the same way Next.js does it
87+ * (note: this will make the build succeed but doesn't guarantee that everything will necessarily work at runtime since
88+ * it's not clear what code and how might be rely on this require call)
89+ *
90+ */
91+ function patchRequireReactDomServerEdge ( config : Config ) {
92+ const distPath = getDistPath ( config ) ;
93+
94+ // Patch .next/standalone/node_modules/next/dist/compiled/next-server/pages.runtime.prod.js
95+ const pagesRuntimeFile = join ( distPath , "compiled" , "next-server" , "pages.runtime.prod.js" ) ;
96+
97+ const code = readFileSync ( pagesRuntimeFile , "utf-8" ) ;
98+ const file = tsParseFile ( code ) ;
99+
100+ // we need to update this function: `e=>{"use strict";e.exports=require("react-dom/server.edge")}`
101+ file . getDescendantsOfKind ( ts . SyntaxKind . ArrowFunction ) . forEach ( ( arrowFunction ) => {
102+ // the function has a single parameter
103+ const p = arrowFunction . getParameters ( ) ;
104+ if ( p . length !== 1 ) {
105+ return ;
106+ }
107+ const parameterName = p [ 0 ] ! . getName ( ) ;
108+ const bodyChildren = arrowFunction . getBody ( ) . getChildren ( ) ;
109+ if (
110+ ! (
111+ bodyChildren . length === 3 &&
112+ bodyChildren [ 0 ] ! . getFullText ( ) === "{" &&
113+ bodyChildren [ 2 ] ! . getFullText ( ) === "}"
114+ )
115+ ) {
116+ return ;
117+ }
118+ const bodyStatements = bodyChildren [ 1 ] ?. getChildren ( ) ;
119+
120+ // the function has only two statements: "use strict" and e.exports=require("react-dom/server.edge")
121+ if (
122+ ! (
123+ bodyStatements ?. length === 2 &&
124+ bodyStatements . every ( ( statement ) => statement . isKind ( ts . SyntaxKind . ExpressionStatement ) )
125+ )
126+ ) {
127+ return ;
128+ }
129+ const bodyExpressionStatements = bodyStatements as [ ts . ExpressionStatement , ts . ExpressionStatement ] ;
130+
131+ const stringLiteralExpression = bodyExpressionStatements [ 0 ] . getExpressionIfKind (
132+ ts . SyntaxKind . StringLiteral
133+ ) ;
134+
135+ // the first statement needs to be "use strict"
136+ if ( stringLiteralExpression ?. getText ( ) !== '"use strict"' ) {
137+ return ;
138+ }
139+
140+ // the second statement (e.exports=require("react-dom/server.edge")) needs to be a binary expression
141+ const binaryExpression = bodyExpressionStatements [ 1 ] . getExpressionIfKind ( ts . SyntaxKind . BinaryExpression ) ;
142+ if ( ! binaryExpression ?. getOperatorToken ( ) . isKind ( ts . SyntaxKind . EqualsToken ) ) {
143+ return ;
144+ }
145+
146+ // on the left we have `${parameterName}.exports`
147+ const binaryLeft = binaryExpression . getLeft ( ) ;
148+ if (
149+ ! binaryLeft . isKind ( ts . SyntaxKind . PropertyAccessExpression ) ||
150+ binaryLeft . getExpressionIfKind ( ts . SyntaxKind . Identifier ) ?. getText ( ) !== parameterName ||
151+ binaryLeft . getName ( ) !== "exports"
152+ ) {
153+ return ;
154+ }
155+
156+ // on the right we have `require("react-dom/server.edge")`
157+ const binaryRight = binaryExpression . getRight ( ) ;
158+ if (
159+ ! binaryRight . isKind ( ts . SyntaxKind . CallExpression ) ||
160+ binaryRight . getExpressionIfKind ( ts . SyntaxKind . Identifier ) ?. getText ( ) !== "require"
161+ ) {
162+ return ;
163+ }
164+ const requireArgs = binaryRight . getArguments ( ) ;
165+ if ( requireArgs . length !== 1 || requireArgs [ 0 ] ! . getText ( ) !== '"react-dom/server.edge"' ) {
166+ return ;
167+ }
168+
169+ arrowFunction . setBodyText (
170+ `
171+ "use strict";
172+ let ReactDOMServer;
173+ try {
174+ ReactDOMServer = require('react-dom/server.edge');
175+ } catch (error) {
176+ if (
177+ error.code !== 'MODULE_NOT_FOUND' &&
178+ error.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED'
179+ ) {
180+ throw error;
181+ }
182+ ReactDOMServer = require('react-dom/server.browser');
183+ }
184+ ${ parameterName } .exports = ReactDOMServer;
185+ }` . replace ( / \n s * / g, " " )
186+ ) ;
187+ } ) ;
188+
189+ const updatedCode = file . print ( ) ;
190+ writeFileSync ( pagesRuntimeFile , updatedCode ) ;
191+ }
0 commit comments