@opennextjs/cloudflare 1.10.0 → 1.11.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/README.md
CHANGED
|
@@ -55,3 +55,30 @@ Deploy your application to production with the following:
|
|
|
55
55
|
# or
|
|
56
56
|
bun opennextjs-cloudflare build && bun opennextjs-cloudflare deploy
|
|
57
57
|
```
|
|
58
|
+
|
|
59
|
+
### Batch Cache Population (Optional, Recommended)
|
|
60
|
+
|
|
61
|
+
For improved performance with large caches, you can enable batch upload by providing R2 credentials via .env or environment variables.
|
|
62
|
+
|
|
63
|
+
Create a `.env` file in your project root (automatically loaded by the CLI):
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
R2_ACCESS_KEY_ID=your_access_key_id
|
|
67
|
+
R2_SECRET_ACCESS_KEY=your_secret_access_key
|
|
68
|
+
CF_ACCOUNT_ID=your_account_id
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
You can also set the environment variables for CI builds.
|
|
72
|
+
|
|
73
|
+
**Note:**
|
|
74
|
+
|
|
75
|
+
You can follow documentation https://developers.cloudflare.com/r2/api/tokens/ for creating API tokens with appropriate permissions for R2 access.
|
|
76
|
+
|
|
77
|
+
**Benefits:**
|
|
78
|
+
|
|
79
|
+
- Significantly faster uploads for large caches using parallel transfers
|
|
80
|
+
- Reduced API calls to Cloudflare
|
|
81
|
+
- Automatically enabled when credentials are provided
|
|
82
|
+
|
|
83
|
+
**Fallback:**
|
|
84
|
+
If these environment variables are not set, the CLI will use standard Wrangler uploads. Both methods work correctly - batch upload is simply faster for large caches.
|
|
@@ -31,6 +31,8 @@ declare global {
|
|
|
31
31
|
CF_PREVIEW_DOMAIN?: string;
|
|
32
32
|
CF_WORKERS_SCRIPTS_API_TOKEN?: string;
|
|
33
33
|
CF_ACCOUNT_ID?: string;
|
|
34
|
+
R2_ACCESS_KEY_ID?: string;
|
|
35
|
+
R2_SECRET_ACCESS_KEY?: string;
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
export type CloudflareContext<CfProperties extends Record<string, unknown> = IncomingRequestCfProperties, Context = ExecutionContext> = {
|
package/dist/cli/build/build.js
CHANGED
|
@@ -13,7 +13,6 @@ import { compileInit } from "./open-next/compile-init.js";
|
|
|
13
13
|
import { compileSkewProtection } from "./open-next/compile-skew-protection.js";
|
|
14
14
|
import { compileDurableObjects } from "./open-next/compileDurableObjects.js";
|
|
15
15
|
import { createServerBundle } from "./open-next/createServerBundle.js";
|
|
16
|
-
import { createWranglerConfigIfNotExistent } from "./utils/index.js";
|
|
17
16
|
import { getVersion } from "./utils/version.js";
|
|
18
17
|
/**
|
|
19
18
|
* Builds the application in a format that can be passed to workerd
|
|
@@ -65,9 +64,6 @@ export async function build(options, config, projectOpts, wranglerConfig) {
|
|
|
65
64
|
await createServerBundle(options);
|
|
66
65
|
await compileDurableObjects(options);
|
|
67
66
|
await bundleServer(options, projectOpts);
|
|
68
|
-
if (!projectOpts.skipWranglerConfigCheck) {
|
|
69
|
-
await createWranglerConfigIfNotExistent(projectOpts);
|
|
70
|
-
}
|
|
71
67
|
logger.info("OpenNext build complete.");
|
|
72
68
|
}
|
|
73
69
|
function ensureNextjsVersionSupported(options) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { build as buildImpl } from "../build/build.js";
|
|
2
|
+
import { createWranglerConfigIfNotExistent } from "../build/utils/index.js";
|
|
2
3
|
import { compileConfig, getNormalizedOptions, nextAppDir, printHeaders, readWranglerConfig, withWranglerOptions, withWranglerPassthroughArgs, } from "./utils.js";
|
|
3
4
|
/**
|
|
4
5
|
* Implementation of the `opennextjs-cloudflare build` command.
|
|
@@ -9,8 +10,15 @@ async function buildCommand(args) {
|
|
|
9
10
|
printHeaders("build");
|
|
10
11
|
const { config, buildDir } = await compileConfig(args.openNextConfigPath);
|
|
11
12
|
const options = getNormalizedOptions(config, buildDir);
|
|
13
|
+
const projectOpts = { ...args, minify: !args.noMinify, sourceDir: nextAppDir };
|
|
14
|
+
// Ask whether a `wrangler.jsonc` should be created when no config file exists.
|
|
15
|
+
// Note: We don't ask when a custom config file is specified via `--config`
|
|
16
|
+
// nor when `--skipWranglerConfigCheck` is used.
|
|
17
|
+
if (!projectOpts.wranglerConfigPath && !args.skipWranglerConfigCheck) {
|
|
18
|
+
await createWranglerConfigIfNotExistent(projectOpts);
|
|
19
|
+
}
|
|
12
20
|
const wranglerConfig = readWranglerConfig(args);
|
|
13
|
-
await buildImpl(options, config,
|
|
21
|
+
await buildImpl(options, config, projectOpts, wranglerConfig);
|
|
14
22
|
}
|
|
15
23
|
/**
|
|
16
24
|
* Add the `build` command to yargs configuration.
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { cpSync, existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import logger from "@opennextjs/aws/logger.js";
|
|
4
5
|
import { globSync } from "glob";
|
|
6
|
+
import rclone from "rclone.js";
|
|
5
7
|
import { tqdm } from "ts-tqdm";
|
|
6
8
|
import { BINDING_NAME as KV_CACHE_BINDING_NAME, NAME as KV_CACHE_NAME, PREFIX_ENV_NAME as KV_CACHE_PREFIX_ENV_NAME, } from "../../api/overrides/incremental-cache/kv-incremental-cache.js";
|
|
7
9
|
import { BINDING_NAME as R2_CACHE_BINDING_NAME, NAME as R2_CACHE_NAME, PREFIX_ENV_NAME as R2_CACHE_PREFIX_ENV_NAME, } from "../../api/overrides/incremental-cache/r2-incremental-cache.js";
|
|
@@ -103,18 +105,112 @@ export function getCacheAssets(opts) {
|
|
|
103
105
|
}
|
|
104
106
|
return assets;
|
|
105
107
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Create a temporary configuration file for batch upload from environment variables
|
|
110
|
+
* @returns Path to the temporary config file or null if env vars not available
|
|
111
|
+
*/
|
|
112
|
+
function createTempRcloneConfig(accessKey, secretKey, accountId) {
|
|
113
|
+
const tempDir = tmpdir();
|
|
114
|
+
const tempConfigPath = path.join(tempDir, `rclone-config-${Date.now()}.conf`);
|
|
115
|
+
const configContent = `[r2]
|
|
116
|
+
type = s3
|
|
117
|
+
provider = Cloudflare
|
|
118
|
+
access_key_id = ${accessKey}
|
|
119
|
+
secret_access_key = ${secretKey}
|
|
120
|
+
endpoint = https://${accountId}.r2.cloudflarestorage.com
|
|
121
|
+
acl = private
|
|
122
|
+
`;
|
|
123
|
+
/**
|
|
124
|
+
* 0o600 is an octal number (the 0o prefix indicates octal in JavaScript)
|
|
125
|
+
* that represents Unix file permissions:
|
|
126
|
+
*
|
|
127
|
+
* - 6 (owner): read (4) + write (2) = readable and writable by the file owner
|
|
128
|
+
* - 0 (group): no permissions for the group
|
|
129
|
+
* - 0 (others): no permissions for anyone else
|
|
130
|
+
*
|
|
131
|
+
* In symbolic notation, this is: rw-------
|
|
132
|
+
*/
|
|
133
|
+
writeFileSync(tempConfigPath, configContent, { mode: 0o600 });
|
|
134
|
+
return tempConfigPath;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Populate R2 incremental cache using batch upload for better performance
|
|
138
|
+
* Uses parallel transfers to significantly speed up cache population
|
|
139
|
+
*/
|
|
140
|
+
async function populateR2IncrementalCacheWithBatchUpload(bucket, prefix, assets, envVars) {
|
|
141
|
+
const accessKey = envVars.R2_ACCESS_KEY_ID || null;
|
|
142
|
+
const secretKey = envVars.R2_SECRET_ACCESS_KEY || null;
|
|
143
|
+
const accountId = envVars.CF_ACCOUNT_ID || null;
|
|
144
|
+
// Ensure all required env vars are set correctly
|
|
145
|
+
if (!accessKey || !secretKey || !accountId) {
|
|
146
|
+
throw new Error("Please set R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, and CF_ACCOUNT_ID environment variables to enable faster batch upload for remote R2.");
|
|
111
147
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
148
|
+
logger.info("\nPopulating remote R2 incremental cache using batch upload...");
|
|
149
|
+
// Create temporary config from env vars - required for batch upload
|
|
150
|
+
const tempConfigPath = createTempRcloneConfig(accessKey, secretKey, accountId);
|
|
151
|
+
if (!tempConfigPath) {
|
|
152
|
+
throw new Error("Failed to create temporary rclone config for R2 batch upload.");
|
|
115
153
|
}
|
|
116
|
-
const
|
|
117
|
-
|
|
154
|
+
const env = {
|
|
155
|
+
...process.env,
|
|
156
|
+
RCLONE_CONFIG: tempConfigPath,
|
|
157
|
+
};
|
|
158
|
+
logger.info("Using batch upload with R2 credentials from environment variables");
|
|
159
|
+
// Create a staging dir in temp directory with proper key paths
|
|
160
|
+
const tempDir = tmpdir();
|
|
161
|
+
const stagingDir = path.join(tempDir, `.r2-staging-${Date.now()}`);
|
|
162
|
+
// Track success to ensure cleanup happens correctly
|
|
163
|
+
let success = null;
|
|
164
|
+
try {
|
|
165
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
166
|
+
for (const { fullPath, key, buildId, isFetch } of assets) {
|
|
167
|
+
const cacheKey = computeCacheKey(key, {
|
|
168
|
+
prefix,
|
|
169
|
+
buildId,
|
|
170
|
+
cacheType: isFetch ? "fetch" : "cache",
|
|
171
|
+
});
|
|
172
|
+
const destPath = path.join(stagingDir, cacheKey);
|
|
173
|
+
mkdirSync(path.dirname(destPath), { recursive: true });
|
|
174
|
+
copyFileSync(fullPath, destPath);
|
|
175
|
+
}
|
|
176
|
+
// Use rclone.js to sync the R2
|
|
177
|
+
const remote = `r2:${bucket}`;
|
|
178
|
+
// Using rclone.js Promise-based API for the copy operation
|
|
179
|
+
await rclone.promises.copy(stagingDir, remote, {
|
|
180
|
+
progress: true,
|
|
181
|
+
transfers: 16,
|
|
182
|
+
checkers: 8,
|
|
183
|
+
env,
|
|
184
|
+
});
|
|
185
|
+
logger.info(`Successfully uploaded ${assets.length} assets to R2 using batch upload`);
|
|
186
|
+
success = true;
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
try {
|
|
190
|
+
// Cleanup temporary staging directory
|
|
191
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
console.warn(`Failed to remove temporary staging directory at ${stagingDir}`);
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
// Cleanup temporary config file
|
|
198
|
+
rmSync(tempConfigPath);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
console.warn(`Failed to remove temporary config at ${tempConfigPath}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!success) {
|
|
205
|
+
throw new Error("R2 batch upload failed, falling back to sequential uploads...");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Populate R2 incremental cache using sequential Wrangler uploads
|
|
210
|
+
* Falls back to this method when batch upload is not available or fails
|
|
211
|
+
*/
|
|
212
|
+
async function populateR2IncrementalCacheWithSequentialUpload(buildOpts, bucket, prefix, assets, populateCacheOptions) {
|
|
213
|
+
logger.info("Using sequential cache uploads.");
|
|
118
214
|
for (const { fullPath, key, buildId, isFetch } of tqdm(assets)) {
|
|
119
215
|
const cacheKey = computeCacheKey(key, {
|
|
120
216
|
prefix,
|
|
@@ -136,6 +232,34 @@ async function populateR2IncrementalCache(buildOpts, config, populateCacheOption
|
|
|
136
232
|
}
|
|
137
233
|
logger.info(`Successfully populated cache with ${assets.length} assets`);
|
|
138
234
|
}
|
|
235
|
+
async function populateR2IncrementalCache(buildOpts, config, populateCacheOptions, envVars) {
|
|
236
|
+
logger.info("\nPopulating R2 incremental cache...");
|
|
237
|
+
const binding = config.r2_buckets.find(({ binding }) => binding === R2_CACHE_BINDING_NAME);
|
|
238
|
+
if (!binding) {
|
|
239
|
+
throw new Error(`No R2 binding ${JSON.stringify(R2_CACHE_BINDING_NAME)} found!`);
|
|
240
|
+
}
|
|
241
|
+
const bucket = binding.bucket_name;
|
|
242
|
+
if (!bucket) {
|
|
243
|
+
throw new Error(`R2 binding ${JSON.stringify(R2_CACHE_BINDING_NAME)} should have a 'bucket_name'`);
|
|
244
|
+
}
|
|
245
|
+
const prefix = envVars[R2_CACHE_PREFIX_ENV_NAME];
|
|
246
|
+
const assets = getCacheAssets(buildOpts);
|
|
247
|
+
// Force sequential upload for local target
|
|
248
|
+
if (populateCacheOptions.target === "local") {
|
|
249
|
+
logger.info("Using sequential upload for local R2 (batch upload only works with remote R2)");
|
|
250
|
+
return await populateR2IncrementalCacheWithSequentialUpload(buildOpts, bucket, prefix, assets, populateCacheOptions);
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
// Attempt batch upload first (using rclone) - only for remote target
|
|
254
|
+
return await populateR2IncrementalCacheWithBatchUpload(bucket, prefix, assets, envVars);
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
logger.warn(`Batch upload failed: ${error instanceof Error ? error.message : error}`);
|
|
258
|
+
logger.info("Falling back to sequential uploads...");
|
|
259
|
+
// Sequential upload fallback (using Wrangler)
|
|
260
|
+
return await populateR2IncrementalCacheWithSequentialUpload(buildOpts, bucket, prefix, assets, populateCacheOptions);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
139
263
|
async function populateKVIncrementalCache(buildOpts, config, populateCacheOptions, envVars) {
|
|
140
264
|
logger.info("\nPopulating KV incremental cache...");
|
|
141
265
|
const binding = config.kv_namespaces.find(({ binding }) => binding === KV_CACHE_BINDING_NAME);
|
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.11.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"opennextjs-cloudflare": "dist/cli/index.js"
|
|
@@ -43,10 +43,12 @@
|
|
|
43
43
|
"homepage": "https://github.com/opennextjs/opennextjs-cloudflare",
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@dotenvx/dotenvx": "1.31.0",
|
|
46
|
-
"@opennextjs/aws": "3.8.
|
|
46
|
+
"@opennextjs/aws": "3.8.5",
|
|
47
|
+
"@types/rclone.js": "^0.6.3",
|
|
47
48
|
"cloudflare": "^4.4.1",
|
|
48
49
|
"enquirer": "^2.4.1",
|
|
49
50
|
"glob": "^11.0.0",
|
|
51
|
+
"rclone.js": "^0.6.6",
|
|
50
52
|
"ts-tqdm": "^0.8.6",
|
|
51
53
|
"yargs": "^18.0.0"
|
|
52
54
|
},
|