Skip to content
5 changes: 5 additions & 0 deletions .changeset/modern-buses-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opennextjs/cloudflare": patch
---

fix(patches): Update patchInstrumentation and loadManifest to work with Next 15.4
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
import { describe, expect, test } from "vitest";

import { getNext14Rule, getNext15Rule } from "./instrumentation.js";
import { getNext14Rule, getNext15Rule, getNext154Rule } from "./instrumentation.js";

describe("LoadInstrumentationModule (Next15)", () => {
const code = `
Expand Down Expand Up @@ -96,3 +96,59 @@ describe("prepareImpl (Next14)", () => {
`);
});
});

describe("getInstrumenationModule (Next15.4)", () => {
const code = `
async function getInstrumentationModule(projectDir, distDir) {
if (cachedInstrumentationModule) {
return cachedInstrumentationModule;
}
try {
cachedInstrumentationModule = (0, _interopdefault.interopDefault)(await require(_nodepath.default.join(projectDir, distDir, "server", \`\${_constants.INSTRUMENTATION_HOOK_FILENAME}.js\`)));
return cachedInstrumentationModule;
} catch (err) {
if ((0, _iserror.default)(err) && err.code !== "ENOENT" && err.code !== "MODULE_NOT_FOUND" && err.code !== "ERR_MODULE_NOT_FOUND") {
throw err;
}
}
}
`;

test("patch when an instrumentation file is not present", async () => {
expect(patchCode(code, getNext154Rule(null))).toMatchInlineSnapshot(`
"async function getInstrumentationModule(projectDir, distDir) {
if (cachedInstrumentationModule) {
return cachedInstrumentationModule;
}
try {
cachedInstrumentationModule = null;
return cachedInstrumentationModule;
} catch (err) {
if ((0, _iserror.default)(err) && err.code !== "ENOENT" && err.code !== "MODULE_NOT_FOUND" && err.code !== "ERR_MODULE_NOT_FOUND") {
throw err;
}
}
}
"
`);
});

test("patch when an instrumentation file is present", async () => {
expect(patchCode(code, getNext154Rule("/_file_exists_/instrumentation.js"))).toMatchInlineSnapshot(`
"async function getInstrumentationModule(projectDir, distDir) {
if (cachedInstrumentationModule) {
return cachedInstrumentationModule;
}
try {
cachedInstrumentationModule = require('/_file_exists_/instrumentation.js');
return cachedInstrumentationModule;
} catch (err) {
if ((0, _iserror.default)(err) && err.code !== "ENOENT" && err.code !== "MODULE_NOT_FOUND" && err.code !== "ERR_MODULE_NOT_FOUND") {
throw err;
}
}
}
"
`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,28 @@ import { join } from "node:path";
import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js";
import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
import type { ContentUpdater, Plugin } from "@opennextjs/aws/plugins/content-updater.js";
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";

import { normalizePath } from "../../utils/normalize-path.js";

export function patchInstrumentation(updater: ContentUpdater, buildOpts: BuildOptions): Plugin {
const builtInstrumentationPath = getBuiltInstrumentationPath(buildOpts);

updater.updateContent("patch-instrumentation-next15-4", [
{
field: {
filter: getCrossPlatformPathRegex(
String.raw`/server/lib/router-utils/instrumentation-globals.external\.js$`,
{
escape: false,
}
),
contentFilter: /getInstrumentationModule\(/,
callback: ({ contents }) => patchCode(contents, getNext154Rule(builtInstrumentationPath)),
},
},
]);

updater.updateContent("patch-instrumentation-next15", [
{
field: {
Expand All @@ -36,6 +52,28 @@ export function patchInstrumentation(updater: ContentUpdater, buildOpts: BuildOp
};
}

export function getNext154Rule(builtInstrumentationPath: string | null) {
return `
rule:
kind: expression_statement
has:
kind: assignment_expression
all:
- has: { pattern: "cachedInstrumentationModule" }
- has: { kind: call_expression, regex: "INSTRUMENTATION_HOOK_FILENAME"}
inside:
kind: try_statement
stopBy: end
has: { regex: "return cachedInstrumentationModule" }
inside:
kind: function_declaration
stopBy: end
has: { field: name, pattern: getInstrumentationModule }
fix: |-
cachedInstrumentationModule = ${builtInstrumentationPath ? `require('${builtInstrumentationPath}')` : "null"};
`;
}

export function getNext15Rule(builtInstrumentationPath: string | null) {
return `
rule:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ async function getLoadManifestRule(buildOpts: BuildOptions) {
const baseDir = join(outputDir, "server-functions/default", getPackagePath(buildOpts));
const dotNextDir = join(baseDir, ".next");

const manifests = await glob(join(dotNextDir, "**/*-manifest.json"), { windowsPathsNoEscape: true });
const manifests = await glob(join(dotNextDir, "**/{*-manifest,required-server-files}.json"), {
windowsPathsNoEscape: true,
});

const returnManifests = (
await Promise.all(
Expand All @@ -62,6 +64,9 @@ function loadManifest($PATH, $$$ARGS) {
fix: `
function loadManifest($PATH, $$$ARGS) {
$PATH = $PATH.replaceAll(${JSON.stringify(sep)}, ${JSON.stringify(posix.sep)});
if ($PATH === "/.next/BUILD_ID") {
return process.env.NEXT_BUILD_ID;
}
${returnManifests}
throw new Error(\`Unexpected loadManifest(\${$PATH}) call!\`);
}`,
Expand Down