@opennextjs/cloudflare 1.19.11 → 1.20.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 +2 -0
- package/dist/api/durable-objects/bucket-cache-purge.js +3 -3
- package/dist/cli/commands/deploy.d.ts +1 -0
- package/dist/cli/commands/deploy.js +1 -0
- package/dist/cli/commands/populate-cache.d.ts +12 -1
- package/dist/cli/commands/populate-cache.js +123 -2
- package/dist/cli/commands/preview.d.ts +1 -0
- package/dist/cli/commands/preview.js +1 -0
- package/dist/cli/commands/upload.d.ts +1 -0
- package/dist/cli/commands/upload.js +1 -0
- package/dist/cli/utils/ensure-r2-bucket.js +9 -2
- package/package.json +9 -1
|
@@ -32,6 +32,8 @@ declare global {
|
|
|
32
32
|
CF_PREVIEW_DOMAIN?: string;
|
|
33
33
|
CF_WORKERS_SCRIPTS_API_TOKEN?: string;
|
|
34
34
|
CF_ACCOUNT_ID?: string;
|
|
35
|
+
R2_ACCESS_KEY_ID?: string;
|
|
36
|
+
R2_SECRET_ACCESS_KEY?: string;
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
export type CloudflareContext<CfProperties extends Record<string, unknown> = IncomingRequestCfProperties, Context = ExecutionContext> = {
|
|
@@ -25,7 +25,7 @@ export class BucketCachePurge extends DurableObject {
|
|
|
25
25
|
// Insert the tag into the sql table
|
|
26
26
|
this.ctx.storage.sql.exec(`
|
|
27
27
|
INSERT OR REPLACE INTO cache_purge (tag)
|
|
28
|
-
VALUES (?)`,
|
|
28
|
+
VALUES (?)`, tag);
|
|
29
29
|
}
|
|
30
30
|
const nextAlarm = await this.ctx.storage.getAlarm();
|
|
31
31
|
if (!nextAlarm) {
|
|
@@ -57,7 +57,7 @@ export class BucketCachePurge extends DurableObject {
|
|
|
57
57
|
this.ctx.storage.sql.exec(`
|
|
58
58
|
DELETE FROM cache_purge
|
|
59
59
|
WHERE tag IN (${tags.map(() => "?").join(",")})
|
|
60
|
-
`, tags.map((row) => row.tag));
|
|
60
|
+
`, ...tags.map((row) => row.tag));
|
|
61
61
|
if (tags.length < MAX_NUMBER_OF_TAGS_PER_PURGE) {
|
|
62
62
|
// If we have less than MAX_NUMBER_OF_TAGS_PER_PURGE tags, we can stop
|
|
63
63
|
tags = [];
|
|
@@ -70,6 +70,6 @@ export class BucketCachePurge extends DurableObject {
|
|
|
70
70
|
`)
|
|
71
71
|
.toArray();
|
|
72
72
|
}
|
|
73
|
-
} while (tags.length
|
|
73
|
+
} while (tags.length > 0);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -24,6 +24,7 @@ export async function deployCommand(args) {
|
|
|
24
24
|
environment: args.env,
|
|
25
25
|
wranglerConfigPath: args.wranglerConfigPath,
|
|
26
26
|
cacheChunkSize: args.cacheChunkSize,
|
|
27
|
+
useRclone: args.rclone,
|
|
27
28
|
shouldUsePreviewId: false,
|
|
28
29
|
}, envVars);
|
|
29
30
|
const deploymentMapping = await getDeploymentMapping(buildOpts, config, envVars);
|
|
@@ -32,16 +32,25 @@ export type PopulateCacheOptions = {
|
|
|
32
32
|
/**
|
|
33
33
|
* Number of concurrent requests when populating the cache.
|
|
34
34
|
* For KV this is the chunk size passed to `wrangler kv bulk put`.
|
|
35
|
-
* For R2 this is the number of concurrent requests to the local worker.
|
|
35
|
+
* For R2 this is the number of concurrent requests to the local worker or rclone transfers.
|
|
36
36
|
*
|
|
37
37
|
* @default 25
|
|
38
38
|
*/
|
|
39
39
|
cacheChunkSize?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Whether to use `rclone` instead of the worker-based R2 cache population path.
|
|
42
|
+
*/
|
|
43
|
+
useRclone?: boolean;
|
|
40
44
|
/**
|
|
41
45
|
* Instructs Wrangler to use the preview namespace or ID defined in the Wrangler config for the remote target.
|
|
42
46
|
*/
|
|
43
47
|
shouldUsePreviewId: boolean;
|
|
44
48
|
};
|
|
49
|
+
export declare function loadRclone(importRclone?: () => Promise<{
|
|
50
|
+
default: typeof import("rclone.js");
|
|
51
|
+
}>): Promise<((...args: Array<string | object>) => import("child_process").ChildProcess) & Record<"test" | "checksum" | "link" | "version" | "config" | "copy" | "delete" | "about" | "authorize" | "backend" | "cat" | "check" | "cleanup" | "config create" | "config delete" | "config disconnect" | "config dump" | "config edit" | "config file" | "config password" | "config providers" | "config reconnect" | "config show" | "config update" | "config userinfo" | "copyto" | "copyurl" | "cryptcheck" | "cryptdecode" | "dedupe" | "deletefile" | "genautocomplete" | "genautocomplete bash" | "genautocomplete fish" | "genautocomplete zsh" | "gendocs" | "hashsum" | "help" | "listremotes" | "ls" | "lsd" | "lsf" | "lsjson" | "lsl" | "md5sum" | "mkdir" | "mount" | "move" | "moveto" | "ncdu" | "obscure" | "purge" | "rc" | "rcat" | "rcd" | "rmdir" | "rmdirs" | "selfupdate" | "serve" | "serve dlna" | "serve ftp" | "serve http" | "serve restic" | "serve sftp" | "serve webdav" | "settier" | "sha1sum" | "size" | "sync" | "touch" | "tree", (...args: Array<string | object>) => import("child_process").ChildProcess> & {
|
|
52
|
+
promises: ((...args: Array<string | object>) => Promise<Buffer<ArrayBufferLike>>) & Record<"test" | "checksum" | "link" | "version" | "config" | "copy" | "delete" | "about" | "authorize" | "backend" | "cat" | "check" | "cleanup" | "config create" | "config delete" | "config disconnect" | "config dump" | "config edit" | "config file" | "config password" | "config providers" | "config reconnect" | "config show" | "config update" | "config userinfo" | "copyto" | "copyurl" | "cryptcheck" | "cryptdecode" | "dedupe" | "deletefile" | "genautocomplete" | "genautocomplete bash" | "genautocomplete fish" | "genautocomplete zsh" | "gendocs" | "hashsum" | "help" | "listremotes" | "ls" | "lsd" | "lsf" | "lsjson" | "lsl" | "md5sum" | "mkdir" | "mount" | "move" | "moveto" | "ncdu" | "obscure" | "purge" | "rc" | "rcat" | "rcd" | "rmdir" | "rmdirs" | "selfupdate" | "serve" | "serve dlna" | "serve ftp" | "serve http" | "serve restic" | "serve sftp" | "serve webdav" | "settier" | "sha1sum" | "size" | "sync" | "touch" | "tree", (...args: Array<string | object>) => Promise<Buffer<ArrayBufferLike>>>;
|
|
53
|
+
}>;
|
|
45
54
|
/**
|
|
46
55
|
* Add the `populateCache` command to yargs configuration, with nested commands for `local` and `remote`.
|
|
47
56
|
*
|
|
@@ -56,4 +65,6 @@ export declare function withPopulateCacheOptions<T extends yargs.Argv>(args: T):
|
|
|
56
65
|
env: string | undefined;
|
|
57
66
|
} & {
|
|
58
67
|
cacheChunkSize: number | undefined;
|
|
68
|
+
} & {
|
|
69
|
+
rclone: boolean;
|
|
59
70
|
}>;
|
|
@@ -46,6 +46,7 @@ async function populateCacheCommand(target, args) {
|
|
|
46
46
|
environment: args.env,
|
|
47
47
|
wranglerConfigPath: args.wranglerConfigPath,
|
|
48
48
|
cacheChunkSize: args.cacheChunkSize,
|
|
49
|
+
useRclone: args.rclone,
|
|
49
50
|
shouldUsePreviewId: false,
|
|
50
51
|
}, envVars);
|
|
51
52
|
}
|
|
@@ -150,6 +151,13 @@ async function populateR2IncrementalCache(buildOpts, config, populateCacheOption
|
|
|
150
151
|
logger.info("No cache assets to populate");
|
|
151
152
|
return;
|
|
152
153
|
}
|
|
154
|
+
if (populateCacheOptions.useRclone) {
|
|
155
|
+
if (populateCacheOptions.target !== "remote") {
|
|
156
|
+
throw new Error("The `--rclone` option can only be used when populating a remote R2 cache.");
|
|
157
|
+
}
|
|
158
|
+
await populateR2IncrementalCacheWithRclone(bucketName, prefix, assets, envVars, populateCacheOptions.cacheChunkSize);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
153
161
|
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
154
162
|
const handlerPath = path.join(currentDir, "../workers/r2-cache.js");
|
|
155
163
|
const isRemote = populateCacheOptions.target === "remote";
|
|
@@ -204,6 +212,113 @@ async function populateR2IncrementalCache(buildOpts, config, populateCacheOption
|
|
|
204
212
|
}
|
|
205
213
|
logger.info(`Successfully populated cache with ${assets.length} entries`);
|
|
206
214
|
}
|
|
215
|
+
async function populateR2IncrementalCacheWithRclone(bucketName, prefix, assets, envVars, cacheChunkSize) {
|
|
216
|
+
const accessKey = envVars.R2_ACCESS_KEY_ID;
|
|
217
|
+
const secretKey = envVars.R2_SECRET_ACCESS_KEY;
|
|
218
|
+
const accountId = envVars.CF_ACCOUNT_ID;
|
|
219
|
+
if (!accessKey || !secretKey || !accountId) {
|
|
220
|
+
throw new Error("R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, and CF_ACCOUNT_ID must be provided to use `rclone`");
|
|
221
|
+
}
|
|
222
|
+
const rclone = await loadRclone();
|
|
223
|
+
logger.info("\nPopulating remote R2 incremental cache using `rclone`...");
|
|
224
|
+
const configDir = await fsp.mkdtemp(path.join(os.tmpdir(), "rclone-config-"));
|
|
225
|
+
const configPath = path.join(configDir, "rclone.conf");
|
|
226
|
+
const stagingDir = await fsp.mkdtemp(path.join(os.tmpdir(), "r2-staging-"));
|
|
227
|
+
const transfers = Math.max(1, cacheChunkSize ?? 25);
|
|
228
|
+
const checkers = Math.max(1, Math.floor(transfers / 2));
|
|
229
|
+
try {
|
|
230
|
+
await fsp.writeFile(configPath, `[r2]\ntype = s3\nprovider = Cloudflare\naccess_key_id = ${accessKey}\nsecret_access_key = ${secretKey}\nendpoint = https://${accountId}.r2.cloudflarestorage.com\nacl = private\n`, { mode: 0o600 });
|
|
231
|
+
const rcloneEnv = {
|
|
232
|
+
...process.env,
|
|
233
|
+
RCLONE_CONFIG: configPath,
|
|
234
|
+
};
|
|
235
|
+
await ensureRcloneExecutable(rclone, rcloneEnv);
|
|
236
|
+
await stageCacheAssets(assets, stagingDir, prefix, transfers);
|
|
237
|
+
await runRcloneCopy(rclone, stagingDir, `r2:${bucketName}`, {
|
|
238
|
+
progress: true,
|
|
239
|
+
transfers,
|
|
240
|
+
checkers,
|
|
241
|
+
env: rcloneEnv,
|
|
242
|
+
stdio: "inherit",
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
await Promise.allSettled([
|
|
247
|
+
fsp.rm(stagingDir, { recursive: true, force: true }),
|
|
248
|
+
fsp.rm(configDir, { recursive: true, force: true }),
|
|
249
|
+
]);
|
|
250
|
+
}
|
|
251
|
+
logger.info(`Successfully populated cache with ${assets.length} entries`);
|
|
252
|
+
}
|
|
253
|
+
async function runRcloneCopy(rclone, source, destination, options) {
|
|
254
|
+
await new Promise((resolve, reject) => {
|
|
255
|
+
const child = rclone.copy(source, destination, options);
|
|
256
|
+
child.once("error", reject);
|
|
257
|
+
child.once("close", (code, signal) => {
|
|
258
|
+
if (code === 0) {
|
|
259
|
+
resolve();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
reject(new Error(`rclone exited ${signal ? `after receiving signal ${signal}` : `with code ${code ?? "unknown"}`}`));
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
async function stageCacheAssets(assets, stagingDir, prefix, maxConcurrency) {
|
|
267
|
+
const pending = new Set();
|
|
268
|
+
for (const asset of assets) {
|
|
269
|
+
const task = stageCacheAsset(asset, stagingDir, prefix).finally(() => pending.delete(task));
|
|
270
|
+
pending.add(task);
|
|
271
|
+
if (pending.size >= maxConcurrency) {
|
|
272
|
+
await Promise.race(pending);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
await Promise.all(pending);
|
|
276
|
+
}
|
|
277
|
+
async function stageCacheAsset(asset, stagingDir, prefix) {
|
|
278
|
+
const cacheKey = computeCacheKey(asset.key, {
|
|
279
|
+
prefix,
|
|
280
|
+
buildId: asset.buildId,
|
|
281
|
+
cacheType: asset.isFetch ? "fetch" : "cache",
|
|
282
|
+
});
|
|
283
|
+
const destination = path.resolve(stagingDir, cacheKey);
|
|
284
|
+
const relativeDestination = path.relative(stagingDir, destination);
|
|
285
|
+
if (relativeDestination.startsWith(`..${path.sep}`) || path.isAbsolute(relativeDestination)) {
|
|
286
|
+
throw new Error(`Cannot stage R2 cache key outside the temporary directory: ${JSON.stringify(cacheKey)}`);
|
|
287
|
+
}
|
|
288
|
+
await fsp.mkdir(path.dirname(destination), { recursive: true });
|
|
289
|
+
try {
|
|
290
|
+
await fsp.link(asset.fullPath, destination);
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "EXDEV") {
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
await fsp.copyFile(asset.fullPath, destination);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function ensureRcloneExecutable(rclone, env) {
|
|
300
|
+
try {
|
|
301
|
+
await rclone.promises.version({ env });
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
305
|
+
throw new Error("The `rclone.js` executable is unavailable. pnpm users must allow its install script with `pnpm approve-builds`, select `rclone.js`, then run `pnpm rebuild rclone.js`.");
|
|
306
|
+
}
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
export async function loadRclone(importRclone = () => import("rclone.js")) {
|
|
311
|
+
try {
|
|
312
|
+
return (await importRclone()).default;
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
if (error instanceof Error &&
|
|
316
|
+
("code" in error ? error.code === "ERR_MODULE_NOT_FOUND" : error.message.includes("rclone.js"))) {
|
|
317
|
+
throw new Error("The `--rclone` option requires the optional `rclone.js` peer dependency. Install it in your project before using this option.");
|
|
318
|
+
}
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
207
322
|
/**
|
|
208
323
|
* Sends cache entries to the R2 worker, one entry per request.
|
|
209
324
|
*
|
|
@@ -431,8 +546,14 @@ export function addPopulateCacheCommand(y) {
|
|
|
431
546
|
.demandCommand(1, 1));
|
|
432
547
|
}
|
|
433
548
|
export function withPopulateCacheOptions(args) {
|
|
434
|
-
return withWranglerOptions(args)
|
|
549
|
+
return withWranglerOptions(args)
|
|
550
|
+
.options("cacheChunkSize", {
|
|
435
551
|
type: "number",
|
|
436
|
-
desc: "Number of
|
|
552
|
+
desc: "Number of concurrent cache entries to process",
|
|
553
|
+
})
|
|
554
|
+
.options("rclone", {
|
|
555
|
+
type: "boolean",
|
|
556
|
+
default: false,
|
|
557
|
+
desc: "Use rclone to populate a remote R2 incremental cache",
|
|
437
558
|
});
|
|
438
559
|
}
|
|
@@ -8,6 +8,7 @@ import type { WithWranglerArgs } from "./utils/utils.js";
|
|
|
8
8
|
export declare function previewCommand(args: WithWranglerArgs<{
|
|
9
9
|
cacheChunkSize?: number;
|
|
10
10
|
remote: boolean;
|
|
11
|
+
rclone: boolean;
|
|
11
12
|
}>): Promise<void>;
|
|
12
13
|
/**
|
|
13
14
|
* Add the `preview` command to yargs configuration.
|
|
@@ -22,6 +22,7 @@ export async function previewCommand(args) {
|
|
|
22
22
|
environment: args.env,
|
|
23
23
|
wranglerConfigPath: args.wranglerConfigPath,
|
|
24
24
|
cacheChunkSize: args.cacheChunkSize,
|
|
25
|
+
useRclone: args.rclone,
|
|
25
26
|
shouldUsePreviewId: args.remote,
|
|
26
27
|
}, envVars);
|
|
27
28
|
const result = runWrangler(buildOpts, ["dev", ...args.wranglerArgs], { logging: "all" });
|
|
@@ -25,6 +25,7 @@ export async function uploadCommand(args) {
|
|
|
25
25
|
environment: args.env,
|
|
26
26
|
wranglerConfigPath: args.wranglerConfigPath,
|
|
27
27
|
cacheChunkSize: args.cacheChunkSize,
|
|
28
|
+
useRclone: args.rclone,
|
|
28
29
|
shouldUsePreviewId: false,
|
|
29
30
|
}, envVars);
|
|
30
31
|
const result = runWrangler(buildOpts, [
|
|
@@ -106,8 +106,15 @@ export async function ensureR2Bucket(projectDir, bucketName, jurisdiction) {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
const client = authCredentials.type === "api_key"
|
|
109
|
-
? new Cloudflare({
|
|
110
|
-
|
|
109
|
+
? new Cloudflare({
|
|
110
|
+
apiKey: authCredentials.apiKey,
|
|
111
|
+
apiEmail: authCredentials.apiEmail,
|
|
112
|
+
defaultHeaders: { "Accept-Encoding": "identity" },
|
|
113
|
+
})
|
|
114
|
+
: new Cloudflare({
|
|
115
|
+
apiToken: authCredentials.token,
|
|
116
|
+
defaultHeaders: { "Accept-Encoding": "identity" },
|
|
117
|
+
});
|
|
111
118
|
const accountId = await getAccountId(client);
|
|
112
119
|
if (!accountId) {
|
|
113
120
|
return { success: false, error: "Could not determine Cloudflare account ID" };
|
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.
|
|
4
|
+
"version": "1.20.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"opennextjs-cloudflare": "dist/cli/index.js"
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"@types/mock-fs": "^4.13.4",
|
|
61
61
|
"@types/node": "^22.12.0",
|
|
62
62
|
"@types/picomatch": "^4.0.0",
|
|
63
|
+
"@types/rclone.js": "^0.6.1",
|
|
63
64
|
"@types/yargs": "^17.0.33",
|
|
64
65
|
"diff": "^8.0.2",
|
|
65
66
|
"esbuild": "^0.27.0",
|
|
@@ -71,6 +72,7 @@
|
|
|
71
72
|
"mock-fs": "^5.4.1",
|
|
72
73
|
"next": "^15.5.18",
|
|
73
74
|
"picomatch": "^4.0.2",
|
|
75
|
+
"rclone.js": "^0.6.6",
|
|
74
76
|
"rimraf": "^6.0.1",
|
|
75
77
|
"typescript": "^5.9.3",
|
|
76
78
|
"typescript-eslint": "^8.48.0",
|
|
@@ -78,8 +80,14 @@
|
|
|
78
80
|
},
|
|
79
81
|
"peerDependencies": {
|
|
80
82
|
"next": ">=15.5.18 <16 || >=16.2.6",
|
|
83
|
+
"rclone.js": "^0.6.6",
|
|
81
84
|
"wrangler": "^4.86.0"
|
|
82
85
|
},
|
|
86
|
+
"peerDependenciesMeta": {
|
|
87
|
+
"rclone.js": {
|
|
88
|
+
"optional": true
|
|
89
|
+
}
|
|
90
|
+
},
|
|
83
91
|
"scripts": {
|
|
84
92
|
"clean": "rimraf dist",
|
|
85
93
|
"build": "pnpm clean && tsc",
|