@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.
- package/dist/api/cloudflare-context.d.ts +6 -3
- package/dist/api/config.d.ts +28 -1
- package/dist/api/config.js +15 -1
- package/dist/api/durable-objects/sharded-tag-cache.d.ts +1 -0
- package/dist/api/durable-objects/sharded-tag-cache.js +16 -0
- package/dist/api/index.d.ts +1 -1
- package/dist/api/overrides/incremental-cache/kv-incremental-cache.d.ts +8 -9
- package/dist/api/overrides/incremental-cache/kv-incremental-cache.js +14 -14
- package/dist/api/overrides/incremental-cache/r2-incremental-cache.d.ts +4 -11
- package/dist/api/overrides/incremental-cache/r2-incremental-cache.js +8 -15
- package/dist/api/overrides/incremental-cache/regional-cache.d.ts +7 -7
- package/dist/api/overrides/incremental-cache/regional-cache.js +16 -13
- package/dist/api/overrides/incremental-cache/static-assets-incremental-cache.d.ts +3 -3
- package/dist/api/overrides/incremental-cache/static-assets-incremental-cache.js +9 -4
- package/dist/api/overrides/internal.d.ts +10 -3
- package/dist/api/overrides/internal.js +7 -0
- package/dist/api/overrides/tag-cache/d1-next-tag-cache.d.ts +1 -0
- package/dist/api/overrides/tag-cache/d1-next-tag-cache.js +20 -0
- package/dist/api/overrides/tag-cache/do-sharded-tag-cache.d.ts +10 -4
- package/dist/api/overrides/tag-cache/do-sharded-tag-cache.js +58 -17
- package/dist/api/overrides/tag-cache/do-sharded-tag-cache.spec.js +9 -7
- package/dist/api/overrides/tag-cache/tag-cache-filter.d.ts +26 -0
- package/dist/api/overrides/tag-cache/tag-cache-filter.js +48 -0
- package/dist/api/overrides/tag-cache/tag-cache-filter.spec.d.ts +1 -0
- package/dist/api/overrides/tag-cache/tag-cache-filter.spec.js +97 -0
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.js +3 -0
- package/dist/cli/build/build.d.ts +1 -1
- package/dist/cli/build/bundle-server.js +18 -4
- package/dist/cli/build/open-next/createServerBundle.js +15 -2
- package/dist/cli/build/patches/investigated/patch-cache.d.ts +1 -0
- package/dist/cli/build/patches/investigated/patch-cache.js +16 -0
- package/dist/cli/build/patches/plugins/eval-manifest.js +1 -1
- package/dist/cli/build/patches/plugins/load-manifest.js +1 -1
- package/dist/cli/build/patches/plugins/pages-router-context.d.ts +11 -0
- package/dist/cli/build/patches/plugins/pages-router-context.js +32 -0
- package/dist/cli/build/patches/plugins/wrangler-external.js +2 -1
- package/dist/cli/build/utils/ensure-cf-config.d.ts +1 -1
- package/dist/cli/build/utils/workerd.d.ts +38 -0
- package/dist/cli/build/utils/workerd.js +80 -0
- package/dist/cli/build/utils/workerd.spec.d.ts +1 -0
- package/dist/cli/build/utils/workerd.spec.js +188 -0
- package/dist/cli/commands/deploy.d.ts +2 -1
- package/dist/cli/commands/deploy.js +1 -0
- package/dist/cli/commands/populate-cache.d.ts +1 -0
- package/dist/cli/commands/populate-cache.js +56 -24
- package/dist/cli/commands/preview.d.ts +2 -1
- package/dist/cli/commands/preview.js +1 -0
- package/dist/cli/commands/upload.d.ts +2 -1
- package/dist/cli/commands/upload.js +1 -0
- package/dist/cli/templates/init.js +3 -0
- package/dist/cli/templates/worker.d.ts +3 -1
- package/dist/cli/utils/run-wrangler.d.ts +0 -1
- package/dist/cli/utils/run-wrangler.js +1 -1
- package/package.json +3 -3
- 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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
222
|
-
await cache.put(
|
|
223
|
-
headers: {
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
});
|
package/dist/cli/args.d.ts
CHANGED
|
@@ -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 "
|
|
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
|
-
//
|
|
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"
|
|
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
|
+
};
|