vinext 1.4.0 → 1.4.2

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.
Files changed (156) hide show
  1. package/README.md +21 -21
  2. package/dist/build/action-owner-manifest.js +1 -0
  3. package/dist/build/prerender.js +3 -2
  4. package/dist/check.js +4 -7
  5. package/dist/client/dev-error-overlay-store.d.ts +1 -0
  6. package/dist/client/dev-error-overlay.d.ts +2 -0
  7. package/dist/client/dev-error-overlay.js +19 -3
  8. package/dist/config/config-matchers.js +4 -2
  9. package/dist/config/destination-params.d.ts +2 -1
  10. package/dist/config/destination-params.js +55 -2
  11. package/dist/config/next-config.d.ts +22 -1
  12. package/dist/config/next-config.js +49 -4
  13. package/dist/entries/app-rsc-entry.js +41 -19
  14. package/dist/entries/app-rsc-manifest.js +4 -0
  15. package/dist/entries/pages-server-entry.js +5 -1
  16. package/dist/index.js +44 -32
  17. package/dist/init-cloudflare.js +18 -2
  18. package/dist/plugins/css-target.d.ts +18 -0
  19. package/dist/plugins/css-target.js +93 -0
  20. package/dist/plugins/strip-server-exports.js +2 -1
  21. package/dist/routing/app-route-graph.d.ts +10 -0
  22. package/dist/routing/app-route-graph.js +15 -1
  23. package/dist/server/api-handler.d.ts +2 -0
  24. package/dist/server/api-handler.js +5 -6
  25. package/dist/server/app-action-forwarding.js +3 -1
  26. package/dist/server/app-browser-entry.js +28 -7
  27. package/dist/server/app-browser-rsc-redirect.js +3 -3
  28. package/dist/server/app-browser-server-action-client.js +2 -2
  29. package/dist/server/app-browser-visible-commit.js +2 -2
  30. package/dist/server/app-elements.d.ts +3 -0
  31. package/dist/server/app-elements.js +14 -1
  32. package/dist/server/app-fallback-renderer.js +2 -2
  33. package/dist/server/app-optimistic-routing.d.ts +12 -1
  34. package/dist/server/app-optimistic-routing.js +40 -19
  35. package/dist/server/app-page-boundary-render.js +7 -12
  36. package/dist/server/app-page-cache-render.js +1 -1
  37. package/dist/server/app-page-dispatch.d.ts +10 -2
  38. package/dist/server/app-page-dispatch.js +73 -31
  39. package/dist/server/app-page-element-builder.d.ts +9 -20
  40. package/dist/server/app-page-element-builder.js +40 -52
  41. package/dist/server/app-page-execution.js +9 -6
  42. package/dist/server/app-page-intercept-segment-config.d.ts +56 -0
  43. package/dist/server/app-page-intercept-segment-config.js +90 -0
  44. package/dist/server/app-page-navigation-params.d.ts +23 -0
  45. package/dist/server/app-page-navigation-params.js +49 -0
  46. package/dist/server/app-page-render.d.ts +5 -3
  47. package/dist/server/app-page-render.js +31 -17
  48. package/dist/server/app-page-request.d.ts +24 -2
  49. package/dist/server/app-page-request.js +10 -10
  50. package/dist/server/app-page-response.js +1 -1
  51. package/dist/server/app-page-route-wiring.d.ts +10 -0
  52. package/dist/server/app-page-route-wiring.js +34 -13
  53. package/dist/server/app-page-segment-plan.js +9 -1
  54. package/dist/server/app-page-segment-state.d.ts +9 -0
  55. package/dist/server/app-page-segment-state.js +54 -1
  56. package/dist/server/app-page-stream.d.ts +10 -8
  57. package/dist/server/app-page-stream.js +2 -1
  58. package/dist/server/app-request-stage-independent-entry.js +1 -1
  59. package/dist/server/app-response-stage-entry.js +1 -0
  60. package/dist/server/app-route-handler-execution.js +3 -2
  61. package/dist/server/app-route-handler-runtime.d.ts +1 -1
  62. package/dist/server/app-route-handler-runtime.js +11 -7
  63. package/dist/server/app-route-module-loader.d.ts +2 -0
  64. package/dist/server/app-route-module-loader.js +1 -0
  65. package/dist/server/app-router-entry.js +1 -1
  66. package/dist/server/app-rsc-cache-busting.d.ts +10 -10
  67. package/dist/server/app-rsc-cache-busting.js +27 -68
  68. package/dist/server/app-rsc-error-handler.js +1 -1
  69. package/dist/server/app-rsc-errors.js +4 -1
  70. package/dist/server/app-rsc-handler.d.ts +10 -1
  71. package/dist/server/app-rsc-handler.js +126 -103
  72. package/dist/server/app-rsc-request-normalization.d.ts +10 -12
  73. package/dist/server/app-rsc-request-normalization.js +11 -14
  74. package/dist/server/app-rsc-response-finalizer.d.ts +3 -0
  75. package/dist/server/app-rsc-response-finalizer.js +11 -1
  76. package/dist/server/app-rsc-response-stage.js +4 -12
  77. package/dist/server/app-rsc-route-matching.d.ts +14 -0
  78. package/dist/server/app-rsc-route-matching.js +42 -18
  79. package/dist/server/app-server-action-execution.js +24 -14
  80. package/dist/server/app-ssr-entry.d.ts +6 -0
  81. package/dist/server/app-ssr-entry.js +13 -8
  82. package/dist/server/app-worker-stages.d.ts +12 -3
  83. package/dist/server/app-worker-stages.js +37 -4
  84. package/dist/server/bfcache-identity.d.ts +7 -0
  85. package/dist/server/bfcache-identity.js +10 -1
  86. package/dist/server/cache-control.d.ts +2 -0
  87. package/dist/server/cache-control.js +3 -1
  88. package/dist/server/cacheability-manifest.js +1 -2
  89. package/dist/server/dev-origin-check.d.ts +2 -1
  90. package/dist/server/dev-origin-check.js +2 -1
  91. package/dist/server/dev-server.js +2 -1
  92. package/dist/server/image-local-patterns.d.ts +21 -0
  93. package/dist/server/image-local-patterns.js +59 -0
  94. package/dist/server/image-optimization.d.ts +28 -22
  95. package/dist/server/image-optimization.js +17 -2
  96. package/dist/server/isr-cache.js +4 -1
  97. package/dist/server/metadata-route-response.js +25 -7
  98. package/dist/server/middleware-matcher.js +19 -6
  99. package/dist/server/middleware-runtime.js +6 -2
  100. package/dist/server/multi-stage.d.ts +7 -3
  101. package/dist/server/nitro-public-files-plugin.js +20 -8
  102. package/dist/server/pages-data-route.d.ts +5 -1
  103. package/dist/server/pages-data-route.js +14 -6
  104. package/dist/server/pages-node-compat.d.ts +1 -1
  105. package/dist/server/pages-node-compat.js +2 -0
  106. package/dist/server/pages-page-data.js +2 -1
  107. package/dist/server/pages-page-handler.js +17 -6
  108. package/dist/server/pages-page-response.d.ts +9 -0
  109. package/dist/server/pages-page-response.js +18 -4
  110. package/dist/server/pages-request-pipeline.js +6 -5
  111. package/dist/server/pages-request-stage-entry.js +25 -4
  112. package/dist/server/pages-router-entry.js +3 -2
  113. package/dist/server/prod-server.d.ts +2 -2
  114. package/dist/server/prod-server.js +59 -17
  115. package/dist/server/request-error-log.d.ts +9 -0
  116. package/dist/server/request-error-log.js +13 -0
  117. package/dist/server/request-pipeline.d.ts +14 -4
  118. package/dist/server/request-pipeline.js +33 -15
  119. package/dist/server/response-stage-cacheability.d.ts +7 -0
  120. package/dist/server/response-stage-cacheability.js +6 -5
  121. package/dist/server/rsc-completion-metadata.js +3 -0
  122. package/dist/server/static-file-cache.d.ts +9 -0
  123. package/dist/server/static-file-cache.js +13 -2
  124. package/dist/server/worker-image-optimization.d.ts +2 -2
  125. package/dist/server/worker-image-optimization.js +18 -3
  126. package/dist/server/worker-utils.d.ts +8 -0
  127. package/dist/server/worker-utils.js +19 -2
  128. package/dist/shims/cache-request-state.d.ts +6 -6
  129. package/dist/shims/cache-request-state.js +6 -36
  130. package/dist/shims/cache-runtime.d.ts +3 -1
  131. package/dist/shims/cache-runtime.js +77 -47
  132. package/dist/shims/cache.d.ts +10 -5
  133. package/dist/shims/cache.js +35 -22
  134. package/dist/shims/client-hook-error.d.ts +2 -0
  135. package/dist/shims/client-hook-error.js +16 -1
  136. package/dist/shims/fetch-cache.js +6 -2
  137. package/dist/shims/headers.d.ts +2 -1
  138. package/dist/shims/headers.js +5 -3
  139. package/dist/shims/image-props.d.ts +6 -0
  140. package/dist/shims/image-props.js +39 -3
  141. package/dist/shims/image.js +3 -3
  142. package/dist/shims/internal/cache-context-accessor.d.ts +1 -1
  143. package/dist/shims/link.js +5 -10
  144. package/dist/shims/navigation.js +2 -3
  145. package/dist/shims/request-state-types.d.ts +2 -1
  146. package/dist/shims/slot.d.ts +9 -2
  147. package/dist/shims/slot.js +76 -11
  148. package/dist/shims/unified-request-context.d.ts +6 -0
  149. package/dist/shims/unified-request-context.js +1 -0
  150. package/dist/utils/base-path.d.ts +7 -0
  151. package/dist/utils/base-path.js +11 -1
  152. package/dist/utils/cache-life-profiles.d.ts +33 -0
  153. package/dist/utils/cache-life-profiles.js +111 -0
  154. package/dist/utils/middleware-request-headers.d.ts +14 -3
  155. package/dist/utils/middleware-request-headers.js +54 -7
  156. package/package.json +1 -1
@@ -1,45 +1,15 @@
1
1
  import { getOrCreateAls } from "./internal/als-registry.js";
2
2
  import { getRequestContext, isInsideUnifiedScope, runWithUnifiedStateMutation } from "./unified-request-context.js";
3
+ import { parseCacheLifeProfiles } from "../utils/cache-life-profiles.js";
3
4
  import { getCdnCacheAdapter } from "./cdn-cache.js";
4
5
  import { getHeadersAccessPhase } from "./internal/headers-state.js";
5
6
  import { _recordUseCacheRootParamRead, _registerCacheContextAccessor, getRegisteredCacheContext } from "./internal/cache-context-accessor.js";
6
7
  //#region src/shims/cache-request-state.ts
7
- const cacheLifeProfiles = {
8
- default: {
9
- revalidate: 900,
10
- expire: 4294967294
11
- },
12
- seconds: {
13
- stale: 30,
14
- revalidate: 1,
15
- expire: 60
16
- },
17
- minutes: {
18
- stale: 300,
19
- revalidate: 60,
20
- expire: 3600
21
- },
22
- hours: {
23
- stale: 300,
24
- revalidate: 3600,
25
- expire: 86400
26
- },
27
- days: {
28
- stale: 300,
29
- revalidate: 86400,
30
- expire: 604800
31
- },
32
- weeks: {
33
- stale: 300,
34
- revalidate: 604800,
35
- expire: 2592e3
36
- },
37
- max: {
38
- stale: 300,
39
- revalidate: 2592e3,
40
- expire: 31536e3
41
- }
42
- };
8
+ /**
9
+ * The built-in profiles merged with next.config `cacheLife`, baked in at
10
+ * build time like Next.js's `process.env.__NEXT_CACHE_LIFE` define.
11
+ */
12
+ const cacheLifeProfiles = parseCacheLifeProfiles(process.env.__VINEXT_CACHE_LIFE_PROFILES);
43
13
  const FALLBACK_KEY = Symbol.for("vinext.cache.fallback");
44
14
  const globalState = globalThis;
45
15
  const cacheAls = getOrCreateAls("vinext.cache.als");
@@ -1,4 +1,4 @@
1
- import { CacheLifeConfig } from "./cache-request-state.js";
1
+ import { CacheLifeConfig } from "../utils/cache-life-profiles.js";
2
2
  import { CacheFlightArguments } from "./cache-flight-arguments.js";
3
3
  //#region src/shims/cache-runtime.d.ts
4
4
  export declare function replayCachedFunction(fn: object, args: CacheFlightArguments): Promise<unknown>;
@@ -16,6 +16,8 @@ export type CacheContext = {
16
16
  tags: string[];
17
17
  /** Cache life configs collected via cacheLife() — minimum-wins rule applies */
18
18
  lifeConfigs: CacheLifeConfig[];
19
+ /** Resolved lives of nested caches; they only constrain fields cacheLife() left unset */
20
+ nestedLifeConfigs: CacheLifeConfig[];
19
21
  /** Cache variant: "default" | "remote" | "private" */
20
22
  variant: string;
21
23
  /** Root params observed while producing this public cache entry. */
@@ -1,6 +1,7 @@
1
1
  import { getOrCreateAls } from "./internal/als-registry.js";
2
2
  import { getRequestContext, isInsideUnifiedScope, runWithUnifiedStateMutation } from "./unified-request-context.js";
3
3
  import { VINEXT_RSC_MARKER_HEADER } from "../server/headers.js";
4
+ import { isInsideWorkStoreScope } from "./internal/work-store-scope.js";
4
5
  import { workUnitAsyncStorage } from "./internal/work-unit-async-storage.js";
5
6
  import { isRouteCacheabilityProbe, recordRouteCacheabilityProbeBailout } from "./cacheability-classification.js";
6
7
  import { shareIsolatePromise } from "../server/isolate-shared-promise.js";
@@ -189,22 +190,46 @@ async function serializeCacheResult(result, rsc) {
189
190
  throw serializationError?.value ?? error;
190
191
  }
191
192
  }
193
+ const CACHE_LIFE_FIELDS = [
194
+ "stale",
195
+ "revalidate",
196
+ "expire"
197
+ ];
198
+ /** Per field, the smallest value any config sets. */
199
+ function minimumCacheLife(configs) {
200
+ const result = {};
201
+ for (const config of configs) for (const field of CACHE_LIFE_FIELDS) {
202
+ const value = config[field];
203
+ const current = result[field];
204
+ if (value !== void 0 && (current === void 0 || value < current)) result[field] = value;
205
+ }
206
+ return result;
207
+ }
192
208
  /**
193
- * Resolve collected cacheLife configs into a single effective config.
194
- * The "minimum-wins" rule: if multiple cacheLife() calls are made,
195
- * each field takes the smallest value across all calls.
209
+ * Resolve a cache scope's effective life, like Next.js's `collectResult`
210
+ * (use-cache-wrapper.ts). A field set by `cacheLife()` takes the minimum
211
+ * across those calls and wins over nested caches. Any other field takes the
212
+ * minimum of the default profile and the nested caches' lives.
196
213
  */
197
- function resolveCacheLife(configs) {
198
- if (configs.length === 0) return { ...cacheLifeProfiles.default };
199
- if (configs.length === 1) return { ...configs[0] };
214
+ function resolveCacheLife(ctx) {
215
+ const explicit = minimumCacheLife(ctx.lifeConfigs);
216
+ const collected = minimumCacheLife([cacheLifeProfiles.default, ...ctx.nestedLifeConfigs]);
200
217
  const result = {};
201
- for (const config of configs) {
202
- if (config.stale !== void 0) result.stale = result.stale !== void 0 ? Math.min(result.stale, config.stale) : config.stale;
203
- if (config.revalidate !== void 0) result.revalidate = result.revalidate !== void 0 ? Math.min(result.revalidate, config.revalidate) : config.revalidate;
204
- if (config.expire !== void 0) result.expire = result.expire !== void 0 ? Math.min(result.expire, config.expire) : config.expire;
218
+ for (const field of CACHE_LIFE_FIELDS) {
219
+ const value = explicit[field] ?? collected[field];
220
+ if (value !== void 0) result[field] = value;
205
221
  }
206
222
  return result;
207
223
  }
224
+ /**
225
+ * Hand a finished cache scope's life to what encloses it, like Next.js's
226
+ * `propagateCacheEntryMetadata`: an enclosing cache collects it as a nested
227
+ * life, and only an outermost cache reaches the request.
228
+ */
229
+ function propagateCacheLife(parentCtx, life) {
230
+ if (parentCtx) parentCtx.nestedLifeConfigs.push(life);
231
+ else _setRequestScopedCacheLife(life);
232
+ }
208
233
  const _PRIVATE_FALLBACK_KEY = Symbol.for("vinext.cacheRuntime.privateFallback");
209
234
  const _g = globalThis;
210
235
  const _privateAls = getOrCreateAls("vinext.cacheRuntime.privateAls");
@@ -416,10 +441,7 @@ function registerCachedFunction(fn, id, variant, options = {}) {
416
441
  const timestamp = Date.now();
417
442
  const { result, ctx, effectiveLife, collectedResult } = await runCachedFunctionWithContext(callFn, callArgs, cacheVariant, (value) => serializeCacheResult(value, rsc));
418
443
  const rootParamNames = ctx.readRootParamNames && ctx.readRootParamNames.size > 0 ? addKnownRootParamNames(id, ctx.readRootParamNames) : knownRootParamsByFunctionId.get(id);
419
- if (propagate) {
420
- recordRequestScopedCacheLife(effectiveLife);
421
- propagateCacheTagsToRequest(ctx.tags);
422
- }
444
+ if (propagate) propagateCacheTagsToRequest(ctx.tags);
423
445
  const revalidateSeconds = effectiveLife.revalidate ?? cacheLifeProfiles.default.revalidate ?? 900;
424
446
  if (collectedResult?.cacheEntry) try {
425
447
  let cacheFunctionInvocation;
@@ -453,32 +475,39 @@ function registerCachedFunction(fn, id, variant, options = {}) {
453
475
  stale: effectiveLife.stale
454
476
  }
455
477
  };
478
+ let write;
456
479
  if (rootParamNames && rootParamNames.size > 0 && rootParams) {
457
480
  const specificCacheKey = coarseCacheKey + computeRootParamsCacheKeySuffix(rootParams, rootParamNames);
458
481
  const redirectTags = [...ctx.tags, ...[...rootParamNames].map((name) => ROOT_PARAM_TAG_PREFIX + name)];
459
- await handler.set(coarseCacheKey, {
460
- kind: "FETCH",
461
- data: {
462
- headers: { [ROOT_PARAM_REDIRECT_HEADER]: "1" },
463
- body: "",
464
- url: coarseCacheKey
465
- },
466
- tags: redirectTags,
467
- revalidate: revalidateSeconds
468
- }, {
469
- ...cacheContext,
470
- tags: redirectTags,
471
- ...coarseCacheKey === cacheKey ? staleEntry && { staleEntry } : staleRedirect && { staleEntry: staleRedirect }
472
- });
482
+ try {
483
+ await handler.set(coarseCacheKey, {
484
+ kind: "FETCH",
485
+ data: {
486
+ headers: { [ROOT_PARAM_REDIRECT_HEADER]: "1" },
487
+ body: "",
488
+ url: coarseCacheKey
489
+ },
490
+ tags: redirectTags,
491
+ revalidate: revalidateSeconds
492
+ }, {
493
+ ...cacheContext,
494
+ tags: redirectTags,
495
+ ...coarseCacheKey === cacheKey ? staleEntry && { staleEntry } : staleRedirect && { staleEntry: staleRedirect }
496
+ });
497
+ } catch {}
473
498
  cacheValue.data.url = specificCacheKey;
474
- await handler.set(specificCacheKey, cacheValue, {
499
+ write = handler.set(specificCacheKey, cacheValue, {
475
500
  ...cacheContext,
476
501
  ...staleEntry && specificCacheKey === cacheKey ? { staleEntry } : {}
477
502
  });
478
- } else await handler.set(cacheKey, cacheValue, {
503
+ } else write = handler.set(cacheKey, cacheValue, {
479
504
  ...cacheContext,
480
505
  ...staleEntry ? { staleEntry } : {}
481
506
  });
507
+ const stored = write.then(() => void 0, () => {});
508
+ if (propagate && isInsideWorkStoreScope()) {
509
+ if (isInsideUnifiedScope()) getRequestContext().executionContext?.waitUntil(stored);
510
+ } else await stored;
482
511
  } catch {}
483
512
  return collectedResult ? collectedResult.result : result;
484
513
  };
@@ -539,16 +568,12 @@ function recordRequestScopedCacheControl(cacheControl) {
539
568
  stale: cacheControl.stale
540
569
  };
541
570
  const parentCtx = cacheContextStorage.getStore();
542
- parentCtx?.lifeConfigs.push(life);
571
+ propagateCacheLife(parentCtx, life);
543
572
  if (parentCtx && parentCtx.variant !== "private" && (cacheControl.revalidate === 0 || cacheControl.expire !== void 0 && cacheControl.expire < DYNAMIC_EXPIRE)) {
544
573
  const error = new NestedDynamicUseCacheError();
545
574
  if (typeof Error.captureStackTrace === "function") Error.captureStackTrace(error, recordRequestScopedCacheControl);
546
575
  parentCtx.dynamicNestedCacheError ??= error;
547
576
  }
548
- _setRequestScopedCacheLife(life);
549
- }
550
- function recordRequestScopedCacheLife(cacheLife) {
551
- _setRequestScopedCacheLife(cacheLife);
552
577
  }
553
578
  /**
554
579
  * Bubble a `"use cache"` scope's tags toward where they can drive invalidation.
@@ -575,8 +600,7 @@ function propagateCacheTagsToRequest(tags) {
575
600
  addCollectedRequestTags(tags);
576
601
  }
577
602
  async function executeWithContext(fn, args, variant, rsc) {
578
- const { result, ctx: _ctx, effectiveLife, collectedResult } = await runCachedFunctionWithContext(fn, args, variant, (value) => serializeCacheResult(value, rsc));
579
- recordRequestScopedCacheLife(effectiveLife);
603
+ const { result, collectedResult } = await runCachedFunctionWithContext(fn, args, variant, (value) => serializeCacheResult(value, rsc));
580
604
  return {
581
605
  result: collectedResult ? collectedResult.result : result,
582
606
  cacheable: collectedResult?.cacheEntry !== null,
@@ -593,6 +617,7 @@ async function runCachedFunctionWithContext(fn, args, variant, collectResult) {
593
617
  const ctx = {
594
618
  tags: [],
595
619
  lifeConfigs: [],
620
+ nestedLifeConfigs: [],
596
621
  variant: variant || "default",
597
622
  readRootParamNames: /* @__PURE__ */ new Set(),
598
623
  hasExplicitRevalidate: false,
@@ -602,21 +627,26 @@ async function runCachedFunctionWithContext(fn, args, variant, collectResult) {
602
627
  };
603
628
  let collectedResult;
604
629
  const workUnitType = variant === "private" ? "private-cache" : "cache";
605
- const result = await workUnitAsyncStorage.run({ type: workUnitType }, () => cacheContextStorage.run(ctx, async () => {
606
- if (!collectResult) return await fn(...args);
607
- let value;
608
- const lazyValue = { then: (onFulfilled, onRejected) => (value ??= new Promise((resolve) => resolve(fn(...args)))).then(onFulfilled, onRejected) };
609
- collectedResult = await collectResult(lazyValue, ctx);
610
- return await lazyValue;
611
- }));
630
+ let result;
631
+ let effectiveLife;
632
+ try {
633
+ result = await workUnitAsyncStorage.run({ type: workUnitType }, () => cacheContextStorage.run(ctx, async () => {
634
+ if (!collectResult) return await fn(...args);
635
+ let value;
636
+ const lazyValue = { then: (onFulfilled, onRejected) => (value ??= new Promise((resolve) => resolve(fn(...args)))).then(onFulfilled, onRejected) };
637
+ collectedResult = await collectResult(lazyValue, ctx);
638
+ return await lazyValue;
639
+ }));
640
+ } finally {
641
+ effectiveLife = resolveCacheLife(ctx);
642
+ propagateCacheLife(parentCtx, effectiveLife);
643
+ if (parentCtx && eagerError && (effectiveLife.revalidate === 0 || effectiveLife.expire !== void 0 && effectiveLife.expire < DYNAMIC_EXPIRE)) parentCtx.dynamicNestedCacheError ??= eagerError;
644
+ }
612
645
  if (ctx.invalidDynamicUsageError) throw ctx.invalidDynamicUsageError;
613
- const effectiveLife = resolveCacheLife(ctx.lifeConfigs);
614
646
  if (parentCtx) {
615
- parentCtx.lifeConfigs.push(effectiveLife);
616
647
  for (const tag of ctx.tags) if (!parentCtx.tags.includes(tag)) parentCtx.tags.push(tag);
617
648
  if (parentCtx.variant !== "private") for (const name of ctx.readRootParamNames ?? []) parentCtx.readRootParamNames?.add(name);
618
649
  }
619
- if (parentCtx && eagerError && (effectiveLife.revalidate === 0 || effectiveLife.expire !== void 0 && effectiveLife.expire < DYNAMIC_EXPIRE)) parentCtx.dynamicNestedCacheError ??= eagerError;
620
650
  if (typeof process !== "undefined" && (process.env.VINEXT_PRERENDER === "1" || process.env.NODE_ENV === "development") && ctx.dynamicNestedCacheError) {
621
651
  if (effectiveLife.revalidate === 0 && !ctx.hasExplicitRevalidate) throw new Error(getNestedCacheZeroRevalidateErrorMessage(), { cause: ctx.dynamicNestedCacheError });
622
652
  if (effectiveLife.expire !== void 0 && effectiveLife.expire < DYNAMIC_EXPIRE && !ctx.hasExplicitExpire) throw new Error(getNestedCacheShortExpireErrorMessage(), { cause: ctx.dynamicNestedCacheError });
@@ -1,7 +1,8 @@
1
+ import { CacheLifeConfig } from "../utils/cache-life-profiles.js";
1
2
  import { ExecutionContextLike, getRequestExecutionContext, runWithExecutionContext } from "./request-context.js";
2
3
  import { CacheControlMetadata, CacheHandler, CacheHandlerContext, CacheHandlerValue, CachedAppPageValue, CachedFetchValue, CachedImageValue, CachedPagesValue, CachedRedirectValue, CachedRouteValue, IncrementalCacheValue, MemoryCacheHandler, NoOpCacheHandler, configureMemoryCacheHandler, getCacheHandler, getDataCacheHandler, registerDataCacheHandler, registerLazyDataCacheHandler, setCacheHandler, setDataCacheHandler } from "./cache-handler.js";
3
4
  import { _recordUseCacheRootParamRead, _registerCacheContextAccessor, getRegisteredCacheContext } from "./internal/cache-context-accessor.js";
4
- import { ACTION_DID_REVALIDATE_DYNAMIC_ONLY, ACTION_DID_REVALIDATE_STATIC_AND_DYNAMIC, ActionRevalidationKind, CacheLifeConfig, CacheState, PendingCdnTagInvalidation, UnstableCacheRevalidationMode, _captureRequestScopedCacheLifeAccessors, _consumeRequestScopedCacheLife, _drainPendingRevalidations, _hasPendingRevalidatedTag, _initRequestScopedCacheState, _isOnDemandRevalidate, _markPendingRevalidatedTag, _peekRequestScopedCacheLife, _queuePendingCdnTagInvalidation, _queuePendingRevalidation, _revalidateCdnTags, _runWithCacheState, _setRequestScopedCacheLife, cacheLifeProfiles, getAndClearActionRevalidationKind, markActionRevalidation, runWithDetachedCacheObservations, shouldServeStaleUnstableCacheEntry } from "./cache-request-state.js";
5
+ import { ACTION_DID_REVALIDATE_DYNAMIC_ONLY, ACTION_DID_REVALIDATE_STATIC_AND_DYNAMIC, ActionRevalidationKind, CacheState, PendingCdnTagInvalidation, UnstableCacheRevalidationMode, _captureRequestScopedCacheLifeAccessors, _consumeRequestScopedCacheLife, _drainPendingRevalidations, _hasPendingRevalidatedTag, _initRequestScopedCacheState, _isOnDemandRevalidate, _markPendingRevalidatedTag, _peekRequestScopedCacheLife, _queuePendingCdnTagInvalidation, _queuePendingRevalidation, _revalidateCdnTags, _runWithCacheState, _setRequestScopedCacheLife, cacheLifeProfiles, getAndClearActionRevalidationKind, markActionRevalidation, runWithDetachedCacheObservations, shouldServeStaleUnstableCacheEntry } from "./cache-request-state.js";
5
6
  //#region src/shims/cache.d.ts
6
7
  /**
7
8
  * Revalidate cached data associated with a specific cache tag.
@@ -101,12 +102,16 @@ export declare function unstable_io(): Promise<void>;
101
102
  /**
102
103
  * Set the cache lifetime for a "use cache" function.
103
104
  *
104
- * Accepts either a built-in profile name (e.g., "hours", "days") or a custom
105
- * configuration object. In Next.js, this only works inside "use cache" functions.
105
+ * Accepts either a profile name (a built-in like "hours", or one configured
106
+ * in next.config `cacheLife`) or a custom configuration object; an unknown
107
+ * profile name throws, like Next.js. In Next.js, this only works inside
108
+ * "use cache" functions.
106
109
  *
107
110
  * When called inside a "use cache" function, this sets the cache TTL.
108
111
  * The "minimum-wins" rule applies: if called multiple times, the shortest
109
- * duration for each field wins.
112
+ * duration for each field wins. Like Next.js, only the fields a call sets are
113
+ * explicit: the others resolve from the default profile and any nested caches
114
+ * (see `resolveCacheLife` in cache-runtime.ts).
110
115
  *
111
116
  * When called outside a "use cache" context, this is a validated no-op.
112
117
  */
@@ -143,4 +148,4 @@ type UnstableCacheOptions = {
143
148
  */
144
149
  export declare function unstable_cache<T extends (...args: any[]) => Promise<any>>(fn: T, keyParts?: string[], options?: UnstableCacheOptions): T;
145
150
  //#endregion
146
- export { ACTION_DID_REVALIDATE_DYNAMIC_ONLY, ACTION_DID_REVALIDATE_STATIC_AND_DYNAMIC, ActionRevalidationKind, CacheControlMetadata, CacheHandler, CacheHandlerContext, CacheHandlerValue, CacheLifeConfig, CacheState, CachedAppPageValue, CachedFetchValue, CachedImageValue, CachedPagesValue, CachedRedirectValue, CachedRouteValue, type ExecutionContextLike, IncrementalCacheValue, MemoryCacheHandler, NoOpCacheHandler, PendingCdnTagInvalidation, UnstableCacheRevalidationMode, _captureRequestScopedCacheLifeAccessors, _consumeRequestScopedCacheLife, _drainPendingRevalidations, _hasPendingRevalidatedTag, _initRequestScopedCacheState, _isOnDemandRevalidate, _markPendingRevalidatedTag, _peekRequestScopedCacheLife, _queuePendingCdnTagInvalidation, _queuePendingRevalidation, _recordUseCacheRootParamRead, _registerCacheContextAccessor, _revalidateCdnTags, _runWithCacheState, _setRequestScopedCacheLife, cacheLifeProfiles, configureMemoryCacheHandler, getAndClearActionRevalidationKind, getCacheHandler, getDataCacheHandler, getRegisteredCacheContext, getRequestExecutionContext, markActionRevalidation, unstable_noStore as noStore, registerDataCacheHandler, registerLazyDataCacheHandler, runWithDetachedCacheObservations, runWithExecutionContext, setCacheHandler, setDataCacheHandler, shouldServeStaleUnstableCacheEntry };
151
+ export { ACTION_DID_REVALIDATE_DYNAMIC_ONLY, ACTION_DID_REVALIDATE_STATIC_AND_DYNAMIC, ActionRevalidationKind, CacheControlMetadata, CacheHandler, CacheHandlerContext, CacheHandlerValue, type CacheLifeConfig, CacheState, CachedAppPageValue, CachedFetchValue, CachedImageValue, CachedPagesValue, CachedRedirectValue, CachedRouteValue, type ExecutionContextLike, IncrementalCacheValue, MemoryCacheHandler, NoOpCacheHandler, PendingCdnTagInvalidation, UnstableCacheRevalidationMode, _captureRequestScopedCacheLifeAccessors, _consumeRequestScopedCacheLife, _drainPendingRevalidations, _hasPendingRevalidatedTag, _initRequestScopedCacheState, _isOnDemandRevalidate, _markPendingRevalidatedTag, _peekRequestScopedCacheLife, _queuePendingCdnTagInvalidation, _queuePendingRevalidation, _recordUseCacheRootParamRead, _registerCacheContextAccessor, _revalidateCdnTags, _runWithCacheState, _setRequestScopedCacheLife, cacheLifeProfiles, configureMemoryCacheHandler, getAndClearActionRevalidationKind, getCacheHandler, getDataCacheHandler, getRegisteredCacheContext, getRequestExecutionContext, markActionRevalidation, unstable_noStore as noStore, registerDataCacheHandler, registerLazyDataCacheHandler, runWithDetachedCacheObservations, runWithExecutionContext, setCacheHandler, setDataCacheHandler, shouldServeStaleUnstableCacheEntry };
@@ -230,29 +230,34 @@ let _unstableIoWarned = false;
230
230
  /**
231
231
  * Set the cache lifetime for a "use cache" function.
232
232
  *
233
- * Accepts either a built-in profile name (e.g., "hours", "days") or a custom
234
- * configuration object. In Next.js, this only works inside "use cache" functions.
233
+ * Accepts either a profile name (a built-in like "hours", or one configured
234
+ * in next.config `cacheLife`) or a custom configuration object; an unknown
235
+ * profile name throws, like Next.js. In Next.js, this only works inside
236
+ * "use cache" functions.
235
237
  *
236
238
  * When called inside a "use cache" function, this sets the cache TTL.
237
239
  * The "minimum-wins" rule applies: if called multiple times, the shortest
238
- * duration for each field wins.
240
+ * duration for each field wins. Like Next.js, only the fields a call sets are
241
+ * explicit: the others resolve from the default profile and any nested caches
242
+ * (see `resolveCacheLife` in cache-runtime.ts).
239
243
  *
240
244
  * When called outside a "use cache" context, this is a validated no-op.
241
245
  */
242
246
  function cacheLife(profile) {
243
247
  let resolvedConfig;
244
248
  if (typeof profile === "string") {
245
- if (!cacheLifeProfiles[profile]) {
246
- console.warn(`[vinext] cacheLife: unknown profile "${profile}". Available profiles: ${Object.keys(cacheLifeProfiles).join(", ")}`);
247
- return;
249
+ const configuredProfile = Object.hasOwn(cacheLifeProfiles, profile) ? cacheLifeProfiles[profile] : void 0;
250
+ if (configuredProfile === void 0) {
251
+ if (Object.hasOwn(cacheLifeProfiles, profile.trim())) throw new Error(`Unknown \`cacheLife()\` profile "${profile}" is not configured in next.config.js\nDid you mean "${profile.trim()}" without the spaces?`);
252
+ throw new Error(`Unknown \`cacheLife()\` profile "${profile}" is not configured in next.config.js\nmodule.exports = {
253
+ cacheLife: {
254
+ "${profile}": ...\n }
255
+ }`);
248
256
  }
249
- resolvedConfig = { ...cacheLifeProfiles[profile] };
257
+ resolvedConfig = { ...configuredProfile };
250
258
  } else if (typeof profile === "object" && profile !== null) {
251
259
  if (profile.expire !== void 0 && profile.revalidate !== void 0 && profile.expire < profile.revalidate) console.warn("[vinext] cacheLife: expire must be >= revalidate");
252
- resolvedConfig = {
253
- ...cacheLifeProfiles.default,
254
- ...profile
255
- };
260
+ resolvedConfig = { ...profile };
256
261
  } else return;
257
262
  try {
258
263
  const ctx = getRegisteredCacheContext();
@@ -260,11 +265,13 @@ function cacheLife(profile) {
260
265
  ctx.lifeConfigs.push(resolvedConfig);
261
266
  if (resolvedConfig.revalidate !== void 0) ctx.hasExplicitRevalidate = true;
262
267
  if (resolvedConfig.expire !== void 0) ctx.hasExplicitExpire = true;
263
- _setRequestScopedCacheLife(resolvedConfig);
264
268
  return;
265
269
  }
266
270
  } catch {}
267
- _setRequestScopedCacheLife(resolvedConfig);
271
+ _setRequestScopedCacheLife(typeof profile === "string" ? resolvedConfig : {
272
+ ...cacheLifeProfiles.default,
273
+ ...profile
274
+ });
268
275
  }
269
276
  /**
270
277
  * Tag a "use cache" function's cached result for on-demand revalidation.
@@ -327,7 +334,7 @@ function unstable_cacheTag(...tags) {
327
334
  }
328
335
  return cacheTag(...tags);
329
336
  }
330
- const UNSTABLE_CACHE_KEY_PREFIX = "unstable_cache:v2";
337
+ const UNSTABLE_CACHE_KEY_PREFIX = "unstable_cache:v3";
331
338
  /**
332
339
  * AsyncLocalStorage to track whether we're inside an unstable_cache() callback.
333
340
  * Stored on globalThis via Symbol so headers.ts can detect the scope without
@@ -396,7 +403,7 @@ function scheduleUnstableCacheBackgroundRevalidation(cacheKey, refresh) {
396
403
  pending.set(cacheKey, trackedRevalidation);
397
404
  waitUntilUnstableCacheRevalidation(trackedRevalidation);
398
405
  }
399
- async function refreshUnstableCacheResult(fn, args, keyedArgs, cacheKey, tags, revalidateSeconds, staleEntry) {
406
+ async function refreshUnstableCacheResult(fn, args, keyedArgs, cacheKey, tags, revalidateSeconds, staleEntry, deferWrite) {
400
407
  const argsUnchanged = serializeArgsOrNull(args) === keyedArgs;
401
408
  const result = await _unstableCacheAls.run(true, () => _unstableCacheCallbackAls.run({ useCacheContext: _useCacheContextAls.getStore() }, () => fn(...args)));
402
409
  if (!argsUnchanged) return result;
@@ -410,12 +417,18 @@ async function refreshUnstableCacheResult(fn, args, keyedArgs, cacheKey, tags, r
410
417
  tags,
411
418
  revalidate: typeof revalidateSeconds === "number" ? revalidateSeconds : false
412
419
  };
413
- await getDataCacheHandler().set(cacheKey, cacheValue, {
414
- fetchCache: true,
415
- tags,
416
- revalidate: revalidateSeconds,
417
- ...staleEntry ? { staleEntry } : {}
420
+ const write = new Promise((resolve) => {
421
+ resolve(getDataCacheHandler().set(cacheKey, cacheValue, {
422
+ fetchCache: true,
423
+ tags,
424
+ revalidate: revalidateSeconds,
425
+ ...staleEntry ? { staleEntry } : {}
426
+ }));
427
+ }).catch((err) => {
428
+ console.error("[vinext] unstable_cache write error:", err);
418
429
  });
430
+ if (deferWrite) getRequestExecutionContext()?.waitUntil(write);
431
+ else await write;
419
432
  return result;
420
433
  }
421
434
  function serializeArgsOrNull(args) {
@@ -458,7 +471,7 @@ function unstable_cache(fn, keyParts, options) {
458
471
  if (cached.ok) {
459
472
  if (existing.cacheState === "stale") {
460
473
  if (shouldServeStaleUnstableCacheEntry()) {
461
- scheduleUnstableCacheBackgroundRevalidation(cacheKey, () => refreshUnstableCacheResult(fn, args, keyedArgs, cacheKey, tags, revalidateSeconds, staleEntry));
474
+ scheduleUnstableCacheBackgroundRevalidation(cacheKey, () => refreshUnstableCacheResult(fn, args, keyedArgs, cacheKey, tags, revalidateSeconds, staleEntry, false));
462
475
  return cached.value;
463
476
  }
464
477
  } else return cached.value;
@@ -466,7 +479,7 @@ function unstable_cache(fn, keyParts, options) {
466
479
  }
467
480
  }
468
481
  if (isDraftMode) return await _unstableCacheAls.run(true, () => fn(...args));
469
- return await refreshUnstableCacheResult(fn, args, keyedArgs, cacheKey, tags, revalidateSeconds, staleEntry);
482
+ return await refreshUnstableCacheResult(fn, args, keyedArgs, cacheKey, tags, revalidateSeconds, staleEntry, !staleEntry && isInsideWorkStoreScope());
470
483
  };
471
484
  return cachedFn;
472
485
  }
@@ -9,4 +9,6 @@
9
9
  */
10
10
  export declare function buildClientHookErrorMessage(hookName: string): string;
11
11
  export declare function throwClientHookError(hookName: string): never;
12
+ /** Rewrites that TypeError into the actionable message, like Next.js's formatServerError. */
13
+ export declare function rewriteClientHookError(error: Error): void;
12
14
  //#endregion
@@ -13,5 +13,20 @@ function buildClientHookErrorMessage(hookName) {
13
13
  function throwClientHookError(hookName) {
14
14
  throw new Error(buildClientHookErrorMessage(hookName));
15
15
  }
16
+ const clientHookPattern = /\b(useState|useEffect|useReducer|useRef|useContext|useLayoutEffect|useInsertionEffect|useSyncExternalStore|useTransition|useImperativeHandle|useDeferredValue|useActionState|useOptimistic|useEffectEvent)\b.*is not a function/;
17
+ /** Rewrites that TypeError into the actionable message, like Next.js's formatServerError. */
18
+ function rewriteClientHookError(error) {
19
+ const match = error.message.match(clientHookPattern);
20
+ if (!match) return;
21
+ const message = buildClientHookErrorMessage(match[1]);
22
+ try {
23
+ if (error.stack) {
24
+ const lines = error.stack.split("\n");
25
+ lines[0] = message;
26
+ error.stack = lines.join("\n");
27
+ }
28
+ error.message = message;
29
+ } catch {}
30
+ }
16
31
  //#endregion
17
- export { buildClientHookErrorMessage, throwClientHookError };
32
+ export { buildClientHookErrorMessage, rewriteClientHookError, throwClientHookError };
@@ -804,7 +804,9 @@ function createPatchedFetch() {
804
804
  cached = null;
805
805
  }
806
806
  if (cached?.value && cached.value.kind === "FETCH" && cached.cacheState !== "stale") {
807
- await lowerFetchCacheRevalidateIfNeeded(handler, cacheKey, cached.value, tags, revalidateSeconds);
807
+ await lowerFetchCacheRevalidateIfNeeded(handler, cacheKey, cached.value, tags, revalidateSeconds).catch((err) => {
808
+ console.error("[vinext] fetch cache write error:", err);
809
+ });
808
810
  const cachedData = cached.value.data;
809
811
  return recordFetchOutcome(buildCachedFetchResponse(cachedData, input), "hit", cacheReason);
810
812
  }
@@ -812,7 +814,9 @@ function createPatchedFetch() {
812
814
  staleEntry = cached;
813
815
  if (shouldRefreshStaleFetchInForeground()) {
814
816
  const freshResponse = await dedupeFetch(input, fetchInit);
815
- await writeFetchCacheResponse(handler, cacheKey, freshResponse, tags, revalidateSeconds, { staleEntry: cached });
817
+ await writeFetchCacheResponse(handler, cacheKey, freshResponse, tags, revalidateSeconds, { staleEntry: cached }).catch((err) => {
818
+ console.error("[vinext] fetch cache write error:", err);
819
+ });
816
820
  return recordFetchOutcome(freshResponse, "miss", cacheReason);
817
821
  }
818
822
  const staleData = cached.value.data;
@@ -168,7 +168,8 @@ export declare function getActiveDraftModeState(): boolean | null;
168
168
  /**
169
169
  * Read the active request's draft-mode state without recording request API usage.
170
170
  * Internal cache implementations use this to bypass persistent reads and writes,
171
- * matching Next.js's request-level `workStore.isDraftMode` guard.
171
+ * matching Next.js's request-level `workStore.isDraftMode` guard. Once the
172
+ * headers context is cleared, the request keeps the state it had then.
172
173
  */
173
174
  export declare function isDraftModeEnabled(): boolean;
174
175
  type DraftModeResult = {
@@ -1,4 +1,4 @@
1
- import { ensureRenderDynamicLatch, isInsideUnifiedScope, runWithUnifiedStateMutation } from "./unified-request-context.js";
1
+ import { ensureRenderDynamicLatch, getRequestContext, isInsideUnifiedScope, runWithUnifiedStateMutation } from "./unified-request-context.js";
2
2
  import { MIDDLEWARE_SET_COOKIE_HEADER } from "../utils/protocol-headers.js";
3
3
  import { buildRequestHeadersFromMiddlewareResponse } from "../utils/middleware-request-headers.js";
4
4
  import { parseEdgeRequestCookieHeader } from "../utils/parse-cookie.js";
@@ -292,6 +292,7 @@ function setHeadersContext(ctx) {
292
292
  state.draftModeCookieHeader = null;
293
293
  state.phase = "render";
294
294
  } else {
295
+ if (state.headersContext && isInsideUnifiedScope()) getRequestContext().draftModeAtHeadersClear = getActiveDraftModeState() === true;
295
296
  state.headersContext = null;
296
297
  state.phase = "render";
297
298
  }
@@ -638,10 +639,11 @@ function getActiveDraftModeState() {
638
639
  /**
639
640
  * Read the active request's draft-mode state without recording request API usage.
640
641
  * Internal cache implementations use this to bypass persistent reads and writes,
641
- * matching Next.js's request-level `workStore.isDraftMode` guard.
642
+ * matching Next.js's request-level `workStore.isDraftMode` guard. Once the
643
+ * headers context is cleared, the request keeps the state it had then.
642
644
  */
643
645
  function isDraftModeEnabled() {
644
- return getActiveDraftModeState() ?? false;
646
+ return getActiveDraftModeState() ?? (isInsideUnifiedScope() && getRequestContext().draftModeAtHeadersClear === true);
645
647
  }
646
648
  function createDraftModeScopeError(expression) {
647
649
  return /* @__PURE__ */ new Error(`${expression} can only be called from a Server Component, Route Handler, or Server Action.`);
@@ -71,6 +71,12 @@ export declare function resolveImageSource(v: {
71
71
  * Configurable via `images.deviceSizes` in next.config.js.
72
72
  */
73
73
  export declare const RESPONSIVE_WIDTHS: number[];
74
+ /**
75
+ * Parse a `quality` prop like Next.js's `getInt` (shared/lib/get-img-props.ts):
76
+ * finite numbers and digit-only strings pass, anything else becomes NaN, which
77
+ * the default loader treats as 75.
78
+ */
79
+ export declare function parseImageQuality(quality: unknown): number | undefined;
74
80
  /**
75
81
  * Build a `/_next/image` optimization URL.
76
82
  *
@@ -52,6 +52,14 @@ const __imageSizes = (() => {
52
52
  ];
53
53
  }
54
54
  })();
55
+ const __imageQualities = (() => {
56
+ try {
57
+ const qualities = JSON.parse(process.env.__VINEXT_IMAGE_QUALITIES ?? "null");
58
+ return Array.isArray(qualities) ? qualities.sort((a, b) => a - b) : null;
59
+ } catch {
60
+ return null;
61
+ }
62
+ })();
55
63
  /**
56
64
  * Whether dangerouslyAllowSVG is enabled in next.config.js.
57
65
  * When false (default), .svg sources auto-skip the optimization endpoint
@@ -195,6 +203,33 @@ function extractLocalDeploymentId(src) {
195
203
  }
196
204
  const __imageEndpoint = `${process.env.__NEXT_ROUTER_BASEPATH ?? ""}/_next/image${process.env.__VINEXT_TRAILING_SLASH === "true" ? "/" : ""}`;
197
205
  /**
206
+ * Parse a `quality` prop like Next.js's `getInt` (shared/lib/get-img-props.ts):
207
+ * finite numbers and digit-only strings pass, anything else becomes NaN, which
208
+ * the default loader treats as 75.
209
+ */
210
+ function parseImageQuality(quality) {
211
+ if (quality === void 0) return void 0;
212
+ if (typeof quality === "number") return Number.isFinite(quality) ? quality : NaN;
213
+ if (typeof quality === "string" && /^[0-9]+$/.test(quality)) return parseInt(quality, 10);
214
+ return NaN;
215
+ }
216
+ /**
217
+ * Round a `quality` prop to the closest entry of `images.qualities`, as the
218
+ * Next.js default loader does (shared/lib/find-closest-quality.ts), so the
219
+ * URL is one the optimization endpoint accepts.
220
+ *
221
+ * With no list configured the prop is used as written (75 when absent). This
222
+ * is deliberate: Next.js 16 defaults the list to `[75]` and would round every
223
+ * quality to 75, but vinext keeps the Next.js 14/15 behaviour for an unset
224
+ * list so migrating apps keep the qualities they ask for (see
225
+ * ImageConfig.qualities in server/image-optimization.ts).
226
+ */
227
+ function findClosestQuality(quality) {
228
+ const q = quality || 75;
229
+ if (!__imageQualities?.length) return q;
230
+ return __imageQualities.reduce((prev, cur) => Math.abs(cur - q) < Math.abs(prev - q) ? cur : prev, __imageQualities[0]);
231
+ }
232
+ /**
198
233
  * Build a `/_next/image` optimization URL.
199
234
  *
200
235
  * In production (Cloudflare Workers), the worker intercepts this path and uses
@@ -202,9 +237,10 @@ const __imageEndpoint = `${process.env.__NEXT_ROUTER_BASEPATH ?? ""}/_next/image
202
237
  * server handles it as a passthrough (serves the original file).
203
238
  */
204
239
  function imageOptimizationUrl(src, width, quality = 75) {
240
+ const q = findClosestQuality(quality);
205
241
  const source = extractLocalDeploymentId(src);
206
242
  const deploymentQuery = source.src.startsWith("/") && source.deploymentId ? `&dpl=${source.deploymentId}` : "";
207
- return `${__imageEndpoint}?url=${encodeURIComponent(source.src)}&w=${width}&q=${quality}${deploymentQuery}`;
243
+ return `${__imageEndpoint}?url=${encodeURIComponent(source.src)}&w=${width}&q=${q}${deploymentQuery}`;
208
244
  }
209
245
  /**
210
246
  * Generate a srcSet string for responsive images.
@@ -283,7 +319,7 @@ function getImageProps(props) {
283
319
  }
284
320
  }
285
321
  }
286
- const imgQuality = typeof _quality === "string" ? Number(_quality) : _quality ?? 75;
322
+ const imgQuality = parseImageQuality(_quality) ?? 75;
287
323
  const resolvedSrc = blockedInProd ? "" : loader ? loader({
288
324
  src,
289
325
  width: imgWidth ?? 0,
@@ -320,4 +356,4 @@ function getImageProps(props) {
320
356
  return { props: Object.assign(imageProps, { "data-nimg": fill ? "fill" : "1" }) };
321
357
  }
322
358
  //#endregion
323
- export { RESPONSIVE_WIDTHS, __dangerouslyAllowSVG, __globallyUnoptimized, __isDev, generateImageAttributes, getFillStyle, getImageProps, imageOptimizationUrl, isRemoteUrl, isSvgUrl, resolveImageSource, sanitizeBlurDataURL, validateRemoteUrl };
359
+ export { RESPONSIVE_WIDTHS, __dangerouslyAllowSVG, __globallyUnoptimized, __isDev, generateImageAttributes, getFillStyle, getImageProps, imageOptimizationUrl, isRemoteUrl, isSvgUrl, parseImageQuality, resolveImageSource, sanitizeBlurDataURL, validateRemoteUrl };
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { useMergedRef } from "./use-merged-ref.js";
3
- import { RESPONSIVE_WIDTHS, __dangerouslyAllowSVG, __globallyUnoptimized, __isDev, generateImageAttributes, getFillStyle, getImageProps, imageOptimizationUrl, isRemoteUrl, isSvgUrl, resolveImageSource, sanitizeBlurDataURL, validateRemoteUrl } from "./image-props.js";
3
+ import { RESPONSIVE_WIDTHS, __dangerouslyAllowSVG, __globallyUnoptimized, __isDev, generateImageAttributes, getFillStyle, getImageProps, imageOptimizationUrl, isRemoteUrl, isSvgUrl, parseImageQuality, resolveImageSource, sanitizeBlurDataURL, validateRemoteUrl } from "./image-props.js";
4
4
  import { forwardRef, useEffect, useLayoutEffect, useRef, useState } from "react";
5
5
  import { jsx } from "react/jsx-runtime";
6
6
  import * as ReactDOM from "react-dom";
@@ -197,7 +197,7 @@ const Image = forwardRef(function Image({ src: srcProp, alt, width, height, fill
197
197
  });
198
198
  }
199
199
  if (loader) {
200
- const resolvedQuality = typeof quality === "string" ? Number(quality) : quality ?? 75;
200
+ const resolvedQuality = parseImageQuality(quality) ?? 75;
201
201
  const resolvedSrc = loader({
202
202
  src,
203
203
  width: imgWidth ?? 0,
@@ -293,7 +293,7 @@ const Image = forwardRef(function Image({ src: srcProp, alt, width, height, fill
293
293
  });
294
294
  }
295
295
  }
296
- const imgQuality = typeof quality === "string" ? Number(quality) : quality ?? 75;
296
+ const imgQuality = parseImageQuality(quality) ?? 75;
297
297
  const skipOptimization = isSvgUrl(src) && !__dangerouslyAllowSVG;
298
298
  const optimizedAttributes = imgWidth && !fill && !skipOptimization ? generateImageAttributes(src, imgWidth, imgQuality, sizes) : void 0;
299
299
  const srcSet = optimizedAttributes ? optimizedAttributes.srcSet : imgWidth && !fill ? RESPONSIVE_WIDTHS.filter((w) => w <= imgWidth * 2).map((w) => `${src} ${w}w`).join(", ") || `${src} ${imgWidth}w` : void 0;