Skip to content

Commit 9ad6714

Browse files
authored
patch node-module-loader dynamic require (#431)
1 parent 0bd0439 commit 9ad6714

File tree

8 files changed

+166
-103
lines changed

8 files changed

+166
-103
lines changed

.changeset/strange-laws-hang.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@opennextjs/cloudflare": patch
3+
---
4+
5+
fix pages api routes
6+
7+
fixed pages api routes by inlining a dynamic require in the `NodeModuleLoader` class
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
test("should not fail on an api route", async ({ page }) => {
4+
const result = await page.goto("/api/hello");
5+
expect(result?.status()).toBe(200);
6+
const body = await result?.json();
7+
expect(body).toEqual({ hello: "world" });
8+
});

packages/cloudflare/src/cli/build/bundle-server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { patchWebpackRuntime } from "./patches/ast/webpack-runtime.js";
1111
import * as patches from "./patches/index.js";
1212
import { inlineBuildId } from "./patches/plugins/build-id.js";
1313
import { ContentUpdater } from "./patches/plugins/content-updater.js";
14+
import { inlineDynamicRequires } from "./patches/plugins/dynamic-requires.js";
1415
import { inlineEvalManifest } from "./patches/plugins/eval-manifest.js";
1516
import { patchFetchCacheSetMissingWaitUntil } from "./patches/plugins/fetch-cache-wait-until.js";
1617
import { inlineFindDir } from "./patches/plugins/find-dir.js";
@@ -20,7 +21,6 @@ import { handleOptionalDependencies } from "./patches/plugins/optional-deps.js";
2021
import { patchDepdDeprecations } from "./patches/plugins/patch-depd-deprecations.js";
2122
import { fixRequire } from "./patches/plugins/require.js";
2223
import { shimRequireHook } from "./patches/plugins/require-hook.js";
23-
import { inlineRequirePage } from "./patches/plugins/require-page.js";
2424
import { setWranglerExternal } from "./patches/plugins/wrangler-external.js";
2525
import { normalizePath, patchCodeWithValidations } from "./utils/index.js";
2626

@@ -88,7 +88,7 @@ export async function bundleServer(buildOpts: BuildOptions): Promise<void> {
8888
conditions: [],
8989
plugins: [
9090
shimRequireHook(buildOpts),
91-
inlineRequirePage(updater, buildOpts),
91+
inlineDynamicRequires(updater, buildOpts),
9292
setWranglerExternal(),
9393
fixRequire(updater),
9494
handleOptionalDependencies(optionalDependencies),
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { readFile } from "node:fs/promises";
2+
import { join, posix, sep } from "node:path";
3+
4+
import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js";
5+
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
6+
import type { Plugin } from "esbuild";
7+
8+
import { normalizePath } from "../../utils/normalize-path.js";
9+
import { patchCode, type RuleConfig } from "../ast/util.js";
10+
import type { ContentUpdater } from "./content-updater.js";
11+
12+
async function getPagesManifests(serverDir: string): Promise<string[]> {
13+
try {
14+
return Object.values(JSON.parse(await readFile(join(serverDir, "pages-manifest.json"), "utf-8")));
15+
} catch {
16+
// The file does not exist
17+
return [];
18+
}
19+
}
20+
21+
async function getAppPathsManifests(serverDir: string): Promise<string[]> {
22+
try {
23+
return Object.values(JSON.parse(await readFile(join(serverDir, "app-paths-manifest.json"), "utf-8")));
24+
} catch {
25+
// The file does not exist
26+
return [];
27+
}
28+
}
29+
30+
function getServerDir(buildOpts: BuildOptions) {
31+
return join(buildOpts.outputDir, "server-functions/default", getPackagePath(buildOpts), ".next/server");
32+
}
33+
34+
function getRequires(idVariable: string, files: string[], serverDir: string) {
35+
// Inline fs access and dynamic requires that are not supported by workerd.
36+
return files
37+
.map(
38+
(file) => `
39+
if (${idVariable}.replaceAll(${JSON.stringify(sep)}, ${JSON.stringify(posix.sep)}).endsWith(${JSON.stringify(normalizePath(file))})) {
40+
return require(${JSON.stringify(join(serverDir, file))});
41+
}`
42+
)
43+
.join("\n");
44+
}
45+
46+
export function inlineDynamicRequires(updater: ContentUpdater, buildOpts: BuildOptions): Plugin {
47+
updater.updateContent(
48+
"inline-node-module-loader",
49+
{
50+
filter: getCrossPlatformPathRegex(
51+
String.raw`/next/dist/server/lib/module-loader/node-module-loader\.js$`,
52+
{ escape: false }
53+
),
54+
contentFilter: /class NodeModuleLoader {/,
55+
},
56+
async ({ contents }) => patchCode(contents, await getNodeModuleLoaderRule(buildOpts))
57+
);
58+
updater.updateContent(
59+
"inline-require-page",
60+
{
61+
filter: getCrossPlatformPathRegex(String.raw`/next/dist/server/require\.js$`, { escape: false }),
62+
contentFilter: /function requirePage\(/,
63+
},
64+
async ({ contents }) => patchCode(contents, await getRequirePageRule(buildOpts))
65+
);
66+
return { name: "inline-dynamic-requires", setup() {} };
67+
}
68+
69+
async function getNodeModuleLoaderRule(buildOpts: BuildOptions) {
70+
const serverDir = getServerDir(buildOpts);
71+
72+
const manifests = await getPagesManifests(serverDir);
73+
74+
const files = manifests.filter((file) => file.endsWith(".js"));
75+
76+
return `
77+
rule:
78+
kind: method_definition
79+
all:
80+
- has:
81+
field: name
82+
regex: ^load$
83+
- has:
84+
field: parameters
85+
has:
86+
kind: required_parameter
87+
pattern: $ID
88+
inside:
89+
stopBy:
90+
kind: class_declaration
91+
kind: class_declaration
92+
has:
93+
field: name
94+
regex: ^NodeModuleLoader$
95+
fix: |
96+
async load($ID) {
97+
${getRequires("$ID", files, serverDir)}
98+
}`;
99+
}
100+
101+
async function getRequirePageRule(buildOpts: BuildOptions) {
102+
const serverDir = getServerDir(buildOpts);
103+
104+
const pagesManifests = await getPagesManifests(serverDir);
105+
const appPathsManifests = await getAppPathsManifests(serverDir);
106+
107+
const manifests = pagesManifests.concat(appPathsManifests);
108+
109+
const htmlFiles = manifests.filter((file) => file.endsWith(".html"));
110+
const jsFiles = manifests.filter((file) => file.endsWith(".js"));
111+
112+
return {
113+
rule: {
114+
pattern: `
115+
function requirePage($PAGE, $DIST_DIR, $IS_APP_PATH) {
116+
const $_ = getPagePath($$$ARGS);
117+
$$$_BODY
118+
}`,
119+
}, // Inline fs access and dynamic require that are not supported by workerd.
120+
fix: `
121+
function requirePage($PAGE, $DIST_DIR, $IS_APP_PATH) {
122+
const pagePath = getPagePath($$$ARGS).replaceAll(${JSON.stringify(sep)}, ${JSON.stringify(posix.sep)});
123+
124+
// html
125+
${(
126+
await Promise.all(
127+
htmlFiles.map(
128+
async (file) => `if (pagePath.endsWith(${JSON.stringify(normalizePath(file))})) {
129+
return ${JSON.stringify(await readFile(join(serverDir, file), "utf-8"))};
130+
}`
131+
)
132+
)
133+
).join("\n")}
134+
// js
135+
process.env.__NEXT_PRIVATE_RUNTIME_TYPE = $IS_APP_PATH ? 'app' : 'pages';
136+
try {
137+
${getRequires("pagePath", jsFiles, serverDir)}
138+
} finally {
139+
process.env.__NEXT_PRIVATE_RUNTIME_TYPE = '';
140+
}
141+
}`,
142+
} satisfies RuleConfig;
143+
}

packages/cloudflare/src/cli/build/patches/plugins/eval-manifest.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* that are not supported by workerd.
44
*/
55

6-
import { join, relative } from "node:path";
6+
import { join, posix, relative, sep } from "node:path";
77

88
import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js";
99
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
@@ -62,8 +62,7 @@ function evalManifest($PATH, $$$ARGS) {
6262
},
6363
fix: `
6464
function evalManifest($PATH, $$$ARGS) {
65-
const { platform } = require('process');
66-
$PATH = platform === 'win32' ? $PATH.replaceAll('\\\\', '/') : $PATH;
65+
$PATH = $PATH.replaceAll(${JSON.stringify(sep)}, ${JSON.stringify(posix.sep)});
6766
${returnManifests}
6867
throw new Error(\`Unexpected evalManifest(\${$PATH}) call!\`);
6968
}`,

packages/cloudflare/src/cli/build/patches/plugins/find-dir.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import { existsSync } from "node:fs";
6-
import { join } from "node:path";
6+
import { join, posix, sep } from "node:path";
77

88
import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js";
99
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
@@ -35,8 +35,7 @@ rule:
3535
pattern: function findDir($DIR, $NAME) { $$$_ }
3636
fix: |-
3737
function findDir($DIR, $NAME) {
38-
const { platform } = require('process');
39-
$DIR = platform === 'win32' ? $DIR.replaceAll('\\\\', '/') : $DIR;
38+
$DIR = $DIR.replaceAll(${JSON.stringify(sep)}, ${JSON.stringify(posix.sep)});
4039
if ($DIR.endsWith(".next/server")) {
4140
if ($NAME === "app") {
4241
return ${appExists};

packages/cloudflare/src/cli/build/patches/plugins/load-manifest.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import { readFile } from "node:fs/promises";
6-
import { join, relative } from "node:path";
6+
import { join, posix, relative, sep } from "node:path";
77

88
import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js";
99
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
@@ -53,8 +53,7 @@ function loadManifest($PATH, $$$ARGS) {
5353
},
5454
fix: `
5555
function loadManifest($PATH, $$$ARGS) {
56-
const { platform } = require('process');
57-
$PATH = platform === 'win32' ? $PATH.replaceAll('\\\\', '/') : $PATH;
56+
$PATH = $PATH.replaceAll(${JSON.stringify(sep)}, ${JSON.stringify(posix.sep)});
5857
${returnManifests}
5958
throw new Error(\`Unexpected loadManifest(\${$PATH}) call!\`);
6059
}`,

packages/cloudflare/src/cli/build/patches/plugins/require-page.ts

Lines changed: 0 additions & 92 deletions
This file was deleted.

0 commit comments

Comments
 (0)