@opennextjs/cloudflare 1.0.0-beta.3 → 1.0.0-beta.4

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.
@@ -119,7 +119,7 @@ declare class ShardedDOTagCache implements NextModeTagCache {
119
119
  writeTags(tags: string[]): Promise<void>;
120
120
  performWriteTagsWithRetry(doId: DOId, tags: string[], lastModified: number, retryNumber?: number): Promise<void>;
121
121
  getCacheInstance(): Promise<Cache | undefined>;
122
- getCacheKey(doId: DOId, tags: string[]): Promise<Request<unknown, CfProperties<unknown>>>;
122
+ getCacheUrlKey(doId: DOId, tags: string[]): string;
123
123
  getFromRegionalCache(doId: DOId, tags: string[]): Promise<Response | undefined>;
124
124
  putToRegionalCache(doId: DOId, tags: string[], hasBeenRevalidated: boolean): Promise<void>;
125
125
  deleteRegionalCache(doId: DOId, tags: string[]): Promise<void>;
@@ -194,8 +194,8 @@ class ShardedDOTagCache {
194
194
  }
195
195
  return this.localCache;
196
196
  }
197
- async getCacheKey(doId, tags) {
198
- return new Request(new URL(`shard/${doId.shardId}?tags=${encodeURIComponent(tags.join(";"))}`, "http://local.cache"));
197
+ getCacheUrlKey(doId, tags) {
198
+ return `http://local.cache/shard/${doId.shardId}?tags=${encodeURIComponent(tags.join(";"))}`;
199
199
  }
200
200
  async getFromRegionalCache(doId, tags) {
201
201
  try {
@@ -204,12 +204,10 @@ class ShardedDOTagCache {
204
204
  const cache = await this.getCacheInstance();
205
205
  if (!cache)
206
206
  return;
207
- const key = await this.getCacheKey(doId, tags);
208
- return cache.match(key);
207
+ return cache.match(this.getCacheUrlKey(doId, tags));
209
208
  }
210
209
  catch (e) {
211
210
  error("Error while fetching from regional cache", e);
212
- return;
213
211
  }
214
212
  }
215
213
  async putToRegionalCache(doId, tags, hasBeenRevalidated) {
@@ -218,9 +216,15 @@ class ShardedDOTagCache {
218
216
  const cache = await this.getCacheInstance();
219
217
  if (!cache)
220
218
  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}` },
219
+ await cache.put(this.getCacheUrlKey(doId, tags), new Response(`${hasBeenRevalidated}`, {
220
+ headers: {
221
+ "cache-control": `max-age=${this.opts.regionalCacheTtlSec ?? 5}`,
222
+ ...(tags.length > 0
223
+ ? {
224
+ "cache-tag": tags.join(","),
225
+ }
226
+ : {}),
227
+ },
224
228
  }));
225
229
  }
226
230
  async deleteRegionalCache(doId, tags) {
@@ -231,8 +235,7 @@ class ShardedDOTagCache {
231
235
  const cache = await this.getCacheInstance();
232
236
  if (!cache)
233
237
  return;
234
- const key = await this.getCacheKey(doId, tags);
235
- await cache.delete(key);
238
+ await cache.delete(this.getCacheUrlKey(doId, tags));
236
239
  }
237
240
  catch (e) {
238
241
  debugCache("Error while deleting from regional cache", e);
@@ -277,15 +277,13 @@ describe("DOShardedTagCache", () => {
277
277
  it("should return the cache key without the random part", async () => {
278
278
  const cache = shardedDOTagCache();
279
279
  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");
280
+ expect(cache.getCacheUrlKey(doId1, ["_N_T_/tag1"])).toBe("http://local.cache/shard/tag-hard;shard-0?tags=_N_T_%2Ftag1");
282
281
  const doId2 = new DOId({
283
282
  baseShardId: "shard-1",
284
283
  numberOfReplicas: 1,
285
284
  shardType: "hard",
286
285
  });
287
- const reqKey2 = await cache.getCacheKey(doId2, ["tag1"]);
288
- expect(reqKey2.url).toBe("http://local.cache/shard/tag-hard;shard-1?tags=tag1");
286
+ expect(cache.getCacheUrlKey(doId2, ["tag1"])).toBe("http://local.cache/shard/tag-hard;shard-1?tags=tag1");
289
287
  });
290
288
  });
291
289
  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,41 @@
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
+ getPathsByTags: tagCache.getPathsByTags
10
+ ? async (tags) => {
11
+ const filteredTags = tags.filter(filterFn);
12
+ if (filteredTags.length === 0) {
13
+ return [];
14
+ }
15
+ return tagCache.getPathsByTags(filteredTags);
16
+ }
17
+ : undefined,
18
+ hasBeenRevalidated: async (tags, lastModified) => {
19
+ const filteredTags = tags.filter(filterFn);
20
+ if (filteredTags.length === 0) {
21
+ return false;
22
+ }
23
+ return tagCache.hasBeenRevalidated(filteredTags, lastModified);
24
+ },
25
+ writeTags: async (tags) => {
26
+ const filteredTags = tags.filter(filterFn);
27
+ if (filteredTags.length === 0) {
28
+ return;
29
+ }
30
+ return tagCache.writeTags(filteredTags);
31
+ },
32
+ };
33
+ }
34
+ /**
35
+ * Filter function to exclude tags that start with "_N_T_".
36
+ * This is used to filter out internal soft tags.
37
+ * Can be used if `revalidatePath` is not used.
38
+ */
39
+ export function softTagFilter(tag) {
40
+ return !tag.startsWith("_N_T_");
41
+ }
@@ -0,0 +1,96 @@
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
+ getPathsByTags: vi.fn(),
7
+ hasBeenRevalidated: vi.fn(),
8
+ writeTags: vi.fn(),
9
+ };
10
+ const filterFn = (tag) => tag.startsWith("valid_");
11
+ describe("withFilter", () => {
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ });
15
+ it("should filter out tags based on writeTags", async () => {
16
+ const tagCache = withFilter({
17
+ tagCache: mockedTagCache,
18
+ filterFn,
19
+ });
20
+ const tags = ["valid_tag", "invalid_tag"];
21
+ await tagCache.writeTags(tags);
22
+ expect(mockedTagCache.writeTags).toHaveBeenCalledWith(["valid_tag"]);
23
+ });
24
+ it("should not call writeTags if no tags are valid", async () => {
25
+ const tagCache = withFilter({
26
+ tagCache: mockedTagCache,
27
+ filterFn,
28
+ });
29
+ const tags = ["invalid_tag"];
30
+ await tagCache.writeTags(tags);
31
+ expect(mockedTagCache.writeTags).not.toHaveBeenCalled();
32
+ });
33
+ it("should filter out tags based on hasBeenRevalidated", async () => {
34
+ const tagCache = withFilter({
35
+ tagCache: mockedTagCache,
36
+ filterFn,
37
+ });
38
+ const tags = ["valid_tag", "invalid_tag"];
39
+ const lastModified = Date.now();
40
+ await tagCache.hasBeenRevalidated(tags, lastModified);
41
+ expect(mockedTagCache.hasBeenRevalidated).toHaveBeenCalledWith(["valid_tag"], lastModified);
42
+ });
43
+ it("should not call hasBeenRevalidated if no tags are valid", async () => {
44
+ const tagCache = withFilter({
45
+ tagCache: mockedTagCache,
46
+ filterFn,
47
+ });
48
+ const tags = ["invalid_tag"];
49
+ const lastModified = Date.now();
50
+ await tagCache.hasBeenRevalidated(tags, lastModified);
51
+ expect(mockedTagCache.hasBeenRevalidated).not.toHaveBeenCalled();
52
+ });
53
+ it("should filter out tags based on getPathsByTags", async () => {
54
+ const tagCache = withFilter({
55
+ tagCache: mockedTagCache,
56
+ filterFn,
57
+ });
58
+ const tags = ["valid_tag", "invalid_tag"];
59
+ await tagCache.getPathsByTags?.(tags);
60
+ expect(mockedTagCache.getPathsByTags).toHaveBeenCalledWith(["valid_tag"]);
61
+ });
62
+ it("should not call getPathsByTags if no tags are valid", async () => {
63
+ const tagCache = withFilter({
64
+ tagCache: mockedTagCache,
65
+ filterFn,
66
+ });
67
+ const tags = ["invalid_tag"];
68
+ await tagCache.getPathsByTags?.(tags);
69
+ expect(mockedTagCache.getPathsByTags).not.toHaveBeenCalled();
70
+ });
71
+ it("should return the correct name", () => {
72
+ const tagCache = withFilter({
73
+ tagCache: mockedTagCache,
74
+ filterFn,
75
+ });
76
+ expect(tagCache.name).toBe("filtered-mocked");
77
+ });
78
+ it("should not create a function if getPathsByTags is not defined", async () => {
79
+ const tagCache = withFilter({
80
+ tagCache: {
81
+ ...mockedTagCache,
82
+ getPathsByTags: undefined,
83
+ },
84
+ filterFn,
85
+ });
86
+ expect(tagCache.getPathsByTags).toBeUndefined();
87
+ });
88
+ it("should filter soft tags", () => {
89
+ const tagCache = withFilter({
90
+ tagCache: mockedTagCache,
91
+ filterFn: softTagFilter,
92
+ });
93
+ tagCache.writeTags(["valid_tag", "_N_T_/", "_N_T_/test", "_N_T_/layout"]);
94
+ expect(mockedTagCache.writeTags).toHaveBeenCalledWith(["valid_tag"]);
95
+ });
96
+ });
@@ -15,6 +15,7 @@ import { inlineFindDir } from "./patches/plugins/find-dir.js";
15
15
  import { patchInstrumentation } from "./patches/plugins/instrumentation.js";
16
16
  import { inlineLoadManifest } from "./patches/plugins/load-manifest.js";
17
17
  import { handleOptionalDependencies } from "./patches/plugins/optional-deps.js";
18
+ import { patchPagesRouterContext } from "./patches/plugins/pages-router-context.js";
18
19
  import { patchDepdDeprecations } from "./patches/plugins/patch-depd-deprecations.js";
19
20
  import { fixRequire } from "./patches/plugins/require.js";
20
21
  import { shimRequireHook } from "./patches/plugins/require-hook.js";
@@ -77,6 +78,7 @@ export async function bundleServer(buildOpts) {
77
78
  fixRequire(updater),
78
79
  handleOptionalDependencies(optionalDependencies),
79
80
  patchInstrumentation(updater, buildOpts),
81
+ patchPagesRouterContext(buildOpts),
80
82
  inlineEvalManifest(updater, buildOpts),
81
83
  inlineFindDir(updater, buildOpts),
82
84
  inlineLoadManifest(updater, buildOpts),
@@ -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
+ };
@@ -0,0 +1,32 @@
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 { compareSemver } from "@opennextjs/aws/build/helper.js";
7
+ export function patchPagesRouterContext(buildOpts) {
8
+ const pathRegex = /^.*\/(?<CONTEXT>.*)\.shared-runtime$/;
9
+ const isAfter15 = compareSemver(buildOpts.nextVersion, ">=", "15.0.0");
10
+ const isAfter153 = compareSemver(buildOpts.nextVersion, ">=", "15.3.0");
11
+ const basePath = `next/dist/server/${isAfter15 ? "" : "future/"}route-modules/pages/vendored/contexts/`;
12
+ return {
13
+ name: "pages-router-context",
14
+ setup: (build) => {
15
+ // If we are after 15.3, we don't need to patch the context anymore
16
+ if (isAfter153) {
17
+ return;
18
+ }
19
+ // We need to modify some imports (i.e. https://github.com/vercel/next.js/blob/48540b836642525b38a2cba40a92b4532c553a52/packages/next/src/server/require-hook.ts#L59-L68)
20
+ build.onResolve({ filter: /.*shared-runtime/ }, async ({ path, resolveDir, ...options }) => {
21
+ const match = path.match(pathRegex);
22
+ if (match && match.groups?.CONTEXT) {
23
+ const newPath = `${basePath}${match.groups.CONTEXT}.js`;
24
+ return await build.resolve(newPath, {
25
+ resolveDir,
26
+ ...options,
27
+ });
28
+ }
29
+ });
30
+ },
31
+ };
32
+ }
@@ -1,4 +1,6 @@
1
1
  export { DOQueueHandler } from "./.build/durable-objects/queue.js";
2
2
  export { DOShardedTagCache } from "./.build/durable-objects/sharded-tag-cache.js";
3
- declare const _default: ExportedHandler<CloudflareEnv>;
3
+ declare const _default: {
4
+ fetch(request: Request<unknown, IncomingRequestCfProperties<unknown>>, env: CloudflareEnv, ctx: ExecutionContext): Promise<any>;
5
+ };
4
6
  export default _default;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@opennextjs/cloudflare",
3
3
  "description": "Cloudflare builder for next apps",
4
- "version": "1.0.0-beta.3",
4
+ "version": "1.0.0-beta.4",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "opennextjs-cloudflare": "dist/cli/index.js"
@@ -68,7 +68,7 @@
68
68
  "vitest": "^2.1.1"
69
69
  },
70
70
  "peerDependencies": {
71
- "wrangler": "^3.114.3 || ^4.7.0"
71
+ "wrangler": "^4.7.0"
72
72
  },
73
73
  "scripts": {
74
74
  "clean": "rimraf dist",