Skip to content

Commit

Permalink
patch require("react-dom/server.edge") calls in `pages.runtime.prod…
Browse files Browse the repository at this point in the history
….js` so that they are `try-catch`ed
  • Loading branch information
dario-piotrowicz committed Jan 8, 2025
1 parent 77e31d5 commit 75ddc40
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 0 deletions.
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,116 @@ 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.some((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 || 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 || !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);
}

0 comments on commit 75ddc40

Please sign in to comment.