Skip to content

Commit 6e590ab

Browse files
make sure instrumentation works with Next 14
1 parent aa4d8fa commit 6e590ab

File tree

4 files changed

+119
-0
lines changed

4 files changed

+119
-0
lines changed

examples/playground14/app/api/instrumentation/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { NextResponse } from "next/server";
22

3+
export const dynamic = "force-dynamic";
4+
35
export function GET() {
46
return NextResponse.json({
57
"nodejs-instrumentation-setup": globalThis["__NODEJS_INSTRUMENTATION_SETUP"] ?? "undefined",

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { inlineFindDir } from "./patches/plugins/find-dir.js";
1717
import { patchLoadInstrumentation } from "./patches/plugins/load-instrumentation.js";
1818
import { inlineLoadManifest } from "./patches/plugins/load-manifest.js";
1919
import { handleOptionalDependencies } from "./patches/plugins/optional-deps.js";
20+
import { patchPrepareImpl } from "./patches/plugins/prepare-impl.js";
2021
import { fixRequire } from "./patches/plugins/require.js";
2122
import { shimRequireHook } from "./patches/plugins/require-hook.js";
2223
import { inlineRequirePage } from "./patches/plugins/require-page.js";
@@ -92,6 +93,7 @@ export async function bundleServer(buildOpts: BuildOptions): Promise<void> {
9293
fixRequire(updater),
9394
handleOptionalDependencies(optionalDependencies),
9495
patchLoadInstrumentation(updater, buildOpts),
96+
patchPrepareImpl(updater, buildOpts),
9597
patchFetchCacheSetMissingWaitUntil(updater),
9698
inlineEvalManifest(updater, buildOpts),
9799
inlineFindDir(updater, buildOpts),
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { describe, expect, test, vi } from "vitest";
2+
3+
import { patchCode } from "../ast/util.js";
4+
import { getRule } from "./prepare-impl.js";
5+
6+
vi.mock(import("node:fs"), async (importOriginal) => {
7+
const mod = await importOriginal();
8+
return {
9+
...mod,
10+
existsSync(path) {
11+
return `${path}`.includes("_file_exists_");
12+
},
13+
};
14+
});
15+
16+
describe("prepareImpl", () => {
17+
const code = `
18+
export default class NextNodeServer extends BaseServer {
19+
async prepareImpl() {
20+
await super.prepareImpl();
21+
if (!this.serverOptions.dev && this.nextConfig.experimental.instrumentationHook) {
22+
try {
23+
const instrumentationHook = await dynamicRequire((0, _path.resolve)(this.serverOptions.dir || ".", this.serverOptions.conf.distDir, "server", _constants1.INSTRUMENTATION_HOOK_FILENAME));
24+
await (instrumentationHook.register == null ? void 0 : instrumentationHook.register.call(instrumentationHook));
25+
} catch (err2) {
26+
if (err2.code !== "MODULE_NOT_FOUND") {
27+
err2.message = \`An error occurred while loading instrumentation hook: \${err2.message}\`;
28+
throw err2;
29+
}
30+
}
31+
}
32+
}
33+
}
34+
`;
35+
36+
test("patch when an instrumentation file is not present", async () => {
37+
expect(patchCode(code, await getRule(null))).toMatchInlineSnapshot(`
38+
"export default class NextNodeServer extends BaseServer {
39+
async prepareImpl() {
40+
await super.prepareImpl();
41+
const instrumentationHook = null;
42+
await (instrumentationHook.register == null ? void 0 : instrumentationHook.register.call(instrumentationHook));
43+
}
44+
}
45+
"
46+
`);
47+
});
48+
49+
test("patch when an instrumentation file is present", async () => {
50+
expect(patchCode(code, await getRule("/_file_exists_/instrumentation.js"))).toMatchInlineSnapshot(`
51+
"export default class NextNodeServer extends BaseServer {
52+
async prepareImpl() {
53+
await super.prepareImpl();
54+
const instrumentationHook = require('/_file_exists_/instrumentation.js');
55+
await (instrumentationHook.register == null ? void 0 : instrumentationHook.register.call(instrumentationHook));
56+
}
57+
}
58+
"
59+
`);
60+
});
61+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* `prepareImpl` uses a dynamic require which is not supported.
3+
*
4+
* `prepareImpl` is the method that sets up instrumentation in Next 14 (this is `loadInstrumentationModule` in Next 15).
5+
*/
6+
7+
import { existsSync } from "node:fs";
8+
import { join } from "node:path";
9+
10+
import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js";
11+
12+
import { patchCode } from "../ast/util.js";
13+
import type { ContentUpdater } from "./content-updater.js";
14+
15+
export function patchPrepareImpl(updater: ContentUpdater, buildOpts: BuildOptions) {
16+
const { outputDir } = buildOpts;
17+
18+
const baseDir = join(outputDir, "server-functions/default", getPackagePath(buildOpts));
19+
const dotNextDir = join(baseDir, ".next");
20+
const maybeBuiltInstrumentationPath = join(dotNextDir, "server", `${INSTRUMENTATION_HOOK_FILENAME}.js`);
21+
const builtInstrumentationPath = existsSync(maybeBuiltInstrumentationPath)
22+
? maybeBuiltInstrumentationPath
23+
: null;
24+
25+
return updater.updateContent(
26+
"patch-prepareImpl",
27+
{ filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/, contentFilter: /async prepareImpl\(/ },
28+
async ({ contents }) => patchCode(contents, await getRule(builtInstrumentationPath))
29+
);
30+
}
31+
32+
export async function getRule(builtInstrumentationPath: string | null) {
33+
return `
34+
rule:
35+
kind: method_definition
36+
any:
37+
- has: { field: name, regex: ^prepareImpl$, pattern: $NAME }
38+
all:
39+
- has: { pattern: dynamicRequire, stopBy: end }
40+
- has: { pattern: $_.INSTRUMENTATION_HOOK_FILENAME, stopBy: end }
41+
fix: |-
42+
async $NAME() {
43+
await super.prepareImpl();
44+
const instrumentationHook = ${builtInstrumentationPath ? `require('${builtInstrumentationPath}')` : "null"};
45+
await (instrumentationHook.register == null ? void 0 : instrumentationHook.register.call(instrumentationHook));
46+
}
47+
`;
48+
}
49+
50+
/**
51+
* Pattern to detect instrumentation hooks file
52+
* (taken from Next.js source: https://github.com/vercel/next.js/blob/1d5820563/packages/next/src/lib/constants.ts#L46-L47)
53+
*/
54+
const INSTRUMENTATION_HOOK_FILENAME = "instrumentation";

0 commit comments

Comments
 (0)