@opennextjs/cloudflare 1.6.1 → 1.6.3

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.
@@ -5,4 +5,5 @@ export declare class DOShardedTagCache extends DurableObject<CloudflareEnv> {
5
5
  getLastRevalidated(tags: string[]): Promise<number>;
6
6
  hasBeenRevalidated(tags: string[], lastModified?: number): Promise<boolean>;
7
7
  writeTags(tags: string[], lastModified: number): Promise<void>;
8
+ getRevalidationTimes(tags: string[]): Promise<Record<string, number>>;
8
9
  }
@@ -34,4 +34,10 @@ export class DOShardedTagCache extends DurableObject {
34
34
  this.sql.exec(`INSERT OR REPLACE INTO revalidations (tag, revalidatedAt) VALUES (?, ?)`, tag, lastModified);
35
35
  });
36
36
  }
37
+ async getRevalidationTimes(tags) {
38
+ const result = this.sql
39
+ .exec(`SELECT tag, revalidatedAt FROM revalidations WHERE tag IN (${tags.map(() => "?").join(", ")})`, ...tags)
40
+ .toArray();
41
+ return Object.fromEntries(result.map((row) => [row.tag, row.revalidatedAt]));
42
+ }
37
43
  }
@@ -1,4 +1,5 @@
1
1
  import type { NextModeTagCache } from "@opennextjs/aws/types/overrides.js";
2
+ import { DOShardedTagCache } from "../../durable-objects/sharded-tag-cache.js";
2
3
  export declare const DEFAULT_WRITE_RETRIES = 3;
3
4
  export declare const DEFAULT_NUM_SHARDS = 4;
4
5
  export declare const NAME = "do-sharded-tag-cache";
@@ -34,6 +35,13 @@ interface ShardedDOTagCacheOptions {
34
35
  * @default 5
35
36
  */
36
37
  regionalCacheTtlSec?: number;
38
+ /**
39
+ * Whether to persist missing tags in the regional cache.
40
+ * This is dangerous if you don't invalidate the Cache API when you revalidate tags as you could end up storing stale data in the data cache.
41
+ *
42
+ * @default false
43
+ */
44
+ regionalCacheDangerouslyPersistMissingTags?: boolean;
37
45
  /**
38
46
  * Enable shard replication to handle higher load.
39
47
  *
@@ -91,7 +99,6 @@ export declare class DOId {
91
99
  interface CacheTagKeyOptions {
92
100
  doId: DOId;
93
101
  tags: string[];
94
- type: "boolean" | "number";
95
102
  }
96
103
  declare class ShardedDOTagCache implements NextModeTagCache {
97
104
  private opts;
@@ -145,9 +152,21 @@ declare class ShardedDOTagCache implements NextModeTagCache {
145
152
  writeTags(tags: string[]): Promise<void>;
146
153
  performWriteTagsWithRetry(doId: DOId, tags: string[], lastModified: number, retryNumber?: number): Promise<void>;
147
154
  getCacheInstance(): Promise<Cache | undefined>;
148
- getCacheUrlKey(opts: CacheTagKeyOptions): string;
149
- getFromRegionalCache(opts: CacheTagKeyOptions): Promise<Response | undefined>;
150
- putToRegionalCache(optsKey: CacheTagKeyOptions, value: number | boolean): Promise<void>;
155
+ getCacheUrlKey(doId: DOId, tag: string): string;
156
+ /**
157
+ * Get the last revalidation time for the tags from the regional cache
158
+ * If the cache is not enabled, it will return an empty array
159
+ * @returns An array of objects with the tag and the last revalidation time
160
+ */
161
+ getFromRegionalCache(opts: CacheTagKeyOptions): Promise<{
162
+ tag: string;
163
+ time: number;
164
+ }[]>;
165
+ putToRegionalCache(optsKey: CacheTagKeyOptions, stub: DurableObjectStub<DOShardedTagCache>): Promise<void>;
166
+ /**
167
+ * Deletes the regional cache for the given tags
168
+ * This is used to ensure that the cache is cleared when the tags are revalidated
169
+ */
151
170
  deleteRegionalCache(optsKey: CacheTagKeyOptions): Promise<void>;
152
171
  }
153
172
  declare const _default: (opts?: ShardedDOTagCacheOptions) => ShardedDOTagCache;
@@ -179,26 +179,25 @@ class ShardedDOTagCache {
179
179
  const { isDisabled } = await this.getConfig();
180
180
  if (isDisabled)
181
181
  return 0;
182
+ if (tags.length === 0)
183
+ return 0; // No tags to check
184
+ const deduplicatedTags = Array.from(new Set(tags)); // We deduplicate the tags to avoid unnecessary requests
182
185
  try {
183
- const shardedTagGroups = this.groupTagsByDO({ tags });
186
+ const shardedTagGroups = this.groupTagsByDO({ tags: deduplicatedTags });
184
187
  const shardedTagRevalidationOutcomes = await Promise.all(shardedTagGroups.map(async ({ doId, tags }) => {
185
- const cachedValue = await this.getFromRegionalCache({ doId, tags, type: "number" });
186
- if (cachedValue) {
187
- const cached = await cachedValue.text();
188
- try {
189
- return parseInt(cached, 10);
190
- }
191
- catch (e) {
192
- debug("Error while parsing cached value", e);
193
- // If we can't parse the cached value, we should just ignore it and go to the durable object
194
- }
188
+ const cachedValue = await this.getFromRegionalCache({ doId, tags });
189
+ // If all the value were found in the regional cache, we can just return the max value
190
+ if (cachedValue.length === tags.length) {
191
+ return Math.max(...cachedValue.map((item) => item.time));
195
192
  }
193
+ // Otherwise we need to check the durable object on the ones that were not found in the cache
194
+ const filteredTags = deduplicatedTags.filter((tag) => !cachedValue.some((item) => item.tag === tag));
196
195
  const stub = this.getDurableObjectStub(doId);
197
- const _lastRevalidated = await stub.getLastRevalidated(tags);
198
- if (!_lastRevalidated) {
199
- getCloudflareContext().ctx.waitUntil(this.putToRegionalCache({ doId, tags, type: "number" }, _lastRevalidated));
200
- }
201
- return _lastRevalidated;
196
+ const lastRevalidated = await stub.getLastRevalidated(filteredTags);
197
+ const result = Math.max(...cachedValue.map((item) => item.time), lastRevalidated);
198
+ // We then need to populate the regional cache with the missing tags
199
+ getCloudflareContext().ctx.waitUntil(this.putToRegionalCache({ doId, tags }, stub));
200
+ return result;
202
201
  }));
203
202
  return Math.max(...shardedTagRevalidationOutcomes);
204
203
  }
@@ -221,17 +220,20 @@ class ShardedDOTagCache {
221
220
  try {
222
221
  const shardedTagGroups = this.groupTagsByDO({ tags });
223
222
  const shardedTagRevalidationOutcomes = await Promise.all(shardedTagGroups.map(async ({ doId, tags }) => {
224
- const cachedValue = await this.getFromRegionalCache({ doId, tags, type: "boolean" });
225
- if (cachedValue) {
226
- return (await cachedValue.text()) === "true";
223
+ const cachedValue = await this.getFromRegionalCache({ doId, tags });
224
+ // If one of the cached values is newer than the lastModified, we can return true
225
+ const cacheHasBeenRevalidated = cachedValue.some((cachedValue) => {
226
+ return (cachedValue.time ?? 0) > (lastModified ?? Date.now());
227
+ });
228
+ if (cacheHasBeenRevalidated) {
229
+ return true;
227
230
  }
228
231
  const stub = this.getDurableObjectStub(doId);
229
232
  const _hasBeenRevalidated = await stub.hasBeenRevalidated(tags, lastModified);
230
- //TODO: Do we want to cache the result if it has been revalidated ?
231
- // If we do so, we risk causing cache MISS even though it has been revalidated elsewhere
232
- // On the other hand revalidating a tag that is used in a lot of places will cause a lot of requests
233
- if (!_hasBeenRevalidated) {
234
- getCloudflareContext().ctx.waitUntil(this.putToRegionalCache({ doId, tags, type: "boolean" }, _hasBeenRevalidated));
233
+ const remainingTags = tags.filter((tag) => !cachedValue.some((item) => item.tag === tag));
234
+ if (remainingTags.length > 0) {
235
+ // We need to put the missing tags in the regional cache
236
+ getCloudflareContext().ctx.waitUntil(this.putToRegionalCache({ doId, tags: remainingTags }, stub));
235
237
  }
236
238
  return _hasBeenRevalidated;
237
239
  }));
@@ -266,10 +268,7 @@ class ShardedDOTagCache {
266
268
  await stub.writeTags(tags, lastModified);
267
269
  // Depending on the shards and the tags, deleting from the regional cache will not work for every tag
268
270
  // We also need to delete both cache
269
- await Promise.all([
270
- this.deleteRegionalCache({ doId, tags, type: "boolean" }),
271
- this.deleteRegionalCache({ doId, tags, type: "number" }),
272
- ]);
271
+ await Promise.all([this.deleteRegionalCache({ doId, tags })]);
273
272
  }
274
273
  catch (e) {
275
274
  error("Error while writing tags", e);
@@ -293,41 +292,79 @@ class ShardedDOTagCache {
293
292
  }
294
293
  return this.localCache;
295
294
  }
296
- getCacheUrlKey(opts) {
297
- const { doId, tags, type } = opts;
298
- return `http://local.cache/shard/${doId.shardId}?type=${type}&tags=${encodeURIComponent(tags.join(";"))}`;
295
+ getCacheUrlKey(doId, tag) {
296
+ return `http://local.cache/shard/${doId.shardId}?tag=${encodeURIComponent(tag)}`;
299
297
  }
298
+ /**
299
+ * Get the last revalidation time for the tags from the regional cache
300
+ * If the cache is not enabled, it will return an empty array
301
+ * @returns An array of objects with the tag and the last revalidation time
302
+ */
300
303
  async getFromRegionalCache(opts) {
301
304
  try {
302
305
  if (!this.opts.regionalCache)
303
- return;
306
+ return [];
304
307
  const cache = await this.getCacheInstance();
305
308
  if (!cache)
306
- return;
307
- return cache.match(this.getCacheUrlKey(opts));
309
+ return [];
310
+ const result = await Promise.all(opts.tags.map(async (tag) => {
311
+ const cachedResponse = await cache.match(this.getCacheUrlKey(opts.doId, tag));
312
+ if (!cachedResponse)
313
+ return null;
314
+ const cachedText = await cachedResponse.text();
315
+ try {
316
+ return { tag, time: parseInt(cachedText, 10) };
317
+ }
318
+ catch (e) {
319
+ debugCache("Error while parsing cached value", e);
320
+ return null;
321
+ }
322
+ }));
323
+ return result.filter((item) => item !== null);
308
324
  }
309
325
  catch (e) {
310
326
  error("Error while fetching from regional cache", e);
327
+ return [];
311
328
  }
312
329
  }
313
- async putToRegionalCache(optsKey, value) {
330
+ async putToRegionalCache(optsKey, stub) {
314
331
  if (!this.opts.regionalCache)
315
332
  return;
316
333
  const cache = await this.getCacheInstance();
317
334
  if (!cache)
318
335
  return;
319
336
  const tags = optsKey.tags;
320
- await cache.put(this.getCacheUrlKey(optsKey), new Response(`${value}`, {
321
- headers: {
322
- "cache-control": `max-age=${this.opts.regionalCacheTtlSec ?? 5}`,
323
- ...(tags.length > 0
324
- ? {
325
- "cache-tag": tags.join(","),
326
- }
327
- : {}),
328
- },
337
+ const tagsLastRevalidated = await stub.getRevalidationTimes(tags);
338
+ await Promise.all(tags.map(async (tag) => {
339
+ let lastRevalidated = tagsLastRevalidated[tag];
340
+ if (lastRevalidated === undefined) {
341
+ if (this.opts.regionalCacheDangerouslyPersistMissingTags) {
342
+ lastRevalidated = 0; // If the tag is not found, we set it to 0 as it means it has never been revalidated before.
343
+ }
344
+ else {
345
+ debugCache("Tag not found in revalidation times", { tag, optsKey });
346
+ return; // If the tag is not found, we skip it
347
+ }
348
+ }
349
+ const cacheKey = this.getCacheUrlKey(optsKey.doId, tag);
350
+ debugCache("Putting to regional cache", { cacheKey, lastRevalidated });
351
+ await cache.put(cacheKey, new Response(lastRevalidated.toString(), {
352
+ status: 200,
353
+ headers: {
354
+ "cache-control": `max-age=${this.opts.regionalCacheTtlSec ?? 5}`,
355
+ ...(tags.length > 0
356
+ ? {
357
+ "cache-tag": tags.join(","),
358
+ }
359
+ : {}),
360
+ },
361
+ }));
329
362
  }));
330
363
  }
364
+ /**
365
+ * Deletes the regional cache for the given tags
366
+ * This is used to ensure that the cache is cleared when the tags are revalidated
367
+ */
331
368
  async deleteRegionalCache(optsKey) {
332
369
  // We never want to crash because of the cache
333
370
  try {
@@ -336,7 +373,11 @@ class ShardedDOTagCache {
336
373
  const cache = await this.getCacheInstance();
337
374
  if (!cache)
338
375
  return;
339
- await cache.delete(this.getCacheUrlKey(optsKey));
376
+ await Promise.all(optsKey.tags.map(async (tag) => {
377
+ const cacheKey = this.getCacheUrlKey(optsKey.doId, tag);
378
+ debugCache("Deleting from regional cache", { cacheKey });
379
+ await cache.delete(cacheKey);
380
+ }));
340
381
  }
341
382
  catch (e) {
342
383
  debugCache("Error while deleting from regional cache", e);
@@ -63,7 +63,7 @@ export async function build(options, config, projectOpts, wranglerConfig) {
63
63
  }
64
64
  await createServerBundle(options);
65
65
  await compileDurableObjects(options);
66
- await bundleServer(options);
66
+ await bundleServer(options, projectOpts);
67
67
  if (!projectOpts.skipWranglerConfigCheck) {
68
68
  await createWranglerConfigIfNotExistent(projectOpts);
69
69
  }
@@ -1,8 +1,9 @@
1
1
  import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
2
+ import type { ProjectOptions } from "../project-options.js";
2
3
  /**
3
4
  * Bundle the Open Next server.
4
5
  */
5
- export declare function bundleServer(buildOpts: BuildOptions): Promise<void>;
6
+ export declare function bundleServer(buildOpts: BuildOptions, projectOpts: ProjectOptions): Promise<void>;
6
7
  /**
7
8
  * This function apply updates to the bundled code.
8
9
  */
@@ -39,7 +39,7 @@ const optionalDependencies = [
39
39
  /**
40
40
  * Bundle the Open Next server.
41
41
  */
42
- export async function bundleServer(buildOpts) {
42
+ export async function bundleServer(buildOpts, projectOpts) {
43
43
  copyPackageCliFiles(packageDistDir, buildOpts);
44
44
  const { appPath, outputDir, monorepoRoot, debug } = buildOpts;
45
45
  const baseManifestPath = path.join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next");
@@ -60,9 +60,9 @@ export async function bundleServer(buildOpts) {
60
60
  format: "esm",
61
61
  target: "esnext",
62
62
  // Minify code as much as possible but stay safe by not renaming identifiers
63
- minifyWhitespace: !debug,
63
+ minifyWhitespace: projectOpts.minify && !debug,
64
64
  minifyIdentifiers: false,
65
- minifySyntax: !debug,
65
+ minifySyntax: projectOpts.minify && !debug,
66
66
  legalComments: "none",
67
67
  metafile: true,
68
68
  // Next traces files using the default conditions from `nft` (`node`, `require`, `import` and `default`)
@@ -154,6 +154,7 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
154
154
  const isAfter141 = buildHelper.compareSemver(options.nextVersion, ">=", "14.1");
155
155
  const isAfter142 = buildHelper.compareSemver(options.nextVersion, ">=", "14.2");
156
156
  const isAfter152 = buildHelper.compareSemver(options.nextVersion, ">=", "15.2.0");
157
+ const isAfter154 = buildHelper.compareSemver(options.nextVersion, ">=", "15.4.0");
157
158
  const disableRouting = isBefore13413 || config.middleware?.external;
158
159
  const plugins = [
159
160
  openNextReplacementPlugin({
@@ -164,6 +165,7 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
164
165
  ...(disableRouting ? ["withRouting"] : []),
165
166
  ...(isAfter142 ? ["patchAsyncStorage"] : []),
166
167
  ...(isAfter141 ? ["appendPrefetch"] : []),
168
+ ...(isAfter154 ? [] : ["setInitialURL"]),
167
169
  ],
168
170
  }),
169
171
  openNextReplacementPlugin({
@@ -37,22 +37,18 @@ function getRequires(idVariable, files, serverDir) {
37
37
  export function inlineDynamicRequires(updater, buildOpts) {
38
38
  updater.updateContent("inline-node-module-loader", [
39
39
  {
40
- field: {
41
- filter: getCrossPlatformPathRegex(String.raw `/module-loader/node-module-loader\.js$`, {
42
- escape: false,
43
- }),
44
- contentFilter: /class NodeModuleLoader {/,
45
- callback: async ({ contents }) => patchCode(contents, await getNodeModuleLoaderRule(buildOpts)),
46
- },
40
+ filter: getCrossPlatformPathRegex(String.raw `/module-loader/node-module-loader\.js$`, {
41
+ escape: false,
42
+ }),
43
+ contentFilter: /class NodeModuleLoader {/,
44
+ callback: async ({ contents }) => patchCode(contents, await getNodeModuleLoaderRule(buildOpts)),
47
45
  },
48
46
  ]);
49
47
  updater.updateContent("inline-require-page", [
50
48
  {
51
- field: {
52
- filter: getCrossPlatformPathRegex(String.raw `/next/dist/server/require\.js$`, { escape: false }),
53
- contentFilter: /function requirePage\(/,
54
- callback: async ({ contents }) => patchCode(contents, await getRequirePageRule(buildOpts)),
55
- },
49
+ filter: getCrossPlatformPathRegex(String.raw `/next/dist/server/require\.js$`, { escape: false }),
50
+ contentFilter: /function requirePage\(/,
51
+ callback: async ({ contents }) => patchCode(contents, await getRequirePageRule(buildOpts)),
56
52
  },
57
53
  ]);
58
54
  return { name: "inline-dynamic-requires", setup() { } };
@@ -9,11 +9,9 @@ import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
9
9
  export function inlineFindDir(updater, buildOpts) {
10
10
  return updater.updateContent("inline-find-dir", [
11
11
  {
12
- field: {
13
- filter: getCrossPlatformPathRegex(String.raw `/next/dist/lib/find-pages-dir\.js$`, { escape: false }),
14
- contentFilter: /function findDir\(/,
15
- callback: async ({ contents }) => patchCode(contents, await getRule(buildOpts)),
16
- },
12
+ filter: getCrossPlatformPathRegex(String.raw `/next/dist/lib/find-pages-dir\.js$`, { escape: false }),
13
+ contentFilter: /function findDir\(/,
14
+ callback: async ({ contents }) => patchCode(contents, await getRule(buildOpts)),
17
15
  },
18
16
  ]);
19
17
  }
@@ -8,31 +8,25 @@ export function patchInstrumentation(updater, buildOpts) {
8
8
  const builtInstrumentationPath = getBuiltInstrumentationPath(buildOpts);
9
9
  updater.updateContent("patch-instrumentation-next15-4", [
10
10
  {
11
- field: {
12
- filter: getCrossPlatformPathRegex(String.raw `/server/lib/router-utils/instrumentation-globals.external\.js$`, {
13
- escape: false,
14
- }),
15
- contentFilter: /getInstrumentationModule\(/,
16
- callback: ({ contents }) => patchCode(contents, getNext154Rule(builtInstrumentationPath)),
17
- },
11
+ filter: getCrossPlatformPathRegex(String.raw `/server/lib/router-utils/instrumentation-globals.external\.js$`, {
12
+ escape: false,
13
+ }),
14
+ contentFilter: /getInstrumentationModule\(/,
15
+ callback: ({ contents }) => patchCode(contents, getNext154Rule(builtInstrumentationPath)),
18
16
  },
19
17
  ]);
20
18
  updater.updateContent("patch-instrumentation-next15", [
21
19
  {
22
- field: {
23
- filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/,
24
- contentFilter: /async loadInstrumentationModule\(/,
25
- callback: ({ contents }) => patchCode(contents, getNext15Rule(builtInstrumentationPath)),
26
- },
20
+ filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/,
21
+ contentFilter: /async loadInstrumentationModule\(/,
22
+ callback: ({ contents }) => patchCode(contents, getNext15Rule(builtInstrumentationPath)),
27
23
  },
28
24
  ]);
29
25
  updater.updateContent("patch-instrumentation-next14", [
30
26
  {
31
- field: {
32
- filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/,
33
- contentFilter: /async prepareImpl\(/,
34
- callback: ({ contents }) => patchCode(contents, getNext14Rule(builtInstrumentationPath)),
35
- },
27
+ filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/,
28
+ contentFilter: /async prepareImpl\(/,
29
+ callback: ({ contents }) => patchCode(contents, getNext14Rule(builtInstrumentationPath)),
36
30
  },
37
31
  ]);
38
32
  return {
@@ -13,16 +13,14 @@ import { normalizePath } from "../../utils/normalize-path.js";
13
13
  export function inlineLoadManifest(updater, buildOpts) {
14
14
  return updater.updateContent("inline-load-manifest", [
15
15
  {
16
- field: {
17
- filter: getCrossPlatformPathRegex(String.raw `/next/dist/server/load-manifest(\.external)?\.js$`, {
18
- escape: false,
19
- }),
20
- contentFilter: /function loadManifest\(/,
21
- callback: async ({ contents }) => {
22
- contents = await patchCode(contents, await getLoadManifestRule(buildOpts));
23
- contents = await patchCode(contents, await getEvalManifestRule(buildOpts));
24
- return contents;
25
- },
16
+ filter: getCrossPlatformPathRegex(String.raw `/next/dist/server/load-manifest(\.external)?\.js$`, {
17
+ escape: false,
18
+ }),
19
+ contentFilter: /function loadManifest\(/,
20
+ callback: async ({ contents }) => {
21
+ contents = await patchCode(contents, await getLoadManifestRule(buildOpts));
22
+ contents = await patchCode(contents, await getEvalManifestRule(buildOpts));
23
+ return contents;
26
24
  },
27
25
  },
28
26
  ]);
@@ -14,21 +14,19 @@ import { normalizePath } from "../../utils/index.js";
14
14
  export function patchNextServer(updater, buildOpts) {
15
15
  return updater.updateContent("next-server", [
16
16
  {
17
- field: {
18
- filter: getCrossPlatformPathRegex(String.raw `/next/dist/server/next-server\.js$`, {
19
- escape: false,
20
- }),
21
- contentFilter: /getBuildId\(/,
22
- callback: async ({ contents }) => {
23
- const { outputDir } = buildOpts;
24
- contents = patchCode(contents, buildIdRule);
25
- const outputPath = path.join(outputDir, "server-functions/default");
26
- const cacheHandler = path.join(outputPath, getPackagePath(buildOpts), "cache.cjs");
27
- contents = patchCode(contents, createCacheHandlerRule(cacheHandler));
28
- const composableCacheHandler = path.join(outputPath, getPackagePath(buildOpts), "composable-cache.cjs");
29
- contents = patchCode(contents, createComposableCacheHandlersRule(composableCacheHandler));
30
- return contents;
31
- },
17
+ filter: getCrossPlatformPathRegex(String.raw `/next/dist/server/next-server\.js$`, {
18
+ escape: false,
19
+ }),
20
+ contentFilter: /getBuildId\(/,
21
+ callback: async ({ contents }) => {
22
+ const { outputDir } = buildOpts;
23
+ contents = patchCode(contents, buildIdRule);
24
+ const outputPath = path.join(outputDir, "server-functions/default");
25
+ const cacheHandler = path.join(outputPath, getPackagePath(buildOpts), "cache.cjs");
26
+ contents = patchCode(contents, createCacheHandlerRule(cacheHandler));
27
+ const composableCacheHandler = path.join(outputPath, getPackagePath(buildOpts), "composable-cache.cjs");
28
+ contents = patchCode(contents, createComposableCacheHandlersRule(composableCacheHandler));
29
+ return contents;
32
30
  },
33
31
  },
34
32
  ]);
@@ -12,14 +12,12 @@ export function patchResolveCache(updater, buildOpts) {
12
12
  const indexPath = path.relative(buildOpts.appBuildOutputPath, path.join(outputPath, packagePath, `index.mjs`));
13
13
  return updater.updateContent("patch-resolve-cache", [
14
14
  {
15
- field: {
16
- filter: getCrossPlatformPathRegex(indexPath),
17
- contentFilter: /cacheHandlerPath/,
18
- callback: async ({ contents }) => {
19
- contents = patchCode(contents, cacheHandlerRule);
20
- contents = patchCode(contents, compositeCacheHandlerRule);
21
- return contents;
22
- },
15
+ filter: getCrossPlatformPathRegex(indexPath),
16
+ contentFilter: /cacheHandlerPath/,
17
+ callback: async ({ contents }) => {
18
+ contents = patchCode(contents, cacheHandlerRule);
19
+ contents = patchCode(contents, compositeCacheHandlerRule);
20
+ return contents;
23
21
  },
24
22
  },
25
23
  ]);
@@ -8,11 +8,9 @@ import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
8
8
  export function patchDepdDeprecations(updater) {
9
9
  return updater.updateContent("patch-depd-deprecations", [
10
10
  {
11
- field: {
12
- filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/,
13
- contentFilter: /argument fn must be a function/,
14
- callback: ({ contents }) => patchCode(contents, rule),
15
- },
11
+ filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/,
12
+ contentFilter: /argument fn must be a function/,
13
+ callback: ({ contents }) => patchCode(contents, rule),
16
14
  },
17
15
  ]);
18
16
  }
@@ -1,44 +1,42 @@
1
1
  export function fixRequire(updater) {
2
2
  return updater.updateContent("fix-require", [
3
3
  {
4
- field: {
5
- filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/,
6
- contentFilter: /.*/,
7
- callback: ({ contents }) => {
8
- // `eval(...)` is not supported by workerd.
9
- contents = contents.replaceAll(`eval("require")`, "require");
10
- // `@opentelemetry` has a few issues.
11
- //
12
- // Next.js has the following code in `next/dist/server/lib/trace/tracer.js`:
13
- //
14
- // try {
15
- // api = require('@opentelemetry/api');
16
- // } catch (err) {
17
- // api = require('next/dist/compiled/@opentelemetry/api');
18
- // }
19
- //
20
- // The intent is to allow users to install their own version of `@opentelemetry/api`.
21
- //
22
- // The problem is that even when users do not explicitly install `@opentelemetry/api`,
23
- // `require('@opentelemetry/api')` resolves to the package which is a dependency
24
- // of Next.
25
- //
26
- // The second problem is that when Next traces files, it would not copy the `api/build/esm`
27
- // folder (used by the `module` conditions in package.json) it would only copy `api/build/src`.
28
- // This could be solved by updating the next config:
29
- //
30
- // const nextConfig: NextConfig = {
31
- // // ...
32
- // outputFileTracingIncludes: {
33
- // "*": ["./node_modules/@opentelemetry/api/build/**/*"],
34
- // },
35
- // };
36
- //
37
- // We can consider doing that when we want to enable users to install their own version
38
- // of `@opentelemetry/api`. For now we simply use the pre-compiled version.
39
- contents = contents.replace(/require\(.@opentelemetry\/api.\)/g, `require("next/dist/compiled/@opentelemetry/api")`);
40
- return contents;
41
- },
4
+ filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/,
5
+ contentFilter: /.*/,
6
+ callback: ({ contents }) => {
7
+ // `eval(...)` is not supported by workerd.
8
+ contents = contents.replaceAll(`eval("require")`, "require");
9
+ // `@opentelemetry` has a few issues.
10
+ //
11
+ // Next.js has the following code in `next/dist/server/lib/trace/tracer.js`:
12
+ //
13
+ // try {
14
+ // api = require('@opentelemetry/api');
15
+ // } catch (err) {
16
+ // api = require('next/dist/compiled/@opentelemetry/api');
17
+ // }
18
+ //
19
+ // The intent is to allow users to install their own version of `@opentelemetry/api`.
20
+ //
21
+ // The problem is that even when users do not explicitly install `@opentelemetry/api`,
22
+ // `require('@opentelemetry/api')` resolves to the package which is a dependency
23
+ // of Next.
24
+ //
25
+ // The second problem is that when Next traces files, it would not copy the `api/build/esm`
26
+ // folder (used by the `module` conditions in package.json) it would only copy `api/build/src`.
27
+ // This could be solved by updating the next config:
28
+ //
29
+ // const nextConfig: NextConfig = {
30
+ // // ...
31
+ // outputFileTracingIncludes: {
32
+ // "*": ["./node_modules/@opentelemetry/api/build/**/*"],
33
+ // },
34
+ // };
35
+ //
36
+ // We can consider doing that when we want to enable users to install their own version
37
+ // of `@opentelemetry/api`. For now we simply use the pre-compiled version.
38
+ contents = contents.replace(/require\(.@opentelemetry\/api.\)/g, `require("next/dist/compiled/@opentelemetry/api")`);
39
+ return contents;
42
40
  },
43
41
  },
44
42
  ]);
@@ -65,13 +65,11 @@ export const patchResRevalidate = {
65
65
  patches: [
66
66
  {
67
67
  versions: ">=14.2.0",
68
- field: {
69
- pathFilter: getCrossPlatformPathRegex(String.raw `(pages-api\.runtime\.prod\.js|node/api-resolver\.js)$`, {
70
- escape: false,
71
- }),
72
- contentFilter: /\.trustHostHeader/,
73
- patchCode: async ({ code }) => patchCode(code, rule),
74
- },
68
+ pathFilter: getCrossPlatformPathRegex(String.raw `(pages-api\.runtime\.prod\.js|node/api-resolver\.js)$`, {
69
+ escape: false,
70
+ }),
71
+ contentFilter: /\.trustHostHeader/,
72
+ patchCode: async ({ code }) => patchCode(code, rule),
75
73
  },
76
74
  ],
77
75
  };
@@ -28,13 +28,11 @@ export const patchUseCacheIO = {
28
28
  patches: [
29
29
  {
30
30
  versions: ">=15.3.1",
31
- field: {
32
- pathFilter: getCrossPlatformPathRegex(String.raw `server/app-render/async-local-storage\.js$`, {
33
- escape: false,
34
- }),
35
- contentFilter: /createSnapshot/,
36
- patchCode: async ({ code }) => patchCode(code, rule),
37
- },
31
+ pathFilter: getCrossPlatformPathRegex(String.raw `server/app-render/async-local-storage\.js$`, {
32
+ escape: false,
33
+ }),
34
+ contentFilter: /createSnapshot/,
35
+ patchCode: async ({ code }) => patchCode(code, rule),
38
36
  },
39
37
  ],
40
38
  };
@@ -5,6 +5,8 @@ import logger from "@opennextjs/aws/logger.js";
5
5
  * @param config OpenNext configuration.
6
6
  */
7
7
  export function ensureCloudflareConfig(config) {
8
+ const mwIsMiddlewareExternal = config.middleware?.external === true;
9
+ const mwConfig = mwIsMiddlewareExternal ? config.middleware : undefined;
8
10
  const requirements = {
9
11
  // Check for the default function
10
12
  dftUseCloudflareWrapper: config.default?.override?.wrapper === "cloudflare-node",
@@ -18,10 +20,10 @@ export function ensureCloudflareConfig(config) {
18
20
  config.default?.override?.queue === "direct" ||
19
21
  typeof config.default?.override?.queue === "function",
20
22
  // Check for the middleware function
21
- mwIsMiddlewareExternal: config.middleware?.external === true,
22
- mwUseCloudflareWrapper: config.middleware?.override?.wrapper === "cloudflare-edge",
23
- mwUseEdgeConverter: config.middleware?.override?.converter === "edge",
24
- mwUseFetchProxy: config.middleware?.override?.proxyExternalRequest === "fetch",
23
+ mwIsMiddlewareExternal,
24
+ mwUseCloudflareWrapper: mwConfig?.override?.wrapper === "cloudflare-edge",
25
+ mwUseEdgeConverter: mwConfig?.override?.converter === "edge",
26
+ mwUseFetchProxy: mwConfig?.override?.proxyExternalRequest === "fetch",
25
27
  hasCryptoExternal: config.edgeExternals?.includes("node:crypto"),
26
28
  };
27
29
  if (config.default?.override?.queue === "direct") {
@@ -28,10 +28,10 @@ import type { WorkerEnvVar } from "./helpers.js";
28
28
  *
29
29
  * @param options Build options
30
30
  * @param config OpenNext config
31
- * @param envVars Environment variables
31
+ * @param workerEnvVars Worker Environment variables (taken from the wrangler config files)
32
32
  * @returns Deployment mapping or undefined
33
33
  */
34
- export declare function getDeploymentMapping(options: BuildOptions, config: OpenNextConfig, envVars: WorkerEnvVar): Promise<Record<string, string> | undefined>;
34
+ export declare function getDeploymentMapping(options: BuildOptions, config: OpenNextConfig, workerEnvVars: WorkerEnvVar): Promise<Record<string, string> | undefined>;
35
35
  /**
36
36
  * Update an existing deployment mapping:
37
37
  * - Replace the "current" version with the latest deployed version
@@ -36,13 +36,17 @@ const MS_PER_DAY = 24 * 3600 * 1000;
36
36
  *
37
37
  * @param options Build options
38
38
  * @param config OpenNext config
39
- * @param envVars Environment variables
39
+ * @param workerEnvVars Worker Environment variables (taken from the wrangler config files)
40
40
  * @returns Deployment mapping or undefined
41
41
  */
42
- export async function getDeploymentMapping(options, config, envVars) {
42
+ export async function getDeploymentMapping(options, config, workerEnvVars) {
43
43
  if (config.cloudflare?.skewProtection?.enabled !== true) {
44
44
  return undefined;
45
45
  }
46
+ // Note that `process.env` is spread after `workerEnvVars` since we do want
47
+ // system environment variables to take precedence over the variables defined
48
+ // in the wrangler config files
49
+ const envVars = { ...workerEnvVars, ...process.env };
46
50
  const nextConfig = loadConfig(path.join(options.appBuildOutputPath, ".next"));
47
51
  const deploymentId = nextConfig.deploymentId;
48
52
  if (!deploymentId) {
@@ -26,7 +26,7 @@ export declare function matchLocalPattern(pattern: LocalPattern, url: URL): bool
26
26
  * @param buffer The image bytes
27
27
  * @returns a content type of undefined for unsupported content
28
28
  */
29
- export declare function detectContentType(buffer: Uint8Array): "image/svg+xml" | "image/jpeg" | "image/png" | "image/gif" | "image/webp" | "image/avif" | "image/x-icon" | "image/x-icns" | "image/tiff" | "image/bmp" | undefined;
29
+ export declare function detectContentType(buffer: Uint8Array): "image/svg+xml" | "image/jpeg" | "image/png" | "image/gif" | "image/webp" | "image/avif" | "image/x-icon" | "image/x-icns" | "image/tiff" | "image/bmp" | "image/jxl" | "image/heic" | "application/pdf" | "image/jp2" | undefined;
30
30
  declare global {
31
31
  var __IMAGES_REMOTE_PATTERNS__: RemotePattern[];
32
32
  var __IMAGES_LOCAL_PATTERNS__: LocalPattern[];
@@ -65,12 +65,15 @@ export async function fetchImage(fetcher, imageUrl, ctx) {
65
65
  contentType = detectContentType(value);
66
66
  }
67
67
  if (!contentType) {
68
- // Fallback to the sanitized upstream header when the type can not be detected
68
+ // Fallback to upstream header when the type can not be detected
69
69
  // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/server/image-optimizer.ts#L748
70
- const header = imgResponse.headers.get("content-type") ?? "";
71
- if (header.startsWith("image/") && !header.includes(",")) {
72
- contentType = header;
73
- }
70
+ contentType = imgResponse.headers.get("content-type") ?? "";
71
+ }
72
+ // Sanitize the content type:
73
+ // - Accept images only
74
+ // - Reject multiple content types
75
+ if (!contentType.startsWith("image/") || contentType.includes(",")) {
76
+ contentType = undefined;
74
77
  }
75
78
  if (contentType && !(contentType === SVG && !__IMAGES_ALLOW_SVG__)) {
76
79
  const headers = new Headers(imgResponse.headers);
@@ -126,12 +129,17 @@ const AVIF = "image/avif";
126
129
  const WEBP = "image/webp";
127
130
  const PNG = "image/png";
128
131
  const JPEG = "image/jpeg";
132
+ const JXL = "image/jxl";
133
+ const JP2 = "image/jp2";
134
+ const HEIC = "image/heic";
129
135
  const GIF = "image/gif";
130
136
  const SVG = "image/svg+xml";
131
137
  const ICO = "image/x-icon";
132
138
  const ICNS = "image/x-icns";
133
139
  const TIFF = "image/tiff";
134
140
  const BMP = "image/bmp";
141
+ // pdf will be rejected (not an `image/...` type)
142
+ const PDF = "application/pdf";
135
143
  /**
136
144
  * Detects the content type by looking at the first few bytes of a file
137
145
  *
@@ -174,4 +182,19 @@ export function detectContentType(buffer) {
174
182
  if ([0x42, 0x4d].every((b, i) => buffer[i] === b)) {
175
183
  return BMP;
176
184
  }
185
+ if ([0xff, 0x0a].every((b, i) => buffer[i] === b)) {
186
+ return JXL;
187
+ }
188
+ if ([0x00, 0x00, 0x00, 0x0c, 0x4a, 0x58, 0x4c, 0x20, 0x0d, 0x0a, 0x87, 0x0a].every((b, i) => buffer[i] === b)) {
189
+ return JXL;
190
+ }
191
+ if ([0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63].every((b, i) => !b || buffer[i] === b)) {
192
+ return HEIC;
193
+ }
194
+ if ([0x25, 0x50, 0x44, 0x46, 0x2d].every((b, i) => buffer[i] === b)) {
195
+ return PDF;
196
+ }
197
+ if ([0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20, 0x0d, 0x0a, 0x87, 0x0a].every((b, i) => buffer[i] === b)) {
198
+ return JP2;
199
+ }
177
200
  }
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.6.1",
4
+ "version": "1.6.3",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "opennextjs-cloudflare": "dist/cli/index.js"
@@ -43,7 +43,7 @@
43
43
  "homepage": "https://github.com/opennextjs/opennextjs-cloudflare",
44
44
  "dependencies": {
45
45
  "@dotenvx/dotenvx": "1.31.0",
46
- "@opennextjs/aws": "3.7.0",
46
+ "@opennextjs/aws": "3.7.2",
47
47
  "cloudflare": "^4.4.1",
48
48
  "enquirer": "^2.4.1",
49
49
  "glob": "^11.0.0",