vinext 1.0.0-beta.2 → 1.0.0-beta.3

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 (126) hide show
  1. package/README.md +6 -9
  2. package/dist/build/client-build-config.js +7 -1
  3. package/dist/check.js +4 -0
  4. package/dist/config/config-matchers.js +4 -1
  5. package/dist/config/next-config.d.ts +12 -3
  6. package/dist/config/next-config.js +4 -0
  7. package/dist/entries/app-browser-entry.d.ts +4 -10
  8. package/dist/entries/app-browser-entry.js +20 -4
  9. package/dist/entries/app-rsc-entry.js +3 -0
  10. package/dist/entries/app-rsc-manifest.js +19 -0
  11. package/dist/entries/pages-client-entry.js +0 -1
  12. package/dist/entries/pages-server-entry.js +10 -3
  13. package/dist/index.js +112 -43
  14. package/dist/init-cloudflare.d.ts +3 -1
  15. package/dist/init-cloudflare.js +59 -5
  16. package/dist/plugins/extensionless-dynamic-import.js +23 -18
  17. package/dist/plugins/ignore-dynamic-requests.js +3 -1
  18. package/dist/plugins/import-meta-url.js +17 -6
  19. package/dist/plugins/require-context.js +20 -15
  20. package/dist/plugins/transform-cache.d.ts +19 -0
  21. package/dist/plugins/transform-cache.js +36 -0
  22. package/dist/routing/app-route-graph.d.ts +14 -6
  23. package/dist/routing/app-route-graph.js +106 -16
  24. package/dist/server/api-handler.d.ts +2 -0
  25. package/dist/server/api-handler.js +3 -3
  26. package/dist/server/app-browser-entry.js +103 -31
  27. package/dist/server/app-browser-navigation-controller.d.ts +23 -21
  28. package/dist/server/app-browser-navigation-controller.js +1 -1
  29. package/dist/server/app-page-cache-finalizer.js +1 -1
  30. package/dist/server/app-page-cache.d.ts +1 -1
  31. package/dist/server/app-page-cache.js +16 -2
  32. package/dist/server/app-page-dispatch.d.ts +8 -0
  33. package/dist/server/app-page-dispatch.js +31 -6
  34. package/dist/server/app-page-element-builder.d.ts +2 -0
  35. package/dist/server/app-page-element-builder.js +33 -7
  36. package/dist/server/app-page-head.d.ts +3 -3
  37. package/dist/server/app-page-head.js +58 -13
  38. package/dist/server/app-page-http-access-fallback-metadata.d.ts +2 -2
  39. package/dist/server/app-page-http-access-fallback-metadata.js +9 -1
  40. package/dist/server/app-page-probe.d.ts +4 -0
  41. package/dist/server/app-page-probe.js +6 -2
  42. package/dist/server/app-page-request.d.ts +3 -0
  43. package/dist/server/app-page-request.js +1 -1
  44. package/dist/server/app-page-response.js +1 -1
  45. package/dist/server/app-page-route-wiring.d.ts +7 -0
  46. package/dist/server/app-page-route-wiring.js +211 -49
  47. package/dist/server/app-pages-bridge.d.ts +1 -1
  48. package/dist/server/app-pages-bridge.js +2 -1
  49. package/dist/server/app-route-handler-cache.js +4 -0
  50. package/dist/server/app-route-handler-dispatch.js +9 -3
  51. package/dist/server/app-route-handler-response.js +1 -1
  52. package/dist/server/app-route-module-loader.d.ts +6 -0
  53. package/dist/server/app-route-module-loader.js +3 -0
  54. package/dist/server/app-router-entry.js +6 -3
  55. package/dist/server/app-rsc-errors.js +10 -0
  56. package/dist/server/app-rsc-handler.js +21 -9
  57. package/dist/server/app-rsc-route-matching.d.ts +9 -0
  58. package/dist/server/app-rsc-route-matching.js +6 -0
  59. package/dist/server/app-ssr-entry.js +4 -4
  60. package/dist/server/cache-control.d.ts +2 -2
  61. package/dist/server/cache-control.js +2 -1
  62. package/dist/server/dev-server.d.ts +3 -1
  63. package/dist/server/dev-server.js +145 -312
  64. package/dist/server/headers.d.ts +2 -2
  65. package/dist/server/headers.js +4 -3
  66. package/dist/server/isr-cache.d.ts +10 -33
  67. package/dist/server/isr-cache.js +33 -43
  68. package/dist/server/isr-decision.d.ts +2 -2
  69. package/dist/server/pages-api-route.d.ts +2 -0
  70. package/dist/server/pages-api-route.js +4 -1
  71. package/dist/server/pages-dev-hydration.js +0 -1
  72. package/dist/server/pages-get-initial-props.d.ts +1 -1
  73. package/dist/server/pages-get-initial-props.js +1 -4
  74. package/dist/server/pages-i18n.js +64 -16
  75. package/dist/server/pages-node-compat.d.ts +2 -0
  76. package/dist/server/pages-node-compat.js +7 -3
  77. package/dist/server/pages-page-data.d.ts +61 -9
  78. package/dist/server/pages-page-data.js +334 -74
  79. package/dist/server/pages-page-handler.d.ts +5 -1
  80. package/dist/server/pages-page-handler.js +115 -19
  81. package/dist/server/pages-page-response.d.ts +5 -3
  82. package/dist/server/pages-page-response.js +23 -26
  83. package/dist/server/pages-request-pipeline.d.ts +7 -0
  84. package/dist/server/pages-request-pipeline.js +5 -1
  85. package/dist/server/pages-revalidate.d.ts +1 -1
  86. package/dist/server/pages-revalidate.js +54 -8
  87. package/dist/server/pages-router-entry.d.ts +1 -0
  88. package/dist/server/pages-router-entry.js +10 -4
  89. package/dist/server/prod-server.d.ts +2 -1
  90. package/dist/server/prod-server.js +37 -13
  91. package/dist/server/revalidation-host.d.ts +11 -0
  92. package/dist/server/revalidation-host.js +19 -0
  93. package/dist/server/worker-revalidation-context.d.ts +13 -0
  94. package/dist/server/worker-revalidation-context.js +29 -0
  95. package/dist/shims/cache-handler.d.ts +1 -1
  96. package/dist/shims/cache-handler.js +10 -7
  97. package/dist/shims/cache-runtime.d.ts +6 -0
  98. package/dist/shims/cache-runtime.js +4 -1
  99. package/dist/shims/cache.js +1 -1
  100. package/dist/shims/error.js +1 -1
  101. package/dist/shims/fetch-cache.js +1 -1
  102. package/dist/shims/font-google-base.js +17 -8
  103. package/dist/shims/font-local.js +41 -10
  104. package/dist/shims/headers.d.ts +10 -13
  105. package/dist/shims/headers.js +85 -36
  106. package/dist/shims/internal/cookie-serialize.d.ts +10 -9
  107. package/dist/shims/internal/cookie-serialize.js +9 -8
  108. package/dist/shims/internal/interpolate-as.d.ts +15 -1
  109. package/dist/shims/internal/interpolate-as.js +27 -2
  110. package/dist/shims/link.js +18 -6
  111. package/dist/shims/metadata.d.ts +7 -17
  112. package/dist/shims/metadata.js +74 -24
  113. package/dist/shims/navigation.js +1 -1
  114. package/dist/shims/request-context.d.ts +4 -1
  115. package/dist/shims/router.js +18 -7
  116. package/dist/shims/script.js +1 -1
  117. package/dist/shims/server.d.ts +7 -6
  118. package/dist/shims/server.js +156 -57
  119. package/dist/shims/unified-request-context.d.ts +21 -2
  120. package/dist/shims/unified-request-context.js +82 -1
  121. package/dist/utils/cache-control-metadata.d.ts +2 -1
  122. package/dist/utils/cache-control-metadata.js +5 -1
  123. package/dist/utils/html-limited-bots.js +1 -1
  124. package/dist/utils/protocol-headers.d.ts +7 -1
  125. package/dist/utils/protocol-headers.js +7 -1
  126. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { NEXTJS_CACHE_HEADER, VINEXT_CACHE_HEADER } from "./headers.js";
2
- import { setCacheStateHeaders } from "./cache-headers.js";
3
2
  import { applyCdnResponseHeaders } from "./cache-control.js";
4
3
  import { buildAppPageCacheValue } from "./isr-cache.js";
4
+ import { setCacheStateHeaders } from "./cache-headers.js";
5
5
  import { readStreamAsText } from "../utils/text-stream.js";
6
6
  import { createEmptyAppPageRenderObservationState } from "./app-page-render-observation.js";
7
7
  //#region src/server/app-page-cache-finalizer.ts
@@ -18,7 +18,7 @@ type AppPageCacheOutcomeMetric = Readonly<{
18
18
  */
19
19
  cacheKey: string;
20
20
  outcome: "hit" | "miss" | "stale";
21
- reason: "empty-entry" | "no-entry" | "non-app-page-entry" | "query-variant-unproven" | "read-error" | "served" | "stale-empty-entry";
21
+ reason: "empty-entry" | "expired" | "no-entry" | "non-app-page-entry" | "query-variant-unproven" | "read-error" | "served" | "stale-empty-entry";
22
22
  }>;
23
23
  type AppPageCacheOutcomeRecorder = (metric: AppPageCacheOutcomeMetric) => void;
24
24
  type AppPageCacheRenderResult = {
@@ -1,9 +1,9 @@
1
1
  import { VINEXT_MOUNTED_SLOTS_HEADER } from "./headers.js";
2
- import { setCacheStateHeaders } from "./cache-headers.js";
3
2
  import { applyCdnResponseHeaders } from "./cache-control.js";
4
3
  import { decideIsr } from "./isr-decision.js";
5
4
  import { buildAppPageCacheValue } from "./isr-cache.js";
6
5
  import { encodeCacheTag } from "../utils/encode-cache-tag.js";
6
+ import { setCacheStateHeaders } from "./cache-headers.js";
7
7
  import { VINEXT_RSC_CONTENT_TYPE, VINEXT_RSC_VARY_HEADER, applyRscCompatibilityIdHeader, applyRscDeploymentIdHeader } from "./app-rsc-cache-busting.js";
8
8
  import { mergeMiddlewareResponseHeaders } from "./middleware-response-headers.js";
9
9
  import { applyEdgeRuntimeHeader } from "./app-page-response.js";
@@ -58,7 +58,7 @@ function hasQueryInvariantAppPageProof(cachedValue) {
58
58
  function resolveRegeneratedAppPageCachePolicy(options) {
59
59
  let revalidateSeconds = options.routeRevalidateSeconds;
60
60
  const renderRevalidateSeconds = options.renderCacheControl?.revalidate;
61
- if (renderRevalidateSeconds !== void 0) revalidateSeconds = revalidateSeconds > 0 ? Math.min(revalidateSeconds, renderRevalidateSeconds) : renderRevalidateSeconds;
61
+ if (typeof renderRevalidateSeconds === "number") revalidateSeconds = revalidateSeconds > 0 ? Math.min(revalidateSeconds, renderRevalidateSeconds) : renderRevalidateSeconds;
62
62
  return {
63
63
  expireSeconds: options.renderCacheControl?.expire ?? options.expireSeconds,
64
64
  revalidateSeconds
@@ -105,6 +105,10 @@ function buildAppPageCachedResponse(cachedValue, options) {
105
105
  });
106
106
  }
107
107
  async function serveAppPageCachedHtml(options, transformValue) {
108
+ if (options.cached?.isExpired) {
109
+ options.isrDebug?.("MISS (expired)", options.pathname);
110
+ return null;
111
+ }
108
112
  if (typeof options.cachedValue.html !== "string" || options.cachedValue.html.length === 0) {
109
113
  if (options.cached?.isStale) options.scheduleRegeneration();
110
114
  options.isrDebug?.(options.emptyDebugMessage, options.pathname);
@@ -137,6 +141,16 @@ async function readAppPageCacheResponse(options) {
137
141
  try {
138
142
  const cached = await options.isrGet(isrKey);
139
143
  const cachedValue = getCachedAppPageValue(cached);
144
+ if (cached?.isExpired) {
145
+ recordAppPageCacheOutcome(options.recordCacheOutcome, {
146
+ artifact,
147
+ cacheKey: isrKey,
148
+ outcome: "miss",
149
+ reason: "expired"
150
+ });
151
+ options.isrDebug?.("MISS (expired)", options.cleanPathname);
152
+ return null;
153
+ }
140
154
  if (cached && !cachedValue) {
141
155
  recordAppPageCacheOutcome(options.recordCacheOutcome, {
142
156
  artifact,
@@ -36,6 +36,8 @@ type AppPageDispatchIntercept<TPage = unknown> = {
36
36
  interceptLayouts?: readonly unknown[] | null;
37
37
  interceptLayoutSegments?: readonly (readonly string[])[] | null;
38
38
  interceptBranchSegments?: readonly string[] | null;
39
+ interceptLoadings?: readonly unknown[] | null;
40
+ interceptLoadingTreePositions?: readonly number[] | null;
39
41
  interceptNotFoundBranchSegments?: readonly string[] | null;
40
42
  notFound?: unknown;
41
43
  notFoundTreePosition?: number | null;
@@ -52,6 +54,8 @@ type AppPageDispatchInterceptOptions<TPage = unknown> = {
52
54
  interceptLayouts?: readonly unknown[] | null;
53
55
  interceptLayoutSegments?: readonly (readonly string[])[] | null;
54
56
  interceptBranchSegments?: readonly string[] | null;
57
+ interceptLoadings?: readonly unknown[] | null;
58
+ interceptLoadingTreePositions?: readonly number[] | null;
55
59
  interceptNotFoundBranchSegments?: readonly string[] | null;
56
60
  interceptNotFound?: unknown;
57
61
  interceptNotFoundTreePosition?: number | null;
@@ -69,6 +73,8 @@ type AppPageModule = {
69
73
  };
70
74
  type AppPageDispatchSlot = {
71
75
  default?: AppPageModule | null;
76
+ loading?: AppPageModule | null;
77
+ loadings?: readonly (AppPageModule | null | undefined)[] | null;
72
78
  page?: AppPageModule | null;
73
79
  slotPatternParts?: readonly string[] | null;
74
80
  slotParamNames?: readonly string[] | null;
@@ -85,6 +91,8 @@ type AppPageDispatchRoute = {
85
91
  layouts: readonly AppPageModule[];
86
92
  layoutTreePositions?: readonly number[];
87
93
  loading?: AppPageModule | null;
94
+ loadings?: readonly (AppPageModule | null | undefined)[];
95
+ loadingTreePositions?: readonly number[];
88
96
  notFound?: AppPageModule | null;
89
97
  notFounds?: readonly (AppPageModule | null | undefined)[];
90
98
  params: readonly string[];
@@ -1,4 +1,4 @@
1
- import { createRequestContext, runWithRequestContext } from "../shims/unified-request-context.js";
1
+ import { closeAfterResponse, createRequestContext, runWithRequestContext } from "../shims/unified-request-context.js";
2
2
  import { getRequestExecutionContext } from "../shims/request-context.js";
3
3
  import "../utils/protocol-headers.js";
4
4
  import { AppElementsWire } from "./app-elements-wire.js";
@@ -23,6 +23,22 @@ import { resolveAppPageMethodResponse } from "./app-page-method.js";
23
23
  import { buildAppPageElement, resolveAppPageIntercept, resolveAppPageInterceptionRerenderTarget, validateAppPageDynamicParams } from "./app-page-request.js";
24
24
  import { renderAppPageLifecycle } from "./app-page-render.js";
25
25
  //#region src/server/app-page-dispatch.ts
26
+ function getActiveLoadingTreePositions(route) {
27
+ const positions = [];
28
+ for (const [index, loadingModule] of (route.loadings ?? []).entries()) {
29
+ if (!loadingModule?.default) continue;
30
+ const treePosition = route.loadingTreePositions?.[index];
31
+ if (treePosition !== void 0) positions.push(treePosition);
32
+ }
33
+ if (positions.length === 0 && route.loading?.default) positions.push(route.routeSegments.length);
34
+ return positions;
35
+ }
36
+ function getAppPageLayoutProbeCount(route, loadingTreePositions) {
37
+ const firstLoadingTreePosition = loadingTreePositions.reduce((first, position) => first === null || position < first ? position : first, null);
38
+ if (firstLoadingTreePosition === null) return route.layouts.length;
39
+ const firstSuspendedLayoutIndex = (route.layoutTreePositions ?? []).findIndex((position) => position > firstLoadingTreePosition);
40
+ return firstSuspendedLayoutIndex === -1 ? route.layouts.length : firstSuspendedLayoutIndex;
41
+ }
26
42
  function resolveAppPageRouteBoundaryModule(route, statusCode) {
27
43
  if (statusCode === 403) return route.forbidden ?? null;
28
44
  if (statusCode === 401) return route.unauthorized ?? null;
@@ -110,7 +126,7 @@ function hasSearchParams(searchParams) {
110
126
  }
111
127
  async function runAppPageRevalidationContext(options, renderFn) {
112
128
  const { createStaticGenerationHeadersContext } = await import("./app-static-generation.js");
113
- return runWithRequestContext(createRequestContext({
129
+ const requestContext = createRequestContext({
114
130
  headersContext: createStaticGenerationHeadersContext({
115
131
  draftModeEnabled: false,
116
132
  draftModeSecret: options.draftModeSecret,
@@ -122,7 +138,8 @@ async function runAppPageRevalidationContext(options, renderFn) {
122
138
  currentForceDynamicFetchDefault: options.dynamicConfig === "force-dynamic",
123
139
  executionContext: getRequestExecutionContext(),
124
140
  unstableCacheRevalidation: "foreground"
125
- }), async () => {
141
+ });
142
+ const revalidation = runWithRequestContext(requestContext, async () => {
126
143
  ensureFetchPatch();
127
144
  setRefreshStaleFetchesInForeground(process.env.VINEXT_PRERENDER === "1");
128
145
  setCurrentFetchSoftTags(buildAppPageTags(options.cleanPathname, [], options.routeSegments));
@@ -133,6 +150,11 @@ async function runAppPageRevalidationContext(options, renderFn) {
133
150
  });
134
151
  return await runWithFetchDedupe(renderFn);
135
152
  });
153
+ try {
154
+ return await revalidation;
155
+ } finally {
156
+ await closeAfterResponse(requestContext);
157
+ }
136
158
  }
137
159
  function toInterceptOptions(interceptionContext, intercept) {
138
160
  return {
@@ -140,6 +162,8 @@ function toInterceptOptions(interceptionContext, intercept) {
140
162
  interceptLayouts: intercept.interceptLayouts,
141
163
  interceptLayoutSegments: intercept.interceptLayoutSegments,
142
164
  interceptBranchSegments: intercept.interceptBranchSegments,
165
+ interceptLoadings: intercept.interceptLoadings,
166
+ interceptLoadingTreePositions: intercept.interceptLoadingTreePositions,
143
167
  interceptNotFoundBranchSegments: intercept.interceptNotFoundBranchSegments,
144
168
  interceptNotFound: intercept.notFound,
145
169
  interceptNotFoundTreePosition: intercept.notFoundTreePosition,
@@ -173,7 +197,8 @@ async function dispatchAppPageInner(options) {
173
197
  const hasRequestSearchParams = !shouldUseEmptySearchParams && hasSearchParams(options.searchParams);
174
198
  const pageSearchParams = shouldUseEmptySearchParams ? new URLSearchParams() : options.searchParams;
175
199
  const layoutParamAccess = createAppLayoutParamAccessTracker();
176
- const hasActiveLoadingBoundary = Boolean(route.loading?.default);
200
+ const activeLoadingTreePositions = getActiveLoadingTreePositions(route);
201
+ const hasActiveLoadingBoundary = activeLoadingTreePositions.length > 0;
177
202
  setCurrentFetchSoftTags(buildAppPageTags(options.cleanPathname, [], route.routeSegments));
178
203
  setCurrentFetchCacheMode(options.fetchCache ?? null);
179
204
  setCurrentForceDynamicFetchDefault(isForceDynamic);
@@ -410,7 +435,7 @@ async function dispatchAppPageInner(options) {
410
435
  });
411
436
  },
412
437
  async probePageSpecialError() {
413
- if (route.loading?.default) return null;
438
+ if (hasActiveLoadingBoundary) return null;
414
439
  return resolveAppPageSpecialError(await probeAppPageThrownError({
415
440
  probePage: () => options.probePage(pageSearchParams),
416
441
  runWithSuppressedHookWarning(probe) {
@@ -500,7 +525,7 @@ async function dispatchAppPageInner(options) {
500
525
  isrSet: options.isrSet,
501
526
  interceptionContext: options.interceptionContext,
502
527
  expireSeconds: options.expireSeconds,
503
- layoutCount: route.layouts.length,
528
+ layoutCount: getAppPageLayoutProbeCount(route, activeLoadingTreePositions),
504
529
  loadSsrHandler: options.loadSsrHandler,
505
530
  middlewareContext: options.middlewareContext,
506
531
  navigationParams,
@@ -22,6 +22,8 @@ type AppPageInterceptOptions<TModule extends AppPageModule = AppPageModule> = {
22
22
  interceptLayouts?: readonly (TModule | null | undefined)[] | null;
23
23
  interceptLayoutSegments?: readonly (readonly string[])[] | null;
24
24
  interceptBranchSegments?: readonly string[] | null;
25
+ interceptLoadings?: readonly (TModule | null | undefined)[] | null;
26
+ interceptLoadingTreePositions?: readonly number[] | null;
25
27
  interceptNotFoundBranchSegments?: readonly string[] | null;
26
28
  interceptNotFound?: TModule | null;
27
29
  interceptNotFoundTreePosition?: number | null;
@@ -15,7 +15,7 @@ import { resolveAppPageParentHttpAccessBoundary } from "./app-page-boundary.js";
15
15
  import { sanitizeErrorForClient } from "./app-rsc-errors.js";
16
16
  import { DEFAULT_GLOBAL_ERROR_MODULE } from "./default-global-error-module.js";
17
17
  import { shouldServeStreamingMetadata } from "./streaming-metadata.js";
18
- import { createElement } from "react";
18
+ import { Suspense, createElement } from "react";
19
19
  //#region src/server/app-page-element-builder.ts
20
20
  function resolveInterceptLayoutParams(branchSegments, layoutSegments, params) {
21
21
  return resolveAppPageBranchParams(branchSegments, layoutSegments.length, params, layoutSegments);
@@ -111,7 +111,7 @@ async function buildPageElements(options) {
111
111
  } : {},
112
112
  ownerTreePosition: route.routeSegments?.length ?? 0
113
113
  } : null;
114
- const parallelRoutes = [...activeParallelRouteHeadInputs.map((input) => input.head), ...primaryParallelRouteHeadInput ? [primaryParallelRouteHeadInput.head] : []];
114
+ const parallelRoutes = [...primaryParallelRouteHeadInput ? [primaryParallelRouteHeadInput.head] : [], ...activeParallelRouteHeadInputs.map((input) => input.head)];
115
115
  const metadataSearchParamsObserver = observeMetadataSearchParamsAccess ? createAppPageSearchParamsObserver() : void 0;
116
116
  const preparedHead = prepareAppPageHead({
117
117
  applyFileBasedMetadata: options.applyFileBasedMetadata,
@@ -210,11 +210,35 @@ async function buildPageElements(options) {
210
210
  const mountedSlotIds = mountedSlotsHeader ? new Set(mountedSlotsHeader.split(" ")) : null;
211
211
  const slotOverrides = buildSlotOverrides(route, params, routePath, opts);
212
212
  let siblingInterceptElement = isSiblingIntercept && EffectivePageComponent ? createPageElement(EffectivePageComponent, pageProps) : null;
213
- if (isSiblingIntercept && siblingInterceptElement !== null && opts?.interceptLayouts?.length) for (let i = opts.interceptLayouts.length - 1; i >= 0; i--) {
214
- const LayoutComponent = opts.interceptLayouts[i]?.default;
215
- if (LayoutComponent) {
216
- const interceptLayoutSegments = opts.interceptLayoutSegments?.[i] ?? [];
217
- siblingInterceptElement = createElement(LayoutComponent, { params: makeThenableParams(resolveInterceptLayoutParams(opts.interceptBranchSegments ?? interceptLayoutSegments, interceptLayoutSegments, effectiveParams)) }, siblingInterceptElement);
213
+ if (isSiblingIntercept && siblingInterceptElement !== null) {
214
+ const layoutIndexesByTreePosition = /* @__PURE__ */ new Map();
215
+ for (const [index, layoutModule] of (opts?.interceptLayouts ?? []).entries()) {
216
+ if (!layoutModule?.default) continue;
217
+ const treePosition = opts?.interceptLayoutSegments?.[index]?.length ?? 0;
218
+ const indexes = layoutIndexesByTreePosition.get(treePosition) ?? [];
219
+ indexes.push(index);
220
+ layoutIndexesByTreePosition.set(treePosition, indexes);
221
+ }
222
+ const loadingIndexesByTreePosition = /* @__PURE__ */ new Map();
223
+ for (const [index, loadingModule] of (opts?.interceptLoadings ?? []).entries()) {
224
+ if (!loadingModule?.default) continue;
225
+ const treePosition = opts?.interceptLoadingTreePositions?.[index];
226
+ if (treePosition !== void 0) loadingIndexesByTreePosition.set(treePosition, index);
227
+ }
228
+ const treePositions = Array.from(/* @__PURE__ */ new Set([...layoutIndexesByTreePosition.keys(), ...loadingIndexesByTreePosition.keys()])).sort((left, right) => left - right);
229
+ for (let index = treePositions.length - 1; index >= 0; index--) {
230
+ const treePosition = treePositions[index];
231
+ const loadingIndex = loadingIndexesByTreePosition.get(treePosition);
232
+ const LoadingComponent = loadingIndex === void 0 ? null : opts?.interceptLoadings?.[loadingIndex]?.default;
233
+ if (LoadingComponent) siblingInterceptElement = createElement(Suspense, { fallback: createElement(LoadingComponent) }, siblingInterceptElement);
234
+ const layoutIndexes = layoutIndexesByTreePosition.get(treePosition) ?? [];
235
+ for (let layoutOffset = layoutIndexes.length - 1; layoutOffset >= 0; layoutOffset--) {
236
+ const layoutIndex = layoutIndexes[layoutOffset];
237
+ const LayoutComponent = opts?.interceptLayouts?.[layoutIndex]?.default;
238
+ if (!LayoutComponent) continue;
239
+ const interceptLayoutSegments = opts?.interceptLayoutSegments?.[layoutIndex] ?? [];
240
+ siblingInterceptElement = createElement(LayoutComponent, { params: makeThenableParams(resolveInterceptLayoutParams(opts?.interceptBranchSegments ?? interceptLayoutSegments, interceptLayoutSegments, effectiveParams)) }, siblingInterceptElement);
241
+ }
218
242
  }
219
243
  }
220
244
  return buildAppPageElements({
@@ -267,6 +291,8 @@ function buildSlotOverrides(route, routeParams, routePath, opts) {
267
291
  branchSegments: opts.interceptBranchSegments ?? null,
268
292
  layoutModules: opts.interceptLayouts || null,
269
293
  layoutSegments: opts.interceptLayoutSegments ?? null,
294
+ loadingModules: opts.interceptLoadings || null,
295
+ loadingTreePositions: opts.interceptLoadingTreePositions ?? null,
270
296
  pageModule: opts.interceptPage,
271
297
  params: opts.interceptParams || routeParams,
272
298
  routeSegments: resolveInterceptedSlotSegments(opts.interceptSourcePageSegments, opts.interceptSlotKey)
@@ -1,7 +1,7 @@
1
1
  import { MetadataFileRoute } from "./metadata-routes.js";
2
2
  import { AppPageParams } from "./app-page-boundary.js";
3
3
  import { ThenableParamsObserver } from "../shims/thenable-params.js";
4
- import { Metadata, Viewport } from "../shims/metadata.js";
4
+ import { Metadata, ResolvedViewport } from "../shims/metadata.js";
5
5
  import { applyFileBasedMetadata } from "./file-based-metadata.js";
6
6
 
7
7
  //#region src/server/app-page-head.d.ts
@@ -96,11 +96,11 @@ type ResolveAppPageHeadResult = {
96
96
  hasSearchParams: boolean;
97
97
  metadata: Metadata | null;
98
98
  pageSearchParams: AppPageSearchParams;
99
- viewport: Viewport;
99
+ viewport: ResolvedViewport;
100
100
  };
101
101
  type PreparedAppPageHead = Omit<ResolveAppPageHeadResult, "metadata" | "viewport"> & {
102
102
  metadata: Promise<Metadata | null>;
103
- viewport: Promise<Viewport>;
103
+ viewport: Promise<ResolvedViewport>;
104
104
  };
105
105
  type AppPageSearchParamsCollection = {
106
106
  hasSearchParams: boolean;
@@ -79,7 +79,7 @@ function resolveActiveParallelRouteHeadInputs(options) {
79
79
  } : {},
80
80
  ownerTreePosition
81
81
  };
82
- });
82
+ }).sort((left, right) => right.ownerTreePosition - left.ownerTreePosition);
83
83
  }
84
84
  function isPresent(value) {
85
85
  return value !== null && value !== void 0;
@@ -165,11 +165,25 @@ async function resolveLayoutMetadata(layoutInputs, params, routeSegments) {
165
165
  }
166
166
  return Promise.all(layoutMetadataPromises);
167
167
  }
168
- async function resolveLayoutViewport(layoutInputs, params, routeSegments) {
169
- return Promise.all(layoutInputs.map((layoutInput) => {
168
+ function resolveLayoutViewport(layoutInputs, params, routeSegments) {
169
+ const viewportPromises = [];
170
+ let accumulatedViewport = Promise.resolve(mergeViewport([]));
171
+ for (const layoutInput of layoutInputs) {
172
+ const parentForLayout = accumulatedViewport;
170
173
  const layoutParams = resolveAppPageSegmentParams(routeSegments, layoutInput.treePosition, params);
171
- return resolveModuleViewport(layoutInput.module, layoutParams);
172
- }));
174
+ const viewportPromise = resolveModuleViewport(layoutInput.module, layoutParams, void 0, parentForLayout);
175
+ viewportPromises.push(viewportPromise);
176
+ viewportPromise.catch(() => null);
177
+ accumulatedViewport = mergeResolvedViewport(parentForLayout, viewportPromise);
178
+ accumulatedViewport.catch(() => null);
179
+ }
180
+ return {
181
+ resolvedViewport: accumulatedViewport,
182
+ viewportResults: Promise.all(viewportPromises)
183
+ };
184
+ }
185
+ function mergeResolvedViewport(parent, viewport) {
186
+ return Promise.all([parent, viewport]).then(([resolvedParent, resolvedViewport]) => resolvedViewport ? mergeViewport([resolvedParent, resolvedViewport]) : resolvedParent);
173
187
  }
174
188
  function getParallelRouteModules(parallelRoute) {
175
189
  return [...parallelRoute.layoutModules ?? [], parallelRoute.layoutModule].filter(isPresent);
@@ -211,16 +225,34 @@ async function resolveParallelRouteMetadata(parallelRoute, fallbackParams, fallb
211
225
  metadataSources
212
226
  };
213
227
  }
214
- async function resolveParallelRouteViewport(parallelRoute, fallbackParams, fallbackRouteSegments, pageSearchParams, searchParamsObserver) {
228
+ function resolveParallelRouteViewport(parallelRoute, fallbackParams, fallbackRouteSegments, pageSearchParams, parent, searchParamsObserver) {
215
229
  const params = parallelRoute.params ?? fallbackParams;
216
230
  const routeSegments = parallelRoute.routeSegments ?? fallbackRouteSegments;
217
231
  const layoutModules = getParallelRouteModules(parallelRoute);
218
232
  const layoutTreePositions = parallelRoute.layoutTreePositions ?? [];
219
233
  const layoutParams = parallelRoute.layoutParams ?? [];
220
- const layoutViewportPromise = Promise.all(layoutModules.map((layoutModule, index) => resolveModuleViewport(layoutModule, layoutParams[index] ?? resolveParallelLayoutParams(routeSegments, layoutTreePositions[index] ?? 0, params))));
221
- const pageViewportPromise = parallelRoute.pageModule ? resolveModuleViewport(parallelRoute.pageModule, params, pageSearchParams, searchParamsObserver) : Promise.resolve(null);
222
- const [layoutViewports, pageViewport] = await Promise.all([layoutViewportPromise, pageViewportPromise]);
223
- return parallelRoute.pageModule ? [...layoutViewports, pageViewport] : layoutViewports;
234
+ const viewportPromises = [];
235
+ let accumulatedViewport = parent;
236
+ for (const [index, layoutModule] of layoutModules.entries()) {
237
+ const parentForLayout = accumulatedViewport;
238
+ const viewportPromise = resolveModuleViewport(layoutModule, layoutParams[index] ?? resolveParallelLayoutParams(routeSegments, layoutTreePositions[index] ?? 0, params), void 0, parentForLayout);
239
+ viewportPromises.push(viewportPromise);
240
+ viewportPromise.catch(() => null);
241
+ accumulatedViewport = mergeResolvedViewport(parentForLayout, viewportPromise);
242
+ accumulatedViewport.catch(() => null);
243
+ }
244
+ if (parallelRoute.pageModule) {
245
+ const parentForPage = accumulatedViewport;
246
+ const viewportPromise = resolveModuleViewport(parallelRoute.pageModule, params, pageSearchParams, parentForPage, searchParamsObserver);
247
+ viewportPromises.push(viewportPromise);
248
+ viewportPromise.catch(() => null);
249
+ accumulatedViewport = mergeResolvedViewport(parentForPage, viewportPromise);
250
+ accumulatedViewport.catch(() => null);
251
+ }
252
+ return {
253
+ resolvedViewport: accumulatedViewport,
254
+ viewportResults: Promise.all(viewportPromises)
255
+ };
224
256
  }
225
257
  /**
226
258
  * Resolve an explicit metadata-source sequence.
@@ -279,16 +311,29 @@ function prepareAppPageHeadInner(options) {
279
311
  const primaryHasDynamicMetadata = layoutInputs.some((input) => hasGenerateMetadata(input.module)) || hasGenerateMetadata(options.pageModule);
280
312
  const { hasSearchParams, pageSearchParams } = collectAppPageSearchParams(options.searchParams);
281
313
  const layoutMetadataPromise = resolveLayoutMetadata(layoutInputs, options.params, routeSegments);
282
- const layoutViewportPromise = resolveLayoutViewport(layoutInputs, options.params, routeSegments);
314
+ const layoutViewport = resolveLayoutViewport(layoutInputs, options.params, routeSegments);
315
+ const layoutViewportPromise = layoutViewport.viewportResults;
283
316
  const layoutMetadataResultsForParent = layoutMetadataPromise.then((metadataResults) => metadataResults.filter(isPresent));
284
317
  layoutMetadataResultsForParent.catch(() => null);
285
318
  const pageParentPromise = layoutMetadataResultsForParent.then((metadataResults) => metadataResults.length > 0 ? mergeMetadataEntries(metadataResults.map((metadata) => ({ metadata }))) : {});
286
319
  pageParentPromise.catch(() => null);
287
320
  const pageMetadataPromise = options.pageModule ? resolveModuleMetadata(options.pageModule, options.params, pageSearchParams, pageParentPromise, options.searchParamsObserver) : Promise.resolve(null);
288
- const pageViewportPromise = options.pageModule ? resolveModuleViewport(options.pageModule, options.params, pageSearchParams, options.searchParamsObserver) : Promise.resolve(null);
289
321
  const parallelRoutes = options.parallelRoutes ?? [];
290
322
  const parallelRouteMetadataPromise = Promise.all(parallelRoutes.map((parallelRoute) => resolveParallelRouteMetadata(parallelRoute, options.params, routeSegments, pageSearchParams, pageParentPromise, options.searchParamsObserver)));
291
- const parallelRouteViewportPromise = Promise.all(parallelRoutes.map((parallelRoute) => resolveParallelRouteViewport(parallelRoute, options.params, routeSegments, pageSearchParams, options.searchParamsObserver)));
323
+ const parallelRouteViewportPromises = [];
324
+ let accumulatedViewport = layoutViewport.resolvedViewport;
325
+ const pageParentViewport = accumulatedViewport;
326
+ const pageViewportPromise = options.pageModule ? resolveModuleViewport(options.pageModule, options.params, pageSearchParams, pageParentViewport, options.searchParamsObserver) : Promise.resolve(null);
327
+ if (options.pageModule) {
328
+ accumulatedViewport = mergeResolvedViewport(pageParentViewport, pageViewportPromise);
329
+ accumulatedViewport.catch(() => null);
330
+ }
331
+ for (const parallelRoute of parallelRoutes) {
332
+ const parallelViewport = resolveParallelRouteViewport(parallelRoute, options.params, routeSegments, pageSearchParams, accumulatedViewport, options.searchParamsObserver);
333
+ parallelRouteViewportPromises.push(parallelViewport.viewportResults);
334
+ accumulatedViewport = parallelViewport.resolvedViewport;
335
+ }
336
+ const parallelRouteViewportPromise = Promise.all(parallelRouteViewportPromises);
292
337
  const hasDynamicMetadata = primaryHasDynamicMetadata || parallelRoutes.some(parallelRouteHasDynamicMetadata);
293
338
  const metadata = Promise.all([
294
339
  layoutMetadataPromise,
@@ -1,6 +1,6 @@
1
1
  import { MetadataFileRoute } from "./metadata-routes.js";
2
2
  import { AppPageParams } from "./app-page-boundary.js";
3
- import { Metadata, Viewport } from "../shims/metadata.js";
3
+ import { Metadata, ResolvedViewport } from "../shims/metadata.js";
4
4
  import { ActiveParallelRouteHeadInput, AppPageHeadModule, ApplyAppPageFileBasedMetadata, OrderedAppPageMetadataSource } from "./app-page-head.js";
5
5
 
6
6
  //#region src/server/app-page-http-access-fallback-metadata.d.ts
@@ -24,6 +24,6 @@ type ResolveHttpAccessFallbackMetadataOptions<TModule extends AppPageHeadModule
24
24
  };
25
25
  declare function createHttpAccessFallbackMetadataPlan<TModule extends AppPageHeadModule>(options: HttpAccessFallbackMetadataPlanOptions<TModule>): OrderedAppPageMetadataSource<TModule>[];
26
26
  declare function resolveHttpAccessFallbackMetadata<TModule extends AppPageHeadModule>(options: ResolveHttpAccessFallbackMetadataOptions<TModule>): Promise<Metadata | null>;
27
- declare function resolveHttpAccessFallbackViewport<TModule extends AppPageHeadModule>(options: HttpAccessFallbackMetadataPlanOptions<TModule>): Promise<Viewport>;
27
+ declare function resolveHttpAccessFallbackViewport<TModule extends AppPageHeadModule>(options: HttpAccessFallbackMetadataPlanOptions<TModule>): Promise<ResolvedViewport>;
28
28
  //#endregion
29
29
  export { createHttpAccessFallbackMetadataPlan, resolveHttpAccessFallbackMetadata, resolveHttpAccessFallbackViewport };
@@ -100,7 +100,15 @@ function resolveHttpAccessFallbackMetadata(options) {
100
100
  });
101
101
  }
102
102
  async function resolveHttpAccessFallbackViewport(options) {
103
- return mergeViewport((await Promise.all(createHttpAccessFallbackPlan(options, "snapshot").map((source) => resolveModuleViewport(source.module, source.params)))).filter(isPresent));
103
+ let accumulatedViewport = Promise.resolve(mergeViewport([]));
104
+ for (const source of createHttpAccessFallbackPlan(options, "snapshot")) {
105
+ const parentForSource = accumulatedViewport;
106
+ const viewportPromise = resolveModuleViewport(source.module, source.params, void 0, parentForSource);
107
+ viewportPromise.catch(() => null);
108
+ accumulatedViewport = Promise.all([parentForSource, viewportPromise]).then(([parent, viewport]) => viewport ? mergeViewport([parent, viewport]) : parent);
109
+ accumulatedViewport.catch(() => null);
110
+ }
111
+ return accumulatedViewport;
104
112
  }
105
113
  //#endregion
106
114
  export { createHttpAccessFallbackMetadataPlan, resolveHttpAccessFallbackMetadata, resolveHttpAccessFallbackViewport };
@@ -40,12 +40,16 @@ type AppPageProbeModule = Readonly<{
40
40
  }> | null | undefined;
41
41
  type AppPageProbeSlot = Readonly<{
42
42
  page?: AppPageProbeModule;
43
+ loading?: AppPageProbeModule;
44
+ loadings?: readonly AppPageProbeModule[] | null;
45
+ loadingTreePositions?: readonly number[] | null;
43
46
  }> | null | undefined;
44
47
  type AppPageProbeRoute = Readonly<{
45
48
  slots?: Readonly<Record<string, AppPageProbeSlot>> | null;
46
49
  }>;
47
50
  type AppPageProbeIntercept = Readonly<{
48
51
  page?: AppPageProbeModule;
52
+ interceptLoadings?: readonly AppPageProbeModule[] | null;
49
53
  matchedParams?: unknown;
50
54
  /**
51
55
  * Key of the parallel-route slot this interception overrides. At render
@@ -1,6 +1,6 @@
1
- import { markAppPagePropsForUseCache } from "../shims/internal/app-page-props-cache-key.js";
2
1
  import { isNextRouterError } from "../shims/navigation-errors.js";
3
2
  import "../shims/navigation-server.js";
3
+ import { markAppPagePropsForUseCache } from "../shims/internal/app-page-props-cache-key.js";
4
4
  import { isPromiseLike } from "../utils/promise.js";
5
5
  import { probeAppPageComponent, probeAppPageLayouts } from "./app-page-execution.js";
6
6
  import { collectAppPageSearchParams } from "./app-page-head.js";
@@ -218,13 +218,17 @@ function buildAppPageProbes(options) {
218
218
  const overriddenSlotKey = intercept?.slotKey ?? null;
219
219
  for (const [slotKey, slot] of Object.entries(route.slots ?? {})) {
220
220
  if (overriddenSlotKey !== null && slotKey === overriddenSlotKey) continue;
221
+ if (slot?.loading?.default || slot?.loadings?.some((loading) => loading?.default)) continue;
221
222
  probes.push(probeAppPage({
222
223
  pageComponent: slot?.page?.default,
223
224
  asyncRouteParams,
224
225
  searchParams
225
226
  }));
226
227
  }
227
- if (intercept) probes.push(probeAppPage({
228
+ const interceptedSlot = intercept?.slotKey ? route.slots?.[intercept.slotKey] : null;
229
+ const interceptedSlotHasRootLoading = Boolean(interceptedSlot?.loading?.default || interceptedSlot?.loadings?.some((loading, index) => loading?.default && interceptedSlot.loadingTreePositions?.[index] === 0));
230
+ const interceptHasLoadingBoundary = Boolean(intercept?.interceptLoadings?.some((loading) => loading?.default) || interceptedSlotHasRootLoading);
231
+ if (intercept && !interceptHasLoadingBoundary) probes.push(probeAppPage({
228
232
  pageComponent: intercept.page?.default,
229
233
  asyncRouteParams: options.makeThenableParams(intercept.matchedParams ?? matchedParams),
230
234
  searchParams
@@ -62,8 +62,11 @@ type AppPageInterceptMatch<TPage = unknown> = {
62
62
  interceptLayouts?: readonly unknown[] | null;
63
63
  interceptLayoutSegments?: readonly (readonly string[])[] | null;
64
64
  interceptBranchSegments?: readonly string[] | null;
65
+ interceptLoadings?: readonly unknown[] | null;
66
+ interceptLoadingTreePositions?: readonly number[] | null;
65
67
  interceptNotFoundBranchSegments?: readonly string[] | null;
66
68
  __loadInterceptLayouts?: readonly (() => Promise<unknown>)[] | null;
69
+ __loadInterceptLoadings?: readonly (() => Promise<unknown>)[] | null;
67
70
  matchedParams: AppPageParams;
68
71
  sourceMatchedParams?: AppPageParams;
69
72
  page: TPage;
@@ -307,7 +307,7 @@ async function resolveAppPageInterceptState(options) {
307
307
  if (loadState) loadState.notFoundLoading = loading;
308
308
  await loading;
309
309
  }
310
- if (intercept.__loadInterceptLayouts) await loadAppInterceptLayouts(intercept);
310
+ if (intercept.__loadInterceptLayouts || intercept.__loadInterceptLoadings) await loadAppInterceptLayouts(intercept);
311
311
  const sourceRoute = await options.getSourceRoute(intercept.sourceRouteIndex);
312
312
  if (!sourceRoute) return { kind: "none" };
313
313
  if (sourceRoute === options.currentRoute) return {
@@ -1,6 +1,6 @@
1
1
  import { VINEXT_DYNAMIC_STALE_TIME_HEADER, VINEXT_MOUNTED_SLOTS_HEADER, VINEXT_PARAMS_HEADER, VINEXT_PRERENDER_CACHE_LIFE_HEADER, VINEXT_RENDERED_PATH_AND_SEARCH_HEADER, VINEXT_TIMING_HEADER } from "./headers.js";
2
- import { setCacheStateHeaders } from "./cache-headers.js";
3
2
  import { NO_STORE_CACHE_CONTROL, STATIC_CACHE_CONTROL, buildRevalidateCacheControl } from "./cache-control.js";
3
+ import { setCacheStateHeaders } from "./cache-headers.js";
4
4
  import { VINEXT_RSC_CONTENT_TYPE, VINEXT_RSC_VARY_HEADER, applyRscCompatibilityIdHeader, applyRscDeploymentIdHeader } from "./app-rsc-cache-busting.js";
5
5
  import { mergeMiddlewareResponseHeaders } from "./middleware-response-headers.js";
6
6
  //#region src/server/app-page-response.ts
@@ -37,6 +37,9 @@ type AppPageRouteWiringSlot<TModule extends AppPageModule = AppPageModule, TErro
37
37
  layout?: TModule | null;
38
38
  layoutIndex: number;
39
39
  loading?: TModule | null;
40
+ loadings?: readonly (TModule | null | undefined)[] | null;
41
+ loadingTreePositions?: readonly number[] | null;
42
+ ownerTreePosition?: number | null;
40
43
  notFound?: TModule | null;
41
44
  notFoundTreePosition?: number | null;
42
45
  page?: TModule | null;
@@ -59,6 +62,8 @@ type AppPageRouteWiringRoute<TModule extends AppPageModule = AppPageModule, TErr
59
62
  layoutTreePositions?: readonly number[] | null;
60
63
  layouts: readonly (TModule | null | undefined)[];
61
64
  loading?: TModule | null;
65
+ loadings?: readonly (TModule | null | undefined)[] | null;
66
+ loadingTreePositions?: readonly number[] | null;
62
67
  notFound?: TModule | null;
63
68
  notFounds?: readonly (TModule | null | undefined)[] | null;
64
69
  notFoundTreePosition?: number | null;
@@ -99,6 +104,8 @@ type AppPageSlotOverride<TModule extends AppPageModule = AppPageModule> = {
99
104
  branchSegments?: readonly string[] | null;
100
105
  layoutSegments?: readonly (readonly string[])[] | null;
101
106
  layoutModules?: readonly (TModule | null | undefined)[] | null;
107
+ loadingModules?: readonly (TModule | null | undefined)[] | null;
108
+ loadingTreePositions?: readonly number[] | null;
102
109
  /**
103
110
  * The page module to render for this slot. Optional — when omitted, the
104
111
  * slot's existing `page` is used (e.g. when the override only changes the