Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/silly-phones-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opennextjs/cloudflare": patch
---

patch `require("react-dom/server.edge")` calls in `pages.runtime.prod.js` so that they are `try-catch`ed
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { readFileSync, statSync, writeFileSync } from "node:fs";
import { join } from "node:path";

import * as ts from "ts-morph";

import { Config } from "../../../config.js";
import { tsParseFile } from "../../utils/index.js";

export function patchWranglerDeps(config: Config) {
console.log("# patchWranglerDeps");
Expand All @@ -23,6 +26,8 @@ export function patchWranglerDeps(config: Config) {

writeFileSync(pagesRuntimeFile, patchedPagesRuntime);

patchRequireReactDomServerEdge(config);

// Patch .next/standalone/node_modules/next/dist/server/lib/trace/tracer.js
//
// Remove the need for an alias in wrangler.toml:
Expand Down Expand Up @@ -67,3 +72,120 @@ function getDistPath(config: Config): string {

throw new Error("Unexpected error: unable to detect the node_modules/next/dist directory");
}

/**
* `react-dom` v>=19 has a `server.edge` export: https://github.com/facebook/react/blob/a160102f3/packages/react-dom/package.json#L79
* 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
*
* Next.js also try-catches importing the `server.edge` export:
* https://github.com/vercel/next.js/blob/6784575/packages/next/src/server/ReactDOMServerPages.js
*
* The issue here is that in the `.next/standalone/node_modules/next/dist/compiled/next-server/pages.runtime.prod.js`
* file for whatever reason there is a non `try-catch`ed require for the `server.edge` export
*
* This functions fixes this issue by wrapping the require in a try-catch block in the same way Next.js does it
* (note: this will make the build succeed but doesn't guarantee that everything will necessarily work at runtime since
* it's not clear what code and how might be rely on this require call)
*
*/
function patchRequireReactDomServerEdge(config: Config) {
const distPath = getDistPath(config);

// Patch .next/standalone/node_modules/next/dist/compiled/next-server/pages.runtime.prod.js
const pagesRuntimeFile = join(distPath, "compiled", "next-server", "pages.runtime.prod.js");

const code = readFileSync(pagesRuntimeFile, "utf-8");
const file = tsParseFile(code);

// we need to update this function: `e=>{"use strict";e.exports=require("react-dom/server.edge")}`
file.getDescendantsOfKind(ts.SyntaxKind.ArrowFunction).forEach((arrowFunction) => {
// the function has a single parameter
const p = arrowFunction.getParameters();
if (p.length !== 1) {
return;
}
const parameterName = p[0]!.getName();
const bodyChildren = arrowFunction.getBody().getChildren();
if (
!(
bodyChildren.length === 3 &&
bodyChildren[0]!.getFullText() === "{" &&
bodyChildren[2]!.getFullText() === "}"
)
) {
return;
}
const bodyStatements = bodyChildren[1]?.getChildren();

// the function has only two statements: "use strict" and e.exports=require("react-dom/server.edge")
if (
!(
bodyStatements?.length === 2 &&
bodyStatements.every((statement) => statement.isKind(ts.SyntaxKind.ExpressionStatement))
)
) {
return;
}
const bodyExpressionStatements = bodyStatements as [ts.ExpressionStatement, ts.ExpressionStatement];

const stringLiteralExpression = bodyExpressionStatements[0].getExpressionIfKind(
ts.SyntaxKind.StringLiteral
);

// the first statement needs to be "use strict"
if (stringLiteralExpression?.getText() !== '"use strict"') {
return;
}

// the second statement (e.exports=require("react-dom/server.edge")) needs to be a binary expression
const binaryExpression = bodyExpressionStatements[1].getExpressionIfKind(ts.SyntaxKind.BinaryExpression);
if (!binaryExpression?.getOperatorToken().isKind(ts.SyntaxKind.EqualsToken)) {
return;
}

// on the left we have `${parameterName}.exports`
const binaryLeft = binaryExpression.getLeft();
if (
!binaryLeft.isKind(ts.SyntaxKind.PropertyAccessExpression) ||
binaryLeft.getExpressionIfKind(ts.SyntaxKind.Identifier)?.getText() !== parameterName ||
binaryLeft.getName() !== "exports"
) {
return;
}

// on the right we have `require("react-dom/server.edge")`
const binaryRight = binaryExpression.getRight();
if (
!binaryRight.isKind(ts.SyntaxKind.CallExpression) ||
binaryRight.getExpressionIfKind(ts.SyntaxKind.Identifier)?.getText() !== "require"
) {
return;
}
const requireArgs = binaryRight.getArguments();
if (requireArgs.length !== 1 || requireArgs[0]!.getText() !== '"react-dom/server.edge"') {
return;
}

arrowFunction.setBodyText(
`
"use strict";
let ReactDOMServer;
try {
ReactDOMServer = require('react-dom/server.edge');
} catch (error) {
if (
error.code !== 'MODULE_NOT_FOUND' &&
error.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED'
) {
throw error;
}
ReactDOMServer = require('react-dom/server.browser');
}
${parameterName}.exports = ReactDOMServer;
}`.replace(/\ns*/g, " ")
);
});

const updatedCode = file.print();
writeFileSync(pagesRuntimeFile, updatedCode);
}
Loading