@opennextjs/cloudflare 1.0.0-beta.3 → 1.0.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.
Files changed (56) hide show
  1. package/dist/api/cloudflare-context.d.ts +6 -3
  2. package/dist/api/config.d.ts +28 -1
  3. package/dist/api/config.js +15 -1
  4. package/dist/api/durable-objects/sharded-tag-cache.d.ts +1 -0
  5. package/dist/api/durable-objects/sharded-tag-cache.js +16 -0
  6. package/dist/api/index.d.ts +1 -1
  7. package/dist/api/overrides/incremental-cache/kv-incremental-cache.d.ts +8 -9
  8. package/dist/api/overrides/incremental-cache/kv-incremental-cache.js +14 -14
  9. package/dist/api/overrides/incremental-cache/r2-incremental-cache.d.ts +4 -11
  10. package/dist/api/overrides/incremental-cache/r2-incremental-cache.js +8 -15
  11. package/dist/api/overrides/incremental-cache/regional-cache.d.ts +7 -7
  12. package/dist/api/overrides/incremental-cache/regional-cache.js +16 -13
  13. package/dist/api/overrides/incremental-cache/static-assets-incremental-cache.d.ts +3 -3
  14. package/dist/api/overrides/incremental-cache/static-assets-incremental-cache.js +9 -4
  15. package/dist/api/overrides/internal.d.ts +10 -3
  16. package/dist/api/overrides/internal.js +7 -0
  17. package/dist/api/overrides/tag-cache/d1-next-tag-cache.d.ts +1 -0
  18. package/dist/api/overrides/tag-cache/d1-next-tag-cache.js +20 -0
  19. package/dist/api/overrides/tag-cache/do-sharded-tag-cache.d.ts +10 -4
  20. package/dist/api/overrides/tag-cache/do-sharded-tag-cache.js +58 -17
  21. package/dist/api/overrides/tag-cache/do-sharded-tag-cache.spec.js +9 -7
  22. package/dist/api/overrides/tag-cache/tag-cache-filter.d.ts +26 -0
  23. package/dist/api/overrides/tag-cache/tag-cache-filter.js +48 -0
  24. package/dist/api/overrides/tag-cache/tag-cache-filter.spec.d.ts +1 -0
  25. package/dist/api/overrides/tag-cache/tag-cache-filter.spec.js +97 -0
  26. package/dist/cli/args.d.ts +2 -0
  27. package/dist/cli/args.js +3 -0
  28. package/dist/cli/build/build.d.ts +1 -1
  29. package/dist/cli/build/bundle-server.js +18 -4
  30. package/dist/cli/build/open-next/createServerBundle.js +15 -2
  31. package/dist/cli/build/patches/investigated/patch-cache.d.ts +1 -0
  32. package/dist/cli/build/patches/investigated/patch-cache.js +16 -0
  33. package/dist/cli/build/patches/plugins/eval-manifest.js +1 -1
  34. package/dist/cli/build/patches/plugins/load-manifest.js +1 -1
  35. package/dist/cli/build/patches/plugins/pages-router-context.d.ts +11 -0
  36. package/dist/cli/build/patches/plugins/pages-router-context.js +32 -0
  37. package/dist/cli/build/patches/plugins/wrangler-external.js +2 -1
  38. package/dist/cli/build/utils/ensure-cf-config.d.ts +1 -1
  39. package/dist/cli/build/utils/workerd.d.ts +38 -0
  40. package/dist/cli/build/utils/workerd.js +80 -0
  41. package/dist/cli/build/utils/workerd.spec.d.ts +1 -0
  42. package/dist/cli/build/utils/workerd.spec.js +188 -0
  43. package/dist/cli/commands/deploy.d.ts +2 -1
  44. package/dist/cli/commands/deploy.js +1 -0
  45. package/dist/cli/commands/populate-cache.d.ts +1 -0
  46. package/dist/cli/commands/populate-cache.js +56 -24
  47. package/dist/cli/commands/preview.d.ts +2 -1
  48. package/dist/cli/commands/preview.js +1 -0
  49. package/dist/cli/commands/upload.d.ts +2 -1
  50. package/dist/cli/commands/upload.js +1 -0
  51. package/dist/cli/templates/init.js +3 -0
  52. package/dist/cli/templates/worker.d.ts +3 -1
  53. package/dist/cli/utils/run-wrangler.d.ts +0 -1
  54. package/dist/cli/utils/run-wrangler.js +1 -1
  55. package/package.json +3 -3
  56. package/templates/wrangler.jsonc +1 -1
@@ -1,4 +1,4 @@
1
- import { error } from "@opennextjs/aws/adapters/logger.js";
1
+ 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";
@@ -113,6 +113,38 @@ class ShardedDOTagCache {
113
113
  db,
114
114
  };
115
115
  }
116
+ async getLastRevalidated(tags) {
117
+ const { isDisabled } = await this.getConfig();
118
+ if (isDisabled)
119
+ return 0;
120
+ try {
121
+ const shardedTagGroups = this.groupTagsByDO({ tags });
122
+ const shardedTagRevalidationOutcomes = await Promise.all(shardedTagGroups.map(async ({ doId, tags }) => {
123
+ const cachedValue = await this.getFromRegionalCache({ doId, tags, type: "number" });
124
+ if (cachedValue) {
125
+ const cached = await cachedValue.text();
126
+ try {
127
+ return parseInt(cached, 10);
128
+ }
129
+ catch (e) {
130
+ debug("Error while parsing cached value", e);
131
+ // If we can't parse the cached value, we should just ignore it and go to the durable object
132
+ }
133
+ }
134
+ const stub = this.getDurableObjectStub(doId);
135
+ const _lastRevalidated = await stub.getLastRevalidated(tags);
136
+ if (!_lastRevalidated) {
137
+ getCloudflareContext().ctx.waitUntil(this.putToRegionalCache({ doId, tags, type: "number" }, _lastRevalidated));
138
+ }
139
+ return _lastRevalidated;
140
+ }));
141
+ return Math.max(...shardedTagRevalidationOutcomes);
142
+ }
143
+ catch (e) {
144
+ error("Error while checking revalidation", e);
145
+ return 0;
146
+ }
147
+ }
116
148
  /**
117
149
  * This function checks if the tags have been revalidated
118
150
  * It is never supposed to throw and in case of error, it will return false
@@ -127,7 +159,7 @@ class ShardedDOTagCache {
127
159
  try {
128
160
  const shardedTagGroups = this.groupTagsByDO({ tags });
129
161
  const shardedTagRevalidationOutcomes = await Promise.all(shardedTagGroups.map(async ({ doId, tags }) => {
130
- const cachedValue = await this.getFromRegionalCache(doId, tags);
162
+ const cachedValue = await this.getFromRegionalCache({ doId, tags, type: "boolean" });
131
163
  if (cachedValue) {
132
164
  return (await cachedValue.text()) === "true";
133
165
  }
@@ -137,7 +169,7 @@ class ShardedDOTagCache {
137
169
  // If we do so, we risk causing cache MISS even though it has been revalidated elsewhere
138
170
  // On the other hand revalidating a tag that is used in a lot of places will cause a lot of requests
139
171
  if (!_hasBeenRevalidated) {
140
- getCloudflareContext().ctx.waitUntil(this.putToRegionalCache(doId, tags, _hasBeenRevalidated));
172
+ getCloudflareContext().ctx.waitUntil(this.putToRegionalCache({ doId, tags, type: "boolean" }, _hasBeenRevalidated));
141
173
  }
142
174
  return _hasBeenRevalidated;
143
175
  }));
@@ -170,7 +202,11 @@ class ShardedDOTagCache {
170
202
  const stub = this.getDurableObjectStub(doId);
171
203
  await stub.writeTags(tags, lastModified);
172
204
  // Depending on the shards and the tags, deleting from the regional cache will not work for every tag
173
- await this.deleteRegionalCache(doId, tags);
205
+ // We also need to delete both cache
206
+ await Promise.all([
207
+ this.deleteRegionalCache({ doId, tags, type: "boolean" }),
208
+ this.deleteRegionalCache({ doId, tags, type: "number" }),
209
+ ]);
174
210
  }
175
211
  catch (e) {
176
212
  error("Error while writing tags", e);
@@ -194,36 +230,42 @@ class ShardedDOTagCache {
194
230
  }
195
231
  return this.localCache;
196
232
  }
197
- async getCacheKey(doId, tags) {
198
- return new Request(new URL(`shard/${doId.shardId}?tags=${encodeURIComponent(tags.join(";"))}`, "http://local.cache"));
233
+ getCacheUrlKey(opts) {
234
+ const { doId, tags, type } = opts;
235
+ return `http://local.cache/shard/${doId.shardId}?type=${type}&tags=${encodeURIComponent(tags.join(";"))}`;
199
236
  }
200
- async getFromRegionalCache(doId, tags) {
237
+ async getFromRegionalCache(opts) {
201
238
  try {
202
239
  if (!this.opts.regionalCache)
203
240
  return;
204
241
  const cache = await this.getCacheInstance();
205
242
  if (!cache)
206
243
  return;
207
- const key = await this.getCacheKey(doId, tags);
208
- return cache.match(key);
244
+ return cache.match(this.getCacheUrlKey(opts));
209
245
  }
210
246
  catch (e) {
211
247
  error("Error while fetching from regional cache", e);
212
- return;
213
248
  }
214
249
  }
215
- async putToRegionalCache(doId, tags, hasBeenRevalidated) {
250
+ async putToRegionalCache(optsKey, value) {
216
251
  if (!this.opts.regionalCache)
217
252
  return;
218
253
  const cache = await this.getCacheInstance();
219
254
  if (!cache)
220
255
  return;
221
- const key = await this.getCacheKey(doId, tags);
222
- await cache.put(key, new Response(`${hasBeenRevalidated}`, {
223
- headers: { "cache-control": `max-age=${this.opts.regionalCacheTtlSec ?? 5}` },
256
+ const tags = optsKey.tags;
257
+ await cache.put(this.getCacheUrlKey(optsKey), new Response(`${value}`, {
258
+ headers: {
259
+ "cache-control": `max-age=${this.opts.regionalCacheTtlSec ?? 5}`,
260
+ ...(tags.length > 0
261
+ ? {
262
+ "cache-tag": tags.join(","),
263
+ }
264
+ : {}),
265
+ },
224
266
  }));
225
267
  }
226
- async deleteRegionalCache(doId, tags) {
268
+ async deleteRegionalCache(optsKey) {
227
269
  // We never want to crash because of the cache
228
270
  try {
229
271
  if (!this.opts.regionalCache)
@@ -231,8 +273,7 @@ class ShardedDOTagCache {
231
273
  const cache = await this.getCacheInstance();
232
274
  if (!cache)
233
275
  return;
234
- const key = await this.getCacheKey(doId, tags);
235
- await cache.delete(key);
276
+ await cache.delete(this.getCacheUrlKey(optsKey));
236
277
  }
237
278
  catch (e) {
238
279
  debugCache("Error while deleting from regional cache", e);
@@ -223,7 +223,11 @@ describe("DOShardedTagCache", () => {
223
223
  cache.deleteRegionalCache = vi.fn();
224
224
  await cache.writeTags(["tag1"]);
225
225
  expect(cache.deleteRegionalCache).toHaveBeenCalled();
226
- expect(cache.deleteRegionalCache).toHaveBeenCalledWith(expect.objectContaining({ key: "tag-hard;shard-1;replica-1" }), ["tag1"]);
226
+ expect(cache.deleteRegionalCache).toHaveBeenCalledWith({
227
+ doId: expect.objectContaining({ key: "tag-hard;shard-1;replica-1" }),
228
+ tags: ["tag1"],
229
+ type: "boolean",
230
+ });
227
231
  // expect(cache.deleteRegionalCache).toHaveBeenCalledWith("tag-hard;shard-1;replica-1", ["tag1"]);
228
232
  });
229
233
  });
@@ -253,7 +257,7 @@ describe("DOShardedTagCache", () => {
253
257
  numberOfReplicas: 1,
254
258
  shardType: "hard",
255
259
  });
256
- expect(await cache.getFromRegionalCache(doId, ["tag1"])).toBeUndefined();
260
+ expect(await cache.getFromRegionalCache({ doId, tags: ["tag1"], type: "boolean" })).toBeUndefined();
257
261
  });
258
262
  it("should call .match on the cache", async () => {
259
263
  // @ts-expect-error - Defined on cloudfare context
@@ -268,7 +272,7 @@ describe("DOShardedTagCache", () => {
268
272
  numberOfReplicas: 1,
269
273
  shardType: "hard",
270
274
  });
271
- expect(await cache.getFromRegionalCache(doId, ["tag1"])).toBe("response");
275
+ expect(await cache.getFromRegionalCache({ doId, tags: ["tag1"], type: "boolean" })).toBe("response");
272
276
  // @ts-expect-error - Defined on cloudfare context
273
277
  globalThis.caches = undefined;
274
278
  });
@@ -277,15 +281,13 @@ describe("DOShardedTagCache", () => {
277
281
  it("should return the cache key without the random part", async () => {
278
282
  const cache = shardedDOTagCache();
279
283
  const doId1 = new DOId({ baseShardId: "shard-0", numberOfReplicas: 1, shardType: "hard" });
280
- const reqKey = await cache.getCacheKey(doId1, ["_N_T_/tag1"]);
281
- expect(reqKey.url).toBe("http://local.cache/shard/tag-hard;shard-0?tags=_N_T_%2Ftag1");
284
+ expect(cache.getCacheUrlKey({ doId: doId1, tags: ["_N_T_/tag1"], type: "boolean" })).toBe("http://local.cache/shard/tag-hard;shard-0?type=boolean&tags=_N_T_%2Ftag1");
282
285
  const doId2 = new DOId({
283
286
  baseShardId: "shard-1",
284
287
  numberOfReplicas: 1,
285
288
  shardType: "hard",
286
289
  });
287
- const reqKey2 = await cache.getCacheKey(doId2, ["tag1"]);
288
- expect(reqKey2.url).toBe("http://local.cache/shard/tag-hard;shard-1?tags=tag1");
290
+ expect(cache.getCacheUrlKey({ doId: doId2, tags: ["tag1"], type: "boolean" })).toBe("http://local.cache/shard/tag-hard;shard-1?type=boolean&tags=tag1");
289
291
  });
290
292
  });
291
293
  describe("performWriteTagsWithRetry", () => {
@@ -0,0 +1,26 @@
1
+ import { NextModeTagCache } from "@opennextjs/aws/types/overrides";
2
+ interface WithFilterOptions {
3
+ /**
4
+ * The original tag cache.
5
+ * Call to this will receive only the filtered tags.
6
+ */
7
+ tagCache: NextModeTagCache;
8
+ /**
9
+ * The function to filter tags.
10
+ * @param tag The tag to filter.
11
+ * @returns true if the tag should be forwarded, false otherwise.
12
+ */
13
+ filterFn: (tag: string) => boolean;
14
+ }
15
+ /**
16
+ * Creates a new tag cache that filters tags based on the provided filter function.
17
+ * This is useful to remove tags that are not used by the app, this could reduce the number of requests to the underlying tag cache.
18
+ */
19
+ export declare function withFilter({ tagCache, filterFn }: WithFilterOptions): NextModeTagCache;
20
+ /**
21
+ * Filter function to exclude tags that start with "_N_T_".
22
+ * This is used to filter out internal soft tags.
23
+ * Can be used if `revalidatePath` is not used.
24
+ */
25
+ export declare function softTagFilter(tag: string): boolean;
26
+ export {};
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Creates a new tag cache that filters tags based on the provided filter function.
3
+ * This is useful to remove tags that are not used by the app, this could reduce the number of requests to the underlying tag cache.
4
+ */
5
+ export function withFilter({ tagCache, filterFn }) {
6
+ return {
7
+ name: `filtered-${tagCache.name}`,
8
+ mode: "nextMode",
9
+ getLastRevalidated: async (tags) => {
10
+ const filteredTags = tags.filter(filterFn);
11
+ if (filteredTags.length === 0) {
12
+ return 0;
13
+ }
14
+ return tagCache.getLastRevalidated(filteredTags);
15
+ },
16
+ getPathsByTags: tagCache.getPathsByTags
17
+ ? async (tags) => {
18
+ const filteredTags = tags.filter(filterFn);
19
+ if (filteredTags.length === 0) {
20
+ return [];
21
+ }
22
+ return tagCache.getPathsByTags(filteredTags);
23
+ }
24
+ : undefined,
25
+ hasBeenRevalidated: async (tags, lastModified) => {
26
+ const filteredTags = tags.filter(filterFn);
27
+ if (filteredTags.length === 0) {
28
+ return false;
29
+ }
30
+ return tagCache.hasBeenRevalidated(filteredTags, lastModified);
31
+ },
32
+ writeTags: async (tags) => {
33
+ const filteredTags = tags.filter(filterFn);
34
+ if (filteredTags.length === 0) {
35
+ return;
36
+ }
37
+ return tagCache.writeTags(filteredTags);
38
+ },
39
+ };
40
+ }
41
+ /**
42
+ * Filter function to exclude tags that start with "_N_T_".
43
+ * This is used to filter out internal soft tags.
44
+ * Can be used if `revalidatePath` is not used.
45
+ */
46
+ export function softTagFilter(tag) {
47
+ return !tag.startsWith("_N_T_");
48
+ }
@@ -0,0 +1,97 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { softTagFilter, withFilter } from "./tag-cache-filter";
3
+ const mockedTagCache = {
4
+ name: "mocked",
5
+ mode: "nextMode",
6
+ getLastRevalidated: vi.fn(),
7
+ getPathsByTags: vi.fn(),
8
+ hasBeenRevalidated: vi.fn(),
9
+ writeTags: vi.fn(),
10
+ };
11
+ const filterFn = (tag) => tag.startsWith("valid_");
12
+ describe("withFilter", () => {
13
+ beforeEach(() => {
14
+ vi.clearAllMocks();
15
+ });
16
+ it("should filter out tags based on writeTags", async () => {
17
+ const tagCache = withFilter({
18
+ tagCache: mockedTagCache,
19
+ filterFn,
20
+ });
21
+ const tags = ["valid_tag", "invalid_tag"];
22
+ await tagCache.writeTags(tags);
23
+ expect(mockedTagCache.writeTags).toHaveBeenCalledWith(["valid_tag"]);
24
+ });
25
+ it("should not call writeTags if no tags are valid", async () => {
26
+ const tagCache = withFilter({
27
+ tagCache: mockedTagCache,
28
+ filterFn,
29
+ });
30
+ const tags = ["invalid_tag"];
31
+ await tagCache.writeTags(tags);
32
+ expect(mockedTagCache.writeTags).not.toHaveBeenCalled();
33
+ });
34
+ it("should filter out tags based on hasBeenRevalidated", async () => {
35
+ const tagCache = withFilter({
36
+ tagCache: mockedTagCache,
37
+ filterFn,
38
+ });
39
+ const tags = ["valid_tag", "invalid_tag"];
40
+ const lastModified = Date.now();
41
+ await tagCache.hasBeenRevalidated(tags, lastModified);
42
+ expect(mockedTagCache.hasBeenRevalidated).toHaveBeenCalledWith(["valid_tag"], lastModified);
43
+ });
44
+ it("should not call hasBeenRevalidated if no tags are valid", async () => {
45
+ const tagCache = withFilter({
46
+ tagCache: mockedTagCache,
47
+ filterFn,
48
+ });
49
+ const tags = ["invalid_tag"];
50
+ const lastModified = Date.now();
51
+ await tagCache.hasBeenRevalidated(tags, lastModified);
52
+ expect(mockedTagCache.hasBeenRevalidated).not.toHaveBeenCalled();
53
+ });
54
+ it("should filter out tags based on getPathsByTags", async () => {
55
+ const tagCache = withFilter({
56
+ tagCache: mockedTagCache,
57
+ filterFn,
58
+ });
59
+ const tags = ["valid_tag", "invalid_tag"];
60
+ await tagCache.getPathsByTags?.(tags);
61
+ expect(mockedTagCache.getPathsByTags).toHaveBeenCalledWith(["valid_tag"]);
62
+ });
63
+ it("should not call getPathsByTags if no tags are valid", async () => {
64
+ const tagCache = withFilter({
65
+ tagCache: mockedTagCache,
66
+ filterFn,
67
+ });
68
+ const tags = ["invalid_tag"];
69
+ await tagCache.getPathsByTags?.(tags);
70
+ expect(mockedTagCache.getPathsByTags).not.toHaveBeenCalled();
71
+ });
72
+ it("should return the correct name", () => {
73
+ const tagCache = withFilter({
74
+ tagCache: mockedTagCache,
75
+ filterFn,
76
+ });
77
+ expect(tagCache.name).toBe("filtered-mocked");
78
+ });
79
+ it("should not create a function if getPathsByTags is not defined", async () => {
80
+ const tagCache = withFilter({
81
+ tagCache: {
82
+ ...mockedTagCache,
83
+ getPathsByTags: undefined,
84
+ },
85
+ filterFn,
86
+ });
87
+ expect(tagCache.getPathsByTags).toBeUndefined();
88
+ });
89
+ it("should filter soft tags", () => {
90
+ const tagCache = withFilter({
91
+ tagCache: mockedTagCache,
92
+ filterFn: softTagFilter,
93
+ });
94
+ tagCache.writeTags(["valid_tag", "_N_T_/", "_N_T_/test", "_N_T_/layout"]);
95
+ expect(mockedTagCache.writeTags).toHaveBeenCalledWith(["valid_tag"]);
96
+ });
97
+ });
@@ -7,10 +7,12 @@ export type Arguments = ({
7
7
  } | {
8
8
  command: "preview" | "deploy" | "upload";
9
9
  passthroughArgs: string[];
10
+ cacheChunkSize?: number;
10
11
  } | {
11
12
  command: "populateCache";
12
13
  target: WranglerTarget;
13
14
  environment?: string;
15
+ cacheChunkSize?: number;
14
16
  }) & {
15
17
  outputDir?: string;
16
18
  };
package/dist/cli/args.js CHANGED
@@ -9,6 +9,7 @@ export function getArgs() {
9
9
  output: { type: "string", short: "o" },
10
10
  noMinify: { type: "boolean", default: false },
11
11
  skipWranglerConfigCheck: { type: "boolean", default: false },
12
+ cacheChunkSize: { type: "string" },
12
13
  },
13
14
  allowPositionals: true,
14
15
  });
@@ -33,6 +34,7 @@ export function getArgs() {
33
34
  command: positionals[0],
34
35
  outputDir,
35
36
  passthroughArgs,
37
+ ...(values.cacheChunkSize && { cacheChunkSize: Number(values.cacheChunkSize) }),
36
38
  };
37
39
  case "populateCache":
38
40
  if (!isWranglerTarget(positionals[1])) {
@@ -43,6 +45,7 @@ export function getArgs() {
43
45
  outputDir,
44
46
  target: positionals[1],
45
47
  environment: getWranglerEnvironmentFlag(passthroughArgs),
48
+ ...(values.cacheChunkSize && { cacheChunkSize: Number(values.cacheChunkSize) }),
46
49
  };
47
50
  default:
48
51
  throw new Error("Error: invalid command, expected 'build' | 'preview' | 'deploy' | 'upload' | 'populateCache'");
@@ -1,5 +1,5 @@
1
1
  import { BuildOptions } from "@opennextjs/aws/build/helper.js";
2
- import { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
2
+ import { OpenNextConfig } from "../../api/config.js";
3
3
  import type { ProjectOptions } from "../project-options.js";
4
4
  /**
5
5
  * Builds the application in a format that can be passed to workerd
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
  import { getPackagePath } from "@opennextjs/aws/build/helper.js";
6
6
  import { ContentUpdater } from "@opennextjs/aws/plugins/content-updater.js";
7
7
  import { build } from "esbuild";
8
+ import { getOpenNextConfig } from "../../api/config.js";
8
9
  import { patchVercelOgLibrary } from "./patches/ast/patch-vercel-og-library.js";
9
10
  import { patchWebpackRuntime } from "./patches/ast/webpack-runtime.js";
10
11
  import * as patches from "./patches/index.js";
@@ -15,6 +16,7 @@ import { inlineFindDir } from "./patches/plugins/find-dir.js";
15
16
  import { patchInstrumentation } from "./patches/plugins/instrumentation.js";
16
17
  import { inlineLoadManifest } from "./patches/plugins/load-manifest.js";
17
18
  import { handleOptionalDependencies } from "./patches/plugins/optional-deps.js";
19
+ import { patchPagesRouterContext } from "./patches/plugins/pages-router-context.js";
18
20
  import { patchDepdDeprecations } from "./patches/plugins/patch-depd-deprecations.js";
19
21
  import { fixRequire } from "./patches/plugins/require.js";
20
22
  import { shimRequireHook } from "./patches/plugins/require-hook.js";
@@ -63,13 +65,13 @@ export async function bundleServer(buildOpts) {
63
65
  // Next traces files using the default conditions from `nft` (`node`, `require`, `import` and `default`)
64
66
  //
65
67
  // Because we use the `node` platform for this build, the "module" condition is used when no conditions are defined.
66
- // We explicitly set the conditions to an empty array to disable the "module" condition in order to match Next tracing.
68
+ // The conditions are always set (should it be to an empty array) to disable the "module" condition.
67
69
  //
68
70
  // See:
69
71
  // - default nft conditions: https://github.com/vercel/nft/blob/2b55b01/readme.md#exports--imports
70
72
  // - Next no explicit override: https://github.com/vercel/next.js/blob/2efcf11/packages/next/src/build/collect-build-traces.ts#L287
71
73
  // - ESBuild `node` platform: https://esbuild.github.io/api/#platform
72
- conditions: [],
74
+ conditions: getOpenNextConfig(buildOpts).cloudflare?.useWorkerdCondition === false ? [] : ["workerd"],
73
75
  plugins: [
74
76
  shimRequireHook(buildOpts),
75
77
  inlineDynamicRequires(updater, buildOpts),
@@ -77,6 +79,7 @@ export async function bundleServer(buildOpts) {
77
79
  fixRequire(updater),
78
80
  handleOptionalDependencies(optionalDependencies),
79
81
  patchInstrumentation(updater, buildOpts),
82
+ patchPagesRouterContext(buildOpts),
80
83
  inlineEvalManifest(updater, buildOpts),
81
84
  inlineFindDir(updater, buildOpts),
82
85
  inlineLoadManifest(updater, buildOpts),
@@ -85,7 +88,7 @@ export async function bundleServer(buildOpts) {
85
88
  // Apply updater updates, must be the last plugin
86
89
  updater.plugin,
87
90
  ],
88
- external: ["./middleware/handler.mjs", "*.wasm"],
91
+ external: ["./middleware/handler.mjs"],
89
92
  alias: {
90
93
  // Note: it looks like node-fetch is actually not necessary for us, so we could replace it with an empty shim
91
94
  // but just to be safe we replace it with a module that re-exports the native fetch
@@ -122,6 +125,11 @@ export async function bundleServer(buildOpts) {
122
125
  // This define should be safe to use for Next 14.2+, earlier versions (13.5 and less) will cause trouble
123
126
  "process.env.__NEXT_EXPERIMENTAL_REACT": `${needsExperimentalReact(nextConfig)}`,
124
127
  },
128
+ banner: {
129
+ // We need to import them here, assigning them to `globalThis` does not work because node:timers use `globalThis` and thus create an infinite loop
130
+ // See https://github.com/cloudflare/workerd/blob/d6a764c/src/node/internal/internal_timers.ts#L56-L70
131
+ js: `import {setInterval, clearInterval, setTimeout, clearTimeout, setImmediate, clearImmediate} from "node:timers"`,
132
+ },
125
133
  platform: "node",
126
134
  });
127
135
  fs.writeFileSync(openNextServerBundle + ".meta.json", JSON.stringify(result.metafile, null, 2));
@@ -130,7 +138,7 @@ export async function bundleServer(buildOpts) {
130
138
  if (isMonorepo) {
131
139
  fs.writeFileSync(path.join(outputPath, "handler.mjs"), `export { handler } from "./${normalizePath(packagePath)}/handler.mjs";`);
132
140
  }
133
- console.log(`\x1b[35mWorker saved in \`${getOutputWorkerPath(buildOpts)}\` 🚀\n\x1b[0m`);
141
+ console.log(`\x1b[35mWorker saved in \`${path.relative(buildOpts.appPath, getOutputWorkerPath(buildOpts))}\` 🚀\n\x1b[0m`);
134
142
  }
135
143
  /**
136
144
  * This function applies patches required for the code to run on workers.
@@ -140,6 +148,7 @@ export async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
140
148
  const patchedCode = await patchCodeWithValidations(code, [
141
149
  ["require", patches.patchRequire],
142
150
  ["cacheHandler", (code) => patches.patchCache(code, buildOpts)],
151
+ ["composableCache", (code) => patches.patchComposableCache(code, buildOpts), { isOptional: true }],
143
152
  [
144
153
  "'require(this.middlewareManifestPath)'",
145
154
  (code) => patches.inlineMiddlewareManifestRequire(code, buildOpts),
@@ -150,6 +159,11 @@ export async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
150
159
  // workers do not support dynamic require nor require.resolve
151
160
  (code) => code.replace('require.resolve("./cache.cjs")', '"unused"'),
152
161
  ],
162
+ [
163
+ "`require.resolve composable cache` call",
164
+ // workers do not support dynamic require nor require.resolve
165
+ (code) => code.replace('require.resolve("./composable-cache.cjs")', '"unused"'),
166
+ ],
153
167
  ]);
154
168
  await writeFile(workerOutputFile, patchedCode);
155
169
  }
@@ -10,7 +10,7 @@ import { copyMiddlewareResources, generateEdgeBundle } from "@opennextjs/aws/bui
10
10
  import * as buildHelper from "@opennextjs/aws/build/helper.js";
11
11
  import { installDependencies } from "@opennextjs/aws/build/installDeps.js";
12
12
  import { applyCodePatches } from "@opennextjs/aws/build/patch/codePatcher.js";
13
- import { patchEnvVars, patchFetchCacheForISR, patchFetchCacheSetMissingWaitUntil, patchNextServer, patchUnstableCacheForISR, } from "@opennextjs/aws/build/patch/patches/index.js";
13
+ import { patchEnvVars, patchFetchCacheForISR, patchFetchCacheSetMissingWaitUntil, patchNextServer, patchUnstableCacheForISR, patchUseCacheForISR, } from "@opennextjs/aws/build/patch/patches/index.js";
14
14
  // TODO: import from patches/index.js when https://github.com/opennextjs/opennextjs-aws/pull/827 is released
15
15
  import { patchBackgroundRevalidation } from "@opennextjs/aws/build/patch/patches/patchBackgroundRevalidation.js";
16
16
  import logger from "@opennextjs/aws/logger.js";
@@ -20,8 +20,10 @@ import { openNextExternalMiddlewarePlugin } from "@opennextjs/aws/plugins/extern
20
20
  import { openNextReplacementPlugin } from "@opennextjs/aws/plugins/replacement.js";
21
21
  import { openNextResolvePlugin } from "@opennextjs/aws/plugins/resolve.js";
22
22
  import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
23
+ import { getOpenNextConfig } from "../../../api/config.js";
23
24
  import { patchResRevalidate } from "../patches/plugins/res-revalidate.js";
24
25
  import { normalizePath } from "../utils/index.js";
26
+ import { copyWorkerdPackages } from "../utils/workerd.js";
25
27
  export async function createServerBundle(options, codeCustomization) {
26
28
  const { config } = options;
27
29
  const foundRoutes = new Set();
@@ -91,7 +93,10 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
91
93
  const outPackagePath = path.join(outputPath, packagePath);
92
94
  fs.mkdirSync(outPackagePath, { recursive: true });
93
95
  const ext = fnOptions.runtime === "deno" ? "mjs" : "cjs";
96
+ // Normal cache
94
97
  fs.copyFileSync(path.join(options.buildDir, `cache.${ext}`), path.join(outPackagePath, "cache.cjs"));
98
+ // Composable cache
99
+ fs.copyFileSync(path.join(options.buildDir, `composable-cache.${ext}`), path.join(outPackagePath, "composable-cache.cjs"));
95
100
  if (fnOptions.runtime === "deno") {
96
101
  addDenoJson(outputPath, packagePath);
97
102
  }
@@ -113,18 +118,24 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
113
118
  // Copy env files
114
119
  buildHelper.copyEnvFile(appBuildOutputPath, packagePath, outputPath);
115
120
  // Copy all necessary traced files
116
- const { tracedFiles, manifests } = await copyTracedFiles({
121
+ const { tracedFiles, manifests, nodePackages } = await copyTracedFiles({
117
122
  buildOutputPath: appBuildOutputPath,
118
123
  packagePath,
119
124
  outputDir: outputPath,
120
125
  routes: fnOptions.routes ?? ["app/page.tsx"],
121
126
  bundledNextServer: isBundled,
122
127
  });
128
+ if (getOpenNextConfig(options).cloudflare?.useWorkerdCondition !== false) {
129
+ // Next does not trace the "workerd" build condition
130
+ // So we need to copy the whole packages using the condition
131
+ await copyWorkerdPackages(options, nodePackages);
132
+ }
123
133
  const additionalCodePatches = codeCustomization?.additionalCodePatches ?? [];
124
134
  await applyCodePatches(options, tracedFiles, manifests, [
125
135
  patchFetchCacheSetMissingWaitUntil,
126
136
  patchFetchCacheForISR,
127
137
  patchUnstableCacheForISR,
138
+ patchUseCacheForISR,
128
139
  patchNextServer,
129
140
  patchEnvVars,
130
141
  patchBackgroundRevalidation,
@@ -142,6 +153,7 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
142
153
  const isBefore13413 = buildHelper.compareSemver(options.nextVersion, "<=", "13.4.13");
143
154
  const isAfter141 = buildHelper.compareSemver(options.nextVersion, ">=", "14.1");
144
155
  const isAfter142 = buildHelper.compareSemver(options.nextVersion, ">=", "14.2");
156
+ const isAfter152 = buildHelper.compareSemver(options.nextVersion, ">=", "15.2.0");
145
157
  const disableRouting = isBefore13413 || config.middleware?.external;
146
158
  const plugins = [
147
159
  openNextReplacementPlugin({
@@ -161,6 +173,7 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
161
173
  ...(disableNextPrebundledReact ? ["requireHooks"] : []),
162
174
  ...(isBefore13413 ? ["trustHostHeader"] : ["requestHandlerHost"]),
163
175
  ...(isAfter141 ? ["experimentalIncrementalCacheHandler"] : ["stableIncrementalCache"]),
176
+ ...(isAfter152 ? [""] : ["composableCache"]),
164
177
  ],
165
178
  }),
166
179
  openNextResolvePlugin({
@@ -11,3 +11,4 @@ import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
11
11
  * instantiated with a dynamic require that uses a string literal for the path.
12
12
  */
13
13
  export declare function patchCache(code: string, buildOpts: BuildOptions): Promise<string>;
14
+ export declare function patchComposableCache(code: string, buildOpts: BuildOptions): Promise<string>;
@@ -22,3 +22,19 @@ const cacheHandler = null;
22
22
  CacheHandler = require('${normalizePath(cacheFile)}').default;
23
23
  `);
24
24
  }
25
+ export async function patchComposableCache(code, buildOpts) {
26
+ const { outputDir } = buildOpts;
27
+ // TODO: switch to mjs
28
+ const outputPath = path.join(outputDir, "server-functions/default");
29
+ const cacheFile = path.join(outputPath, getPackagePath(buildOpts), "composable-cache.cjs");
30
+ //TODO: Do we want to move this to the new CodePatcher ?
31
+ return code.replace("const { cacheHandlers } = this.nextConfig.experimental", `
32
+ const cacheHandlers = null;
33
+ const handlersSymbol = Symbol.for('@next/cache-handlers');
34
+ const handlersMapSymbol = Symbol.for('@next/cache-handlers-map');
35
+ const handlersSetSymbol = Symbol.for('@next/cache-handlers-set');
36
+ globalThis[handlersMapSymbol] = new Map();
37
+ globalThis[handlersMapSymbol].set("default", require('${normalizePath(cacheFile)}').default);
38
+ globalThis[handlersSetSymbol] = new Set(globalThis[handlersMapSymbol].values());
39
+ `);
40
+ }
@@ -12,7 +12,7 @@ export function inlineEvalManifest(updater, buildOpts) {
12
12
  return updater.updateContent("inline-eval-manifest", [
13
13
  {
14
14
  field: {
15
- filter: getCrossPlatformPathRegex(String.raw `/next/dist/server/load-manifest\.js$`, {
15
+ filter: getCrossPlatformPathRegex(String.raw `/next/dist/server/load-manifest(\.external)?\.js$`, {
16
16
  escape: false,
17
17
  }),
18
18
  contentFilter: /function evalManifest\(/,
@@ -12,7 +12,7 @@ export function inlineLoadManifest(updater, buildOpts) {
12
12
  return updater.updateContent("inline-load-manifest", [
13
13
  {
14
14
  field: {
15
- filter: getCrossPlatformPathRegex(String.raw `/next/dist/server/load-manifest\.js$`, {
15
+ filter: getCrossPlatformPathRegex(String.raw `/next/dist/server/load-manifest(\.external)?\.js$`, {
16
16
  escape: false,
17
17
  }),
18
18
  contentFilter: /function loadManifest\(/,
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ESBuild plugin to handle pages router context.
3
+ *
4
+ * We need to change the import path for the pages router context to use the one provided in `pages-runtime.prod.js`
5
+ */
6
+ import { BuildOptions } from "@opennextjs/aws/build/helper.js";
7
+ import type { PluginBuild } from "esbuild";
8
+ export declare function patchPagesRouterContext(buildOpts: BuildOptions): {
9
+ name: string;
10
+ setup: (build: PluginBuild) => void;
11
+ };