@opennextjs/cloudflare 1.8.4 → 1.9.0

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.
@@ -14,6 +14,7 @@ declare global {
14
14
  NEXT_INC_CACHE_R2_BUCKET?: R2Bucket;
15
15
  [R2_CACHE_PREFIX_ENV_NAME]?: string;
16
16
  NEXT_TAG_CACHE_D1?: D1Database;
17
+ NEXT_TAG_CACHE_KV?: KVNamespace;
17
18
  NEXT_TAG_CACHE_DO_SHARDED?: DurableObjectNamespace<DOShardedTagCache>;
18
19
  NEXT_TAG_CACHE_DO_SHARDED_DLQ?: Queue;
19
20
  NEXT_CACHE_DO_QUEUE?: DurableObjectNamespace<DOQueueHandler>;
@@ -1,6 +1,6 @@
1
1
  import { error } from "@opennextjs/aws/adapters/logger.js";
2
2
  import { getCloudflareContext } from "../../cloudflare-context.js";
3
- import { debugCache, FALLBACK_BUILD_ID } from "../internal.js";
3
+ import { debugCache, FALLBACK_BUILD_ID, isPurgeCacheEnabled } from "../internal.js";
4
4
  import { NAME as KV_CACHE_NAME } from "./kv-incremental-cache.js";
5
5
  const ONE_MINUTE_IN_SECONDS = 60;
6
6
  const THIRTY_MINUTES_IN_SECONDS = ONE_MINUTE_IN_SECONDS * 30;
@@ -28,8 +28,7 @@ class RegionalCache {
28
28
  throw new Error("The KV incremental cache does not need a regional cache.");
29
29
  }
30
30
  this.name = this.store.name;
31
- this.opts.shouldLazilyUpdateOnCacheHit ??=
32
- this.opts.mode === "long-lived" && !this.#hasAutomaticCachePurging;
31
+ this.opts.shouldLazilyUpdateOnCacheHit ??= this.opts.mode === "long-lived" && !isPurgeCacheEnabled();
33
32
  }
34
33
  get #bypassTagCacheOnCacheHit() {
35
34
  if (this.opts.bypassTagCacheOnCacheHit !== undefined) {
@@ -37,12 +36,7 @@ class RegionalCache {
37
36
  return this.opts.bypassTagCacheOnCacheHit;
38
37
  }
39
38
  // Otherwise we default to whether the automatic cache purging is enabled or not
40
- return this.#hasAutomaticCachePurging;
41
- }
42
- get #hasAutomaticCachePurging() {
43
- // The `?` is required at `openNextConfig?` or the Open Next build fails because of a type error
44
- const cdnInvalidation = globalThis.openNextConfig?.default?.override?.cdnInvalidation;
45
- return cdnInvalidation !== undefined && cdnInvalidation !== "dummy";
39
+ return isPurgeCacheEnabled();
46
40
  }
47
41
  async get(key, cacheType) {
48
42
  try {
@@ -12,5 +12,6 @@ export type KeyOptions = {
12
12
  buildId: string | undefined;
13
13
  };
14
14
  export declare function computeCacheKey(key: string, options: KeyOptions): string;
15
+ export declare function isPurgeCacheEnabled(): boolean;
15
16
  export declare function purgeCacheByTags(tags: string[]): Promise<void>;
16
17
  export declare function internalPurgeCacheByTags(env: CloudflareEnv, tags: string[]): Promise<"missing-credentials" | "rate-limit-exceeded" | "purge-failed" | "purge-success">;
@@ -13,6 +13,11 @@ export function computeCacheKey(key, options) {
13
13
  const hash = createHash("sha256").update(key).digest("hex");
14
14
  return `${prefix}/${buildId}/${hash}.${cacheType}`.replace(/\/+/g, "/");
15
15
  }
16
+ export function isPurgeCacheEnabled() {
17
+ // The `?` is required at `openNextConfig?` or the Open Next build fails because of a type error
18
+ const cdnInvalidation = globalThis.openNextConfig?.default?.override?.cdnInvalidation;
19
+ return cdnInvalidation !== undefined && cdnInvalidation !== "dummy";
20
+ }
16
21
  export async function purgeCacheByTags(tags) {
17
22
  const { env } = getCloudflareContext();
18
23
  // We have a durable object for purging cache
@@ -1,6 +1,6 @@
1
1
  import { error } from "@opennextjs/aws/adapters/logger.js";
2
2
  import { getCloudflareContext } from "../../cloudflare-context.js";
3
- import { debugCache, FALLBACK_BUILD_ID, purgeCacheByTags } from "../internal.js";
3
+ import { debugCache, FALLBACK_BUILD_ID, isPurgeCacheEnabled, purgeCacheByTags } from "../internal.js";
4
4
  export const NAME = "d1-next-mode-tag-cache";
5
5
  export const BINDING_NAME = "NEXT_TAG_CACHE_D1";
6
6
  export class D1NextModeTagCache {
@@ -21,9 +21,9 @@ export class D1NextModeTagCache {
21
21
  return (result.results[0]?.time ?? 0);
22
22
  }
23
23
  catch (e) {
24
- error(e);
25
24
  // By default we don't want to crash here, so we return false
26
25
  // We still log the error though so we can debug it
26
+ error(e);
27
27
  return 0;
28
28
  }
29
29
  }
@@ -47,20 +47,21 @@ export class D1NextModeTagCache {
47
47
  }
48
48
  async writeTags(tags) {
49
49
  const { isDisabled, db } = this.getConfig();
50
- // TODO: Remove `tags.length === 0` when https://github.com/opennextjs/opennextjs-aws/pull/828 is used
51
50
  if (isDisabled || tags.length === 0)
52
51
  return Promise.resolve();
53
52
  await db.batch(tags.map((tag) => db
54
53
  .prepare(`INSERT INTO revalidations (tag, revalidatedAt) VALUES (?, ?)`)
55
54
  .bind(this.getCacheKey(tag), Date.now())));
56
- await purgeCacheByTags(tags);
55
+ // TODO: See https://github.com/opennextjs/opennextjs-aws/issues/986
56
+ if (isPurgeCacheEnabled()) {
57
+ await purgeCacheByTags(tags);
58
+ }
57
59
  }
58
60
  getConfig() {
59
61
  const db = getCloudflareContext().env[BINDING_NAME];
60
62
  if (!db)
61
63
  debugCache("No D1 database found");
62
- const isDisabled = !!globalThis.openNextConfig
63
- .dangerous?.disableTagCache;
64
+ const isDisabled = Boolean(globalThis.openNextConfig.dangerous?.disableTagCache);
64
65
  return !db || isDisabled
65
66
  ? { isDisabled: true }
66
67
  : {
@@ -2,7 +2,7 @@ import { debug, error } from "@opennextjs/aws/adapters/logger.js";
2
2
  import { generateShardId } from "@opennextjs/aws/core/routing/queue.js";
3
3
  import { IgnorableError } from "@opennextjs/aws/utils/error.js";
4
4
  import { getCloudflareContext } from "../../cloudflare-context.js";
5
- import { debugCache, purgeCacheByTags } from "../internal.js";
5
+ import { debugCache, isPurgeCacheEnabled, purgeCacheByTags } from "../internal.js";
6
6
  export const DEFAULT_WRITE_RETRIES = 3;
7
7
  export const DEFAULT_NUM_SHARDS = 4;
8
8
  export const NAME = "do-sharded-tag-cache";
@@ -115,7 +115,10 @@ class ShardedDOTagCache {
115
115
  await Promise.all(shardedTagGroups.map(async ({ doId, tags }) => {
116
116
  await this.performWriteTagsWithRetry(doId, tags, currentTime);
117
117
  }));
118
- await purgeCacheByTags(tags);
118
+ // TODO: See https://github.com/opennextjs/opennextjs-aws/issues/986
119
+ if (isPurgeCacheEnabled()) {
120
+ await purgeCacheByTags(tags);
121
+ }
119
122
  }
120
123
  /**
121
124
  * The following methods are public only because they are accessed from the tests
@@ -0,0 +1,32 @@
1
+ import type { NextModeTagCache } from "@opennextjs/aws/types/overrides.js";
2
+ export declare const NAME = "kv-next-mode-tag-cache";
3
+ export declare const BINDING_NAME = "NEXT_TAG_CACHE_KV";
4
+ /**
5
+ * Tag Cache based on a KV namespace
6
+ *
7
+ * Warning:
8
+ * This implementation is considered experimental for now.
9
+ * KV is eventually consistent and can take up to 60s to reflect the last write.
10
+ * This means that:
11
+ * - revalidations can take up to 60s to apply
12
+ * - when a page depends on multiple tags they can be inconsistent for up to 60s.
13
+ * It also means that cached data could be outdated for one tag when other tags
14
+ * are revalidated resulting in the page being generated based on outdated data.
15
+ */
16
+ export declare class KVNextModeTagCache implements NextModeTagCache {
17
+ readonly mode: "nextMode";
18
+ readonly name = "kv-next-mode-tag-cache";
19
+ getLastRevalidated(tags: string[]): Promise<number>;
20
+ hasBeenRevalidated(tags: string[], lastModified?: number): Promise<boolean>;
21
+ writeTags(tags: string[]): Promise<void>;
22
+ /**
23
+ * Returns the KV namespace when it exists and tag cache is not disabled.
24
+ *
25
+ * @returns KV namespace or undefined
26
+ */
27
+ private getKv;
28
+ protected getCacheKey(key: string): string;
29
+ protected getBuildId(): string;
30
+ }
31
+ declare const _default: KVNextModeTagCache;
32
+ export default _default;
@@ -0,0 +1,78 @@
1
+ import { error } from "@opennextjs/aws/adapters/logger.js";
2
+ import { getCloudflareContext } from "../../cloudflare-context.js";
3
+ import { FALLBACK_BUILD_ID, isPurgeCacheEnabled, purgeCacheByTags } from "../internal.js";
4
+ export const NAME = "kv-next-mode-tag-cache";
5
+ export const BINDING_NAME = "NEXT_TAG_CACHE_KV";
6
+ /**
7
+ * Tag Cache based on a KV namespace
8
+ *
9
+ * Warning:
10
+ * This implementation is considered experimental for now.
11
+ * KV is eventually consistent and can take up to 60s to reflect the last write.
12
+ * This means that:
13
+ * - revalidations can take up to 60s to apply
14
+ * - when a page depends on multiple tags they can be inconsistent for up to 60s.
15
+ * It also means that cached data could be outdated for one tag when other tags
16
+ * are revalidated resulting in the page being generated based on outdated data.
17
+ */
18
+ export class KVNextModeTagCache {
19
+ mode = "nextMode";
20
+ name = NAME;
21
+ async getLastRevalidated(tags) {
22
+ const kv = this.getKv();
23
+ if (!kv) {
24
+ return 0;
25
+ }
26
+ try {
27
+ const keys = tags.map((tag) => this.getCacheKey(tag));
28
+ // Use the `json` type to get back numbers/null
29
+ const result = await kv.get(keys, { type: "json" });
30
+ const revalidations = [...result.values()].filter((v) => v != null);
31
+ return revalidations.length === 0 ? 0 : Math.max(...revalidations);
32
+ }
33
+ catch (e) {
34
+ // By default we don't want to crash here, so we return false
35
+ // We still log the error though so we can debug it
36
+ error(e);
37
+ return 0;
38
+ }
39
+ }
40
+ async hasBeenRevalidated(tags, lastModified) {
41
+ return (await this.getLastRevalidated(tags)) > (lastModified ?? Date.now());
42
+ }
43
+ async writeTags(tags) {
44
+ const kv = this.getKv();
45
+ if (!kv || tags.length === 0) {
46
+ return Promise.resolve();
47
+ }
48
+ const timeMs = String(Date.now());
49
+ await Promise.all(tags.map(async (tag) => {
50
+ await kv.put(this.getCacheKey(tag), timeMs);
51
+ }));
52
+ // TODO: See https://github.com/opennextjs/opennextjs-aws/issues/986
53
+ if (isPurgeCacheEnabled()) {
54
+ await purgeCacheByTags(tags);
55
+ }
56
+ }
57
+ /**
58
+ * Returns the KV namespace when it exists and tag cache is not disabled.
59
+ *
60
+ * @returns KV namespace or undefined
61
+ */
62
+ getKv() {
63
+ const kv = getCloudflareContext().env[BINDING_NAME];
64
+ if (!kv) {
65
+ error(`No KV binding ${BINDING_NAME} found`);
66
+ return undefined;
67
+ }
68
+ const isDisabled = Boolean(globalThis.openNextConfig.dangerous?.disableTagCache);
69
+ return isDisabled ? undefined : kv;
70
+ }
71
+ getCacheKey(key) {
72
+ return `${this.getBuildId()}/${key}`.replaceAll("//", "/");
73
+ }
74
+ getBuildId() {
75
+ return process.env.NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
76
+ }
77
+ }
78
+ export default new KVNextModeTagCache();
@@ -13,7 +13,7 @@ import { inlineFindDir } from "./patches/plugins/find-dir.js";
13
13
  import { patchInstrumentation } from "./patches/plugins/instrumentation.js";
14
14
  import { inlineLoadManifest } from "./patches/plugins/load-manifest.js";
15
15
  import { patchNextServer } from "./patches/plugins/next-server.js";
16
- import { patchResolveCache } from "./patches/plugins/open-next.js";
16
+ import { patchResolveCache, patchSetWorkingDirectory } from "./patches/plugins/open-next.js";
17
17
  import { handleOptionalDependencies } from "./patches/plugins/optional-deps.js";
18
18
  import { patchPagesRouterContext } from "./patches/plugins/pages-router-context.js";
19
19
  import { patchDepdDeprecations } from "./patches/plugins/patch-depd-deprecations.js";
@@ -90,6 +90,7 @@ export async function bundleServer(buildOpts, projectOpts) {
90
90
  patchRouteModules(updater, buildOpts),
91
91
  patchDepdDeprecations(updater),
92
92
  patchResolveCache(updater, buildOpts),
93
+ patchSetWorkingDirectory(updater, buildOpts),
93
94
  // Apply updater updates, must be the last plugin
94
95
  updater.plugin,
95
96
  ],
@@ -6,3 +6,5 @@ import type { ContentUpdater, Plugin } from "@opennextjs/aws/plugins/content-upd
6
6
  export declare function patchResolveCache(updater: ContentUpdater, buildOpts: BuildOptions): Plugin;
7
7
  export declare const cacheHandlerRule = "\nrule:\n pattern: var cacheHandlerPath = __require.resolve(\"./cache.cjs\");\nfix: |-\n var cacheHandlerPath = \"\";\n";
8
8
  export declare const compositeCacheHandlerRule = "\nrule:\n pattern: var composableCacheHandlerPath = __require.resolve(\"./composable-cache.cjs\");\nfix: |-\n var composableCacheHandlerPath = \"\";\n";
9
+ export declare function patchSetWorkingDirectory(updater: ContentUpdater, buildOpts: BuildOptions): Plugin;
10
+ export declare const workingDirectoryRule = "\nrule:\n pattern: function setNextjsServerWorkingDirectory() { $$$BODY }\nfix: |-\n function setNextjsServerWorkingDirectory() {\n }\n";
@@ -34,3 +34,25 @@ rule:
34
34
  fix: |-
35
35
  var composableCacheHandlerPath = "";
36
36
  `;
37
+ export function patchSetWorkingDirectory(updater, buildOpts) {
38
+ const { outputDir } = buildOpts;
39
+ const packagePath = getPackagePath(buildOpts);
40
+ const outputPath = path.join(outputDir, "server-functions/default");
41
+ const indexPath = path.relative(buildOpts.appBuildOutputPath, path.join(outputPath, packagePath, `index.mjs`));
42
+ return updater.updateContent("do-not-set-working-directory", [
43
+ {
44
+ filter: getCrossPlatformPathRegex(indexPath),
45
+ contentFilter: /function setNextjsServerWorkingDirectory\(/,
46
+ callback: async ({ contents }) => patchCode(contents, workingDirectoryRule),
47
+ },
48
+ ]);
49
+ }
50
+ // `setNextjsServerWorkingDirectory` calls `process.chdir("")` which errors because the directory does not exists
51
+ // See https://github.com/opennextjs/opennextjs-cloudflare/issues/899
52
+ export const workingDirectoryRule = `
53
+ rule:
54
+ pattern: function setNextjsServerWorkingDirectory() { $$$BODY }
55
+ fix: |-
56
+ function setNextjsServerWorkingDirectory() {
57
+ }
58
+ `;
@@ -2,6 +2,6 @@ export { DOQueueHandler } from "./.build/durable-objects/queue.js";
2
2
  export { DOShardedTagCache } from "./.build/durable-objects/sharded-tag-cache.js";
3
3
  export { BucketCachePurge } from "./.build/durable-objects/bucket-cache-purge.js";
4
4
  declare const _default: {
5
- fetch(request: Request<unknown, IncomingRequestCfProperties<unknown>>, env: CloudflareEnv, ctx: ExecutionContext): Promise<any>;
5
+ fetch(request: Request<unknown, IncomingRequestCfProperties<unknown>>, env: CloudflareEnv, ctx: ExecutionContext<unknown>): Promise<any>;
6
6
  };
7
7
  export default _default;
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.8.4",
4
+ "version": "1.9.0",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "opennextjs-cloudflare": "dist/cli/index.js"
@@ -51,7 +51,7 @@
51
51
  "yargs": "^18.0.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@cloudflare/workers-types": "^4.20250224.0",
54
+ "@cloudflare/workers-types": "^4.20250917.0",
55
55
  "@eslint/js": "^9.11.1",
56
56
  "@tsconfig/strictest": "^2.0.5",
57
57
  "@types/mock-fs": "^4.13.4",
@@ -74,7 +74,7 @@
74
74
  "vitest": "^2.1.1"
75
75
  },
76
76
  "peerDependencies": {
77
- "wrangler": "^4.24.4"
77
+ "wrangler": "^4.38.0"
78
78
  },
79
79
  "scripts": {
80
80
  "clean": "rimraf dist",