@opennextjs/cloudflare 1.20.2 → 1.20.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ import { printHeader } from "@opennextjs/aws/build/utils.js";
8
8
  import logger from "@opennextjs/aws/logger.js";
9
9
  import { ensureNextjsVersionSupported } from "../utils/nextjs-support.js";
10
10
  import { bundleServer } from "./bundle-server.js";
11
+ import { bundleNodeMiddleware } from "./open-next/bundle-node-middleware.js";
11
12
  import { compileCacheAssetsManifestSqlFile } from "./open-next/compile-cache-assets-manifest.js";
12
13
  import { compileEnvFiles } from "./open-next/compile-env-files.js";
13
14
  import { compileImages } from "./open-next/compile-images.js";
@@ -62,10 +63,9 @@ export async function build(options, config, projectOpts, wranglerConfig, allowU
62
63
  setStandaloneBuildMode(options);
63
64
  buildNextjsApp(options);
64
65
  }
65
- // Make sure no Node.js middleware is used
66
- if (useNodeMiddleware(options)) {
67
- logger.error("Node.js middleware is not currently supported. Consider switching to Edge Middleware.");
68
- process.exit(1);
66
+ const hasNodeMiddleware = useNodeMiddleware(options);
67
+ if (hasNodeMiddleware) {
68
+ logger.warn("Node.js middleware support is experimental in cloudflare, and not officially maintained by OpenNext maintainers. Use at your own risk.");
69
69
  }
70
70
  // Generate deployable bundle
71
71
  printHeader("Generating bundle");
@@ -77,6 +77,9 @@ export async function build(options, config, projectOpts, wranglerConfig, allowU
77
77
  await compileSkewProtection(options, config);
78
78
  // Compile middleware
79
79
  await createMiddleware(options, { forceOnlyBuildOnce: true });
80
+ if (hasNodeMiddleware) {
81
+ await bundleNodeMiddleware(options);
82
+ }
80
83
  createStaticAssets(options, { useBasePath: true });
81
84
  if (config.dangerous?.disableIncrementalCache !== true) {
82
85
  const { useTagCache, metaFiles } = createCacheAssets(options);
@@ -52,7 +52,7 @@ export async function bundleServer(buildOpts, projectOpts) {
52
52
  const nextConfig = JSON.parse(fs.readFileSync(serverFiles, "utf-8")).config;
53
53
  const useTurbopack = buildHelper.getBundlerRuntime(buildOpts) === "turbopack";
54
54
  console.log(`\x1b[35m⚙️ Bundling the OpenNext server...\n\x1b[0m`);
55
- await patchWebpackRuntime(buildOpts);
55
+ await patchWebpackRuntime(path.join(dotNextPath, "server"));
56
56
  const useOg = patchVercelOgLibrary(buildOpts);
57
57
  const outputPath = path.join(outputDir, "server-functions", "default");
58
58
  const packagePath = getPackagePath(buildOpts);
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Bundles the Node.js middleware (`proxy.ts` / `middleware.ts` with the `nodejs` runtime)
3
+ * into a Workers compatible `middleware/handler.mjs`.
4
+ *
5
+ * NOTE: Running Next.js Node.js middleware on workerd is experimental and is not supported
6
+ * by the OpenNext maintainers. It re-bundles the middleware compiled by Next.js, which is an
7
+ * internal output that can change between Next.js versions.
8
+ *
9
+ * `@opennextjs/aws` bundles the external middleware for a Node.js server:
10
+ * the OpenNext config is read from the filesystem at runtime and the middleware compiled
11
+ * by Next.js is loaded with `await import("./.next/server/middleware.js")`.
12
+ *
13
+ * workerd can not access the filesystem nor load modules at runtime, so the handler
14
+ * built by `@opennextjs/aws` is replaced with a fully self-contained bundle:
15
+ *
16
+ * - the config manifests are inlined by `openNextEdgePlugins` (as for the edge middleware)
17
+ * - the middleware compiled by Next.js is statically bundled from the traced files that
18
+ * `@opennextjs/aws` copies to `middleware/<package path>/.next/server/middleware.js`
19
+ */
20
+ import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
21
+ import { type Plugin } from "esbuild";
22
+ /**
23
+ * Resolves the middleware compiled by Next.js to the copy created by `copyTracedFiles`.
24
+ *
25
+ * `@opennextjs/aws`'s `nodeMiddlewareHandler` loads the middleware with a dynamic
26
+ * `await import("./.next/server/middleware.js")` that nothing on the aws side resolves (it relies
27
+ * on the adapter bundling it), so this resolves that specifier to the traced copy.
28
+ */
29
+ export declare function setCompiledMiddlewarePlugin(compiledMiddlewarePath: string): Plugin;
30
+ /**
31
+ * Makes the Node.js builtins used by the bundled code (i.e. `require("crypto")` or
32
+ * `import from "node:crypto"`) resolve to the modules workerd provides via `nodejs_compat`.
33
+ *
34
+ * `require` calls are converted into a virtual CommonJS module re-exporting the builtin.
35
+ * The virtual module has to stay CommonJS - esbuild classifies it as such because it assigns to
36
+ * `module.exports` and uses no `export` keyword - so that `require("crypto")` receives the module
37
+ * workerd provides rather than an esbuild `__toCommonJS` wrapper built from its named exports,
38
+ * which would drop whatever the named exports do not expose and add a synthetic `__esModule`.
39
+ *
40
+ * The conversion is kept in sync with `handleRequireCallsToNodeJSBuiltins` in wrangler's
41
+ * `hybrid-nodejs-compat` esbuild plugin, which is the source of truth:
42
+ * https://github.com/cloudflare/workers-sdk/blob/c457bfc6b5a575586354f5b0ad7a1100eff915fe/packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts#L102-L133
43
+ *
44
+ * It differs on a single intentional point: the builtin is imported as a namespace and
45
+ * `mod.default ?? mod` is used rather than a default import, so that a builtin without a `default`
46
+ * export can not fail to link in workerd. wrangler applies the same fallback to its unenv aliases
47
+ * in `handleUnenvAliasedPackages`.
48
+ */
49
+ export declare function nodeBuiltinsPlugin(): Plugin;
50
+ export declare function bundleNodeMiddleware(options: BuildOptions): Promise<void>;
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Bundles the Node.js middleware (`proxy.ts` / `middleware.ts` with the `nodejs` runtime)
3
+ * into a Workers compatible `middleware/handler.mjs`.
4
+ *
5
+ * NOTE: Running Next.js Node.js middleware on workerd is experimental and is not supported
6
+ * by the OpenNext maintainers. It re-bundles the middleware compiled by Next.js, which is an
7
+ * internal output that can change between Next.js versions.
8
+ *
9
+ * `@opennextjs/aws` bundles the external middleware for a Node.js server:
10
+ * the OpenNext config is read from the filesystem at runtime and the middleware compiled
11
+ * by Next.js is loaded with `await import("./.next/server/middleware.js")`.
12
+ *
13
+ * workerd can not access the filesystem nor load modules at runtime, so the handler
14
+ * built by `@opennextjs/aws` is replaced with a fully self-contained bundle:
15
+ *
16
+ * - the config manifests are inlined by `openNextEdgePlugins` (as for the edge middleware)
17
+ * - the middleware compiled by Next.js is statically bundled from the traced files that
18
+ * `@opennextjs/aws` copies to `middleware/<package path>/.next/server/middleware.js`
19
+ */
20
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
21
+ import { builtinModules, isBuiltin } from "node:module";
22
+ import path from "node:path";
23
+ import { getBundlerRuntime, getPackagePath } from "@opennextjs/aws/build/helper.js";
24
+ import logger from "@opennextjs/aws/logger.js";
25
+ import { ContentUpdater } from "@opennextjs/aws/plugins/content-updater.js";
26
+ import { openNextEdgePlugins } from "@opennextjs/aws/plugins/edge.js";
27
+ import { openNextExternalMiddlewarePlugin } from "@opennextjs/aws/plugins/externalMiddleware.js";
28
+ import { openNextReplacementPlugin } from "@opennextjs/aws/plugins/replacement.js";
29
+ import { openNextResolvePlugin } from "@opennextjs/aws/plugins/resolve.js";
30
+ import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
31
+ import { build } from "esbuild";
32
+ import { glob } from "glob";
33
+ import { normalizePath } from "../../utils/normalize-path.js";
34
+ import { patchWebpackRuntime } from "../patches/ast/webpack-runtime.js";
35
+ import { patchInstrumentation } from "../patches/plugins/instrumentation.js";
36
+ import { patchTurbopackRuntimeCode, patchTurbopackWasmChunkCode } from "../patches/plugins/turbopack.js";
37
+ import { setWranglerExternal } from "../patches/plugins/wrangler-external.js";
38
+ /**
39
+ * Inlines the chunks of the middleware compiled by Next.js.
40
+ *
41
+ * Both the webpack and the Turbopack runtimes resolve the chunks they need at runtime,
42
+ * which workerd does not support. The same patches as for the server are used to inline them.
43
+ *
44
+ * @param options Build options.
45
+ * @param dotNextServerDir The `.next/server` directory of the middleware output.
46
+ */
47
+ async function inlineMiddlewareChunks(options, dotNextServerDir) {
48
+ if (getBundlerRuntime(options) !== "turbopack") {
49
+ await patchWebpackRuntime(dotNextServerDir);
50
+ return;
51
+ }
52
+ const runtimePath = path.join(dotNextServerDir, "chunks/[turbopack]_runtime.js");
53
+ if (!existsSync(runtimePath)) {
54
+ throw new Error(`Turbopack runtime not found at ${runtimePath}`);
55
+ }
56
+ // The Turbopack runtime resolves the chunks relative to the `.next` directory.
57
+ // `.wasm` chunks are needed too: they are inlined as static imports by `loadWasmChunk`.
58
+ const tracedFiles = await glob(path.join(dotNextServerDir, "**/*.{js,wasm}"), {
59
+ windowsPathsNoEscape: true,
60
+ });
61
+ writeFileSync(runtimePath, patchTurbopackRuntimeCode({
62
+ code: readFileSync(runtimePath, "utf-8"),
63
+ filePath: normalizePath(runtimePath),
64
+ tracedFiles,
65
+ }));
66
+ // Since Next.js 16.3 the wasm helpers are emitted in the chunks rather than in the runtime.
67
+ for (const chunkPath of tracedFiles) {
68
+ if (!chunkPath.endsWith(".js") || normalizePath(chunkPath) === normalizePath(runtimePath)) {
69
+ continue;
70
+ }
71
+ const code = readFileSync(chunkPath, "utf-8");
72
+ const patched = patchTurbopackWasmChunkCode({ code, tracedFiles });
73
+ if (patched !== code) {
74
+ writeFileSync(chunkPath, patched);
75
+ }
76
+ }
77
+ }
78
+ /**
79
+ * Resolves the middleware compiled by Next.js to the copy created by `copyTracedFiles`.
80
+ *
81
+ * `@opennextjs/aws`'s `nodeMiddlewareHandler` loads the middleware with a dynamic
82
+ * `await import("./.next/server/middleware.js")` that nothing on the aws side resolves (it relies
83
+ * on the adapter bundling it), so this resolves that specifier to the traced copy.
84
+ */
85
+ export function setCompiledMiddlewarePlugin(compiledMiddlewarePath) {
86
+ return {
87
+ name: "compiled-middleware",
88
+ setup(build) {
89
+ build.onResolve({ filter: getCrossPlatformPathRegex("./.next/server/middleware.js") }, () => ({
90
+ path: compiledMiddlewarePath,
91
+ }));
92
+ },
93
+ };
94
+ }
95
+ /**
96
+ * Makes the Node.js builtins used by the bundled code (i.e. `require("crypto")` or
97
+ * `import from "node:crypto"`) resolve to the modules workerd provides via `nodejs_compat`.
98
+ *
99
+ * `require` calls are converted into a virtual CommonJS module re-exporting the builtin.
100
+ * The virtual module has to stay CommonJS - esbuild classifies it as such because it assigns to
101
+ * `module.exports` and uses no `export` keyword - so that `require("crypto")` receives the module
102
+ * workerd provides rather than an esbuild `__toCommonJS` wrapper built from its named exports,
103
+ * which would drop whatever the named exports do not expose and add a synthetic `__esModule`.
104
+ *
105
+ * The conversion is kept in sync with `handleRequireCallsToNodeJSBuiltins` in wrangler's
106
+ * `hybrid-nodejs-compat` esbuild plugin, which is the source of truth:
107
+ * https://github.com/cloudflare/workers-sdk/blob/c457bfc6b5a575586354f5b0ad7a1100eff915fe/packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts#L102-L133
108
+ *
109
+ * It differs on a single intentional point: the builtin is imported as a namespace and
110
+ * `mod.default ?? mod` is used rather than a default import, so that a builtin without a `default`
111
+ * export can not fail to link in workerd. wrangler applies the same fallback to its unenv aliases
112
+ * in `handleUnenvAliasedPackages`.
113
+ */
114
+ export function nodeBuiltinsPlugin() {
115
+ const namespace = "node-builtins";
116
+ // Match only Node.js builtins so esbuild does not call back on every import:
117
+ // the `node:` prefixed form and the bare names (`crypto`, `fs`, ...).
118
+ const builtinsFilter = new RegExp(`^(node:|(${builtinModules.join("|")})$)`);
119
+ return {
120
+ name: namespace,
121
+ setup(build) {
122
+ build.onResolve({ filter: builtinsFilter }, ({ path: specifier, kind }) => {
123
+ if (!isBuiltin(specifier)) {
124
+ return undefined;
125
+ }
126
+ const prefixed = specifier.startsWith("node:") ? specifier : `node:${specifier}`;
127
+ return kind === "require-call" ? { path: prefixed, namespace } : { path: prefixed, external: true };
128
+ });
129
+ build.onLoad({ filter: /^node:/, namespace }, ({ path: builtin }) => ({
130
+ contents: `
131
+ import * as mod from "${builtin}";
132
+ module.exports = mod.default ?? mod;
133
+ `,
134
+ loader: "js",
135
+ }));
136
+ },
137
+ };
138
+ }
139
+ export async function bundleNodeMiddleware(options) {
140
+ const { config, outputDir } = options;
141
+ const middlewareDir = path.join(outputDir, "middleware");
142
+ const dotNextServerDir = path.join(middlewareDir, getPackagePath(options), ".next/server");
143
+ const compiledMiddleware = path.join(dotNextServerDir, "middleware.js");
144
+ if (!existsSync(compiledMiddleware)) {
145
+ throw new Error(`Compiled Node.js middleware not found at ${compiledMiddleware}`);
146
+ }
147
+ // The bundler runtime resolves the chunks of the middleware at runtime, which workerd does
148
+ // not support. Inline the chunks so that they are statically bundled.
149
+ await inlineMiddlewareChunks(options, dotNextServerDir);
150
+ logger.info("Bundling Node.js middleware...");
151
+ const middlewareConfig = config.middleware?.external ? config.middleware : undefined;
152
+ const overrides = {
153
+ ...middlewareConfig?.override,
154
+ originResolver: middlewareConfig?.originResolver,
155
+ };
156
+ function override(target) {
157
+ // String and lazy loaded overrides are supported, see `buildEdgeBundle`
158
+ return typeof overrides[target] === "string" || typeof overrides[target] === "function"
159
+ ? overrides[target]
160
+ : undefined;
161
+ }
162
+ const includeCache = config.dangerous?.enableCacheInterception;
163
+ // `next/dist/server/lib/trace/tracer.js` requires `@opentelemetry/api`, an optional
164
+ // dependency that most apps do not install. On the edge runtime Next.js does not fall
165
+ // back to its compiled copy when the require throws, so alias to that copy - but only
166
+ // when the app has not installed the real package, otherwise the real one is used.
167
+ const hasOpentelemetry = existsSync(path.join(options.appBuildOutputPath, "node_modules", "@opentelemetry", "api"));
168
+ const updater = new ContentUpdater(options);
169
+ await build({
170
+ entryPoints: [path.join(options.openNextDistDir, "adapters", "middleware.js")],
171
+ outfile: path.join(middlewareDir, "handler.mjs"),
172
+ allowOverwrite: true,
173
+ bundle: true,
174
+ format: "esm",
175
+ target: "es2022",
176
+ platform: "neutral",
177
+ minify: options.minify,
178
+ sourcemap: options.debug ? "inline" : false,
179
+ sourcesContent: false,
180
+ treeShaking: true,
181
+ conditions: ["module"],
182
+ mainFields: ["module", "main"],
183
+ external: ["node:*", "./open-next.config.mjs"],
184
+ define: {
185
+ // The base of the middleware compiled by Next.js is runtime agnostic. "edge" selects its
186
+ // Web API code paths (which workerd supports) over the Node.js server paths (which it does
187
+ // not): it also skips `setup-node-env.external.js`, which patches read-only workerd globals.
188
+ // Node.js builtins used by the middleware are still provided by workerd via `nodejs_compat`.
189
+ "process.env.NEXT_RUNTIME": '"edge"',
190
+ "process.env.NODE_ENV": '"production"',
191
+ },
192
+ alias: {
193
+ // See `hasOpentelemetry` above.
194
+ ...(hasOpentelemetry ? {} : { "@opentelemetry/api": "next/dist/compiled/@opentelemetry/api" }),
195
+ },
196
+ plugins: [
197
+ openNextResolvePlugin({
198
+ overrides: {
199
+ wrapper: override("wrapper") ?? "cloudflare-edge",
200
+ converter: override("converter") ?? "edge",
201
+ ...(includeCache
202
+ ? {
203
+ tagCache: override("tagCache"),
204
+ incrementalCache: override("incrementalCache"),
205
+ queue: override("queue"),
206
+ }
207
+ : {}),
208
+ originResolver: override("originResolver") ?? "pattern-env",
209
+ proxyExternalRequest: override("proxyExternalRequest") ?? "fetch",
210
+ },
211
+ fnName: "middleware",
212
+ }),
213
+ openNextReplacementPlugin({
214
+ name: "externalMiddlewareOverrides",
215
+ target: getCrossPlatformPathRegex("adapters/middleware.js"),
216
+ deletes: includeCache ? [] : ["includeCacheInMiddleware"],
217
+ }),
218
+ // Handle the middleware with the OpenNext Node.js middleware handler
219
+ openNextExternalMiddlewarePlugin(path.join(options.openNextDistDir, "core", "nodeMiddlewareHandler.js")),
220
+ setCompiledMiddlewarePlugin(compiledMiddleware),
221
+ // `.wasm` and `.bin` files are bundled by wrangler, not by this build
222
+ setWranglerExternal(),
223
+ // Must be registered before `openNextEdgePlugins` to handle `require("node:*")` calls
224
+ nodeBuiltinsPlugin(),
225
+ // Inline the config manifests
226
+ openNextEdgePlugins({
227
+ nextDir: path.join(options.appBuildOutputPath, ".next"),
228
+ isInCloudflare: true,
229
+ }),
230
+ // Next.js 16.3 registers the instrumentation hook from the middleware itself when the
231
+ // middleware does not run on the edge runtime, by dynamically requiring
232
+ // `.next/server/instrumentation.js` - which workerd does not support.
233
+ //
234
+ // The guard Next.js uses (`process.env.NEXT_RUNTIME !== "edge"`) is inlined to `"nodejs"`
235
+ // when Next.js compiles the middleware, so the `define` above can not eliminate the branch.
236
+ // The loader is stubbed out instead, which matches what the edge runtime does here: it has
237
+ // no instrumentation entry to register, and the server function - which shares the isolate -
238
+ // already registers the hook.
239
+ // See https://github.com/opennextjs/opennextjs-cloudflare/issues/1362
240
+ patchInstrumentation(updater, options, { loadInstrumentation: false }),
241
+ // Apply updater updates, must be the last plugin
242
+ updater.plugin,
243
+ ],
244
+ banner: {
245
+ js: `
246
+ import { Buffer } from "node:buffer";
247
+ globalThis.Buffer = Buffer;
248
+
249
+ // Next.js' compiled middleware references \`AsyncLocalStorage\` as a global. workerd only
250
+ // exposes it via \`node:async_hooks\` (not as a global, even with \`nodejs_compat\`), so it is
251
+ // assigned here - as \`@opennextjs/aws\`'s edge middleware bundler does.
252
+ import { AsyncLocalStorage } from "node:async_hooks";
253
+ globalThis.AsyncLocalStorage = AsyncLocalStorage;
254
+
255
+ // Next.js sets \`__import_unsupported\` on \`globalThis\` with \`configurable: false\`.
256
+ // When the middleware and the server share a Worker, the second call would throw,
257
+ // so it is skipped when the property is already set. Otherwise it runs as usual.
258
+ // See https://github.com/vercel/next.js/blob/5b7833e3/packages/next/src/server/web/globals.ts#L94-L98
259
+ const defaultDefineProperty = Object.defineProperty;
260
+ Object.defineProperty = function (o, p, a) {
261
+ if (p === "__import_unsupported" && Boolean(globalThis.__import_unsupported)) {
262
+ // \`Object.defineProperty\` returns the object it was passed.
263
+ return o;
264
+ }
265
+ return defaultDefineProperty(o, p, a);
266
+ };
267
+
268
+ globalThis.openNextDebug = ${options.debug};
269
+ globalThis.openNextVersion = "${options.openNextVersion}";
270
+ globalThis.nextVersion = "${options.nextVersion}";
271
+ `,
272
+ },
273
+ });
274
+ }
@@ -19,11 +19,10 @@
19
19
  * For a single chunk:
20
20
  * require("./chunks/CHUNK_ID.js");
21
21
  */
22
- import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
23
22
  export declare function buildMultipleChunksRule(chunks: number[]): string;
24
23
  export declare const singleChunkRule = "\nrule:\n pattern: ($CHUNK_ID, $_PROMISES) => { $$$ }\n inside: {pattern: $_.$_.require = $$$_, stopBy: end}\n all:\n - has: {pattern: $INSTALL(require(\"./chunks/\" + $$$)), stopBy: end}\n - has: {pattern: $SELF_ID == $CHUNK_ID, stopBy: end}\n - has: {pattern: \"$INSTALLED_CHUNK[$CHUNK_ID] = 1\", stopBy: end}\nfix: |\n ($CHUNK_ID, _) => {\n if (!$INSTALLED_CHUNK[$CHUNK_ID]) {\n try {\n $INSTALL(require(\"./chunks/$SELF_ID.js\"));\n } catch {}\n }\n }\n";
25
24
  /**
26
25
  * Fixes the webpack-runtime.js and webpack-api-runtime.js files by inlining
27
26
  * the webpack dynamic requires.
28
27
  */
29
- export declare function patchWebpackRuntime(buildOpts: BuildOptions): Promise<void>;
28
+ export declare function patchWebpackRuntime(dotNextServerDir: string): Promise<void>;
@@ -21,7 +21,6 @@
21
21
  */
22
22
  import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
23
23
  import { join } from "node:path";
24
- import { getPackagePath } from "@opennextjs/aws/build/helper.js";
25
24
  import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
26
25
  // Inline the code when there are multiple chunks
27
26
  export function buildMultipleChunksRule(chunks) {
@@ -68,15 +67,16 @@ fix: |
68
67
  * Fixes the webpack-runtime.js and webpack-api-runtime.js files by inlining
69
68
  * the webpack dynamic requires.
70
69
  */
71
- export async function patchWebpackRuntime(buildOpts) {
72
- const { outputDir } = buildOpts;
73
- const dotNextServerDir = join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next/server");
70
+ export async function patchWebpackRuntime(dotNextServerDir) {
74
71
  // Look for all the chunks.
75
- const chunks = readdirSync(join(dotNextServerDir, "chunks"))
76
- .filter((chunk) => /^\d+\.js$/.test(chunk))
77
- .map((chunk) => {
78
- return Number(chunk.replace(/\.js$/, ""));
79
- });
72
+ const chunksDir = join(dotNextServerDir, "chunks");
73
+ const chunks = existsSync(chunksDir)
74
+ ? readdirSync(chunksDir)
75
+ .filter((chunk) => /^\d+\.js$/.test(chunk))
76
+ .map((chunk) => {
77
+ return Number(chunk.replace(/\.js$/, ""));
78
+ })
79
+ : [];
80
80
  patchFile(join(dotNextServerDir, "webpack-runtime.js"), chunks);
81
81
  patchFile(join(dotNextServerDir, "webpack-api-runtime.js"), chunks);
82
82
  }
@@ -1,6 +1,21 @@
1
1
  import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
2
2
  import type { ContentUpdater, Plugin } from "@opennextjs/aws/plugins/content-updater.js";
3
- export declare function patchInstrumentation(updater: ContentUpdater, buildOpts: BuildOptions): Plugin;
3
+ /**
4
+ * Replaces the dynamic loading of the instrumentation hook, which workerd does not support.
5
+ *
6
+ * Next.js loads `.next/server/instrumentation.js` with a `require()` call built from a path
7
+ * computed at runtime. The loaders are rewritten to either statically `require()` the file that
8
+ * the Next.js build emitted, or to resolve to `null` when the app has no instrumentation hook.
9
+ *
10
+ * @param updater The content updater applying the patches.
11
+ * @param buildOpts The open-next build options.
12
+ * @param options.loadInstrumentation When `false`, the loaders always resolve to `null` so that the
13
+ * instrumentation hook is never registered by this bundle. Defaults to `true`.
14
+ * @returns An esbuild plugin.
15
+ */
16
+ export declare function patchInstrumentation(updater: ContentUpdater, buildOpts: BuildOptions, { loadInstrumentation }?: {
17
+ loadInstrumentation?: boolean;
18
+ }): Plugin;
4
19
  export declare function getNext154Rule(builtInstrumentationPath: string | null): string;
5
20
  export declare function getNext15Rule(builtInstrumentationPath: string | null): string;
6
21
  export declare function getNext14Rule(builtInstrumentationPath: string | null): string;
@@ -4,8 +4,21 @@ import { getPackagePath } from "@opennextjs/aws/build/helper.js";
4
4
  import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
5
5
  import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
6
6
  import { normalizePath } from "../../../utils/normalize-path.js";
7
- export function patchInstrumentation(updater, buildOpts) {
8
- const builtInstrumentationPath = getBuiltInstrumentationPath(buildOpts);
7
+ /**
8
+ * Replaces the dynamic loading of the instrumentation hook, which workerd does not support.
9
+ *
10
+ * Next.js loads `.next/server/instrumentation.js` with a `require()` call built from a path
11
+ * computed at runtime. The loaders are rewritten to either statically `require()` the file that
12
+ * the Next.js build emitted, or to resolve to `null` when the app has no instrumentation hook.
13
+ *
14
+ * @param updater The content updater applying the patches.
15
+ * @param buildOpts The open-next build options.
16
+ * @param options.loadInstrumentation When `false`, the loaders always resolve to `null` so that the
17
+ * instrumentation hook is never registered by this bundle. Defaults to `true`.
18
+ * @returns An esbuild plugin.
19
+ */
20
+ export function patchInstrumentation(updater, buildOpts, { loadInstrumentation = true } = {}) {
21
+ const builtInstrumentationPath = loadInstrumentation ? getBuiltInstrumentationPath(buildOpts) : null;
9
22
  updater.updateContent("patch-instrumentation-next15-4", [
10
23
  {
11
24
  filter: getCrossPlatformPathRegex(String.raw `/server/lib/router-utils/instrumentation-globals.external\.js$`, {
@@ -15,6 +15,63 @@ export declare const replaceLoadWebAssemblyModuleRule = "\nrule:\n kind: functi
15
15
  * the synchronous `WebAssembly.instantiate` to produce the instance's exports.
16
16
  */
17
17
  export declare const replaceLoadWebAssemblyRule = "\nrule:\n kind: function_declaration\n has:\n field: name\n regex: \"^loadWebAssembly$\"\nfix: |-\n async function loadWebAssembly(chunkPath, _edgeModule, imports) {\n const mod = await loadWasmChunk(chunkPath);\n const { exports } = await WebAssembly.instantiate(mod, imports);\n return exports;\n }\n";
18
+ /**
19
+ * Replace the `compileModule` helper that Next.js 16.3 emits in the chunks.
20
+ *
21
+ * Next.js 16.3 moved the wasm loading out of `[turbopack]_runtime.js`: it is now emitted on
22
+ * demand in the chunks as `[turbopack-wasm]/node/loadWasm.ts`, which reads the `.wasm` file from
23
+ * the filesystem and compiles it with `WebAssembly.compileStreaming`. Neither is supported by
24
+ * workerd so the helper is rewritten to use the generated `loadWasmChunk`.
25
+ *
26
+ * The emitted code is minified, so the rule matches on the shape of the helper rather than on its
27
+ * name: an async function taking the chunk path as its only parameter and returning the result of
28
+ * `WebAssembly.compileStreaming(...)`.
29
+ *
30
+ * See https://github.com/opennextjs/opennextjs-cloudflare/issues/1342
31
+ */
32
+ export declare const replaceCompileModuleRule = "\nrule:\n all:\n - pattern:\n context: async function $NAME($PATH) { $$$BODY }\n selector: function_declaration\n - has:\n field: body\n has:\n kind: return_statement\n has:\n pattern: WebAssembly.compileStreaming($$$)\n stopBy: end\nfix: |-\n async function $NAME($PATH) {\n return loadWasmChunk($PATH);\n }\n";
33
+ /**
34
+ * Replace the `instantiate` helper that Next.js 16.3 emits in the chunks.
35
+ *
36
+ * The counterpart of {@link replaceCompileModuleRule} for the helper instantiating the module:
37
+ * `WebAssembly.instantiateStreaming` is not supported by workerd either, so the chunk is loaded
38
+ * with the generated `loadWasmChunk` and instantiated with the synchronous `WebAssembly.instantiate`.
39
+ *
40
+ * The rule matches an async function taking the chunk path and the imports as parameters, and
41
+ * declaring a variable from the result of `WebAssembly.instantiateStreaming(...)`.
42
+ */
43
+ export declare const replaceInstantiateModuleRule = "\nrule:\n all:\n - pattern:\n context: async function $NAME($PATH, $IMPORTS) { $$$BODY }\n selector: function_declaration\n - has:\n field: body\n has:\n kind: lexical_declaration\n has:\n pattern: WebAssembly.instantiateStreaming($$$)\n stopBy: end\nfix: |-\n async function $NAME($PATH, $IMPORTS) {\n const module = await loadWasmChunk($PATH);\n const { exports } = await WebAssembly.instantiate(module, $IMPORTS);\n return exports;\n }\n";
44
+ /**
45
+ * Inline the dynamic chunk requires of the Turbopack runtime.
46
+ *
47
+ * The runtime resolves the chunks it needs at runtime, which workerd does not support.
48
+ * The chunks are inlined so that they are statically bundled.
49
+ *
50
+ * @param code The code of the Turbopack runtime.
51
+ * @param filePath Path to the Turbopack runtime, in the OpenNext output.
52
+ * @param tracedFiles The files traced by Next.js, copied next to the runtime.
53
+ * @returns The patched code.
54
+ */
55
+ export declare function patchTurbopackRuntimeCode({ code, filePath, tracedFiles, }: {
56
+ code: string;
57
+ filePath: string;
58
+ tracedFiles: string[];
59
+ }): string;
60
+ /**
61
+ * Replace the wasm helpers that Next.js 16.3 emits in the Turbopack chunks.
62
+ *
63
+ * Until Next.js 16.2 the wasm loaders lived in `[turbopack]_runtime.js` and were patched by
64
+ * {@link patchTurbopackRuntimeCode}. Next.js 16.3 emits them on demand in the chunks instead, so
65
+ * the chunks need to be patched too.
66
+ *
67
+ * @param code The code of the Turbopack chunk.
68
+ * @param tracedFiles The files traced by Next.js.
69
+ * @returns The patched code, or `code` unchanged when the chunk has no wasm helper.
70
+ */
71
+ export declare function patchTurbopackWasmChunkCode({ code, tracedFiles, }: {
72
+ code: string;
73
+ tracedFiles: string[];
74
+ }): string;
18
75
  export declare const patchTurbopackRuntime: CodePatcher;
19
76
  /**
20
77
  * Generate a `loadWasmChunk` function that maps a `.next`-relative chunk path to a
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
3
+ import { applyRule, parseCode, patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
4
4
  import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
5
5
  import { normalizePath } from "../../../utils/normalize-path.js";
6
6
  const inlineChunksRule = `
@@ -48,6 +48,68 @@ fix: |-
48
48
  return exports;
49
49
  }
50
50
  `;
51
+ /**
52
+ * Replace the `compileModule` helper that Next.js 16.3 emits in the chunks.
53
+ *
54
+ * Next.js 16.3 moved the wasm loading out of `[turbopack]_runtime.js`: it is now emitted on
55
+ * demand in the chunks as `[turbopack-wasm]/node/loadWasm.ts`, which reads the `.wasm` file from
56
+ * the filesystem and compiles it with `WebAssembly.compileStreaming`. Neither is supported by
57
+ * workerd so the helper is rewritten to use the generated `loadWasmChunk`.
58
+ *
59
+ * The emitted code is minified, so the rule matches on the shape of the helper rather than on its
60
+ * name: an async function taking the chunk path as its only parameter and returning the result of
61
+ * `WebAssembly.compileStreaming(...)`.
62
+ *
63
+ * See https://github.com/opennextjs/opennextjs-cloudflare/issues/1342
64
+ */
65
+ export const replaceCompileModuleRule = `
66
+ rule:
67
+ all:
68
+ - pattern:
69
+ context: async function $NAME($PATH) { $$$BODY }
70
+ selector: function_declaration
71
+ - has:
72
+ field: body
73
+ has:
74
+ kind: return_statement
75
+ has:
76
+ pattern: WebAssembly.compileStreaming($$$)
77
+ stopBy: end
78
+ fix: |-
79
+ async function $NAME($PATH) {
80
+ return loadWasmChunk($PATH);
81
+ }
82
+ `;
83
+ /**
84
+ * Replace the `instantiate` helper that Next.js 16.3 emits in the chunks.
85
+ *
86
+ * The counterpart of {@link replaceCompileModuleRule} for the helper instantiating the module:
87
+ * `WebAssembly.instantiateStreaming` is not supported by workerd either, so the chunk is loaded
88
+ * with the generated `loadWasmChunk` and instantiated with the synchronous `WebAssembly.instantiate`.
89
+ *
90
+ * The rule matches an async function taking the chunk path and the imports as parameters, and
91
+ * declaring a variable from the result of `WebAssembly.instantiateStreaming(...)`.
92
+ */
93
+ export const replaceInstantiateModuleRule = `
94
+ rule:
95
+ all:
96
+ - pattern:
97
+ context: async function $NAME($PATH, $IMPORTS) { $$$BODY }
98
+ selector: function_declaration
99
+ - has:
100
+ field: body
101
+ has:
102
+ kind: lexical_declaration
103
+ has:
104
+ pattern: WebAssembly.instantiateStreaming($$$)
105
+ stopBy: end
106
+ fix: |-
107
+ async function $NAME($PATH, $IMPORTS) {
108
+ const module = await loadWasmChunk($PATH);
109
+ const { exports } = await WebAssembly.instantiate(module, $IMPORTS);
110
+ return exports;
111
+ }
112
+ `;
51
113
  /**
52
114
  * Discover Turbopack external module mappings by reading symlinks in .next/node_modules/.
53
115
  *
@@ -221,6 +283,52 @@ function discoverExternalSubpaths(mappings, tracedFiles) {
221
283
  }
222
284
  return subpaths;
223
285
  }
286
+ /**
287
+ * Inline the dynamic chunk requires of the Turbopack runtime.
288
+ *
289
+ * The runtime resolves the chunks it needs at runtime, which workerd does not support.
290
+ * The chunks are inlined so that they are statically bundled.
291
+ *
292
+ * @param code The code of the Turbopack runtime.
293
+ * @param filePath Path to the Turbopack runtime, in the OpenNext output.
294
+ * @param tracedFiles The files traced by Next.js, copied next to the runtime.
295
+ * @returns The patched code.
296
+ */
297
+ export function patchTurbopackRuntimeCode({ code, filePath, tracedFiles, }) {
298
+ tracedFiles = tracedFiles.map(normalizePath);
299
+ filePath = normalizePath(filePath);
300
+ const mappings = discoverExternalModuleMappings(filePath);
301
+ const externalImportRule = buildExternalImportRule(mappings, tracedFiles, code);
302
+ let patched = patchCode(code, externalImportRule);
303
+ patched = patchCode(patched, inlineChunksRule);
304
+ patched = patchCode(patched, replaceLoadWebAssemblyModuleRule);
305
+ patched = patchCode(patched, replaceLoadWebAssemblyRule);
306
+ return `${patched}
307
+ ${inlineChunksFn(tracedFiles)}\n${loadWasmChunkFn(tracedFiles)}`;
308
+ }
309
+ /**
310
+ * Replace the wasm helpers that Next.js 16.3 emits in the Turbopack chunks.
311
+ *
312
+ * Until Next.js 16.2 the wasm loaders lived in `[turbopack]_runtime.js` and were patched by
313
+ * {@link patchTurbopackRuntimeCode}. Next.js 16.3 emits them on demand in the chunks instead, so
314
+ * the chunks need to be patched too.
315
+ *
316
+ * @param code The code of the Turbopack chunk.
317
+ * @param tracedFiles The files traced by Next.js.
318
+ * @returns The patched code, or `code` unchanged when the chunk has no wasm helper.
319
+ */
320
+ export function patchTurbopackWasmChunkCode({ code, tracedFiles, }) {
321
+ const root = parseCode(code);
322
+ // The 2 rules can not match the same function: they differ by the arity of the helper.
323
+ const edits = [
324
+ ...applyRule(replaceCompileModuleRule, root).edits,
325
+ ...applyRule(replaceInstantiateModuleRule, root).edits,
326
+ ];
327
+ if (edits.length === 0) {
328
+ return code;
329
+ }
330
+ return `${root.commitEdits(edits)}\n${loadWasmChunkFn(tracedFiles.map(normalizePath))}`;
331
+ }
224
332
  export const patchTurbopackRuntime = {
225
333
  name: "inline-turbopack-chunks",
226
334
  patches: [
@@ -230,18 +338,18 @@ export const patchTurbopackRuntime = {
230
338
  escape: false,
231
339
  }),
232
340
  contentFilter: /loadRuntimeChunkPath/,
233
- patchCode: async ({ code, tracedFiles, filePath }) => {
234
- tracedFiles = tracedFiles.map(normalizePath);
235
- filePath = normalizePath(filePath);
236
- const mappings = discoverExternalModuleMappings(filePath);
237
- const externalImportRule = buildExternalImportRule(mappings, tracedFiles, code);
238
- let patched = patchCode(code, externalImportRule);
239
- patched = patchCode(patched, inlineChunksRule);
240
- patched = patchCode(patched, replaceLoadWebAssemblyModuleRule);
241
- patched = patchCode(patched, replaceLoadWebAssemblyRule);
242
- return `${patched}
243
- ${inlineChunksFn(tracedFiles)}\n${loadWasmChunkFn(tracedFiles)}`;
244
- },
341
+ patchCode: async ({ code, tracedFiles, filePath }) => patchTurbopackRuntimeCode({ code, filePath, tracedFiles }),
342
+ },
343
+ {
344
+ // Next.js 16.3.0 moved the wasm loaders out of the Turbopack runtime and started
345
+ // emitting them on demand in the chunks that need them. Earlier versions are handled
346
+ // by the runtime patch above.
347
+ versions: ">=16.3.0",
348
+ pathFilter: getCrossPlatformPathRegex(String.raw `/\.next/server/chunks/.+\.js$`, {
349
+ escape: false,
350
+ }),
351
+ contentFilter: /WebAssembly\.(compileStreaming|instantiateStreaming)/,
352
+ patchCode: async ({ code, tracedFiles }) => patchTurbopackWasmChunkCode({ code, tracedFiles }),
245
353
  },
246
354
  ],
247
355
  };
@@ -34,7 +34,7 @@ export function findWranglerConfig(appDir) {
34
34
  * @returns An object containing a `cachingEnabled` which indicates whether caching has been set up during the wrangler
35
35
  * config file creation or not
36
36
  */
37
- export async function createWranglerConfigFile(projectDir, defaultCompatDate = "2026-04-15") {
37
+ export async function createWranglerConfigFile(projectDir, defaultCompatDate = "2026-08-01") {
38
38
  const workerName = getWorkerName(projectDir);
39
39
  const compatibilityDate = (await getLatestCompatDate()) ?? defaultCompatDate;
40
40
  const wranglerConfigStr = readFileSync(join(getPackageTemplatesDirPath(), "wrangler.jsonc"), "utf8")
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@opennextjs/cloudflare",
3
3
  "description": "Cloudflare builder for next apps",
4
- "version": "1.20.2",
4
+ "version": "1.20.3",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "opennextjs-cloudflare": "dist/cli/index.js"
@@ -44,7 +44,7 @@
44
44
  "dependencies": {
45
45
  "@ast-grep/napi": "^0.40.5",
46
46
  "@dotenvx/dotenvx": "1.31.0",
47
- "@opennextjs/aws": "4.1.0",
47
+ "@opennextjs/aws": "4.1.1",
48
48
  "ci-info": "^4.2.0",
49
49
  "cloudflare": "^4.4.1",
50
50
  "comment-json": "^4.5.1",
@@ -70,7 +70,7 @@
70
70
  "eslint-plugin-unicorn": "^55.0.0",
71
71
  "globals": "^15.9.0",
72
72
  "mock-fs": "^5.4.1",
73
- "next": "^15.5.21",
73
+ "next": "^15.5.24",
74
74
  "picomatch": "^4.0.2",
75
75
  "rclone.js": "^0.6.6",
76
76
  "rimraf": "^6.0.1",
@@ -79,9 +79,9 @@
79
79
  "vitest": "^4.1.4"
80
80
  },
81
81
  "peerDependencies": {
82
- "next": ">=15.5.21 <16 || >=16.2.11",
82
+ "next": ">=15.5.24 <16 || >=16.3.3",
83
83
  "rclone.js": "^0.6.6",
84
- "wrangler": "^4.86.0"
84
+ "wrangler": "^4.125.0"
85
85
  },
86
86
  "peerDependenciesMeta": {
87
87
  "rclone.js": {