@opennextjs/cloudflare 0.6.4 → 0.6.6

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.
@@ -1,3 +1,4 @@
1
+ import type { GetPlatformProxyOptions } from "wrangler";
1
2
  import type { DurableObjectQueueHandler } from "./durable-objects/queue";
2
3
  import { DOShardedTagCache } from "./durable-objects/sharded-tag-cache";
3
4
  declare global {
@@ -49,5 +50,6 @@ export declare function getCloudflareContext<CfProperties extends Record<string,
49
50
  * with the open-next Cloudflare adapter
50
51
  *
51
52
  * Note: this function should only be called inside the Next.js config file, and although async it doesn't need to be `await`ed
53
+ * @param options options on how the function should operate and if/where to persist the platform data
52
54
  */
53
- export declare function initOpenNextCloudflareForDev(): Promise<void>;
55
+ export declare function initOpenNextCloudflareForDev(options?: GetPlatformProxyOptions): Promise<void>;
@@ -71,12 +71,17 @@ async function getCloudflareContextAsync() {
71
71
  * with the open-next Cloudflare adapter
72
72
  *
73
73
  * Note: this function should only be called inside the Next.js config file, and although async it doesn't need to be `await`ed
74
+ * @param options options on how the function should operate and if/where to persist the platform data
74
75
  */
75
- export async function initOpenNextCloudflareForDev() {
76
+ export async function initOpenNextCloudflareForDev(options) {
76
77
  const shouldInitializationRun = shouldContextInitializationRun();
77
78
  if (!shouldInitializationRun)
78
79
  return;
79
- const context = await getCloudflareContextFromWrangler();
80
+ if (options?.environment && process.env.NEXT_DEV_WRANGLER_ENV) {
81
+ console.warn(`'initOpenNextCloudflareForDev' has been called with an environment option while NEXT_DEV_WRANGLER_ENV is set.` +
82
+ ` NEXT_DEV_WRANGLER_ENV will be ignored and the environment will be set to: '${options.environment}'`);
83
+ }
84
+ const context = await getCloudflareContextFromWrangler(options);
80
85
  addCloudflareContextToNodejsGlobal(context);
81
86
  await monkeyPatchVmModuleEdgeContext(context);
82
87
  }
@@ -131,12 +136,14 @@ async function monkeyPatchVmModuleEdgeContext(cloudflareContext) {
131
136
  *
132
137
  * @returns the cloudflare context ready for use
133
138
  */
134
- async function getCloudflareContextFromWrangler() {
139
+ async function getCloudflareContextFromWrangler(options) {
135
140
  // Note: we never want wrangler to be bundled in the Next.js app, that's why the import below looks like it does
136
141
  const { getPlatformProxy } = await import(/* webpackIgnore: true */ `${"__wrangler".replaceAll("_", "")}`);
142
+ // This allows the selection of a wrangler environment while running in next dev mode
143
+ const environment = options?.environment ?? process.env.NEXT_DEV_WRANGLER_ENV;
137
144
  const { env, cf, ctx } = await getPlatformProxy({
138
- // This allows the selection of a wrangler environment while running in next dev mode
139
- environment: process.env.NEXT_DEV_WRANGLER_ENV,
145
+ ...options,
146
+ environment,
140
147
  });
141
148
  return {
142
149
  env,
@@ -221,13 +221,11 @@ export class DurableObjectQueueHandler extends DurableObject {
221
221
  try {
222
222
  if (this.disableSQLite)
223
223
  return false;
224
- const numNewer = this.sql
225
- .exec("SELECT COUNT(*) as numNewer FROM sync WHERE id = ? AND lastSuccess > ? LIMIT 1", `${msg.MessageBody.host}${msg.MessageBody.url}`, Math.round(msg.MessageBody.lastModified / 1000))
226
- .one().numNewer;
227
- return numNewer > 0;
228
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
224
+ return (this.sql
225
+ .exec("SELECT 1 FROM sync WHERE id = ? AND lastSuccess > ? LIMIT 1", `${msg.MessageBody.host}${msg.MessageBody.url}`, Math.round(msg.MessageBody.lastModified / 1000))
226
+ .toArray().length > 0);
229
227
  }
230
- catch (e) {
228
+ catch {
231
229
  return false;
232
230
  }
233
231
  }
@@ -9,10 +9,9 @@ export class DOShardedTagCache extends DurableObject {
9
9
  });
10
10
  }
11
11
  async hasBeenRevalidated(tags, lastModified) {
12
- const result = this.sql
13
- .exec(`SELECT COUNT(*) as cnt FROM revalidations WHERE tag IN (${tags.map(() => "?").join(", ")}) AND revalidatedAt > ?`, ...tags, lastModified ?? Date.now())
14
- .one();
15
- return result.cnt > 0;
12
+ return (this.sql
13
+ .exec(`SELECT 1 FROM revalidations WHERE tag IN (${tags.map(() => "?").join(", ")}) AND revalidatedAt > ? LIMIT 1`, ...tags, lastModified ?? Date.now())
14
+ .toArray().length > 0);
16
15
  }
17
16
  async writeTags(tags, lastModified) {
18
17
  tags.forEach((tag) => {
@@ -10,12 +10,10 @@ export class D1NextModeTagCache {
10
10
  return false;
11
11
  try {
12
12
  const result = await db
13
- .prepare(`SELECT COUNT(*) as cnt FROM revalidations WHERE tag IN (${tags.map(() => "?").join(", ")}) AND revalidatedAt > ? LIMIT 1`)
13
+ .prepare(`SELECT 1 FROM revalidations WHERE tag IN (${tags.map(() => "?").join(", ")}) AND revalidatedAt > ? LIMIT 1`)
14
14
  .bind(...tags.map((tag) => this.getCacheKey(tag)), lastModified ?? Date.now())
15
- .first();
16
- if (!result)
17
- throw new RecoverableError(`D1 select failed for ${tags}`);
18
- return result.cnt > 0;
15
+ .raw();
16
+ return result.length > 0;
19
17
  }
20
18
  catch (e) {
21
19
  error(e);
@@ -87,7 +87,7 @@ export async function bundleServer(buildOpts) {
87
87
  // Apply updater updates, must be the last plugin
88
88
  updater.plugin,
89
89
  ],
90
- external: ["./middleware/handler.mjs"],
90
+ external: ["./middleware/handler.mjs", "*.wasm"],
91
91
  alias: {
92
92
  // Note: it looks like node-fetch is actually not necessary for us, so we could replace it with an empty shim
93
93
  // but just to be safe we replace it with a module that re-exports the native fetch
@@ -2,10 +2,11 @@
2
2
  // Adapted for cloudflare workers
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
+ import { loadMiddlewareManifest } from "@opennextjs/aws/adapters/config/util.js";
5
6
  import { bundleNextServer } from "@opennextjs/aws/build/bundleNextServer.js";
6
7
  import { compileCache } from "@opennextjs/aws/build/compileCache.js";
7
8
  import { copyTracedFiles } from "@opennextjs/aws/build/copyTracedFiles.js";
8
- import { generateEdgeBundle } from "@opennextjs/aws/build/edge/createEdgeBundle.js";
9
+ import { copyMiddlewareResources, generateEdgeBundle } from "@opennextjs/aws/build/edge/createEdgeBundle.js";
9
10
  import * as buildHelper from "@opennextjs/aws/build/helper.js";
10
11
  import { installDependencies } from "@opennextjs/aws/build/installDeps.js";
11
12
  import { applyCodePatches } from "@opennextjs/aws/build/patch/codePatcher.js";
@@ -86,25 +87,28 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
86
87
  // `.next/standalone/package/path` (ie. `.next`, `server.js`).
87
88
  // We need to output the handler file inside the package path.
88
89
  const packagePath = buildHelper.getPackagePath(options);
89
- fs.mkdirSync(path.join(outputPath, packagePath), { recursive: true });
90
+ const outPackagePath = path.join(outputPath, packagePath);
91
+ fs.mkdirSync(outPackagePath, { recursive: true });
90
92
  const ext = fnOptions.runtime === "deno" ? "mjs" : "cjs";
91
- fs.copyFileSync(path.join(options.buildDir, `cache.${ext}`), path.join(outputPath, packagePath, "cache.cjs"));
93
+ fs.copyFileSync(path.join(options.buildDir, `cache.${ext}`), path.join(outPackagePath, "cache.cjs"));
92
94
  if (fnOptions.runtime === "deno") {
93
95
  addDenoJson(outputPath, packagePath);
94
96
  }
95
97
  // Bundle next server if necessary
96
98
  const isBundled = fnOptions.experimentalBundledNextServer ?? false;
97
99
  if (isBundled) {
98
- await bundleNextServer(path.join(outputPath, packagePath), appPath, {
100
+ await bundleNextServer(outPackagePath, appPath, {
99
101
  minify: options.minify,
100
102
  });
101
103
  }
102
104
  // Copy middleware
103
105
  if (!config.middleware?.external) {
104
- fs.copyFileSync(path.join(options.buildDir, "middleware.mjs"), path.join(outputPath, packagePath, "middleware.mjs"));
106
+ fs.copyFileSync(path.join(options.buildDir, "middleware.mjs"), path.join(outPackagePath, "middleware.mjs"));
107
+ const middlewareManifest = loadMiddlewareManifest(path.join(options.appBuildOutputPath, ".next"));
108
+ copyMiddlewareResources(options, middlewareManifest.middleware["/"], outPackagePath);
105
109
  }
106
110
  // Copy open-next.config.mjs
107
- buildHelper.copyOpenNextConfig(options.buildDir, path.join(outputPath, packagePath), true);
111
+ buildHelper.copyOpenNextConfig(options.buildDir, outPackagePath, true);
108
112
  // Copy env files
109
113
  buildHelper.copyEnvFile(appBuildOutputPath, packagePath, outputPath);
110
114
  // Copy all necessary traced files
@@ -164,7 +168,7 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
164
168
  : []),
165
169
  openNextEdgePlugins({
166
170
  nextDir: path.join(options.appBuildOutputPath, ".next"),
167
- isInCloudfare: true,
171
+ isInCloudflare: true,
168
172
  }),
169
173
  ];
170
174
  const outfileExt = fnOptions.runtime === "deno" ? "ts" : "mjs";
@@ -228,7 +232,7 @@ function addMonorepoEntrypoint(outputPath, packagePath) {
228
232
  // the Lambda function to be able to find the handler at
229
233
  // the root of the bundle. We will create a dummy `index.mjs`
230
234
  // that re-exports the real handler.
231
- fs.writeFileSync(path.join(outputPath, "index.mjs"), `export * from "./${normalizePath(packagePath)}/index.mjs";`);
235
+ fs.writeFileSync(path.join(outputPath, "index.mjs"), `export { handler } from "./${normalizePath(packagePath)}/index.mjs";`);
232
236
  }
233
237
  async function minifyServerBundle(outputDir) {
234
238
  logger.info("Minimizing server function...");
@@ -82,7 +82,14 @@ rule:
82
82
  regex: ^NodeModuleLoader$
83
83
  fix: |
84
84
  async load($ID) {
85
- ${getRequires("$ID", files, serverDir)}
85
+ ${buildOpts.debug
86
+ ? ` try {
87
+ ${getRequires("$ID", files, serverDir)}
88
+ } catch (e) {
89
+ console.error('Exception in NodeModuleLoader', e);
90
+ throw e;
91
+ }`
92
+ : getRequires("$ID", files, serverDir)}
86
93
  }`;
87
94
  }
88
95
  async function getRequirePageRule(buildOpts) {
@@ -112,7 +119,14 @@ function requirePage($PAGE, $DIST_DIR, $IS_APP_PATH) {
112
119
  process.env.__NEXT_PRIVATE_RUNTIME_TYPE = $IS_APP_PATH ? 'app' : 'pages';
113
120
  try {
114
121
  ${getRequires("pagePath", jsFiles, serverDir)}
115
- } finally {
122
+ } ${buildOpts.debug
123
+ ? `
124
+ catch (e) {
125
+ console.error("Exception in requirePage", e);
126
+ throw e;
127
+ }`
128
+ : ``}
129
+ finally {
116
130
  process.env.__NEXT_PRIVATE_RUNTIME_TYPE = '';
117
131
  }
118
132
  }`,
@@ -1,8 +1,15 @@
1
1
  import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
2
+ // Remove an instantiation of `AbortController` from the runtime.
3
+ //
4
+ // Solves https://github.com/cloudflare/workerd/issues/3657:
5
+ // - The `AbortController` is meant for the client side, but ends in the server code somehow.
6
+ // That's why we can get ride of it. See https://github.com/vercel/next.js/pull/73975/files.
7
+ // - Top level instantiation of `AbortController` are not supported by workerd as of March, 2025.
8
+ // See https://github.com/cloudflare/workerd/issues/3657
9
+ // - As Next code is not more executed at top level, we do not need to apply this patch
10
+ // See https://github.com/opennextjs/opennextjs-cloudflare/pull/497
11
+ //
2
12
  // We try to be as specific as possible to avoid patching the wrong thing here
3
- // It seems that there is a bug in the worker runtime. When the AbortController is created outside of the request context it throws an error (not sure if it's expected or not) except in this case. https://github.com/cloudflare/workerd/issues/3657
4
- // It fails while requiring the `app-page.runtime.prod.js` file, but instead of throwing an error, it just return an empty object for the `require('app-page.runtime.prod.js')` call which makes every request to an app router page fail.
5
- // If it's a bug in workerd and it's not expected to throw an error, we can remove this patch.
6
13
  export const abortControllerRule = `
7
14
  rule:
8
15
  all:
@@ -65,18 +72,7 @@ fix:
65
72
  'true'
66
73
  `;
67
74
  export function patchNextMinimal(updater) {
68
- updater.updateContent("patch-abortController-next15.2", [
69
- {
70
- field: {
71
- filter: /app-page(-experimental)?\.runtime\.prod\.js$/,
72
- contentFilter: /new AbortController/,
73
- callback: ({ contents }) => {
74
- return patchCode(contents, abortControllerRule);
75
- },
76
- },
77
- },
78
- ]);
79
- updater.updateContent("patch-next-minimal", [
75
+ return updater.updateContent("patch-next-minimal", [
80
76
  {
81
77
  field: {
82
78
  filter: /next-server\.(js)$/,
@@ -87,8 +83,4 @@ export function patchNextMinimal(updater) {
87
83
  },
88
84
  },
89
85
  ]);
90
- return {
91
- name: "patch-abortController",
92
- setup() { },
93
- };
94
86
  }
@@ -1,4 +1,5 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
+ import process from "node:process";
2
3
  // @ts-expect-error: resolved by wrangler build
3
4
  import * as nextEnvVars from "./env/next-env.mjs";
4
5
  const cloudflareContextALS = new AsyncLocalStorage();
@@ -54,6 +55,11 @@ function populateProcessEnv(url, env) {
54
55
  if (processEnvPopulated) {
55
56
  return;
56
57
  }
58
+ // Some packages rely on `process.version` and `process.versions.node` (i.e. Jose@4)
59
+ // TODO: Remove when https://github.com/unjs/unenv/pull/493 is merged
60
+ Object.assign(process, { version: process.version || "v22.14.0" });
61
+ // @ts-expect-error Node type does not match workerd
62
+ Object.assign(process.versions, { node: "22.14.0", ...process.versions });
57
63
  processEnvPopulated = true;
58
64
  for (const [key, value] of Object.entries(env)) {
59
65
  if (typeof value === "string") {
@@ -63,7 +69,7 @@ function populateProcessEnv(url, env) {
63
69
  const mode = env.NEXTJS_ENV ?? "production";
64
70
  if (nextEnvVars[mode]) {
65
71
  for (const key in nextEnvVars[mode]) {
66
- process.env[key] = nextEnvVars[mode][key];
72
+ process.env[key] ??= nextEnvVars[mode][key];
67
73
  }
68
74
  }
69
75
  // Set the default Origin for the origin resolver.
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": "0.6.4",
4
+ "version": "0.6.6",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "opennextjs-cloudflare": "dist/cli/index.js"
@@ -43,7 +43,7 @@
43
43
  "homepage": "https://github.com/opennextjs/opennextjs-cloudflare",
44
44
  "dependencies": {
45
45
  "@dotenvx/dotenvx": "1.31.0",
46
- "@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@798",
46
+ "@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@802",
47
47
  "enquirer": "^2.4.1",
48
48
  "glob": "^11.0.0"
49
49
  },
@@ -67,7 +67,7 @@
67
67
  "vitest": "^2.1.1"
68
68
  },
69
69
  "peerDependencies": {
70
- "wrangler": "^3.114.1 || ^4.0.0"
70
+ "wrangler": "^3.114.1 || ^4.6.0"
71
71
  },
72
72
  "scripts": {
73
73
  "clean": "rimraf dist",