@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.
- 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 +50 -12
- package/dist/api/overrides/tag-cache/do-sharded-tag-cache.spec.js +9 -5
- package/dist/api/overrides/tag-cache/tag-cache-filter.js +7 -0
- package/dist/api/overrides/tag-cache/tag-cache-filter.spec.js +1 -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 +16 -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/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 +62 -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/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,29 +230,31 @@ class ShardedDOTagCache {
|
|
|
194
230
|
}
|
|
195
231
|
return this.localCache;
|
|
196
232
|
}
|
|
197
|
-
getCacheUrlKey(
|
|
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
|
-
return cache.match(this.getCacheUrlKey(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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);
|
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";
|
|
@@ -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
|
-
//
|
|
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"
|
|
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
|
-
|
|
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,
|
|
@@ -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 {};
|