From 75ddc40ecb3143d3dc80424e3943f68cd8f02680 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Wed, 8 Jan 2025 15:37:55 +0100 Subject: [PATCH] patch `require("react-dom/server.edge")` calls in `pages.runtime.prod.js` so that they are `try-catch`ed --- .changeset/silly-phones-accept.md | 5 + .../patches/to-investigate/wrangler-deps.ts | 118 ++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 .changeset/silly-phones-accept.md diff --git a/.changeset/silly-phones-accept.md b/.changeset/silly-phones-accept.md new file mode 100644 index 00000000..0059674c --- /dev/null +++ b/.changeset/silly-phones-accept.md @@ -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 diff --git a/packages/cloudflare/src/cli/build/patches/to-investigate/wrangler-deps.ts b/packages/cloudflare/src/cli/build/patches/to-investigate/wrangler-deps.ts index 6fed53ec..25892f06 100644 --- a/packages/cloudflare/src/cli/build/patches/to-investigate/wrangler-deps.ts +++ b/packages/cloudflare/src/cli/build/patches/to-investigate/wrangler-deps.ts @@ -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"); @@ -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: @@ -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); +}