vinext 1.0.1 → 1.1.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.
Files changed (159) hide show
  1. package/README.md +72 -1
  2. package/dist/build/app-page-static-eligibility.d.ts +15 -0
  3. package/dist/build/app-page-static-eligibility.js +82 -0
  4. package/dist/build/prerender-paths.js +3 -78
  5. package/dist/build/prerender.js +33 -8
  6. package/dist/build/server-manifest.d.ts +7 -0
  7. package/dist/build/server-manifest.js +11 -1
  8. package/dist/cache/cache-adapters-virtual.d.ts +7 -1
  9. package/dist/cache/cache-adapters-virtual.js +3 -3
  10. package/dist/client/navigation-runtime.d.ts +11 -0
  11. package/dist/client/navigation-runtime.js +1 -1
  12. package/dist/config/config-matchers.js +3 -1
  13. package/dist/config/next-config.d.ts +7 -0
  14. package/dist/config/next-config.js +18 -3
  15. package/dist/entries/app-rsc-entry.d.ts +2 -0
  16. package/dist/entries/app-rsc-entry.js +64 -43
  17. package/dist/index.d.ts +4 -1
  18. package/dist/index.js +59 -30
  19. package/dist/plugins/cache-flight-codec.d.ts +19 -0
  20. package/dist/plugins/cache-flight-codec.js +68 -0
  21. package/dist/plugins/commonjs-esm-facade.d.ts +35 -0
  22. package/dist/plugins/commonjs-esm-facade.js +80 -0
  23. package/dist/plugins/fonts.d.ts +8 -11
  24. package/dist/plugins/fonts.js +71 -34
  25. package/dist/routing/file-matcher.d.ts +1 -0
  26. package/dist/routing/file-matcher.js +1 -1
  27. package/dist/server/api-handler.js +17 -7
  28. package/dist/server/app-browser-entry.js +11 -7
  29. package/dist/server/app-browser-history-controller.d.ts +2 -0
  30. package/dist/server/app-browser-history-controller.js +8 -0
  31. package/dist/server/app-browser-interception-context.js +26 -2
  32. package/dist/server/app-browser-navigation-controller.d.ts +7 -1
  33. package/dist/server/app-browser-navigation-controller.js +93 -10
  34. package/dist/server/app-fallback-renderer.d.ts +2 -0
  35. package/dist/server/app-fallback-renderer.js +1 -0
  36. package/dist/server/app-history-state.d.ts +3 -0
  37. package/dist/server/app-history-state.js +7 -0
  38. package/dist/server/app-middleware.js +2 -1
  39. package/dist/server/app-optimistic-routing.js +1 -1
  40. package/dist/server/app-page-boundary-render.d.ts +5 -0
  41. package/dist/server/app-page-boundary-render.js +6 -4
  42. package/dist/server/app-page-cache-finalizer.d.ts +11 -0
  43. package/dist/server/app-page-cache-finalizer.js +41 -7
  44. package/dist/server/app-page-cache-render.d.ts +18 -0
  45. package/dist/server/app-page-cache-render.js +63 -43
  46. package/dist/server/app-page-cache.d.ts +5 -0
  47. package/dist/server/app-page-cache.js +27 -7
  48. package/dist/server/app-page-dispatch.d.ts +9 -1
  49. package/dist/server/app-page-dispatch.js +47 -30
  50. package/dist/server/app-page-element-builder.d.ts +7 -0
  51. package/dist/server/app-page-element-builder.js +27 -42
  52. package/dist/server/app-page-execution.d.ts +15 -12
  53. package/dist/server/app-page-execution.js +30 -40
  54. package/dist/server/app-page-head.d.ts +5 -0
  55. package/dist/server/app-page-head.js +9 -3
  56. package/dist/server/app-page-probe.d.ts +0 -109
  57. package/dist/server/app-page-probe.js +3 -136
  58. package/dist/server/app-page-render-identity.d.ts +0 -1
  59. package/dist/server/app-page-render-identity.js +1 -1
  60. package/dist/server/app-page-render-observation.js +2 -2
  61. package/dist/server/app-page-render.d.ts +13 -5
  62. package/dist/server/app-page-render.js +219 -119
  63. package/dist/server/app-page-request.d.ts +0 -1
  64. package/dist/server/app-page-request.js +1 -2
  65. package/dist/server/app-page-response.d.ts +4 -3
  66. package/dist/server/app-page-response.js +4 -3
  67. package/dist/server/app-page-route-wiring.d.ts +17 -5
  68. package/dist/server/app-page-route-wiring.js +55 -33
  69. package/dist/server/app-page-rsc-render-status.d.ts +48 -0
  70. package/dist/server/app-page-rsc-render-status.js +54 -0
  71. package/dist/server/app-page-stream.d.ts +15 -8
  72. package/dist/server/app-page-stream.js +6 -0
  73. package/dist/server/app-request-stage-dispatch.js +1 -1
  74. package/dist/server/app-request-stage-independent-entry.js +4 -4
  75. package/dist/server/app-response-header-provenance.d.ts +2 -0
  76. package/dist/server/app-response-header-provenance.js +10 -4
  77. package/dist/server/app-route-handler-dispatch.js +1 -1
  78. package/dist/server/app-router-entry.js +3 -3
  79. package/dist/server/app-rsc-handler.d.ts +4 -0
  80. package/dist/server/app-rsc-handler.js +212 -96
  81. package/dist/server/app-rsc-request-normalization.d.ts +3 -1
  82. package/dist/server/app-rsc-request-normalization.js +4 -2
  83. package/dist/server/app-rsc-response-stage.d.ts +1 -1
  84. package/dist/server/app-rsc-response-stage.js +6 -4
  85. package/dist/server/app-rsc-route-matching.d.ts +9 -1
  86. package/dist/server/app-rsc-route-matching.js +36 -19
  87. package/dist/server/app-server-action-execution.js +1 -1
  88. package/dist/server/app-ssr-entry.d.ts +3 -2
  89. package/dist/server/app-ssr-entry.js +6 -4
  90. package/dist/server/app-ssr-stream.d.ts +6 -1
  91. package/dist/server/app-ssr-stream.js +34 -15
  92. package/dist/server/app-worker-stages.d.ts +2 -2
  93. package/dist/server/app-worker-stages.js +3 -3
  94. package/dist/server/cacheability-request.js +9 -5
  95. package/dist/server/config-headers.js +2 -0
  96. package/dist/server/dev-server.js +12 -2
  97. package/dist/server/headers.d.ts +14 -0
  98. package/dist/server/headers.js +15 -1
  99. package/dist/server/image-optimization.d.ts +16 -0
  100. package/dist/server/image-optimization.js +26 -6
  101. package/dist/server/isr-cache.js +2 -2
  102. package/dist/server/middleware-matcher-pattern.js +8 -4
  103. package/dist/server/middleware-response-headers.js +2 -0
  104. package/dist/server/multi-stage.d.ts +8 -0
  105. package/dist/server/navigation-planner.js +1 -1
  106. package/dist/server/node-response-signal.d.ts +16 -0
  107. package/dist/server/node-response-signal.js +26 -0
  108. package/dist/server/open-redirect.d.ts +47 -0
  109. package/dist/server/open-redirect.js +74 -1
  110. package/dist/server/pages-page-handler.js +21 -4
  111. package/dist/server/pages-request-stage-entry.js +8 -4
  112. package/dist/server/prod-server.d.ts +8 -7
  113. package/dist/server/prod-server.js +153 -69
  114. package/dist/server/request-pipeline.d.ts +20 -8
  115. package/dist/server/request-pipeline.js +21 -7
  116. package/dist/server/response-compression.d.ts +43 -0
  117. package/dist/server/response-compression.js +111 -0
  118. package/dist/server/seed-cache.js +10 -7
  119. package/dist/server/socket-error-backstop.d.ts +14 -0
  120. package/dist/server/socket-error-backstop.js +27 -4
  121. package/dist/shims/cache-adapter-instantiate.d.ts +50 -0
  122. package/dist/shims/cache-adapter-instantiate.js +118 -0
  123. package/dist/shims/cache-callable-runtime.d.ts +2 -40
  124. package/dist/shims/cache-callable-runtime.js +50 -168
  125. package/dist/shims/cache-flight-arguments.d.ts +22 -0
  126. package/dist/shims/cache-flight-arguments.js +56 -0
  127. package/dist/shims/cache-request-state.d.ts +15 -0
  128. package/dist/shims/cache-request-state.js +31 -8
  129. package/dist/shims/cache-runtime.d.ts +6 -19
  130. package/dist/shims/cache-runtime.js +165 -225
  131. package/dist/shims/cache.d.ts +2 -2
  132. package/dist/shims/cache.js +5 -4
  133. package/dist/shims/cacheability-classification.d.ts +7 -0
  134. package/dist/shims/error-boundary-navigation.d.ts +7 -0
  135. package/dist/shims/error-boundary-navigation.js +19 -1
  136. package/dist/shims/error-boundary.d.ts +10 -10
  137. package/dist/shims/error-boundary.js +26 -7
  138. package/dist/shims/fetch-cache.d.ts +6 -0
  139. package/dist/shims/fetch-cache.js +19 -1
  140. package/dist/shims/form.js +1 -1
  141. package/dist/shims/headers.d.ts +13 -0
  142. package/dist/shims/headers.js +47 -4
  143. package/dist/shims/internal/app-page-props-cache-key.d.ts +5 -2
  144. package/dist/shims/internal/app-page-props-cache-key.js +24 -15
  145. package/dist/shims/internal/headers-state.d.ts +0 -1
  146. package/dist/shims/metadata.js +3 -3
  147. package/dist/shims/navigation-errors.d.ts +6 -0
  148. package/dist/shims/navigation-errors.js +11 -1
  149. package/dist/shims/navigation.d.ts +7 -0
  150. package/dist/shims/navigation.js +14 -4
  151. package/dist/shims/request-context.d.ts +9 -0
  152. package/dist/shims/thenable-params.d.ts +4 -0
  153. package/dist/shims/thenable-params.js +25 -3
  154. package/dist/shims/unified-request-context.js +1 -0
  155. package/dist/utils/mdx-scan.d.ts +5 -0
  156. package/dist/utils/mdx-scan.js +8 -1
  157. package/dist/utils/regex-safety.d.ts +10 -0
  158. package/dist/utils/regex-safety.js +70 -18
  159. package/package.json +3 -3
@@ -1,6 +1,6 @@
1
1
  import { stripBasePath } from "../utils/base-path.js";
2
2
  import { clearAppNavigationFailureTarget, getAppNavigationFailureTarget } from "../client/app-nav-failure-handler.js";
3
- import { claimAppRouterScrollIntentForCommit, consumeAppRouterScrollIntent } from "../shims/app-router-scroll-state.js";
3
+ import { claimAppRouterScrollIntentForCommit, clearAppRouterScrollIntent, consumeAppRouterScrollIntent } from "../shims/app-router-scroll-state.js";
4
4
  import { resolveServerActionOperationLane, shouldScheduleRefreshForDiscardedServerAction } from "./app-browser-action-result.js";
5
5
  import { resolveActiveRoutePaths } from "./app-history-state.js";
6
6
  import { activateNavigationSnapshot, clearPendingPathname, commitClientNavigationState, createSnapshotPathAndSearch } from "../shims/navigation.js";
@@ -76,10 +76,15 @@ function createAppBrowserNavigationController(deps = {}) {
76
76
  let activeNavigationId = 0;
77
77
  let pendingUserNavigationId = null;
78
78
  let pendingUserNavigationLane = null;
79
+ let committingNavigationRenderId = null;
80
+ let pendingUserNavigationCommitStarted = false;
79
81
  let latestHmrUpdateId = 0;
80
82
  const pendingNavigationCommits = /* @__PURE__ */ new Map();
81
83
  const pendingNavigationFailureTargets = /* @__PURE__ */ new Map();
82
84
  const pendingNavigationPrePaintEffects = /* @__PURE__ */ new Map();
85
+ const snapshotRenderIds = /* @__PURE__ */ new Set();
86
+ let pendingUserNavigationShellHref = null;
87
+ let retiredNavigationUrl = null;
83
88
  let setBrowserRouterState = null;
84
89
  let browserRouterStateRef = null;
85
90
  let activePendingBrowserRouterState = null;
@@ -113,6 +118,9 @@ function createAppBrowserNavigationController(deps = {}) {
113
118
  activeNavigationId += 1;
114
119
  pendingUserNavigationId = activeNavigationId;
115
120
  pendingUserNavigationLane = null;
121
+ pendingUserNavigationCommitStarted = false;
122
+ pendingUserNavigationShellHref = null;
123
+ retiredNavigationUrl = null;
116
124
  return activeNavigationId;
117
125
  }
118
126
  function getActiveNavigationId() {
@@ -159,9 +167,63 @@ function createAppBrowserNavigationController(deps = {}) {
159
167
  if (isCurrentNavigation(navId)) {
160
168
  pendingUserNavigationId = null;
161
169
  pendingUserNavigationLane = null;
170
+ pendingUserNavigationShellHref = null;
171
+ retiredNavigationUrl = null;
162
172
  clearPendingPathname(navId);
163
173
  }
164
174
  }
175
+ /**
176
+ * Discard the user navigation that has not committed yet, keeping
177
+ * `visibleState` (the committed tree) on screen. Next.js does the same when
178
+ * a raw history.pushState/replaceState dispatches ACTION_RESTORE: the
179
+ * pending action is marked discarded and the restore commits the current
180
+ * tree in a transition, which replaces the navigation's suspended one.
181
+ * Returns whether a navigation was discarded.
182
+ */
183
+ function discardPendingNavigation(visibleState) {
184
+ if (pendingUserNavigationId === null || pendingUserNavigationCommitStarted) return false;
185
+ if (pendingUserNavigationShellHref !== null) {
186
+ retiredNavigationUrl = {
187
+ navigationId: pendingUserNavigationId,
188
+ href: pendingUserNavigationShellHref
189
+ };
190
+ for (const renderId of pendingNavigationPrePaintEffects.keys()) releaseRenderSnapshot(renderId);
191
+ return false;
192
+ }
193
+ activeNavigationId += 1;
194
+ pendingUserNavigationId = null;
195
+ pendingUserNavigationLane = null;
196
+ clearAppRouterScrollIntent();
197
+ for (const renderId of pendingNavigationPrePaintEffects.keys()) {
198
+ pendingNavigationPrePaintEffects.delete(renderId);
199
+ releaseRenderSnapshot(renderId);
200
+ }
201
+ clearCommittedNavigationFailureTargets(nextNavigationRenderId);
202
+ settleNavigationCommits(nextNavigationRenderId, false);
203
+ if (visibleState && setBrowserRouterState) {
204
+ const setter = setBrowserRouterState;
205
+ startTransition(() => {
206
+ setter(visibleState);
207
+ });
208
+ }
209
+ return true;
210
+ }
211
+ /**
212
+ * Run the URL update of a navigation render React is committing right now.
213
+ * It normally runs in NavigationCommitSignal's layout effect, after the
214
+ * route's own layout effects. Next.js writes the URL in an insertion effect
215
+ * instead, so a raw history write from a destination layout effect lands on
216
+ * top of the destination entry. Called before such a write to keep that order.
217
+ */
218
+ function flushCommittingNavigationUrl() {
219
+ if (committingNavigationRenderId !== null) drainPrePaintEffects(committingNavigationRenderId);
220
+ }
221
+ function releaseRenderSnapshot(renderId) {
222
+ if (snapshotRenderIds.delete(renderId)) commitClientNavigationStateImpl(void 0, { releaseSnapshot: true });
223
+ }
224
+ function isRetiredNavigationUrl(navId, targetHref) {
225
+ return retiredNavigationUrl !== null && retiredNavigationUrl.navigationId === navId && retiredNavigationUrl.href === normalizeBrowserHref(targetHref);
226
+ }
165
227
  function queuePrePaintNavigationEffect(renderId, effect) {
166
228
  if (!effect) return;
167
229
  pendingNavigationPrePaintEffects.set(renderId, effect);
@@ -182,7 +244,7 @@ function createAppBrowserNavigationController(deps = {}) {
182
244
  if (id > upToRenderId) continue;
183
245
  pendingNavigationPrePaintEffects.delete(id);
184
246
  if (id === upToRenderId) effect();
185
- else commitClientNavigationStateImpl(void 0, { releaseSnapshot: true });
247
+ else releaseRenderSnapshot(id);
186
248
  }
187
249
  }
188
250
  /**
@@ -199,7 +261,17 @@ function createAppBrowserNavigationController(deps = {}) {
199
261
  pendingCommit.resolve(didCommit);
200
262
  }
201
263
  }
264
+ /** Runs when React starts committing a render, before any layout effect. */
265
+ function beginNavigationRenderCommit(renderId) {
266
+ committingNavigationRenderId = renderId;
267
+ const committingCommit = pendingNavigationCommits.get(renderId);
268
+ const committingOperation = committingCommit?.committedState?.activeOperation;
269
+ if (committingCommit && committingOperation?.navigationCommitKind === "detached" && committingOperation.navigationId === pendingUserNavigationId) pendingUserNavigationShellHref = normalizeBrowserHref(committingCommit.targetHref);
270
+ else pendingUserNavigationCommitStarted = true;
271
+ clearCommittedNavigationFailureTargets(renderId);
272
+ }
202
273
  function commitNavigationRender(renderId) {
274
+ committingNavigationRenderId = null;
203
275
  drainPrePaintEffects(renderId);
204
276
  settleNavigationCommits(renderId, true);
205
277
  }
@@ -239,7 +311,7 @@ function createAppBrowserNavigationController(deps = {}) {
239
311
  }
240
312
  function NavigationCommitSignal({ renderId, children }) {
241
313
  useInsertionEffect(() => {
242
- clearCommittedNavigationFailureTargets(renderId);
314
+ beginNavigationRenderCommit(renderId);
243
315
  }, [renderId]);
244
316
  useLayoutEffect(() => {
245
317
  commitNavigationRender(renderId);
@@ -371,10 +443,10 @@ function createAppBrowserNavigationController(deps = {}) {
371
443
  pendingNavigationCommits.set(renderId, {
372
444
  committedState: null,
373
445
  onCommittedState: options.onCommittedState,
374
- resolve
446
+ resolve,
447
+ targetHref: options.targetHref
375
448
  });
376
449
  });
377
- let snapshotActivated = false;
378
450
  try {
379
451
  const pendingOptions = {
380
452
  currentState: options.navigationInitiationState,
@@ -425,7 +497,7 @@ function createAppBrowserNavigationController(deps = {}) {
425
497
  const approvedCommit = approval.approvedCommit;
426
498
  if (approvedCommit === null) throw new Error("[vinext] Commit decision did not approve a visible commit");
427
499
  const approvedVisibleState = applyApprovedVisibleCommit(getBrowserRouterState(), approvedCommit);
428
- queuePrePaintNavigationEffect(renderId, options.createNavigationCommitEffect({
500
+ const commitEffect = options.createNavigationCommitEffect({
429
501
  activeRoutePaths: resolveActiveRoutePaths(approvedVisibleState.slotBindings),
430
502
  bfcacheIds: approvedVisibleState.bfcacheIds,
431
503
  href: options.targetHref,
@@ -434,10 +506,18 @@ function createAppBrowserNavigationController(deps = {}) {
434
506
  params: options.params,
435
507
  previousNextUrl: approvedCommit.previousNextUrl,
436
508
  targetHistoryIndex: options.targetHistoryIndex
437
- }));
509
+ });
510
+ queuePrePaintNavigationEffect(renderId, () => {
511
+ commitEffect({
512
+ keepCurrentUrl: isRetiredNavigationUrl(options.navId, options.targetHref),
513
+ releaseSnapshot: snapshotRenderIds.delete(renderId)
514
+ });
515
+ });
438
516
  claimAppRouterScrollIntentForCommit(options.scrollIntent, renderId);
439
- activateNavigationSnapshot();
440
- snapshotActivated = true;
517
+ if (!isRetiredNavigationUrl(options.navId, options.targetHref)) {
518
+ activateNavigationSnapshot();
519
+ snapshotRenderIds.add(renderId);
520
+ }
441
521
  dispatchApprovedVisibleCommit(renderId, approvedCommit, options.pendingRouterState, shouldForceSynchronousCommit(options) ? "synchronous" : options.visibleCommitMode ?? "transition");
442
522
  if (options.navigationResponseCompletion) options.navigationResponseCompletion.then(() => {
443
523
  if (!isCurrentNavigation(options.navId) || !hasBrowserRouterState()) return;
@@ -451,7 +531,7 @@ function createAppBrowserNavigationController(deps = {}) {
451
531
  pendingNavigationFailureTargets.delete(renderId);
452
532
  pendingNavigationPrePaintEffects.delete(renderId);
453
533
  pendingNavigationCommits.delete(renderId);
454
- if (snapshotActivated) commitClientNavigationStateImpl(options.navId);
534
+ if (snapshotRenderIds.delete(renderId)) commitClientNavigationStateImpl(options.navId);
455
535
  settlePendingBrowserRouterState(options.pendingRouterState);
456
536
  resolveCommitted?.(false);
457
537
  throw error;
@@ -530,11 +610,14 @@ function createAppBrowserNavigationController(deps = {}) {
530
610
  attachBrowserRouterState,
531
611
  beginPendingBrowserRouterState,
532
612
  finalizeNavigation,
613
+ discardPendingNavigation,
614
+ flushCommittingNavigationUrl,
533
615
  restoreHistorySnapshotVisibleState,
534
616
  renderNavigationPayload,
535
617
  commitSameUrlNavigatePayload,
536
618
  hmrReplaceTree,
537
619
  drainPrePaintEffects,
620
+ beginNavigationRenderCommit,
538
621
  commitNavigationRender,
539
622
  clearCommittedNavigationFailureTargets,
540
623
  NavigationCommitSignal
@@ -96,6 +96,8 @@ type AppFallbackRenderer<TModule extends AppPageModule = AppPageModule> = {
96
96
  boundaryComponent?: AppPageComponent | null;
97
97
  boundaryModule?: TModule | null;
98
98
  intercept?: AppPageInterceptOptions<TModule> | null;
99
+ /** The document may be stored, so it must not carry the request's query. */
100
+ isCacheCandidate?: boolean;
99
101
  layouts?: readonly (TModule | null | undefined)[] | null;
100
102
  matchedParams?: AppPageParams;
101
103
  }, scriptNonce: string | undefined, middlewareContext: AppPageMiddlewareContext, callContext?: AppFallbackRendererCallContext) => Promise<Response | null>;
@@ -86,6 +86,7 @@ function createAppFallbackRenderer(options) {
86
86
  getNavigationContext,
87
87
  globalErrorModule: effectiveGlobalErrorModule,
88
88
  intercept: opts?.intercept ?? null,
89
+ isCacheCandidate: opts?.isCacheCandidate,
89
90
  isEdgeRuntime: callContext?.isEdgeRuntime,
90
91
  isRscRequest,
91
92
  layoutModules: useGlobalNotFound ? [] : opts?.layouts ?? null,
@@ -29,6 +29,8 @@ export declare class HistoryStateSnapshotCache<TState> {
29
29
  historyIndex: number | null;
30
30
  state: TState;
31
31
  }): void;
32
+ /** Point every entry that remembered `previous` at `next` instead. */
33
+ replaceState(previous: TState, next: TState): void;
32
34
  resolveRestore(options: {
33
35
  currentBfcacheVersion: number;
34
36
  guarded: boolean;
@@ -51,6 +53,7 @@ export declare class RestorableClientStateController<TState> {
51
53
  historyIndex: number | null;
52
54
  state: TState;
53
55
  }): void;
56
+ replaceHistoryStateSnapshots(previous: TState, next: TState): void;
54
57
  resolveHistoryStateSnapshotRestore(historyState: unknown): HistoryStateSnapshotRestoreDecision<TState>;
55
58
  }
56
59
  export declare function createHistoryStateWithPreviousNextUrl(state: unknown, previousNextUrl: string | null): HistoryStateRecord | null;
@@ -31,6 +31,10 @@ var HistoryStateSnapshotCache = class {
31
31
  const oldestIndex = this.#snapshots.keys().next().value;
32
32
  if (typeof oldestIndex === "number") this.#snapshots.delete(oldestIndex);
33
33
  }
34
+ /** Point every entry that remembered `previous` at `next` instead. */
35
+ replaceState(previous, next) {
36
+ for (const snapshot of this.#snapshots.values()) if (snapshot.state === previous) snapshot.state = next;
37
+ }
34
38
  resolveRestore(options) {
35
39
  const targetHistoryIndex = readHistoryStateTraversalIndex(options.historyState);
36
40
  if (targetHistoryIndex === null) return {
@@ -111,6 +115,9 @@ var RestorableClientStateController = class {
111
115
  state: options.state
112
116
  });
113
117
  }
118
+ replaceHistoryStateSnapshots(previous, next) {
119
+ this.#snapshots.replaceState(previous, next);
120
+ }
114
121
  resolveHistoryStateSnapshotRestore(historyState) {
115
122
  return this.#snapshots.resolveRestore({
116
123
  currentBfcacheVersion: this.#currentBfcacheVersion,
@@ -49,7 +49,8 @@ function requestWithMiddlewareRequestHeaders(request, middlewareHeaders) {
49
49
  const init = {
50
50
  method: request.method,
51
51
  headers: nextHeaders,
52
- body: request.body
52
+ body: request.body,
53
+ signal: request.signal
53
54
  };
54
55
  if (request.body) Object.defineProperty(init, "duplex", {
55
56
  value: "half",
@@ -1,9 +1,9 @@
1
1
  import { isUnknownRecord } from "../utils/record.js";
2
2
  import { buildParams, decodeMatchedParams, splitPathnameForRouteMatch } from "../routing/utils.js";
3
3
  import { stripBasePath } from "../utils/base-path.js";
4
+ import { extractRawRoutePatternParams, matchRoutePattern } from "../routing/route-pattern.js";
4
5
  import { AppElementsWire } from "./app-elements-wire.js";
5
6
  import "./app-elements.js";
6
- import { extractRawRoutePatternParams, matchRoutePattern } from "../routing/route-pattern.js";
7
7
  import { createNestedBfcacheSlotSegmentId, deriveBfcacheSegmentIdentity, isNestedBfcacheSlotSegmentId } from "./bfcache-identity.js";
8
8
  import { stripRscCacheBustingSearchParam, stripRscSuffix } from "./app-rsc-cache-busting.js";
9
9
  import { canonicalizeAppPageParams, resolveAppPagePatternStateKey, resolveAppPageSemanticSegmentStateKey } from "./app-page-segment-state.js";
@@ -49,6 +49,11 @@ type AppPageBoundaryRenderCommonOptions<TModule extends AppPageModule = AppPageM
49
49
  getFontStyles: () => string[];
50
50
  getNavigationContext: () => NavigationContext | null;
51
51
  globalErrorModule?: TModule | null;
52
+ /**
53
+ * The document may be stored under a query-free key, so its SSR
54
+ * `useSearchParams()` waits as a cache candidate's does.
55
+ */
56
+ isCacheCandidate?: boolean;
52
57
  isEdgeRuntime?: boolean;
53
58
  isRscRequest: boolean;
54
59
  loadSsrHandler: () => Promise<AppPageSsrHandler>;
@@ -6,19 +6,20 @@ import "./app-elements.js";
6
6
  import DefaultGlobalError from "../shims/default-global-error.js";
7
7
  import { ErrorBoundary, GlobalErrorBoundary, SerializedErrorBoundary } from "../shims/error-boundary.js";
8
8
  import { LayoutSegmentProvider } from "../shims/layout-segment-context.js";
9
+ import { withUseCacheLayoutMarker } from "../shims/internal/app-page-props-cache-key.js";
9
10
  import { MetadataHead, ViewportHead } from "../shims/metadata.js";
10
11
  import { resolveAppPageBranchParams, resolveAppPageSegmentParams } from "./app-page-params.js";
11
12
  import { stripRscSuffix } from "./app-rsc-cache-busting.js";
12
13
  import { buildRscRedirectFlightStream } from "./app-rsc-redirect-flight.js";
13
14
  import { bufferAppPageBinaryStream, buildAppPageSpecialErrorResponse, resolveAppPageSpecialError } from "./app-page-execution.js";
14
- import { resolveActiveParallelRouteHeadInputs, resolveAppPageHead } from "./app-page-head.js";
15
15
  import { createAppPageLayoutEntries, createAppPageSourcePage } from "./app-page-route-wiring.js";
16
+ import { createAppPageFontData, createAppPageRscErrorTracker, createAppPageSsrErrorHandler, renderAppPageHtmlResponse } from "./app-page-stream.js";
16
17
  import { buildClientHookErrorMessage } from "../shims/client-hook-error.js";
18
+ import { resolveActiveParallelRouteHeadInputs, resolveAppPageHead } from "./app-page-head.js";
17
19
  import { resolveHttpAccessFallbackMetadata, resolveHttpAccessFallbackViewport } from "./app-page-http-access-fallback-metadata.js";
18
20
  import "./app-rsc-route-matching.js";
19
21
  import { renderAppPageBoundaryResponse, resolveAppPageErrorBoundary, resolveAppPageHttpAccessBoundaryModule, wrapAppPageBoundaryElement } from "./app-page-boundary.js";
20
22
  import { resolveSlotParamOverrides } from "./app-page-element-builder.js";
21
- import { createAppPageFontData, createAppPageRscErrorTracker, createAppPageSsrErrorHandler, renderAppPageHtmlResponse } from "./app-page-stream.js";
22
23
  import { Fragment, createElement } from "react";
23
24
  //#region src/server/app-page-boundary-render.ts
24
25
  const DEFAULT_GLOBAL_ERROR_COMPONENT = DefaultGlobalError;
@@ -56,10 +57,10 @@ function wrapRenderedBoundaryElement(options) {
56
57
  });
57
58
  },
58
59
  renderLayout(LayoutComponent, children, asyncParams) {
59
- return createElement(LayoutComponent, {
60
+ return createElement(LayoutComponent, withUseCacheLayoutMarker(LayoutComponent, {
60
61
  children,
61
62
  params: asyncParams
62
- });
63
+ }));
63
64
  },
64
65
  renderLayoutSegmentProvider(segmentMap, children) {
65
66
  return createElement(LayoutSegmentProvider, { segmentMap }, children);
@@ -153,6 +154,7 @@ async function renderAppPageBoundaryElementResponse(options) {
153
154
  clearRequestContext: options.clearRequestContext,
154
155
  fontData,
155
156
  fontLinkHeader: options.buildFontLinkHeader(fontData.preloads),
157
+ isCacheCandidate: options.isCacheCandidate,
156
158
  isEdgeRuntime: options.isEdgeRuntime,
157
159
  middlewareHeaders: options.middlewareContext.headers,
158
160
  navigationContext: options.getNavigationContext() ?? {
@@ -2,6 +2,7 @@ import { RenderObservation } from "./cache-proof.js";
2
2
  import { AppRscRenderMode } from "./app-rsc-render-mode.js";
3
3
  import { AppPageRenderObservationState } from "./app-page-render-observation.js";
4
4
  import { AppPageCacheSetter } from "./isr-cache.js";
5
+ import { AppPageRscRenderStatus } from "./app-page-rsc-render-status.js";
5
6
  //#region src/server/app-page-cache-finalizer.d.ts
6
7
  type AppPageDebugLogger = (event: string, detail: string) => void;
7
8
  type AppPageRscCacheKeyBuilder = (pathname: string, mountedSlotsHeader?: string | null, renderMode?: AppRscRenderMode, interceptionContext?: string | null, interceptionId?: string | null) => string;
@@ -26,6 +27,8 @@ type FinalizeAppPageCacheabilityEvaluationOptions = {
26
27
  * cacheable even when a cacheLife resolves during the render.
27
28
  */
28
29
  isStaticEligible: boolean;
30
+ /** An RSC render's status, from its document's shell. */
31
+ resolveRscRenderStatus?: () => Promise<AppPageRscRenderStatus>;
29
32
  revalidateSeconds: number | null;
30
33
  };
31
34
  type FinalizeAppPageHtmlCacheResponseOptions = {
@@ -53,6 +56,12 @@ type FinalizeAppPageHtmlCacheResponseOptions = {
53
56
  isStaticEligible: boolean;
54
57
  revalidateSeconds: number | null;
55
58
  linkHeader: string | null;
59
+ /**
60
+ * The status and headers stored with both entries, for a render that ended
61
+ * in a special error: 404/403/401, or 307/308 with its `location`.
62
+ */
63
+ status?: number;
64
+ headers?: Record<string, string>;
56
65
  waitUntil?: (promise: Promise<void>) => void;
57
66
  };
58
67
  type ScheduleAppPageRscCacheWriteOptions = {
@@ -73,6 +82,8 @@ type ScheduleAppPageRscCacheWriteOptions = {
73
82
  mountedSlotsHeader?: string | null;
74
83
  omitPendingDynamicCacheState?: boolean;
75
84
  renderMode?: AppRscRenderMode;
85
+ /** The render's status, from its document's shell. Without it the render is a 200. */
86
+ resolveRscRenderStatus?: () => Promise<AppPageRscRenderStatus>;
76
87
  preserveClientResponseHeaders?: boolean;
77
88
  expireSeconds?: number;
78
89
  isStaticEligible: boolean;
@@ -10,6 +10,7 @@ import { deferUntilStreamConsumed } from "./defer-until-stream-consumed.js";
10
10
  import { markFrameworkLinkHeaders } from "./app-response-header-provenance.js";
11
11
  import { hasQueryInvariantRenderProof } from "./cache-proof.js";
12
12
  import { createEmptyAppPageRenderObservationState } from "./app-page-render-observation.js";
13
+ import { resolveAppPageRscResponseStatus } from "./app-page-rsc-render-status.js";
13
14
  //#region src/server/app-page-cache-finalizer.ts
14
15
  function applyPendingDynamicCdnHeaders(headers, tags, options = {}) {
15
16
  const cacheable = headers.get("Cache-Control") ?? "";
@@ -50,6 +51,22 @@ function resolveAppPageCacheControl(options) {
50
51
  function appPageCacheControlHeader(cacheControl) {
51
52
  return cacheControl.revalidate === Infinity ? STATIC_CACHE_CONTROL : buildRevalidateCacheControl(cacheControl.revalidate, cacheControl.expire);
52
53
  }
54
+ /**
55
+ * The completed response replaces a streamed 200 with the status Next.js sends
56
+ * for the render's special error, and stores that.
57
+ */
58
+ function applyRscRenderStatus(outcome, renderStatus) {
59
+ if (renderStatus.kind === "page") return outcome;
60
+ if (renderStatus.kind === "unstorable") return {
61
+ cacheable: false,
62
+ reason: "render ended in a special error Next.js doesn't store"
63
+ };
64
+ return {
65
+ ...outcome,
66
+ status: resolveAppPageRscResponseStatus(renderStatus.status),
67
+ ...renderStatus.headers ? { headers: renderStatus.headers } : {}
68
+ };
69
+ }
53
70
  function finalizeEvaluatedAppPageResponse(response, options) {
54
71
  if (!isRouteCacheabilityEvaluation()) return null;
55
72
  const complete = deferRouteCacheability();
@@ -90,6 +107,14 @@ function finalizeEvaluatedAppPageResponse(response, options) {
90
107
  reason: "render did not produce a cache policy"
91
108
  };
92
109
  }
110
+ if (outcome.cacheable && options.resolveRscRenderStatus) {
111
+ const cacheableOutcome = outcome;
112
+ options.resolveRscRenderStatus().then((renderStatus) => complete(applyRscRenderStatus(cacheableOutcome, renderStatus)), () => complete({
113
+ cacheable: false,
114
+ reason: "render status could not be resolved"
115
+ }));
116
+ return;
117
+ }
93
118
  complete(outcome);
94
119
  };
95
120
  if (!response.body) {
@@ -131,15 +156,15 @@ function finalizeAppPageHtmlCacheResponse(response, options) {
131
156
  markFrameworkLinkHeaders(clientResponse.headers, options.linkHeader);
132
157
  return clientResponse;
133
158
  }
134
- if (!response.body) return response;
135
- const [streamForClient, streamForCache] = response.body.tee();
159
+ const [streamForClient, streamForCache] = response.body ? response.body.tee() : [null, null];
136
160
  const htmlKey = options.isrHtmlKey(options.cleanPathname);
137
161
  const rscKey = options.isrRscKey(options.cleanPathname, null, void 0, options.interceptionContext, options.interceptionId);
138
162
  const clientHeaders = new Headers(response.headers);
139
163
  if (options.preserveClientResponseHeaders !== true) applyPendingDynamicCdnHeaders(clientHeaders, options.getPageTags(), { omitCacheState: options.omitPendingDynamicCacheState === true });
140
164
  const cachePromise = (async () => {
141
165
  try {
142
- let cachedHtml = await readStreamAsText(streamForCache);
166
+ let cachedHtml = streamForCache ? await readStreamAsText(streamForCache) : "";
167
+ const rscData = options.capturedRscDataPromise ? await options.capturedRscDataPromise : void 0;
143
168
  if (options.capturedDynamicUsageBeforeContextCleanup?.() === true || options.consumeDynamicUsage()) {
144
169
  options.isrDebug?.("HTML cache write skipped (dynamic usage during render)", htmlKey);
145
170
  return;
@@ -168,18 +193,22 @@ function finalizeAppPageHtmlCacheResponse(response, options) {
168
193
  state: observationState
169
194
  });
170
195
  const linkHeader = options.linkHeader;
196
+ const status = options.status ?? 200;
171
197
  if (!hasQueryInvariantRenderProof(htmlRenderObservation) || !hasQueryInvariantRenderProof(rscRenderObservation)) {
172
198
  options.isrDebug?.("HTML cache write skipped (searchParams not proven unread)", htmlKey);
173
199
  return;
174
200
  }
175
- const writes = [options.isrSet(htmlKey, buildAppPageCacheValue(cachedHtml, void 0, 200, htmlRenderObservation, linkHeader ? { link: linkHeader } : void 0), {
201
+ const writes = [options.isrSet(htmlKey, buildAppPageCacheValue(cachedHtml, void 0, status, htmlRenderObservation, linkHeader ? {
202
+ ...options.headers,
203
+ link: linkHeader
204
+ } : options.headers), {
176
205
  cacheControl,
177
206
  tags: pageTags
178
207
  })];
179
- if (options.capturedRscDataPromise) writes.push(options.capturedRscDataPromise.then((rscData) => options.isrSet(rscKey, buildAppPageCacheValue("", rscData, 200, rscRenderObservation), {
208
+ if (rscData) writes.push(options.isrSet(rscKey, buildAppPageCacheValue("", rscData, status, rscRenderObservation, options.headers), {
180
209
  cacheControl,
181
210
  tags: pageTags
182
- })));
211
+ }));
183
212
  await Promise.all(writes);
184
213
  options.isrDebug?.("HTML cache written", htmlKey);
185
214
  } catch (cacheError) {
@@ -243,7 +272,12 @@ function scheduleAppPageRscCacheWrite(options) {
243
272
  options.isrDebug?.("RSC cache write skipped (searchParams not proven unread)", rscKey);
244
273
  return;
245
274
  }
246
- await options.isrSet(rscKey, buildAppPageCacheValue("", rscData, 200, rscRenderObservation), {
275
+ const renderStatus = await options.resolveRscRenderStatus?.() ?? { kind: "page" };
276
+ if (renderStatus.kind === "unstorable") {
277
+ options.isrDebug?.("RSC cache write skipped (unstorable special error)", rscKey);
278
+ return;
279
+ }
280
+ await options.isrSet(rscKey, renderStatus.kind === "special-error" ? buildAppPageCacheValue("", rscData, renderStatus.status, rscRenderObservation, renderStatus.headers) : buildAppPageCacheValue("", rscData, 200, rscRenderObservation), {
247
281
  cacheControl,
248
282
  tags: pageTags
249
283
  });
@@ -24,11 +24,18 @@ export type RenderAppPageCacheArtifactsOptions = {
24
24
  getNavigationContext: () => NavigationContext | null;
25
25
  loadSsrHandler: () => Promise<AppPageSsrHandler>;
26
26
  mountedSlotsHeader?: string | null;
27
+ /** Start mirroring Flight into Next.js's `self.__next_f` once this returns true. */
28
+ mirrorNextFlight?: () => boolean;
27
29
  navigationParams: Record<string, unknown>;
28
30
  isCapturedRscError?: (error: unknown) => boolean;
29
31
  onError: (error: unknown, requestInfo: unknown, errorContext: unknown) => unknown;
30
32
  onSsrError?: (error: unknown) => unknown;
31
33
  reactMaxHeadersLength?: number;
34
+ /**
35
+ * Render the document of a special error that rejected the shell, or return
36
+ * null to fail the render with it.
37
+ */
38
+ renderShellSpecialError?: (error: unknown) => Promise<AppPageCacheSpecialErrorDocument | null>;
32
39
  renderToReadableStream: (element: AppPageRenderableElement, options: {
33
40
  onError: (error: unknown, requestInfo: unknown, errorContext: unknown) => unknown;
34
41
  }) => ReadableStream<Uint8Array>;
@@ -37,13 +44,24 @@ export type RenderAppPageCacheArtifactsOptions = {
37
44
  waitForAllReady?: boolean;
38
45
  isForceStatic?: boolean;
39
46
  };
47
+ type AppPageCacheSpecialErrorDocument = {
48
+ /** Response headers stored with the entries, such as a redirect's `location`. */
49
+ headers?: Record<string, string>;
50
+ html: string;
51
+ /** The renderer's own `Link` header, without middleware's. */
52
+ linkHeader?: string;
53
+ status: number;
54
+ };
40
55
  export type RenderAppPageCacheArtifactsResult = {
41
56
  cacheControl?: CacheControlMetadata;
57
+ headers?: Record<string, string>;
42
58
  html: string;
43
59
  htmlRenderObservation: ReturnType<typeof createAppPageRenderObservation>;
44
60
  linkHeader?: string;
45
61
  rscData?: ArrayBuffer;
46
62
  rscRenderObservation?: ReturnType<typeof createAppPageRenderObservation>;
63
+ /** The status of a render whose shell ended in a special error. */
64
+ status?: number;
47
65
  tags: string[];
48
66
  /** The render used a dynamic API, so its output must not be stored. */
49
67
  usedDynamicApi: boolean;
@@ -26,34 +26,49 @@ async function renderAppPageCacheArtifactsImpl(options, renderSpan) {
26
26
  const rscCapture = teeAppPageRscStreamForCapture(rscStream, options.captureRscData);
27
27
  const capturedRscDataRef = { value: null };
28
28
  const fontPreloads = options.getFontPreloads();
29
- const htmlResult = await (await options.loadSsrHandler()).handleSsr(rscCapture.ssrStream, options.getNavigationContext(), {
30
- links: options.getFontLinks(),
31
- styles: options.getFontStyles(),
32
- preloads: fontPreloads
33
- }, {
34
- basePath: options.basePath,
35
- clientTraceMetadata: options.clientTraceMetadata,
36
- reactMaxHeadersLength: options.reactMaxHeadersLength,
37
- rootParams: options.rootParams,
38
- waitForAllReady: options.waitForAllReady,
39
- isStaticGeneration: true,
40
- isForceStatic: options.isForceStatic,
41
- onSsrError: options.onSsrError && options.isCapturedRscError ? createAppPageSsrErrorHandler((error) => {
42
- recordAppPageRenderError(renderSpan, error);
43
- return options.onSsrError?.(error);
44
- }, options.isCapturedRscError) : options.onSsrError ? (error) => {
45
- recordAppPageRenderError(renderSpan, error);
46
- return options.onSsrError?.(error);
47
- } : void 0,
48
- ...rscCapture.sideStream ? {
49
- sideStream: rscCapture.sideStream,
50
- capturedRscDataRef
51
- } : {}
52
- });
53
- const htmlStream = isAppSsrRenderResult(htmlResult) ? htmlResult.htmlStream : htmlResult;
54
- const reactLinkHeader = isAppSsrRenderResult(htmlResult) ? htmlResult.linkHeader : void 0;
55
- const linkHeader = buildAppPageLinkHeader(reactLinkHeader, buildAppPageFontLinkHeader(fontPreloads), options.reactMaxHeadersLength);
56
- const html = await readStreamAsText(htmlStream);
29
+ const ssrHandler = await options.loadSsrHandler();
30
+ let htmlResult = null;
31
+ let specialErrorDocument = null;
32
+ try {
33
+ htmlResult = await ssrHandler.handleSsr(rscCapture.ssrStream, options.getNavigationContext(), {
34
+ links: options.getFontLinks(),
35
+ styles: options.getFontStyles(),
36
+ preloads: fontPreloads
37
+ }, {
38
+ basePath: options.basePath,
39
+ clientTraceMetadata: options.clientTraceMetadata,
40
+ reactMaxHeadersLength: options.reactMaxHeadersLength,
41
+ rootParams: options.rootParams,
42
+ waitForAllReady: options.waitForAllReady,
43
+ mirrorNextFlight: options.mirrorNextFlight,
44
+ isStaticGeneration: true,
45
+ isForceStatic: options.isForceStatic,
46
+ onSsrError: options.onSsrError && options.isCapturedRscError ? createAppPageSsrErrorHandler((error) => {
47
+ recordAppPageRenderError(renderSpan, error);
48
+ return options.onSsrError?.(error);
49
+ }, options.isCapturedRscError) : options.onSsrError ? (error) => {
50
+ recordAppPageRenderError(renderSpan, error);
51
+ return options.onSsrError?.(error);
52
+ } : void 0,
53
+ ...rscCapture.sideStream ? {
54
+ sideStream: rscCapture.sideStream,
55
+ capturedRscDataRef
56
+ } : {}
57
+ });
58
+ } catch (error) {
59
+ if (!options.renderShellSpecialError) throw error;
60
+ await capturedRscDataRef.value?.catch(() => {});
61
+ specialErrorDocument = await options.renderShellSpecialError(error);
62
+ if (!specialErrorDocument) throw error;
63
+ }
64
+ let html = specialErrorDocument?.html ?? "";
65
+ let linkHeader = specialErrorDocument?.linkHeader;
66
+ if (htmlResult) {
67
+ const htmlStream = isAppSsrRenderResult(htmlResult) ? htmlResult.htmlStream : htmlResult;
68
+ const reactLinkHeader = isAppSsrRenderResult(htmlResult) ? htmlResult.linkHeader : void 0;
69
+ linkHeader = buildAppPageLinkHeader(reactLinkHeader, buildAppPageFontLinkHeader(fontPreloads), options.reactMaxHeadersLength);
70
+ html = await readStreamAsText(htmlStream);
71
+ }
57
72
  let rscData;
58
73
  if (options.captureRscData) {
59
74
  const capturedPromise = capturedRscDataRef.value;
@@ -65,24 +80,29 @@ async function renderAppPageCacheArtifactsImpl(options, renderSpan) {
65
80
  const observationState = consumeAppPageRenderObservationState();
66
81
  consumeInvalidDynamicUsageError();
67
82
  const usedDynamicApi = consumeDynamicUsage() || isRenderDynamicLatched();
83
+ const htmlRenderObservation = createAppPageRenderObservation({
84
+ boundaryOutcome: { kind: "success" },
85
+ cacheability: "public",
86
+ cacheTags: tags,
87
+ cleanPathname: options.cleanPathname,
88
+ completeness: "complete",
89
+ output: createAppPageHtmlOutputScope({
90
+ element: options.element,
91
+ renderEpoch: null,
92
+ rootBoundaryId: null,
93
+ routePattern: options.route.pattern
94
+ }),
95
+ params: options.navigationParams,
96
+ state: observationState
97
+ });
68
98
  const result = {
69
99
  html,
70
- htmlRenderObservation: createAppPageRenderObservation({
71
- boundaryOutcome: { kind: "success" },
72
- cacheability: "public",
73
- cacheTags: tags,
74
- cleanPathname: options.cleanPathname,
75
- completeness: "complete",
76
- output: createAppPageHtmlOutputScope({
77
- element: options.element,
78
- renderEpoch: null,
79
- rootBoundaryId: null,
80
- routePattern: options.route.pattern
81
- }),
82
- params: options.navigationParams,
83
- state: observationState
84
- }),
100
+ htmlRenderObservation,
85
101
  ...linkHeader ? { linkHeader } : {},
102
+ ...specialErrorDocument ? {
103
+ headers: specialErrorDocument.headers,
104
+ status: specialErrorDocument.status
105
+ } : {},
86
106
  tags,
87
107
  usedDynamicApi,
88
108
  cacheControl: typeof cacheLife?.revalidate === "number" ? {