Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
7 changes: 7 additions & 0 deletions .changeset/strange-laws-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@opennextjs/cloudflare": patch
---

fix pages api routes

fixed pages api routes by inlining a dynamic require in the `NodeModuleLoader` class
8 changes: 8 additions & 0 deletions examples/e2e/pages-router/e2e/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { expect, test } from "@playwright/test";

test("should not fail on an api route", async ({ page }) => {
const result = await page.goto("/api/hello");
expect(result?.status()).toBe(200);
const body = await result?.json();
expect(body).toEqual({ hello: "world" });
});
4 changes: 2 additions & 2 deletions packages/cloudflare/src/cli/build/bundle-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { patchWebpackRuntime } from "./patches/ast/webpack-runtime.js";
import * as patches from "./patches/index.js";
import { inlineBuildId } from "./patches/plugins/build-id.js";
import { ContentUpdater } from "./patches/plugins/content-updater.js";
import { inlineDynamicRequires } from "./patches/plugins/dynamic-requires.js";
import { inlineEvalManifest } from "./patches/plugins/eval-manifest.js";
import { patchFetchCacheSetMissingWaitUntil } from "./patches/plugins/fetch-cache-wait-until.js";
import { inlineFindDir } from "./patches/plugins/find-dir.js";
Expand All @@ -20,7 +21,6 @@ import { handleOptionalDependencies } from "./patches/plugins/optional-deps.js";
import { patchDepdDeprecations } from "./patches/plugins/patch-depd-deprecations.js";
import { fixRequire } from "./patches/plugins/require.js";
import { shimRequireHook } from "./patches/plugins/require-hook.js";
import { inlineRequirePage } from "./patches/plugins/require-page.js";
import { setWranglerExternal } from "./patches/plugins/wrangler-external.js";
import { normalizePath, patchCodeWithValidations } from "./utils/index.js";

Expand Down Expand Up @@ -88,7 +88,7 @@ export async function bundleServer(buildOpts: BuildOptions): Promise<void> {
conditions: [],
plugins: [
shimRequireHook(buildOpts),
inlineRequirePage(updater, buildOpts),
inlineDynamicRequires(updater, buildOpts),
setWranglerExternal(),
fixRequire(updater),
handleOptionalDependencies(optionalDependencies),
Expand Down
144 changes: 144 additions & 0 deletions packages/cloudflare/src/cli/build/patches/plugins/dynamic-requires.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { readFile } from "node:fs/promises";
import { join, posix, sep } from "node:path";

import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js";
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
import type { Plugin } from "esbuild";

import { normalizePath } from "../../utils/normalize-path.js";
import { patchCode, type RuleConfig } from "../ast/util.js";
import type { ContentUpdater } from "./content-updater.js";

async function getPagesManifests(serverDir: string): Promise<string[]> {
try {
return Object.values(JSON.parse(await readFile(join(serverDir, "pages-manifest.json"), "utf-8")));
} catch {
// The file does not exist
return [];
}
}

async function getAppPathsManifests(serverDir: string): Promise<string[]> {
try {
return Object.values(JSON.parse(await readFile(join(serverDir, "app-paths-manifest.json"), "utf-8")));
} catch {
// The file does not exist
return [];
}
}

function getServerDir(buildOpts: BuildOptions) {
return join(buildOpts.outputDir, "server-functions/default", getPackagePath(buildOpts), ".next/server");
}

function getRequires(idVariable: string, files: string[], serverDir: string) {
// Inline fs access and dynamic requires that are not supported by workerd.
return files
.map(
(file) => `
if (${idVariable}.replaceAll(${JSON.stringify(sep)}, ${JSON.stringify(posix.sep)}).endsWith(${JSON.stringify(normalizePath(file))})) {
return require(${JSON.stringify(join(serverDir, file))});
}`
)
.join("\n");
}

export function inlineDynamicRequires(updater: ContentUpdater, buildOpts: BuildOptions): Plugin {
updater.updateContent(
"inline-node-module-loader",
{
filter: getCrossPlatformPathRegex(
String.raw`/next/dist/server/lib/module-loader/node-module-loader\.js$`,
{ escape: false }
),
contentFilter: /class NodeModuleLoader {/,
},
async ({ contents }) => patchCode(contents, await getNodeModuleLoaderRule(buildOpts))
);
updater.updateContent(
"inline-require-page",
{
filter: getCrossPlatformPathRegex(String.raw`/next/dist/server/require\.js$`, { escape: false }),
contentFilter: /function requirePage\(/,
},
async ({ contents }) => patchCode(contents, await getRequirePageRule(buildOpts))
);
return { name: "inline-dynamic-requires", setup() {} };
}

async function getNodeModuleLoaderRule(buildOpts: BuildOptions) {
const serverDir = getServerDir(buildOpts);

const manifests = await getPagesManifests(serverDir);

const files = manifests.filter((file) => file.endsWith(".js"));

return `
rule:
kind: method_definition
all:
- has:
field: name
regex: ^load$
- has:
field: parameters
has:
kind: required_parameter
pattern: $ID
inside:
stopBy:
kind: class_declaration
kind: class_declaration
has:
field: name
regex: ^NodeModuleLoader$
fix: |
async load($ID) {
${getRequires("$ID", files, serverDir)}
}`;
}

async function getRequirePageRule(buildOpts: BuildOptions) {
const serverDir = getServerDir(buildOpts);

const pagesManifests = await getPagesManifests(serverDir);
const appPathsManifests = await getAppPathsManifests(serverDir);

const manifests = pagesManifests.concat(appPathsManifests);

const htmlFiles = manifests.filter((file) => file.endsWith(".html"));
const jsFiles = manifests.filter((file) => file.endsWith(".js"));

return {
rule: {
pattern: `
function requirePage($PAGE, $DIST_DIR, $IS_APP_PATH) {
const $_ = getPagePath($$$ARGS);
$$$_BODY
}`,
}, // Inline fs access and dynamic require that are not supported by workerd.
fix: `
function requirePage($PAGE, $DIST_DIR, $IS_APP_PATH) {
const { platform } = require('process');
const pagePath = platform === 'win32' ? getPagePath($$$ARGS).replaceAll('\\\\', '/') : getPagePath($$$ARGS);

// html
${(
await Promise.all(
htmlFiles.map(
async (file) => `if (pagePath.endsWith(${JSON.stringify(normalizePath(file))})) {
return ${JSON.stringify(await readFile(join(serverDir, file), "utf-8"))};
}`
)
)
).join("\n")}
// js
process.env.__NEXT_PRIVATE_RUNTIME_TYPE = $IS_APP_PATH ? 'app' : 'pages';
try {
${getRequires("pagePath", jsFiles, serverDir)}
} finally {
process.env.__NEXT_PRIVATE_RUNTIME_TYPE = '';
}
}`,
} satisfies RuleConfig;
}
92 changes: 0 additions & 92 deletions packages/cloudflare/src/cli/build/patches/plugins/require-page.ts

This file was deleted.