@opennextjs/cloudflare 1.0.0-beta.4 → 1.0.1

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 (51) 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 +50 -12
  21. package/dist/api/overrides/tag-cache/do-sharded-tag-cache.spec.js +9 -5
  22. package/dist/api/overrides/tag-cache/tag-cache-filter.js +7 -0
  23. package/dist/api/overrides/tag-cache/tag-cache-filter.spec.js +1 -0
  24. package/dist/cli/args.d.ts +2 -0
  25. package/dist/cli/args.js +3 -0
  26. package/dist/cli/build/build.d.ts +1 -1
  27. package/dist/cli/build/bundle-server.js +16 -4
  28. package/dist/cli/build/open-next/createServerBundle.js +15 -2
  29. package/dist/cli/build/patches/investigated/patch-cache.d.ts +1 -0
  30. package/dist/cli/build/patches/investigated/patch-cache.js +16 -0
  31. package/dist/cli/build/patches/plugins/eval-manifest.js +1 -1
  32. package/dist/cli/build/patches/plugins/load-manifest.js +1 -1
  33. package/dist/cli/build/patches/plugins/wrangler-external.js +2 -1
  34. package/dist/cli/build/utils/ensure-cf-config.d.ts +1 -1
  35. package/dist/cli/build/utils/workerd.d.ts +38 -0
  36. package/dist/cli/build/utils/workerd.js +80 -0
  37. package/dist/cli/build/utils/workerd.spec.d.ts +1 -0
  38. package/dist/cli/build/utils/workerd.spec.js +188 -0
  39. package/dist/cli/commands/deploy.d.ts +2 -1
  40. package/dist/cli/commands/deploy.js +1 -0
  41. package/dist/cli/commands/populate-cache.d.ts +1 -0
  42. package/dist/cli/commands/populate-cache.js +62 -24
  43. package/dist/cli/commands/preview.d.ts +2 -1
  44. package/dist/cli/commands/preview.js +1 -0
  45. package/dist/cli/commands/upload.d.ts +2 -1
  46. package/dist/cli/commands/upload.js +1 -0
  47. package/dist/cli/templates/init.js +3 -0
  48. package/dist/cli/utils/run-wrangler.d.ts +0 -1
  49. package/dist/cli/utils/run-wrangler.js +1 -1
  50. package/package.json +3 -3
  51. 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,29 +230,31 @@ class ShardedDOTagCache {
194
230
  }
195
231
  return this.localCache;
196
232
  }
197
- getCacheUrlKey(doId, tags) {
198
- return `http://local.cache/shard/${doId.shardId}?tags=${encodeURIComponent(tags.join(";"))}`;
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
- return cache.match(this.getCacheUrlKey(doId, tags));
244
+ return cache.match(this.getCacheUrlKey(opts));
208
245
  }
209
246
  catch (e) {
210
247
  error("Error while fetching from regional cache", e);
211
248
  }
212
249
  }
213
- async putToRegionalCache(doId, tags, hasBeenRevalidated) {
250
+ async putToRegionalCache(optsKey, value) {
214
251
  if (!this.opts.regionalCache)
215
252
  return;
216
253
  const cache = await this.getCacheInstance();
217
254
  if (!cache)
218
255
  return;
219
- await cache.put(this.getCacheUrlKey(doId, tags), new Response(`${hasBeenRevalidated}`, {
256
+ const tags = optsKey.tags;
257
+ await cache.put(this.getCacheUrlKey(optsKey), new Response(`${value}`, {
220
258
  headers: {
221
259
  "cache-control": `max-age=${this.opts.regionalCacheTtlSec ?? 5}`,
222
260
  ...(tags.length > 0
@@ -227,7 +265,7 @@ class ShardedDOTagCache {
227
265
  },
228
266
  }));
229
267
  }
230
- async deleteRegionalCache(doId, tags) {
268
+ async deleteRegionalCache(optsKey) {
231
269
  // We never want to crash because of the cache
232
270
  try {
233
271
  if (!this.opts.regionalCache)
@@ -235,7 +273,7 @@ class ShardedDOTagCache {
235
273
  const cache = await this.getCacheInstance();
236
274
  if (!cache)
237
275
  return;
238
- await cache.delete(this.getCacheUrlKey(doId, tags));
276
+ await cache.delete(this.getCacheUrlKey(optsKey));
239
277
  }
240
278
  catch (e) {
241
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,13 +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
- expect(cache.getCacheUrlKey(doId1, ["_N_T_/tag1"])).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");
281
285
  const doId2 = new DOId({
282
286
  baseShardId: "shard-1",
283
287
  numberOfReplicas: 1,
284
288
  shardType: "hard",
285
289
  });
286
- expect(cache.getCacheUrlKey(doId2, ["tag1"])).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");
287
291
  });
288
292
  });
289
293
  describe("performWriteTagsWithRetry", () => {
@@ -6,6 +6,13 @@ export function withFilter({ tagCache, filterFn }) {
6
6
  return {
7
7
  name: `filtered-${tagCache.name}`,
8
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
+ },
9
16
  getPathsByTags: tagCache.getPathsByTags
10
17
  ? async (tags) => {
11
18
  const filteredTags = tags.filter(filterFn);
@@ -3,6 +3,7 @@ import { softTagFilter, withFilter } from "./tag-cache-filter";
3
3
  const mockedTagCache = {
4
4
  name: "mocked",
5
5
  mode: "nextMode",
6
+ getLastRevalidated: vi.fn(),
6
7
  getPathsByTags: vi.fn(),
7
8
  hasBeenRevalidated: vi.fn(),
8
9
  writeTags: vi.fn(),
@@ -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";
@@ -64,13 +65,13 @@ export async function bundleServer(buildOpts) {
64
65
  // Next traces files using the default conditions from `nft` (`node`, `require`, `import` and `default`)
65
66
  //
66
67
  // Because we use the `node` platform for this build, the "module" condition is used when no conditions are defined.
67
- // 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.
68
69
  //
69
70
  // See:
70
71
  // - default nft conditions: https://github.com/vercel/nft/blob/2b55b01/readme.md#exports--imports
71
72
  // - Next no explicit override: https://github.com/vercel/next.js/blob/2efcf11/packages/next/src/build/collect-build-traces.ts#L287
72
73
  // - ESBuild `node` platform: https://esbuild.github.io/api/#platform
73
- conditions: [],
74
+ conditions: getOpenNextConfig(buildOpts).cloudflare?.useWorkerdCondition === false ? [] : ["workerd"],
74
75
  plugins: [
75
76
  shimRequireHook(buildOpts),
76
77
  inlineDynamicRequires(updater, buildOpts),
@@ -87,7 +88,7 @@ export async function bundleServer(buildOpts) {
87
88
  // Apply updater updates, must be the last plugin
88
89
  updater.plugin,
89
90
  ],
90
- external: ["./middleware/handler.mjs", "*.wasm"],
91
+ external: ["./middleware/handler.mjs"],
91
92
  alias: {
92
93
  // Note: it looks like node-fetch is actually not necessary for us, so we could replace it with an empty shim
93
94
  // but just to be safe we replace it with a module that re-exports the native fetch
@@ -124,6 +125,11 @@ export async function bundleServer(buildOpts) {
124
125
  // This define should be safe to use for Next 14.2+, earlier versions (13.5 and less) will cause trouble
125
126
  "process.env.__NEXT_EXPERIMENTAL_REACT": `${needsExperimentalReact(nextConfig)}`,
126
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
+ },
127
133
  platform: "node",
128
134
  });
129
135
  fs.writeFileSync(openNextServerBundle + ".meta.json", JSON.stringify(result.metafile, null, 2));
@@ -132,7 +138,7 @@ export async function bundleServer(buildOpts) {
132
138
  if (isMonorepo) {
133
139
  fs.writeFileSync(path.join(outputPath, "handler.mjs"), `export { handler } from "./${normalizePath(packagePath)}/handler.mjs";`);
134
140
  }
135
- 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`);
136
142
  }
137
143
  /**
138
144
  * This function applies patches required for the code to run on workers.
@@ -142,6 +148,7 @@ export async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
142
148
  const patchedCode = await patchCodeWithValidations(code, [
143
149
  ["require", patches.patchRequire],
144
150
  ["cacheHandler", (code) => patches.patchCache(code, buildOpts)],
151
+ ["composableCache", (code) => patches.patchComposableCache(code, buildOpts), { isOptional: true }],
145
152
  [
146
153
  "'require(this.middlewareManifestPath)'",
147
154
  (code) => patches.inlineMiddlewareManifestRequire(code, buildOpts),
@@ -152,6 +159,11 @@ export async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
152
159
  // workers do not support dynamic require nor require.resolve
153
160
  (code) => code.replace('require.resolve("./cache.cjs")', '"unused"'),
154
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
+ ],
155
167
  ]);
156
168
  await writeFile(workerOutputFile, patchedCode);
157
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\(/,
@@ -19,7 +19,8 @@ export function setWranglerExternal() {
19
19
  name: "wrangler-externals",
20
20
  setup: async (build) => {
21
21
  const namespace = "wrangler-externals-plugin";
22
- build.onResolve({ filter: /(\.bin|\.wasm\?module)$/ }, ({ path, importer }) => {
22
+ //TODO: Ideally in the future we would like to analyze the files in case they are using wasm in a Node way (i.e. WebAssembly.instantiate)
23
+ build.onResolve({ filter: /(\.bin|\.wasm(\?module)?)$/ }, ({ path, importer }) => {
23
24
  return {
24
25
  path: resolve(dirname(importer), path),
25
26
  namespace,
@@ -1,4 +1,4 @@
1
- import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
1
+ import type { OpenNextConfig } from "../../../api/config.js";
2
2
  /**
3
3
  * Ensures open next is configured for cloudflare.
4
4
  *
@@ -0,0 +1,38 @@
1
+ import type { BuildOptions } from "@opennextjs/aws/build/helper.js";
2
+ /**
3
+ * This function transforms the exports (or imports) object from the package.json
4
+ * to only include the build condition if found (e.g. "workerd") and remove everything else.
5
+ * If no build condition is found, it keeps everything as is.
6
+ * It also returns a boolean indicating if the build condition was found.
7
+ * @param conditionMap The exports (or imports) object from the package.json
8
+ * @param condition The build condition to look for
9
+ * @returns An object with the transformed exports and a boolean indicating if the build condition was found
10
+ */
11
+ export declare function transformBuildCondition(conditionMap: {
12
+ [key: string]: unknown;
13
+ }, condition: string): {
14
+ transformedExports: {
15
+ [key: string]: unknown;
16
+ };
17
+ hasBuildCondition: boolean;
18
+ };
19
+ interface PackageJson {
20
+ name: string;
21
+ exports?: {
22
+ [key: string]: unknown;
23
+ };
24
+ imports?: {
25
+ [key: string]: unknown;
26
+ };
27
+ }
28
+ /**
29
+ *
30
+ * @param json The package.json object
31
+ * @returns An object with the transformed package.json and a boolean indicating if the build condition was found
32
+ */
33
+ export declare function transformPackageJson(json: PackageJson): {
34
+ transformed: PackageJson;
35
+ hasBuildCondition: boolean;
36
+ };
37
+ export declare function copyWorkerdPackages(options: BuildOptions, nodePackages: Map<string, string>): Promise<void>;
38
+ export {};
@@ -0,0 +1,80 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { loadConfig } from "@opennextjs/aws/adapters/config/util.js";
4
+ import logger from "@opennextjs/aws/logger.js";
5
+ import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
6
+ /**
7
+ * This function transforms the exports (or imports) object from the package.json
8
+ * to only include the build condition if found (e.g. "workerd") and remove everything else.
9
+ * If no build condition is found, it keeps everything as is.
10
+ * It also returns a boolean indicating if the build condition was found.
11
+ * @param conditionMap The exports (or imports) object from the package.json
12
+ * @param condition The build condition to look for
13
+ * @returns An object with the transformed exports and a boolean indicating if the build condition was found
14
+ */
15
+ export function transformBuildCondition(conditionMap, condition) {
16
+ const transformed = {};
17
+ const hasTopLevelBuildCondition = Object.keys(conditionMap).some((key) => key === condition && typeof conditionMap[key] === "string");
18
+ let hasBuildCondition = hasTopLevelBuildCondition;
19
+ for (const [key, value] of Object.entries(conditionMap)) {
20
+ if (typeof value === "object" && value != null) {
21
+ const { transformedExports, hasBuildCondition: innerBuildCondition } = transformBuildCondition(value, condition);
22
+ transformed[key] = transformedExports;
23
+ hasBuildCondition ||= innerBuildCondition;
24
+ }
25
+ else {
26
+ // If it doesn't have the build condition, we need to keep everything as is
27
+ // If it has the build condition, we need to keep only the build condition
28
+ // and remove everything else
29
+ if (!hasTopLevelBuildCondition) {
30
+ transformed[key] = value;
31
+ }
32
+ else if (key === condition) {
33
+ transformed[key] = value;
34
+ }
35
+ }
36
+ }
37
+ return { transformedExports: transformed, hasBuildCondition };
38
+ }
39
+ /**
40
+ *
41
+ * @param json The package.json object
42
+ * @returns An object with the transformed package.json and a boolean indicating if the build condition was found
43
+ */
44
+ export function transformPackageJson(json) {
45
+ const transformed = structuredClone(json);
46
+ let hasBuildCondition = false;
47
+ if (json.exports) {
48
+ const exp = transformBuildCondition(json.exports, "workerd");
49
+ transformed.exports = exp.transformedExports;
50
+ hasBuildCondition ||= exp.hasBuildCondition;
51
+ }
52
+ if (json.imports) {
53
+ const imp = transformBuildCondition(json.imports, "workerd");
54
+ transformed.imports = imp.transformedExports;
55
+ hasBuildCondition ||= imp.hasBuildCondition;
56
+ }
57
+ return { transformed, hasBuildCondition };
58
+ }
59
+ export async function copyWorkerdPackages(options, nodePackages) {
60
+ const isNodeModuleRegex = getCrossPlatformPathRegex(`.*/node_modules/(?<pkg>.*)`, { escape: false });
61
+ // Copy full external packages when they use "workerd" build condition
62
+ const nextConfig = loadConfig(path.join(options.appBuildOutputPath, ".next"));
63
+ const externalPackages = nextConfig.serverExternalPackages ?? [];
64
+ for (const [src, dst] of nodePackages.entries()) {
65
+ try {
66
+ const pkgJson = JSON.parse(await fs.readFile(path.join(src, "package.json"), "utf8"));
67
+ const { transformed, hasBuildCondition } = transformPackageJson(pkgJson);
68
+ const match = src.match(isNodeModuleRegex);
69
+ if (match?.groups?.pkg && externalPackages.includes(match.groups.pkg) && hasBuildCondition) {
70
+ logger.debug(`Copying package using a workerd condition: ${path.relative(options.appPath, src)} -> ${path.relative(options.appPath, dst)}`);
71
+ await fs.cp(src, dst, { recursive: true, force: true });
72
+ // Overwrite with the transformed package.json
73
+ await fs.writeFile(path.join(dst, "package.json"), JSON.stringify(transformed), "utf8");
74
+ }
75
+ }
76
+ catch {
77
+ logger.error(`Failed to copy ${src}`);
78
+ }
79
+ }
80
+ }
@@ -0,0 +1 @@
1
+ export {};