@opennextjs/cloudflare 1.19.5 → 1.19.7

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.
@@ -1,2 +1,27 @@
1
1
  import type { CodePatcher } from "@opennextjs/aws/build/patch/codePatcher.js";
2
+ /**
3
+ * Replace Turbopack's `loadWebAssemblyModule` with a call to our generated `loadWasmChunk`.
4
+ *
5
+ * The original implementation uses `WebAssembly.compileStreaming`, which is not available
6
+ * in workerd. `loadWasmChunk` resolves the chunk via a static `import()` switch so the
7
+ * bundler can statically discover and bundle each `.wasm` chunk.
8
+ */
9
+ export declare const replaceLoadWebAssemblyModuleRule = "\nrule:\n kind: function_declaration\n has:\n field: name\n regex: \"^loadWebAssemblyModule$\"\nfix: |-\n function loadWebAssemblyModule(chunkPath, _edgeModule) {\n return loadWasmChunk(chunkPath);\n }\n";
10
+ /**
11
+ * Replace Turbopack's `loadWebAssembly` with a synchronous-instantiation variant.
12
+ *
13
+ * The original implementation uses `WebAssembly.instantiateStreaming`, which is not
14
+ * available in workerd. We load the compiled module via `loadWasmChunk` and then call
15
+ * the synchronous `WebAssembly.instantiate` to produce the instance's exports.
16
+ */
17
+ export declare const replaceLoadWebAssemblyRule = "\nrule:\n kind: function_declaration\n has:\n field: name\n regex: \"^loadWebAssembly$\"\nfix: |-\n async function loadWebAssembly(chunkPath, _edgeModule, imports) {\n const mod = await loadWasmChunk(chunkPath);\n const { exports } = await WebAssembly.instantiate(mod, imports);\n return exports;\n }\n";
2
18
  export declare const patchTurbopackRuntime: CodePatcher;
19
+ /**
20
+ * Generate a `loadWasmChunk` function that maps a `.next`-relative chunk path to a
21
+ * statically-importable `.wasm` module.
22
+ *
23
+ * The replacement rules for Turbopack's `loadWebAssembly{,Module}` delegate to this
24
+ * function. Because the imports are emitted as string literals, the bundler can
25
+ * statically discover every wasm chunk and include them in the final build.
26
+ */
27
+ export declare function loadWasmChunkFn(tracedFiles: string[]): string;
@@ -9,6 +9,44 @@ rule:
9
9
  fix:
10
10
  requireChunk(chunkPath)
11
11
  `;
12
+ /**
13
+ * Replace Turbopack's `loadWebAssemblyModule` with a call to our generated `loadWasmChunk`.
14
+ *
15
+ * The original implementation uses `WebAssembly.compileStreaming`, which is not available
16
+ * in workerd. `loadWasmChunk` resolves the chunk via a static `import()` switch so the
17
+ * bundler can statically discover and bundle each `.wasm` chunk.
18
+ */
19
+ export const replaceLoadWebAssemblyModuleRule = `
20
+ rule:
21
+ kind: function_declaration
22
+ has:
23
+ field: name
24
+ regex: "^loadWebAssemblyModule$"
25
+ fix: |-
26
+ function loadWebAssemblyModule(chunkPath, _edgeModule) {
27
+ return loadWasmChunk(chunkPath);
28
+ }
29
+ `;
30
+ /**
31
+ * Replace Turbopack's `loadWebAssembly` with a synchronous-instantiation variant.
32
+ *
33
+ * The original implementation uses `WebAssembly.instantiateStreaming`, which is not
34
+ * available in workerd. We load the compiled module via `loadWasmChunk` and then call
35
+ * the synchronous `WebAssembly.instantiate` to produce the instance's exports.
36
+ */
37
+ export const replaceLoadWebAssemblyRule = `
38
+ rule:
39
+ kind: function_declaration
40
+ has:
41
+ field: name
42
+ regex: "^loadWebAssembly$"
43
+ fix: |-
44
+ async function loadWebAssembly(chunkPath, _edgeModule, imports) {
45
+ const mod = await loadWasmChunk(chunkPath);
46
+ const { exports } = await WebAssembly.instantiate(mod, imports);
47
+ return exports;
48
+ }
49
+ `;
12
50
  /**
13
51
  * Discover Turbopack external module mappings by reading symlinks in .next/node_modules/.
14
52
  *
@@ -196,7 +234,10 @@ export const patchTurbopackRuntime = {
196
234
  const externalImportRule = buildExternalImportRule(mappings, tracedFiles, code);
197
235
  let patched = patchCode(code, externalImportRule);
198
236
  patched = patchCode(patched, inlineChunksRule);
199
- return `${patched}\n${inlineChunksFn(tracedFiles)}`;
237
+ patched = patchCode(patched, replaceLoadWebAssemblyModuleRule);
238
+ patched = patchCode(patched, replaceLoadWebAssemblyRule);
239
+ return `${patched}
240
+ ${inlineChunksFn(tracedFiles)}\n${loadWasmChunkFn(tracedFiles)}`;
200
241
  },
201
242
  },
202
243
  ],
@@ -230,3 +271,27 @@ ${chunks
230
271
  }
231
272
  `;
232
273
  }
274
+ /**
275
+ * Generate a `loadWasmChunk` function that maps a `.next`-relative chunk path to a
276
+ * statically-importable `.wasm` module.
277
+ *
278
+ * The replacement rules for Turbopack's `loadWebAssembly{,Module}` delegate to this
279
+ * function. Because the imports are emitted as string literals, the bundler can
280
+ * statically discover every wasm chunk and include them in the final build.
281
+ */
282
+ export function loadWasmChunkFn(tracedFiles) {
283
+ const wasmFiles = tracedFiles.filter((f) => f.endsWith(".wasm"));
284
+ const cases = wasmFiles
285
+ .map((absPath) => ({ absPath, relPath: absPath.replace(/.*\/\.next\//, "") }))
286
+ .map(({ absPath, relPath }) => ` case "${relPath}": return (await import("${absPath}")).default;`)
287
+ .join("\n");
288
+ return `
289
+ async function loadWasmChunk(chunkPath) {
290
+ switch (chunkPath) {
291
+ ${cases}
292
+ default:
293
+ throw new Error(\`Unknown wasm chunk: \${chunkPath}\`);
294
+ }
295
+ }
296
+ `;
297
+ }
@@ -14,24 +14,23 @@ import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
14
14
  */
15
15
  export function transformBuildCondition(conditionMap, condition) {
16
16
  const transformed = {};
17
- const hasTopLevelBuildCondition = Object.keys(conditionMap).some((key) => key === condition && typeof conditionMap[key] === "string");
17
+ const hasTopLevelBuildCondition = condition in conditionMap && conditionMap[condition] != null;
18
18
  let hasBuildCondition = hasTopLevelBuildCondition;
19
19
  for (const [key, value] of Object.entries(conditionMap)) {
20
20
  if (typeof value === "object" && value != null) {
21
21
  const { transformedExports, hasBuildCondition: innerBuildCondition } = transformBuildCondition(value, condition);
22
+ // If a build condition is present at this level but a sibling
23
+ // subtree doesn't contain the build condition, we can drop it entirely.
24
+ if (hasTopLevelBuildCondition && key !== condition && !innerBuildCondition) {
25
+ continue;
26
+ }
22
27
  transformed[key] = transformedExports;
23
28
  hasBuildCondition ||= innerBuildCondition;
24
29
  }
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
- }
30
+ else if (!hasTopLevelBuildCondition || key === condition) {
31
+ // If there is no build condition at this level or this is a non-object build condition,
32
+ // we need to keep the child condition as is.
33
+ transformed[key] = value;
35
34
  }
36
35
  }
37
36
  return { transformedExports: transformed, hasBuildCondition };
@@ -4,6 +4,10 @@ import type { Unstable_Config as WranglerConfig } from "wrangler";
4
4
  import type yargs from "yargs";
5
5
  import { type WorkerEnvVar } from "./utils/helpers.js";
6
6
  import type { WranglerTarget } from "./utils/run-wrangler.js";
7
+ export declare const MAX_REQUEST_RETRIES = 15;
8
+ export declare const BASE_RETRY_DELAY_MS = 250;
9
+ export declare const MAX_RETRY_DELAY_MS = 10000;
10
+ export declare const BACKOFF_FACTOR: number;
7
11
  export declare function populateCache(buildOpts: BuildOptions, config: OpenNextConfig, wranglerConfig: WranglerConfig, populateCacheOptions: PopulateCacheOptions, envVars: WorkerEnvVar): Promise<void>;
8
12
  export type CacheAsset = {
9
13
  isFetch: boolean;
@@ -19,6 +19,14 @@ import { normalizePath } from "../utils/normalize-path.js";
19
19
  import { getEnvFromPlatformProxy, quoteShellMeta } from "./utils/helpers.js";
20
20
  import { runWrangler } from "./utils/run-wrangler.js";
21
21
  import { getNormalizedOptions, printHeaders, readWranglerConfig, retrieveCompiledConfig, withWranglerOptions, withWranglerPassthroughArgs, } from "./utils/utils.js";
22
+ // Maximum number of attempts to send the request
23
+ export const MAX_REQUEST_RETRIES = 15;
24
+ // Base delay for retries
25
+ export const BASE_RETRY_DELAY_MS = 250;
26
+ // Maximum delay for retries, used to calculate the backoff factor
27
+ export const MAX_RETRY_DELAY_MS = 10_000;
28
+ // Backoff factor for retries, calculated to ensure that the delay grows exponentially up to the maximum delay
29
+ export const BACKOFF_FACTOR = (MAX_RETRY_DELAY_MS / BASE_RETRY_DELAY_MS) ** (1 / (MAX_REQUEST_RETRIES - 1));
22
30
  /**
23
31
  * Implementation of the `opennextjs-cloudflare populateCache` command.
24
32
  *
@@ -199,49 +207,32 @@ async function populateR2IncrementalCache(buildOpts, config, populateCacheOption
199
207
  /**
200
208
  * Sends cache entries to the R2 worker, one entry per request.
201
209
  *
202
- * Up to `concurrency` requests are in-flight at any given time.
203
- * Retry logic for transient R2 write failures is handled by the worker.
204
- *
205
210
  * @param options
206
211
  * @param options.workerUrl - The URL of the local R2 worker's `/populate` endpoint.
207
212
  * @param options.assets - The cache assets to write, as collected by {@link getCacheAssets}.
208
213
  * @param options.prefix - Optional prefix prepended to each R2 key.
209
- * @param options.concurrency - Maximum number of concurrent in-flight requests.
214
+ * @param options.maxConcurrency - Maximum number of concurrent in-flight requests.
210
215
  * @returns Resolves when all entries have been written successfully.
211
216
  * @throws {Error} If any entry fails after all retries or encounters a non-retryable error.
212
217
  */
213
218
  async function sendEntriesToR2Worker(options) {
214
219
  const { workerUrl, assets, prefix, maxConcurrency } = options;
215
- // Build the list of entries to send (key + filename).
216
- // File contents are read lazily in sendEntryToR2Worker to avoid
217
- // loading all cache values into memory at once.
218
- const entries = assets.map(({ fullPath, key, buildId, isFetch }) => ({
219
- key: computeCacheKey(key, {
220
- prefix,
221
- buildId,
222
- cacheType: isFetch ? "fetch" : "cache",
223
- }),
224
- filename: fullPath,
225
- }));
226
- // Use a concurrency-limited loop with a progress bar.
227
- // `pending` tracks in-flight promises so we can cap concurrency.
228
220
  const pending = new Set();
229
- let concurrency = 1;
230
- for (const entry of tqdm(entries)) {
221
+ for (const asset of tqdm(assets)) {
222
+ const { fullPath, key, buildId, isFetch } = asset;
231
223
  const task = sendEntryToR2Worker({
232
224
  workerUrl,
233
- key: entry.key,
234
- filename: entry.filename,
225
+ key: computeCacheKey(key, {
226
+ prefix,
227
+ buildId,
228
+ cacheType: isFetch ? "fetch" : "cache",
229
+ }),
230
+ filename: fullPath,
235
231
  }).finally(() => pending.delete(task));
236
232
  pending.add(task);
237
233
  // If we've reached the concurrency limit, wait for one to finish.
238
- if (pending.size >= concurrency) {
234
+ if (pending.size >= maxConcurrency) {
239
235
  await Promise.race(pending);
240
- // Increase concurrency gradually to avoid overwhelming the worker
241
- // with too many requests at once.
242
- if (concurrency < maxConcurrency) {
243
- concurrency++;
244
- }
245
236
  }
246
237
  }
247
238
  await Promise.all(pending);
@@ -261,9 +252,7 @@ class RetryableWorkerError extends Error {
261
252
  */
262
253
  async function sendEntryToR2Worker(options) {
263
254
  const { workerUrl, key, filename } = options;
264
- const CLIENT_RETRY_ATTEMPTS = 5;
265
- const CLIENT_RETRY_BASE_DELAY_MS = 250;
266
- for (let attempt = 0; attempt < CLIENT_RETRY_ATTEMPTS; attempt++) {
255
+ for (let attempt = 0; attempt < MAX_REQUEST_RETRIES; attempt++) {
267
256
  try {
268
257
  let response;
269
258
  try {
@@ -280,9 +269,7 @@ async function sendEntryToR2Worker(options) {
280
269
  });
281
270
  }
282
271
  catch (e) {
283
- throw new RetryableWorkerError(`Failed to send request to R2 worker: ${e instanceof Error ? e.message : String(e)}`, {
284
- cause: e,
285
- });
272
+ throw new RetryableWorkerError(`Failed to send request to R2 worker: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
286
273
  }
287
274
  const body = await response.text();
288
275
  let result;
@@ -290,6 +277,7 @@ async function sendEntryToR2Worker(options) {
290
277
  result = JSON.parse(body);
291
278
  }
292
279
  catch (e) {
280
+ // https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1102
293
281
  if (body.includes("Worker exceeded resource limits")) {
294
282
  throw new RetryableWorkerError("Worker exceeded resource limits", { cause: e });
295
283
  }
@@ -300,21 +288,20 @@ async function sendEntryToR2Worker(options) {
300
288
  cause: e,
301
289
  });
302
290
  }
303
- if (!result.success && response.status >= 500) {
304
- throw new RetryableWorkerError(result.error);
305
- }
306
291
  if (!result.success) {
307
- throw new Error(`Failed to write "${key}" to R2: ${result.error}`);
292
+ throw response.status >= 500
293
+ ? new RetryableWorkerError(result.error)
294
+ : new Error(`Failed to write "${key}" to R2: ${result.error}`);
308
295
  }
309
296
  return;
310
297
  }
311
298
  catch (e) {
312
- if (e instanceof RetryableWorkerError && attempt < CLIENT_RETRY_ATTEMPTS - 1) {
299
+ if (e instanceof RetryableWorkerError && attempt < MAX_REQUEST_RETRIES - 1) {
313
300
  logger.error(`Attempt ${attempt + 1} to write "${key}" failed with a retryable error: ${e.message}. Retrying...`);
314
- await setTimeout(CLIENT_RETRY_BASE_DELAY_MS * Math.pow(2, attempt));
301
+ await setTimeout(BASE_RETRY_DELAY_MS * Math.pow(BACKOFF_FACTOR, attempt));
315
302
  continue;
316
303
  }
317
- throw new Error(`Failed to write "${key}" to R2 after ${CLIENT_RETRY_ATTEMPTS} attempts`, {
304
+ throw new Error(`Failed to write "${key}" to R2 after ${MAX_REQUEST_RETRIES} attempts`, {
318
305
  cause: e,
319
306
  });
320
307
  }
@@ -404,7 +391,8 @@ function populateD1TagCache(buildOpts, config, populateCacheOptions) {
404
391
  target: populateCacheOptions.target,
405
392
  environment: populateCacheOptions.environment,
406
393
  configPath: populateCacheOptions.wranglerConfigPath,
407
- logging: "error",
394
+ // Do not log errors since the ALTER TABLE command will fail if the columns already exist.
395
+ logging: "none",
408
396
  });
409
397
  logger.info("\nSuccessfully created D1 table");
410
398
  }
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.19.5",
4
+ "version": "1.19.7",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "opennextjs-cloudflare": "dist/cli/index.js"
@@ -54,7 +54,7 @@
54
54
  "yargs": "^18.0.0"
55
55
  },
56
56
  "devDependencies": {
57
- "@cloudflare/workers-types": "^4.20260423.1",
57
+ "@cloudflare/workers-types": "^4.20260426.1",
58
58
  "@eslint/js": "^9.11.1",
59
59
  "@tsconfig/strictest": "^2.0.5",
60
60
  "@types/mock-fs": "^4.13.4",
@@ -69,7 +69,7 @@
69
69
  "eslint-plugin-unicorn": "^55.0.0",
70
70
  "globals": "^15.9.0",
71
71
  "mock-fs": "^5.4.1",
72
- "next": "~15.5.15",
72
+ "next": "^15.5.16",
73
73
  "picomatch": "^4.0.2",
74
74
  "rimraf": "^6.0.1",
75
75
  "typescript": "^5.9.3",
@@ -77,7 +77,7 @@
77
77
  "vitest": "^4.1.4"
78
78
  },
79
79
  "peerDependencies": {
80
- "next": ">=15.5.15 <16 || >=16.2.3",
80
+ "next": ">=15.5.16 <16 || >=16.2.5",
81
81
  "wrangler": "^4.86.0"
82
82
  },
83
83
  "scripts": {