this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(cloudflare): add optional R2 batch uploads via rclone for cache population (#925)

authored by

krzysztof-palka-monogo and committed by
GitHub
62fee713 0c655c3c

+511 -17
+48
.changeset/rclone-batch-upload.md
··· 1 + --- 2 + "@opennextjs/cloudflare": minor 3 + --- 4 + 5 + feature: optional batch upload for faster R2 cache population 6 + 7 + This update adds optional batch upload support for R2 cache population, significantly improving upload performance for large caches when enabled via .env or environment variables. 8 + 9 + **Key Changes:** 10 + 11 + 1. **Optional Batch Upload**: Configure R2 credentials via .env or environment variables to enable faster batch uploads: 12 + 13 + - `R2_ACCESS_KEY_ID` 14 + - `R2_SECRET_ACCESS_KEY` 15 + - `CF_ACCOUNT_ID` 16 + 17 + 2. **Automatic Detection**: When credentials are detected, batch upload is automatically used for better performance 18 + 19 + 3. **Smart Fallback**: If credentials are not configured, the CLI falls back to standard Wrangler uploads with a helpful message about enabling batch upload for better performance 20 + 21 + **All deployment commands support batch upload:** 22 + 23 + - `populateCache` - Explicit cache population 24 + - `deploy` - Deploy with cache population 25 + - `upload` - Upload version with cache population 26 + - `preview` - Preview with cache population 27 + 28 + **Performance Benefits (when batch upload is enabled):** 29 + 30 + - Parallel transfer capabilities (32 concurrent transfers) 31 + - Significantly faster for large caches 32 + - Reduced API calls to Cloudflare 33 + 34 + **Usage:** 35 + 36 + Add the credentials in a `.env`/`.dev.vars` file in your project root: 37 + 38 + ```bash 39 + R2_ACCESS_KEY_ID=your_key 40 + R2_SECRET_ACCESS_KEY=your_secret 41 + CF_ACCOUNT_ID=your_account 42 + ``` 43 + 44 + You can also set the environment variables for CI builds. 45 + 46 + **Note:** 47 + 48 + You can follow documentation https://developers.cloudflare.com/r2/api/tokens/ for creating API tokens with appropriate permissions for R2 access.
+27
packages/cloudflare/README.md
··· 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.
+2
packages/cloudflare/package.json
··· 54 54 "dependencies": { 55 55 "@dotenvx/dotenvx": "catalog:", 56 56 "@opennextjs/aws": "3.8.4", 57 + "@types/rclone.js": "^0.6.3", 57 58 "cloudflare": "^4.4.1", 58 59 "enquirer": "^2.4.1", 59 60 "glob": "catalog:", 61 + "rclone.js": "^0.6.6", 60 62 "ts-tqdm": "^0.8.6", 61 63 "yargs": "catalog:" 62 64 },
+6 -1
packages/cloudflare/src/api/cloudflare-context.ts
··· 79 79 CF_PREVIEW_DOMAIN?: string; 80 80 // Should have the `Workers Scripts:Read` permission 81 81 CF_WORKERS_SCRIPTS_API_TOKEN?: string; 82 - // Cloudflare account id 82 + 83 + // Cloudflare account id - needed for skew protection and R2 batch population 83 84 CF_ACCOUNT_ID?: string; 85 + 86 + // R2 API credentials for batch cache population (optional, enables faster uploads) 87 + R2_ACCESS_KEY_ID?: string; 88 + R2_SECRET_ACCESS_KEY?: string; 84 89 } 85 90 } 86 91
+216 -2
packages/cloudflare/src/cli/commands/populate-cache.spec.ts
··· 3 3 4 4 import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; 5 5 import mockFs from "mock-fs"; 6 - import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 + import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from "vitest"; 7 7 8 - import { getCacheAssets } from "./populate-cache.js"; 8 + import { getCacheAssets, populateCache } from "./populate-cache.js"; 9 9 10 10 describe("getCacheAssets", () => { 11 11 beforeAll(() => { ··· 68 68 `); 69 69 }); 70 70 }); 71 + 72 + vi.mock("../utils/run-wrangler.js", () => ({ 73 + runWrangler: vi.fn(), 74 + })); 75 + 76 + vi.mock("./helpers.js", () => ({ 77 + getEnvFromPlatformProxy: vi.fn(async () => ({})), 78 + quoteShellMeta: vi.fn((s) => s), 79 + })); 80 + 81 + // Mock rclone.js promises API to simulate successful copy operations by default 82 + vi.mock("rclone.js", () => ({ 83 + default: { 84 + promises: { 85 + copy: vi.fn(() => Promise.resolve("")), 86 + }, 87 + }, 88 + })); 89 + 90 + describe("populateCache", () => { 91 + // Test fixtures 92 + const createTestBuildOptions = (): BuildOptions => 93 + ({ 94 + outputDir: "/test/output", 95 + }) as BuildOptions; 96 + 97 + const createTestOpenNextConfig = () => ({ 98 + default: { 99 + override: { 100 + incrementalCache: "cf-r2-incremental-cache", 101 + }, 102 + }, 103 + }); 104 + 105 + const createTestWranglerConfig = () => ({ 106 + r2_buckets: [ 107 + { 108 + binding: "NEXT_INC_CACHE_R2_BUCKET", 109 + bucket_name: "test-bucket", 110 + }, 111 + ], 112 + }); 113 + 114 + const createTestPopulateCacheOptions = () => ({ 115 + target: "local" as const, 116 + shouldUsePreviewId: false, 117 + }); 118 + 119 + const setupMockFileSystem = () => { 120 + mockFs({ 121 + "/test/output": { 122 + cache: { 123 + buildID: { 124 + path: { 125 + to: { 126 + "test.cache": JSON.stringify({ data: "test" }), 127 + }, 128 + }, 129 + }, 130 + }, 131 + }, 132 + }); 133 + }; 134 + 135 + describe("R2 incremental cache", () => { 136 + afterEach(() => { 137 + mockFs.restore(); 138 + vi.unstubAllEnvs(); 139 + }); 140 + 141 + test("uses sequential upload for local target (skips batch upload)", async () => { 142 + const { runWrangler } = await import("../utils/run-wrangler.js"); 143 + const rcloneModule = (await import("rclone.js")).default; 144 + 145 + setupMockFileSystem(); 146 + vi.mocked(runWrangler).mockClear(); 147 + vi.mocked(rcloneModule.promises.copy).mockClear(); 148 + 149 + // Test with local target - should skip batch upload even with credentials 150 + await populateCache( 151 + createTestBuildOptions(), 152 + createTestOpenNextConfig() as any, // eslint-disable-line @typescript-eslint/no-explicit-any 153 + createTestWranglerConfig() as any, // eslint-disable-line @typescript-eslint/no-explicit-any 154 + { target: "local" as const, shouldUsePreviewId: false }, 155 + { 156 + R2_ACCESS_KEY_ID: "test_access_key", 157 + R2_SECRET_ACCESS_KEY: "test_secret_key", 158 + CF_ACCOUNT_ID: "test_account_id", 159 + } as any // eslint-disable-line @typescript-eslint/no-explicit-any 160 + ); 161 + 162 + // Should use sequential upload (runWrangler), not batch upload (rclone.js) 163 + expect(runWrangler).toHaveBeenCalled(); 164 + expect(rcloneModule.promises.copy).not.toHaveBeenCalled(); 165 + }); 166 + 167 + test("uses sequential upload when R2 credentials are not provided", async () => { 168 + const { runWrangler } = await import("../utils/run-wrangler.js"); 169 + const rcloneModule = (await import("rclone.js")).default; 170 + 171 + setupMockFileSystem(); 172 + vi.mocked(runWrangler).mockClear(); 173 + vi.mocked(rcloneModule.promises.copy).mockClear(); 174 + 175 + // Test uses partial types for simplicity - full config not needed 176 + // Pass empty envVars to simulate no R2 credentials 177 + await populateCache( 178 + createTestBuildOptions(), 179 + createTestOpenNextConfig() as any, // eslint-disable-line @typescript-eslint/no-explicit-any 180 + createTestWranglerConfig() as any, // eslint-disable-line @typescript-eslint/no-explicit-any 181 + createTestPopulateCacheOptions(), 182 + {} as any // eslint-disable-line @typescript-eslint/no-explicit-any 183 + ); 184 + 185 + expect(runWrangler).toHaveBeenCalled(); 186 + expect(rcloneModule.promises.copy).not.toHaveBeenCalled(); 187 + }); 188 + 189 + test("uses batch upload with temporary config for remote target when R2 credentials are provided", async () => { 190 + const rcloneModule = (await import("rclone.js")).default; 191 + 192 + setupMockFileSystem(); 193 + vi.mocked(rcloneModule.promises.copy).mockClear(); 194 + 195 + // Test uses partial types for simplicity - full config not needed 196 + // Pass envVars with R2 credentials and remote target to enable batch upload 197 + await populateCache( 198 + createTestBuildOptions(), 199 + createTestOpenNextConfig() as any, // eslint-disable-line @typescript-eslint/no-explicit-any 200 + createTestWranglerConfig() as any, // eslint-disable-line @typescript-eslint/no-explicit-any 201 + { target: "remote" as const, shouldUsePreviewId: false }, 202 + { 203 + R2_ACCESS_KEY_ID: "test_access_key", 204 + R2_SECRET_ACCESS_KEY: "test_secret_key", 205 + CF_ACCOUNT_ID: "test_account_id", 206 + } as any // eslint-disable-line @typescript-eslint/no-explicit-any 207 + ); 208 + 209 + // Verify batch upload was used with correct parameters and temporary config 210 + expect(rcloneModule.promises.copy).toHaveBeenCalledWith( 211 + expect.any(String), // staging directory 212 + "r2:test-bucket", 213 + expect.objectContaining({ 214 + progress: true, 215 + transfers: 16, 216 + checkers: 8, 217 + env: expect.objectContaining({ 218 + RCLONE_CONFIG: expect.stringMatching(/rclone-config-\d+\.conf$/), 219 + }), 220 + }) 221 + ); 222 + }); 223 + 224 + test("handles rclone errors with status > 0 for remote target", async () => { 225 + const { runWrangler } = await import("../utils/run-wrangler.js"); 226 + const rcloneModule = (await import("rclone.js")).default; 227 + 228 + setupMockFileSystem(); 229 + 230 + // Mock rclone failure - Promise rejection 231 + vi.mocked(rcloneModule.promises.copy).mockRejectedValueOnce( 232 + new Error("rclone copy failed with exit code 7") 233 + ); 234 + 235 + vi.mocked(runWrangler).mockClear(); 236 + 237 + // Pass envVars with R2 credentials and remote target to enable batch upload (which will fail) 238 + await populateCache( 239 + createTestBuildOptions(), 240 + createTestOpenNextConfig() as any, // eslint-disable-line @typescript-eslint/no-explicit-any 241 + createTestWranglerConfig() as any, // eslint-disable-line @typescript-eslint/no-explicit-any 242 + { target: "remote" as const, shouldUsePreviewId: false }, 243 + { 244 + R2_ACCESS_KEY_ID: "test_access_key", 245 + R2_SECRET_ACCESS_KEY: "test_secret_key", 246 + CF_ACCOUNT_ID: "test_account_id", 247 + } as any // eslint-disable-line @typescript-eslint/no-explicit-any 248 + ); 249 + 250 + // Should fall back to sequential upload when batch upload fails 251 + expect(runWrangler).toHaveBeenCalled(); 252 + }); 253 + 254 + test("handles rclone errors with stderr output for remote target", async () => { 255 + const { runWrangler } = await import("../utils/run-wrangler.js"); 256 + const rcloneModule = (await import("rclone.js")).default; 257 + 258 + setupMockFileSystem(); 259 + 260 + // Mock rclone error - Promise rejection with stderr message 261 + vi.mocked(rcloneModule.promises.copy).mockRejectedValueOnce( 262 + new Error("ERROR : Failed to copy: AccessDenied: Access Denied (403)") 263 + ); 264 + 265 + vi.mocked(runWrangler).mockClear(); 266 + 267 + // Pass envVars with R2 credentials and remote target to enable batch upload (which will fail) 268 + await populateCache( 269 + createTestBuildOptions(), 270 + createTestOpenNextConfig() as any, // eslint-disable-line @typescript-eslint/no-explicit-any 271 + createTestWranglerConfig() as any, // eslint-disable-line @typescript-eslint/no-explicit-any 272 + { target: "remote" as const, shouldUsePreviewId: false }, 273 + { 274 + R2_ACCESS_KEY_ID: "test_access_key", 275 + R2_SECRET_ACCESS_KEY: "test_secret_key", 276 + CF_ACCOUNT_ID: "test_account_id", 277 + } as any // eslint-disable-line @typescript-eslint/no-explicit-any 278 + ); 279 + 280 + // Should fall back to standard upload when batch upload fails 281 + expect(runWrangler).toHaveBeenCalled(); 282 + }); 283 + }); 284 + });
+181 -14
packages/cloudflare/src/cli/commands/populate-cache.ts
··· 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 4 5 import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; ··· 11 12 } from "@opennextjs/aws/types/open-next.js"; 12 13 import type { IncrementalCache, TagCache } from "@opennextjs/aws/types/overrides.js"; 13 14 import { globSync } from "glob"; 15 + import rclone from "rclone.js"; 14 16 import { tqdm } from "ts-tqdm"; 15 17 import type { Unstable_Config as WranglerConfig } from "wrangler"; 16 18 import type yargs from "yargs"; ··· 204 206 shouldUsePreviewId: boolean; 205 207 }; 206 208 207 - async function populateR2IncrementalCache( 208 - buildOpts: BuildOptions, 209 - config: WranglerConfig, 210 - populateCacheOptions: PopulateCacheOptions, 209 + /** 210 + * Create a temporary configuration file for batch upload from environment variables 211 + * @returns Path to the temporary config file or null if env vars not available 212 + */ 213 + function createTempRcloneConfig(accessKey: string, secretKey: string, accountId: string): string | null { 214 + const tempDir = tmpdir(); 215 + const tempConfigPath = path.join(tempDir, `rclone-config-${Date.now()}.conf`); 216 + 217 + const configContent = `[r2] 218 + type = s3 219 + provider = Cloudflare 220 + access_key_id = ${accessKey} 221 + secret_access_key = ${secretKey} 222 + endpoint = https://${accountId}.r2.cloudflarestorage.com 223 + acl = private 224 + `; 225 + 226 + /** 227 + * 0o600 is an octal number (the 0o prefix indicates octal in JavaScript) 228 + * that represents Unix file permissions: 229 + * 230 + * - 6 (owner): read (4) + write (2) = readable and writable by the file owner 231 + * - 0 (group): no permissions for the group 232 + * - 0 (others): no permissions for anyone else 233 + * 234 + * In symbolic notation, this is: rw------- 235 + */ 236 + writeFileSync(tempConfigPath, configContent, { mode: 0o600 }); 237 + return tempConfigPath; 238 + } 239 + 240 + /** 241 + * Populate R2 incremental cache using batch upload for better performance 242 + * Uses parallel transfers to significantly speed up cache population 243 + */ 244 + async function populateR2IncrementalCacheWithBatchUpload( 245 + bucket: string, 246 + prefix: string | undefined, 247 + assets: CacheAsset[], 211 248 envVars: WorkerEnvVar 212 249 ) { 213 - logger.info("\nPopulating R2 incremental cache..."); 250 + const accessKey = envVars.R2_ACCESS_KEY_ID || null; 251 + const secretKey = envVars.R2_SECRET_ACCESS_KEY || null; 252 + const accountId = envVars.CF_ACCOUNT_ID || null; 214 253 215 - const binding = config.r2_buckets.find(({ binding }) => binding === R2_CACHE_BINDING_NAME); 216 - if (!binding) { 217 - throw new Error(`No R2 binding ${JSON.stringify(R2_CACHE_BINDING_NAME)} found!`); 254 + // Ensure all required env vars are set correctly 255 + if (!accessKey || !secretKey || !accountId) { 256 + throw new Error( 257 + "Please set R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, and CF_ACCOUNT_ID environment variables to enable faster batch upload for remote R2." 258 + ); 218 259 } 219 260 220 - const bucket = binding.bucket_name; 221 - if (!bucket) { 222 - throw new Error(`R2 binding ${JSON.stringify(R2_CACHE_BINDING_NAME)} should have a 'bucket_name'`); 261 + logger.info("\nPopulating remote R2 incremental cache using batch upload..."); 262 + 263 + // Create temporary config from env vars - required for batch upload 264 + const tempConfigPath = createTempRcloneConfig(accessKey, secretKey, accountId); 265 + if (!tempConfigPath) { 266 + throw new Error("Failed to create temporary rclone config for R2 batch upload."); 267 + } 268 + 269 + const env = { 270 + ...process.env, 271 + RCLONE_CONFIG: tempConfigPath, 272 + }; 273 + 274 + logger.info("Using batch upload with R2 credentials from environment variables"); 275 + 276 + // Create a staging dir in temp directory with proper key paths 277 + const tempDir = tmpdir(); 278 + const stagingDir = path.join(tempDir, `.r2-staging-${Date.now()}`); 279 + 280 + // Track success to ensure cleanup happens correctly 281 + let success = null; 282 + 283 + try { 284 + mkdirSync(stagingDir, { recursive: true }); 285 + 286 + for (const { fullPath, key, buildId, isFetch } of assets) { 287 + const cacheKey = computeCacheKey(key, { 288 + prefix, 289 + buildId, 290 + cacheType: isFetch ? "fetch" : "cache", 291 + }); 292 + const destPath = path.join(stagingDir, cacheKey); 293 + mkdirSync(path.dirname(destPath), { recursive: true }); 294 + copyFileSync(fullPath, destPath); 295 + } 296 + 297 + // Use rclone.js to sync the R2 298 + const remote = `r2:${bucket}`; 299 + 300 + // Using rclone.js Promise-based API for the copy operation 301 + await rclone.promises.copy(stagingDir, remote, { 302 + progress: true, 303 + transfers: 16, 304 + checkers: 8, 305 + env, 306 + }); 307 + 308 + logger.info(`Successfully uploaded ${assets.length} assets to R2 using batch upload`); 309 + success = true; 310 + } finally { 311 + try { 312 + // Cleanup temporary staging directory 313 + rmSync(stagingDir, { recursive: true, force: true }); 314 + } catch { 315 + console.warn(`Failed to remove temporary staging directory at ${stagingDir}`); 316 + } 317 + 318 + try { 319 + // Cleanup temporary config file 320 + rmSync(tempConfigPath); 321 + } catch { 322 + console.warn(`Failed to remove temporary config at ${tempConfigPath}`); 323 + } 223 324 } 224 325 225 - const prefix = envVars[R2_CACHE_PREFIX_ENV_NAME]; 326 + if (!success) { 327 + throw new Error("R2 batch upload failed, falling back to sequential uploads..."); 328 + } 329 + } 226 330 227 - const assets = getCacheAssets(buildOpts); 331 + /** 332 + * Populate R2 incremental cache using sequential Wrangler uploads 333 + * Falls back to this method when batch upload is not available or fails 334 + */ 335 + async function populateR2IncrementalCacheWithSequentialUpload( 336 + buildOpts: BuildOptions, 337 + bucket: string, 338 + prefix: string | undefined, 339 + assets: CacheAsset[], 340 + populateCacheOptions: PopulateCacheOptions 341 + ) { 342 + logger.info("Using sequential cache uploads."); 228 343 229 344 for (const { fullPath, key, buildId, isFetch } of tqdm(assets)) { 230 345 const cacheKey = computeCacheKey(key, { ··· 250 365 ); 251 366 } 252 367 logger.info(`Successfully populated cache with ${assets.length} assets`); 368 + } 369 + 370 + async function populateR2IncrementalCache( 371 + buildOpts: BuildOptions, 372 + config: WranglerConfig, 373 + populateCacheOptions: PopulateCacheOptions, 374 + envVars: WorkerEnvVar 375 + ) { 376 + logger.info("\nPopulating R2 incremental cache..."); 377 + 378 + const binding = config.r2_buckets.find(({ binding }) => binding === R2_CACHE_BINDING_NAME); 379 + if (!binding) { 380 + throw new Error(`No R2 binding ${JSON.stringify(R2_CACHE_BINDING_NAME)} found!`); 381 + } 382 + 383 + const bucket = binding.bucket_name; 384 + if (!bucket) { 385 + throw new Error(`R2 binding ${JSON.stringify(R2_CACHE_BINDING_NAME)} should have a 'bucket_name'`); 386 + } 387 + 388 + const prefix = envVars[R2_CACHE_PREFIX_ENV_NAME]; 389 + 390 + const assets = getCacheAssets(buildOpts); 391 + 392 + // Force sequential upload for local target 393 + if (populateCacheOptions.target === "local") { 394 + logger.info("Using sequential upload for local R2 (batch upload only works with remote R2)"); 395 + return await populateR2IncrementalCacheWithSequentialUpload( 396 + buildOpts, 397 + bucket, 398 + prefix, 399 + assets, 400 + populateCacheOptions 401 + ); 402 + } 403 + 404 + try { 405 + // Attempt batch upload first (using rclone) - only for remote target 406 + return await populateR2IncrementalCacheWithBatchUpload(bucket, prefix, assets, envVars); 407 + } catch (error) { 408 + logger.warn(`Batch upload failed: ${error instanceof Error ? error.message : error}`); 409 + logger.info("Falling back to sequential uploads..."); 410 + 411 + // Sequential upload fallback (using Wrangler) 412 + return await populateR2IncrementalCacheWithSequentialUpload( 413 + buildOpts, 414 + bucket, 415 + prefix, 416 + assets, 417 + populateCacheOptions 418 + ); 419 + } 253 420 } 254 421 255 422 async function populateKVIncrementalCache(
+31
pnpm-lock.yaml
··· 1077 1077 '@opennextjs/aws': 1078 1078 specifier: 3.8.4 1079 1079 version: 3.8.4 1080 + '@types/rclone.js': 1081 + specifier: ^0.6.3 1082 + version: 0.6.3 1080 1083 cloudflare: 1081 1084 specifier: ^4.4.1 1082 1085 version: 4.4.1 ··· 1086 1089 glob: 1087 1090 specifier: 'catalog:' 1088 1091 version: 11.0.0 1092 + rclone.js: 1093 + specifier: ^0.6.6 1094 + version: 0.6.6 1089 1095 ts-tqdm: 1090 1096 specifier: ^0.8.6 1091 1097 version: 0.8.6 ··· 4727 4733 '@types/range-parser@1.2.7': 4728 4734 resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} 4729 4735 4736 + '@types/rclone.js@0.6.3': 4737 + resolution: {integrity: sha512-BssKAAVRY//fxGKso8SatyOwiD7X0toDofNnVxZlIXmN7UHrn2UBTxldNAjgUvWA91qJyeEPfKmeJpZVhLugXg==} 4738 + 4730 4739 '@types/react-dom@18.3.0': 4731 4740 resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} 4732 4741 ··· 5029 5038 resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} 5030 5039 engines: {node: '>=0.4.0'} 5031 5040 hasBin: true 5041 + 5042 + adm-zip@0.5.16: 5043 + resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} 5044 + engines: {node: '>=12.0'} 5032 5045 5033 5046 agent-base@6.0.2: 5034 5047 resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} ··· 8427 8440 resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} 8428 8441 hasBin: true 8429 8442 8443 + rclone.js@0.6.6: 8444 + resolution: {integrity: sha512-Dxh34cab/fNjFq5SSm0fYLNkGzG2cQSBy782UW9WwxJCEiVO4cGXkvaXcNlgv817dK8K8PuQ+NHUqSAMMhWujQ==} 8445 + engines: {node: '>=12'} 8446 + cpu: [arm, arm64, mips, mipsel, x32, x64] 8447 + os: [darwin, freebsd, linux, openbsd, sunos, win32] 8448 + hasBin: true 8449 + 8430 8450 react-dom@18.3.1: 8431 8451 resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} 8432 8452 peerDependencies: ··· 14175 14195 14176 14196 '@types/range-parser@1.2.7': {} 14177 14197 14198 + '@types/rclone.js@0.6.3': 14199 + dependencies: 14200 + '@types/node': 20.14.10 14201 + 14178 14202 '@types/react-dom@18.3.0': 14179 14203 dependencies: 14180 14204 '@types/react': 19.0.8 ··· 14762 14786 acorn@8.14.1: {} 14763 14787 14764 14788 acorn@8.15.0: {} 14789 + 14790 + adm-zip@0.5.16: {} 14765 14791 14766 14792 agent-base@6.0.2: 14767 14793 dependencies: ··· 19101 19127 ini: 1.3.8 19102 19128 minimist: 1.2.8 19103 19129 strip-json-comments: 2.0.1 19130 + 19131 + rclone.js@0.6.6: 19132 + dependencies: 19133 + adm-zip: 0.5.16 19134 + mri: 1.2.0 19104 19135 19105 19136 react-dom@18.3.1(react@18.3.1): 19106 19137 dependencies: