@opennextjs/cloudflare 1.19.3 → 1.19.5

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.
@@ -21,10 +21,11 @@ export class DOQueueHandler extends DurableObject {
21
21
  disableSQLite;
22
22
  constructor(ctx, env) {
23
23
  super(ctx, env);
24
- this.service = env.WORKER_SELF_REFERENCE;
25
24
  // If there is no service binding, we throw an error because we can't revalidate without it
26
- if (!this.service)
25
+ if (!env.WORKER_SELF_REFERENCE) {
27
26
  throw new IgnorableError("No service binding for cache revalidation worker");
27
+ }
28
+ this.service = env.WORKER_SELF_REFERENCE;
28
29
  this.sql = ctx.storage.sql;
29
30
  this.maxRevalidations = env.NEXT_CACHE_DO_QUEUE_MAX_REVALIDATION
30
31
  ? parseInt(env.NEXT_CACHE_DO_QUEUE_MAX_REVALIDATION)
@@ -121,7 +122,7 @@ export class DOQueueHandler extends DurableObject {
121
122
  if (!this.disableSQLite) {
122
123
  this.sql.exec("INSERT OR REPLACE INTO sync (id, lastSuccess, buildId) VALUES (?, unixepoch(), ?)",
123
124
  // We cannot use the deduplication id because it's not unique per route - every time a route is revalidated, the deduplication id is different.
124
- `${host}${url}`, process.env.__NEXT_BUILD_ID);
125
+ `${host}${url}`, process.env.__OPEN_NEXT_BUILD_ID);
125
126
  }
126
127
  // If everything went well, we can remove the route from the failed state
127
128
  this.routeInFailedState.delete(msg.MessageDeduplicationId);
@@ -184,7 +185,7 @@ export class DOQueueHandler extends DurableObject {
184
185
  }
185
186
  this.routeInFailedState.set(msg.MessageDeduplicationId, updatedFailedState);
186
187
  if (!this.disableSQLite) {
187
- this.sql.exec("INSERT OR REPLACE INTO failed_state (id, data, buildId) VALUES (?, ?, ?)", msg.MessageDeduplicationId, JSON.stringify(updatedFailedState), process.env.__NEXT_BUILD_ID);
188
+ this.sql.exec("INSERT OR REPLACE INTO failed_state (id, data, buildId) VALUES (?, ?, ?)", msg.MessageDeduplicationId, JSON.stringify(updatedFailedState), process.env.__OPEN_NEXT_BUILD_ID);
188
189
  }
189
190
  // We probably want to do something if routeInFailedState is becoming too big, at least log it
190
191
  await this.addAlarm();
@@ -214,8 +215,8 @@ export class DOQueueHandler extends DurableObject {
214
215
  this.sql.exec("CREATE TABLE IF NOT EXISTS sync (id TEXT PRIMARY KEY, lastSuccess INTEGER, buildId TEXT)");
215
216
  // Before doing anything else, we clear the DB for any potential old data
216
217
  // TODO: extract this to a function so that it could be called by the user at another time than init
217
- this.sql.exec("DELETE FROM failed_state WHERE buildId != ?", process.env.__NEXT_BUILD_ID);
218
- this.sql.exec("DELETE FROM sync WHERE buildId != ?", process.env.__NEXT_BUILD_ID);
218
+ this.sql.exec("DELETE FROM failed_state WHERE buildId != ?", process.env.__OPEN_NEXT_BUILD_ID);
219
+ this.sql.exec("DELETE FROM sync WHERE buildId != ?", process.env.__OPEN_NEXT_BUILD_ID);
219
220
  const failedStateCursor = this.sql.exec("SELECT * FROM failed_state");
220
221
  for (const row of failedStateCursor) {
221
222
  this.routeInFailedState.set(row.id, JSON.parse(row.data));
@@ -76,7 +76,7 @@ class KVIncrementalCache {
76
76
  getKVKey(key, cacheType) {
77
77
  return computeCacheKey(key, {
78
78
  prefix: getCloudflareContext().env[PREFIX_ENV_NAME],
79
- buildId: process.env.NEXT_BUILD_ID,
79
+ buildId: process.env.OPEN_NEXT_BUILD_ID,
80
80
  cacheType,
81
81
  });
82
82
  }
@@ -60,7 +60,7 @@ class R2IncrementalCache {
60
60
  getR2Key(key, cacheType) {
61
61
  return computeCacheKey(key, {
62
62
  prefix: getCloudflareContext().env[PREFIX_ENV_NAME],
63
- buildId: process.env.NEXT_BUILD_ID,
63
+ buildId: process.env.OPEN_NEXT_BUILD_ID,
64
64
  cacheType,
65
65
  });
66
66
  }
@@ -17,20 +17,32 @@ type Options = {
17
17
  */
18
18
  defaultLongLivedTtlSec?: number;
19
19
  /**
20
- * Whether the regional cache entry should be updated in the background or not when it experiences
21
- * a cache hit.
20
+ * Whether the regional cache entry should be updated in the background on regional cache hits.
22
21
  *
23
- * @default `true` in `long-lived` mode when cache purge is not used, `false` otherwise.
22
+ * NOTE: Use the default value unless you know what you are doing. It is set to:
23
+ * - Next < 16:
24
+ * `true` in `long-lived` mode when cache purge is not used, `false` otherwise.
25
+ * - Next >= 16:
26
+ * `!bypassTagCacheOnCacheHit`
24
27
  */
25
28
  shouldLazilyUpdateOnCacheHit?: boolean;
26
29
  /**
27
- * Whether on cache hits the tagCache should be skipped or not. Skipping the tagCache allows requests to be
28
- * handled faster,
30
+ * Whether the tagCache should be skipped on regional cache hits.
29
31
  *
30
- * Note: When this is enabled, make sure that the cache gets purged
31
- * either by enabling the auto cache purging feature or manually.
32
+ * Note:
33
+ * - Skipping the tagCache allows requests to be handled faster
34
+ * - When `true`, make sure the cache gets purged
35
+ * either by enabling the auto cache purging feature or manually
32
36
  *
33
- * @default `true` if the auto cache purging is enabled, `false` otherwise.
37
+ * `true` is not compatible with SWR types of revalidateTag
38
+ * i.e. on Next 16+, anything different than `revalidateTag("tag", { expire: 0 })`.
39
+ * That's why the default is `false` for Next 16+ which uses SWR by default.
40
+ *
41
+ * NOTE: Use the default value unless you know what you are doing. It is set to:
42
+ * - Next <16:
43
+ * `true` if the auto cache purging is enabled, `false` otherwise.
44
+ * - Next >= 16:
45
+ * `false`
34
46
  */
35
47
  bypassTagCacheOnCacheHit?: boolean;
36
48
  };
@@ -52,7 +64,6 @@ interface PutToCacheInput {
52
64
  * API is refreshed from the cache store on cache hits (for the long-lived mode).
53
65
  */
54
66
  declare class RegionalCache implements IncrementalCache {
55
- #private;
56
67
  private store;
57
68
  private opts;
58
69
  name: string;
@@ -1,4 +1,5 @@
1
1
  import { error } from "@opennextjs/aws/adapters/logger.js";
2
+ import { compareSemver } from "@opennextjs/aws/utils/semver.js";
2
3
  import { getCloudflareContext } from "../../cloudflare-context.js";
3
4
  import { debugCache, FALLBACK_BUILD_ID, isPurgeCacheEnabled } from "../internal.js";
4
5
  const ONE_MINUTE_IN_SECONDS = 60;
@@ -24,15 +25,27 @@ class RegionalCache {
24
25
  this.store = store;
25
26
  this.opts = opts;
26
27
  this.name = this.store.name;
27
- // `shouldLazilyUpdateOnCacheHit` is not needed when cache purge is enabled.
28
- this.opts.shouldLazilyUpdateOnCacheHit ??= this.opts.mode === "long-lived" && !isPurgeCacheEnabled();
29
- }
30
- get #bypassTagCacheOnCacheHit() {
31
- if (this.opts.bypassTagCacheOnCacheHit !== undefined) {
32
- return this.opts.bypassTagCacheOnCacheHit;
28
+ // `globalThis.nextVersion` is only defined at runtime but not when the Open Next build runs.
29
+ // The options do no matter at build time so we can skip setting them.
30
+ const { nextVersion } = globalThis;
31
+ if (nextVersion) {
32
+ if (compareSemver(nextVersion, "<", "16")) {
33
+ // Next < 16
34
+ this.opts.shouldLazilyUpdateOnCacheHit ??= this.opts.mode === "long-lived" && !isPurgeCacheEnabled();
35
+ this.opts.bypassTagCacheOnCacheHit ??= isPurgeCacheEnabled();
36
+ }
37
+ else {
38
+ // Next >= 16
39
+ this.opts.bypassTagCacheOnCacheHit ??= false;
40
+ if (this.opts.bypassTagCacheOnCacheHit) {
41
+ debugCache("RegionalCache", `bypassTagCacheOnCacheHit is not recommended for Next 16+ as it is not compatible with SWR tags. Make sure to always use \`revalidateTag\` with \`{ expire: 0 }\` if you want to bypass the tag cache.`);
42
+ }
43
+ this.opts.shouldLazilyUpdateOnCacheHit ??= !this.opts.bypassTagCacheOnCacheHit;
44
+ if (this.opts.shouldLazilyUpdateOnCacheHit !== this.opts.bypassTagCacheOnCacheHit) {
45
+ debugCache("RegionalCache", `\`shouldLazilyUpdateOnCacheHit\` and \`bypassTagCacheOnCacheHit\` are mutually exclusive for Next 16+.`);
46
+ }
47
+ }
33
48
  }
34
- // When `bypassTagCacheOnCacheHit` is not set, we default to whether the automatic cache purging is enabled or not
35
- return isPurgeCacheEnabled();
36
49
  }
37
50
  async get(key, cacheType) {
38
51
  try {
@@ -55,7 +68,7 @@ class RegionalCache {
55
68
  const responseJson = await cachedResponse.json();
56
69
  return {
57
70
  ...responseJson,
58
- shouldBypassTagCache: this.#bypassTagCacheOnCacheHit,
71
+ shouldBypassTagCache: this.opts.bypassTagCacheOnCacheHit,
59
72
  };
60
73
  }
61
74
  const rawEntry = await this.store.get(key, cacheType);
@@ -109,7 +122,7 @@ class RegionalCache {
109
122
  return this.localCache;
110
123
  }
111
124
  getCacheUrlKey(key, cacheType) {
112
- const buildId = process.env.NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
125
+ const buildId = process.env.OPEN_NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
113
126
  return "http://cache.local" + `/${buildId}/${key}`.replace(/\/+/g, "/") + `.${cacheType ?? "cache"}`;
114
127
  }
115
128
  async putToCache({ key, cacheType, entry }) {
@@ -43,7 +43,7 @@ class StaticAssetsIncrementalCache {
43
43
  if (cacheType === "composable") {
44
44
  throw new Error("Composable cache is not supported in static assets incremental cache");
45
45
  }
46
- const buildId = process.env.NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
46
+ const buildId = process.env.OPEN_NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
47
47
  const name = (cacheType === "fetch"
48
48
  ? `${CACHE_DIR}/__fetch/${buildId}/${key}`
49
49
  : `${CACHE_DIR}/${buildId}/${key}.cache`).replace(/\/+/g, "/");
@@ -80,8 +80,13 @@ export class D1NextModeTagCache {
80
80
  const isStale = [...result.values()].some((v) => {
81
81
  if (v == null)
82
82
  return false;
83
- const { stale, expire } = v;
84
- if (stale == null || stale <= (lastModified ?? now))
83
+ const { revalidatedAt, stale, expire } = v;
84
+ // A tag is stale when both its stale and revalidatedAt timestamps are newer than the page.
85
+ // revalidatedAt > lastModified ensures the revalidation that set this stale window happened
86
+ // after the page was generated, preventing a stale signal from a previous ISR cycle.
87
+ const lastModifiedOrNow = lastModified ?? now;
88
+ const isInStaleWindow = stale != null && revalidatedAt > lastModifiedOrNow && lastModifiedOrNow <= stale;
89
+ if (!isInStaleWindow)
85
90
  return false;
86
91
  return expire == null || expire > now;
87
92
  });
@@ -153,7 +158,7 @@ export class D1NextModeTagCache {
153
158
  return `${this.getBuildId()}/${key}`.replaceAll("//", "/");
154
159
  }
155
160
  getBuildId() {
156
- return process.env.NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
161
+ return process.env.OPEN_NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
157
162
  }
158
163
  /**
159
164
  * @returns request scoped in-memory cache for tag values, or undefined if ALS is not available.
@@ -89,8 +89,13 @@ class ShardedDOTagCache {
89
89
  const result = [...tagData.values()].some((data) => {
90
90
  if (data == null)
91
91
  return false;
92
- const { stale, expire } = data;
93
- if (stale == null || stale <= (lastModified ?? now))
92
+ const { revalidatedAt, stale, expire } = data;
93
+ // A tag is stale when both its stale timestamp and its revalidatedAt are newer than the page.
94
+ // revalidatedAt > lastModified ensures the revalidation that set this stale window happened
95
+ // after the page was generated, preventing a stale signal from a previous ISR cycle.
96
+ const lastModifiedOrNow = lastModified ?? now;
97
+ const isInStaleWindow = stale != null && revalidatedAt > lastModifiedOrNow && lastModifiedOrNow <= stale;
98
+ if (!isInStaleWindow)
94
99
  return false;
95
100
  return expire == null || expire > now;
96
101
  });
@@ -144,9 +144,14 @@ export class KVNextModeTagCache {
144
144
  if (v == null)
145
145
  return false;
146
146
  const stale = getStale(v);
147
- if (stale == null || stale <= (lastModified ?? now))
148
- return false;
149
147
  const expire = getExpire(v);
148
+ // A tag is stale when both its stale timestamp and its revalidatedAt are newer than the page.
149
+ // revalidatedAt > lastModified ensures the revalidation that set this stale window happened
150
+ // after the page was generated, preventing a stale signal from a previous ISR cycle.
151
+ const lastModifiedOrNow = lastModified ?? now;
152
+ const isInStaleWindow = stale != null && getRevalidatedAt(v) > lastModifiedOrNow && lastModifiedOrNow <= stale;
153
+ if (!isInStaleWindow)
154
+ return false;
150
155
  return expire == null || expire > now;
151
156
  });
152
157
  debugCache("KVNextModeTagCache", `isStale tags=${tags} lastModified=${lastModified} -> ${isStale}`);
@@ -175,7 +180,7 @@ export class KVNextModeTagCache {
175
180
  return `${this.getBuildId()}/${key}`.replaceAll("//", "/");
176
181
  }
177
182
  getBuildId() {
178
- return process.env.NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
183
+ return process.env.OPEN_NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
179
184
  }
180
185
  /**
181
186
  * @returns request scoped in-memory cache for tag values, or undefined if ALS is not available.
@@ -10,7 +10,7 @@ interface WithFilterOptions {
10
10
  * @param tag The tag to filter.
11
11
  * @returns true if the tag should be forwarded, false otherwise.
12
12
  */
13
- filterFn: (tag: string | NextModeTagCacheWriteInput) => boolean;
13
+ filterFn: (tag: NextModeTagCacheWriteInput) => boolean;
14
14
  }
15
15
  /**
16
16
  * Creates a new tag cache that filters tags based on the provided filter function.
@@ -22,5 +22,5 @@ export declare function withFilter({ tagCache, filterFn }: WithFilterOptions): N
22
22
  * This is used to filter out internal soft tags.
23
23
  * Can be used if `revalidatePath` is not used.
24
24
  */
25
- export declare function softTagFilter(tag: string | NextModeTagCacheWriteInput): boolean;
25
+ export declare function softTagFilter(tag: NextModeTagCacheWriteInput): boolean;
26
26
  export {};
@@ -38,6 +38,19 @@ export async function build(options, config, projectOpts, wranglerConfig, allowU
38
38
  const { aws, cloudflare } = getVersion();
39
39
  logger.info(`@opennextjs/cloudflare version: ${cloudflare}`);
40
40
  logger.info(`@opennextjs/aws version: ${aws}`);
41
+ if (wranglerConfig.compatibility_date) {
42
+ const sixMonthsAgoMs = Date.now() - 6 * 30 * 24 * 60 * 60 * 1000;
43
+ const compatDateMs = new Date(wranglerConfig.compatibility_date).getTime();
44
+ if (!isNaN(compatDateMs)) {
45
+ const dateMessage = `workerd compatibility_date: ${wranglerConfig.compatibility_date}`;
46
+ if (compatDateMs < sixMonthsAgoMs) {
47
+ logger.warn(`${dateMessage}, consider updating your wrangler config to a more recent date to benefit from the latest features and fixes.`);
48
+ }
49
+ else {
50
+ logger.info(`${dateMessage}`);
51
+ }
52
+ }
53
+ }
41
54
  // Clean the output directory before building the Next app.
42
55
  buildHelper.initOutputDir(options);
43
56
  if (projectOpts.skipNextBuild) {
@@ -100,12 +100,17 @@ export async function bundleServer(buildOpts, projectOpts) {
100
100
  // Apply updater updates, must be the last plugin
101
101
  updater.plugin,
102
102
  ],
103
- external: [
104
- "./middleware/handler.mjs",
105
- // Do not bundle og when it is not used
106
- ...(useOg ? [] : ["next/dist/compiled/@vercel/og/index.edge.js"]),
107
- ],
103
+ external: ["./middleware/handler.mjs"],
108
104
  alias: {
105
+ // When @vercel/og is not used, alias the edge entry to a throwing shim so the
106
+ // dynamic `import("next/dist/compiled/@vercel/og/index.edge.js")` call site
107
+ // emitted by Next.js does not drag the library (~800 KiB) and its
108
+ // `resvg.wasm` (~1.4 MiB) into the Worker bundle.
109
+ ...(useOg
110
+ ? {}
111
+ : {
112
+ "next/dist/compiled/@vercel/og/index.edge.js": path.join(buildOpts.outputDir, "cloudflare-templates/shims/throw.js"),
113
+ }),
109
114
  // Workers have `fetch` so the `node-fetch` polyfill is not needed
110
115
  "next/dist/compiled/node-fetch": path.join(buildOpts.outputDir, "cloudflare-templates/shims/fetch.js"),
111
116
  // Workers have builtin Web Sockets
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import path from "node:path";
3
- import { loadBuildId, loadPrerenderManifest } from "@opennextjs/aws/adapters/config/util.js";
3
+ import { loadBuildId, loadConfig, loadPrerenderManifest } from "@opennextjs/aws/adapters/config/util.js";
4
4
  import { esbuildSync } from "@opennextjs/aws/build/helper.js";
5
5
  export function compileDurableObjects(buildOpts) {
6
6
  const _require = createRequire(import.meta.url);
@@ -13,6 +13,7 @@ export function compileDurableObjects(buildOpts) {
13
13
  const prerenderManifest = loadPrerenderManifest(buildOutputDotNextDir);
14
14
  const previewModeId = prerenderManifest?.preview?.previewModeId;
15
15
  const BUILD_ID = loadBuildId(buildOutputDotNextDir);
16
+ const nextConfig = loadConfig(buildOutputDotNextDir);
16
17
  return esbuildSync({
17
18
  entryPoints,
18
19
  bundle: true,
@@ -22,7 +23,7 @@ export function compileDurableObjects(buildOpts) {
22
23
  external: ["cloudflare:workers"],
23
24
  define: {
24
25
  "process.env.__NEXT_PREVIEW_MODE_ID": `"${previewModeId}"`,
25
- "process.env.__NEXT_BUILD_ID": `"${BUILD_ID}"`,
26
+ "process.env.__OPEN_NEXT_BUILD_ID": JSON.stringify(nextConfig.deploymentId ?? BUILD_ID),
26
27
  },
27
28
  }, buildOpts);
28
29
  }
@@ -97,13 +97,20 @@ async function migrateCommand(args) {
97
97
  }
98
98
  catch (error) {
99
99
  logger.error("Failed to update package.json", error.message);
100
- logger.warn("\nPlease ensure that your package.json contains the following scripts:\n" +
101
- console.log([...Object.entries(openNextScripts)].map(([key, value]) => ` - ${key}: ${value}`).join("\n")) +
102
- "\n");
100
+ logger.warn(`Please ensure that your package.json contains the following scripts:
101
+ ${[...Object.entries(openNextScripts)].map(([k, v]) => ` - ${k}: ${v}`).join("\n")}
102
+ `);
103
103
  }
104
104
  const gitIgnoreExists = fs.existsSync(".gitignore");
105
105
  printStepTitle(`${gitIgnoreExists ? "Updating" : "Creating"} .gitignore file`);
106
- conditionalAppendFileSync(".gitignore", "# OpenNext\n.open-next\n", {
106
+ conditionalAppendFileSync(".gitignore", `# OpenNext
107
+ .open-next
108
+
109
+ # wrangler files
110
+ .wrangler
111
+ .dev.vars*
112
+ !.dev.vars.example
113
+ `, {
107
114
  appendIf: (content) => !content.includes(".open-next"),
108
115
  appendPrefix: "\n",
109
116
  });
@@ -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-02-01") {
37
+ export async function createWranglerConfigFile(projectDir, defaultCompatDate = "2026-04-15") {
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.19.3",
4
+ "version": "1.19.5",
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": "3.10.2",
47
+ "@opennextjs/aws": "3.10.4",
48
48
  "ci-info": "^4.2.0",
49
49
  "cloudflare": "^4.4.1",
50
50
  "comment-json": "^4.5.1",
@@ -54,7 +54,7 @@
54
54
  "yargs": "^18.0.0"
55
55
  },
56
56
  "devDependencies": {
57
- "@cloudflare/workers-types": "^4.20260114.0",
57
+ "@cloudflare/workers-types": "^4.20260423.1",
58
58
  "@eslint/js": "^9.11.1",
59
59
  "@tsconfig/strictest": "^2.0.5",
60
60
  "@types/mock-fs": "^4.13.4",
@@ -78,7 +78,7 @@
78
78
  },
79
79
  "peerDependencies": {
80
80
  "next": ">=15.5.15 <16 || >=16.2.3",
81
- "wrangler": "^4.65.0"
81
+ "wrangler": "^4.86.0"
82
82
  },
83
83
  "scripts": {
84
84
  "clean": "rimraf dist",