Skip to content

Commit dbcc4be

Browse files
patch require("react-dom/server.edge") calls in pages.runtime.prod.js so that they are try-catched (#235)
* patch `require("react-dom/server.edge")` calls in `pages.runtime.prod.js` so that they are `try-catch`ed
1 parent 77e31d5 commit dbcc4be

File tree

2 files changed

+127
-0
lines changed

2 files changed

+127
-0
lines changed

.changeset/silly-phones-accept.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opennextjs/cloudflare": patch
3+
---
4+
5+
patch `require("react-dom/server.edge")` calls in `pages.runtime.prod.js` so that they are `try-catch`ed

packages/cloudflare/src/cli/build/patches/to-investigate/wrangler-deps.ts

+122
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { readFileSync, statSync, writeFileSync } from "node:fs";
22
import { join } from "node:path";
33

4+
import * as ts from "ts-morph";
5+
46
import { Config } from "../../../config.js";
7+
import { tsParseFile } from "../../utils/index.js";
58

69
export 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(/\ns*/g, " ")
186+
);
187+
});
188+
189+
const updatedCode = file.print();
190+
writeFileSync(pagesRuntimeFile, updatedCode);
191+
}

0 commit comments

Comments
 (0)