vinext 1.0.0 → 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 (263) hide show
  1. package/README.md +72 -1
  2. package/THIRD_PARTY_LICENSES.md +26 -0
  3. package/dist/build/app-page-static-eligibility.d.ts +15 -0
  4. package/dist/build/app-page-static-eligibility.js +82 -0
  5. package/dist/build/lifecycle.js +10 -10
  6. package/dist/build/pages-client-assets-module.js +6 -1
  7. package/dist/build/precompress.js +1 -1
  8. package/dist/build/prerender-paths.js +6 -85
  9. package/dist/build/prerender.d.ts +8 -1
  10. package/dist/build/prerender.js +167 -46
  11. package/dist/build/run-prerender.js +4 -3
  12. package/dist/build/server-entry.d.ts +2 -0
  13. package/dist/build/server-entry.js +7 -1
  14. package/dist/build/server-manifest.d.ts +7 -0
  15. package/dist/build/server-manifest.js +11 -1
  16. package/dist/cache/cache-adapters-virtual.d.ts +7 -1
  17. package/dist/cache/cache-adapters-virtual.js +3 -3
  18. package/dist/client/browser-globals.d.ts +4 -0
  19. package/dist/client/browser-globals.js +5 -0
  20. package/dist/client/navigation-runtime.d.ts +11 -1
  21. package/dist/client/navigation-runtime.js +3 -3
  22. package/dist/config/config-matchers.js +3 -1
  23. package/dist/config/dotenv.js +14 -3
  24. package/dist/config/next-config.d.ts +7 -0
  25. package/dist/config/next-config.js +18 -3
  26. package/dist/entries/app-rsc-entry.d.ts +2 -0
  27. package/dist/entries/app-rsc-entry.js +67 -45
  28. package/dist/index.d.ts +4 -1
  29. package/dist/index.js +200 -100
  30. package/dist/init-cloudflare.d.ts +1 -1
  31. package/dist/init-cloudflare.js +6 -6
  32. package/dist/init-platform.js +1 -1
  33. package/dist/init.js +4 -2
  34. package/dist/plugins/cache-flight-codec.d.ts +19 -0
  35. package/dist/plugins/cache-flight-codec.js +68 -0
  36. package/dist/plugins/commonjs-esm-facade.d.ts +35 -0
  37. package/dist/plugins/commonjs-esm-facade.js +80 -0
  38. package/dist/plugins/commonjs-syntax.d.ts +16 -0
  39. package/dist/plugins/commonjs-syntax.js +22 -0
  40. package/dist/plugins/fonts.d.ts +8 -11
  41. package/dist/plugins/fonts.js +71 -34
  42. package/dist/plugins/ignore-dynamic-requests.d.ts +11 -0
  43. package/dist/plugins/ignore-dynamic-requests.js +54 -3
  44. package/dist/plugins/import-meta-url.js +6 -6
  45. package/dist/plugins/og-asset-ownership.d.ts +2 -0
  46. package/dist/plugins/og-asset-ownership.js +24 -13
  47. package/dist/plugins/og-assets.d.ts +18 -0
  48. package/dist/plugins/og-assets.js +50 -8
  49. package/dist/plugins/og-harfbuzz.js +38 -27
  50. package/dist/plugins/optimize-imports.js +2 -1
  51. package/dist/plugins/require-context.d.ts +1 -0
  52. package/dist/plugins/require-context.js +28 -1
  53. package/dist/plugins/scan-build-css.d.ts +12 -0
  54. package/dist/plugins/scan-build-css.js +39 -0
  55. package/dist/plugins/strip-server-exports.d.ts +2 -0
  56. package/dist/plugins/strip-server-exports.js +3 -1
  57. package/dist/plugins/transform-result.d.ts +18 -0
  58. package/dist/plugins/transform-result.js +23 -2
  59. package/dist/plugins/typeof-window.d.ts +10 -0
  60. package/dist/plugins/typeof-window.js +33 -9
  61. package/dist/routing/app-route-graph.d.ts +0 -1
  62. package/dist/routing/app-route-graph.js +2 -4
  63. package/dist/routing/app-route-ids.d.ts +3 -0
  64. package/dist/routing/app-route-ids.js +6 -0
  65. package/dist/routing/file-matcher.d.ts +1 -0
  66. package/dist/routing/file-matcher.js +1 -1
  67. package/dist/server/api-handler.js +17 -7
  68. package/dist/server/app-bfcache-id.d.ts +1 -0
  69. package/dist/server/app-bfcache-id.js +7 -1
  70. package/dist/server/app-bfcache-identity.js +1 -2
  71. package/dist/server/app-browser-entry.js +15 -9
  72. package/dist/server/app-browser-history-controller.d.ts +2 -0
  73. package/dist/server/app-browser-history-controller.js +8 -0
  74. package/dist/server/app-browser-hydration.d.ts +1 -1
  75. package/dist/server/app-browser-hydration.js +2 -2
  76. package/dist/server/app-browser-interception-context.js +26 -2
  77. package/dist/server/app-browser-navigation-controller.d.ts +7 -1
  78. package/dist/server/app-browser-navigation-controller.js +94 -11
  79. package/dist/server/app-browser-state.js +1 -1
  80. package/dist/server/app-browser-stream.d.ts +1 -1
  81. package/dist/server/app-fallback-renderer.d.ts +2 -0
  82. package/dist/server/app-fallback-renderer.js +1 -0
  83. package/dist/server/app-history-state.d.ts +3 -1
  84. package/dist/server/app-history-state.js +9 -5
  85. package/dist/server/app-middleware.js +2 -1
  86. package/dist/server/app-optimistic-routing.js +1 -1
  87. package/dist/server/app-page-boundary-render.d.ts +5 -0
  88. package/dist/server/app-page-boundary-render.js +6 -4
  89. package/dist/server/app-page-cache-finalizer.d.ts +11 -0
  90. package/dist/server/app-page-cache-finalizer.js +41 -7
  91. package/dist/server/app-page-cache-render.d.ts +18 -0
  92. package/dist/server/app-page-cache-render.js +65 -44
  93. package/dist/server/app-page-cache.d.ts +5 -0
  94. package/dist/server/app-page-cache.js +27 -7
  95. package/dist/server/app-page-dispatch.d.ts +9 -1
  96. package/dist/server/app-page-dispatch.js +47 -30
  97. package/dist/server/app-page-element-builder.d.ts +7 -0
  98. package/dist/server/app-page-element-builder.js +27 -42
  99. package/dist/server/app-page-execution.d.ts +15 -12
  100. package/dist/server/app-page-execution.js +30 -40
  101. package/dist/server/app-page-head.d.ts +5 -0
  102. package/dist/server/app-page-head.js +9 -3
  103. package/dist/server/app-page-probe.d.ts +0 -109
  104. package/dist/server/app-page-probe.js +3 -136
  105. package/dist/server/app-page-render-identity.d.ts +0 -1
  106. package/dist/server/app-page-render-identity.js +1 -1
  107. package/dist/server/app-page-render-observation.js +2 -2
  108. package/dist/server/app-page-render.d.ts +13 -5
  109. package/dist/server/app-page-render.js +219 -118
  110. package/dist/server/app-page-request.d.ts +0 -1
  111. package/dist/server/app-page-request.js +1 -2
  112. package/dist/server/app-page-response.d.ts +4 -3
  113. package/dist/server/app-page-response.js +4 -3
  114. package/dist/server/app-page-route-wiring.d.ts +17 -5
  115. package/dist/server/app-page-route-wiring.js +55 -33
  116. package/dist/server/app-page-rsc-render-status.d.ts +48 -0
  117. package/dist/server/app-page-rsc-render-status.js +54 -0
  118. package/dist/server/app-page-search-params-observation.js +1 -1
  119. package/dist/server/app-page-stream.d.ts +15 -8
  120. package/dist/server/app-page-stream.js +7 -1
  121. package/dist/server/app-page-tracing.js +2 -1
  122. package/dist/server/app-pages-bridge.js +3 -2
  123. package/dist/server/app-render-abort-error.d.ts +3 -0
  124. package/dist/server/app-render-abort-error.js +8 -0
  125. package/dist/server/app-request-stage-dispatch.js +1 -1
  126. package/dist/server/app-request-stage-independent-entry.js +8 -11
  127. package/dist/server/app-response-header-provenance.d.ts +2 -0
  128. package/dist/server/app-response-header-provenance.js +10 -4
  129. package/dist/server/app-route-handler-cache.d.ts +2 -1
  130. package/dist/server/app-route-handler-cache.js +11 -7
  131. package/dist/server/app-route-handler-dispatch.js +11 -4
  132. package/dist/server/app-route-handler-execution.d.ts +9 -1
  133. package/dist/server/app-route-handler-execution.js +28 -13
  134. package/dist/server/app-route-handler-policy.js +2 -2
  135. package/dist/server/app-route-handler-response.d.ts +1 -1
  136. package/dist/server/app-route-handler-response.js +14 -5
  137. package/dist/server/app-router-entry.d.ts +2 -1
  138. package/dist/server/app-router-entry.js +8 -12
  139. package/dist/server/app-rsc-errors.d.ts +0 -1
  140. package/dist/server/app-rsc-errors.js +2 -6
  141. package/dist/server/app-rsc-handler.d.ts +5 -0
  142. package/dist/server/app-rsc-handler.js +229 -102
  143. package/dist/server/app-rsc-request-normalization.d.ts +4 -2
  144. package/dist/server/app-rsc-request-normalization.js +7 -4
  145. package/dist/server/app-rsc-response-finalizer.js +4 -1
  146. package/dist/server/app-rsc-response-stage.d.ts +1 -1
  147. package/dist/server/app-rsc-response-stage.js +6 -4
  148. package/dist/server/app-rsc-route-matching.d.ts +9 -1
  149. package/dist/server/app-rsc-route-matching.js +37 -20
  150. package/dist/server/app-server-action-execution.d.ts +2 -1
  151. package/dist/server/app-server-action-execution.js +1 -1
  152. package/dist/server/app-ssr-entry.d.ts +3 -2
  153. package/dist/server/app-ssr-entry.js +9 -7
  154. package/dist/server/app-ssr-search-params-gate.js +1 -1
  155. package/dist/server/app-ssr-stream.d.ts +6 -1
  156. package/dist/server/app-ssr-stream.js +35 -16
  157. package/dist/server/app-worker-stages.d.ts +2 -2
  158. package/dist/server/app-worker-stages.js +3 -3
  159. package/dist/server/cache-control.js +3 -11
  160. package/dist/server/cacheability-request.js +56 -52
  161. package/dist/server/config-headers.js +3 -1
  162. package/dist/server/dev-server.js +23 -5
  163. package/dist/server/headers.d.ts +16 -0
  164. package/dist/server/headers.js +19 -1
  165. package/dist/server/http-error-responses.js +1 -0
  166. package/dist/server/image-optimization.d.ts +16 -0
  167. package/dist/server/image-optimization.js +26 -6
  168. package/dist/server/isr-cache.d.ts +2 -0
  169. package/dist/server/isr-cache.js +8 -3
  170. package/dist/server/metadata-route-response.js +94 -37
  171. package/dist/server/middleware-matcher-pattern.js +8 -4
  172. package/dist/server/middleware-response-headers.js +2 -0
  173. package/dist/server/multi-stage.d.ts +8 -0
  174. package/dist/server/navigation-planner.js +1 -1
  175. package/dist/server/node-response-signal.d.ts +16 -0
  176. package/dist/server/node-response-signal.js +26 -0
  177. package/dist/server/open-redirect.d.ts +47 -0
  178. package/dist/server/open-redirect.js +74 -1
  179. package/dist/server/pages-api-route.js +8 -2
  180. package/dist/server/pages-asset-tags.d.ts +1 -0
  181. package/dist/server/pages-asset-tags.js +73 -22
  182. package/dist/server/pages-client-assets.d.ts +2 -0
  183. package/dist/server/pages-page-data.d.ts +6 -1
  184. package/dist/server/pages-page-data.js +71 -65
  185. package/dist/server/pages-page-handler.js +50 -15
  186. package/dist/server/pages-page-response.js +3 -1
  187. package/dist/server/pages-request-pipeline.d.ts +2 -1
  188. package/dist/server/pages-request-pipeline.js +20 -5
  189. package/dist/server/pages-request-stage-entry.js +14 -8
  190. package/dist/server/prerender-manifest.d.ts +6 -0
  191. package/dist/server/prerender-manifest.js +1 -1
  192. package/dist/server/prod-server.d.ts +8 -7
  193. package/dist/server/prod-server.js +152 -78
  194. package/dist/server/request-pipeline.d.ts +20 -8
  195. package/dist/server/request-pipeline.js +21 -7
  196. package/dist/server/response-compression.d.ts +43 -0
  197. package/dist/server/response-compression.js +111 -0
  198. package/dist/server/response-stage-cacheability.js +1 -1
  199. package/dist/server/seed-cache.js +10 -7
  200. package/dist/server/socket-error-backstop.d.ts +14 -0
  201. package/dist/server/socket-error-backstop.js +27 -4
  202. package/dist/server/worker-revalidation-context.d.ts +2 -1
  203. package/dist/server/worker-revalidation-context.js +1 -0
  204. package/dist/server/workers-tracing.d.ts +17 -0
  205. package/dist/server/workers-tracing.js +48 -8
  206. package/dist/shims/cache-adapter-instantiate.d.ts +50 -0
  207. package/dist/shims/cache-adapter-instantiate.js +118 -0
  208. package/dist/shims/cache-callable-runtime.d.ts +2 -40
  209. package/dist/shims/cache-callable-runtime.js +50 -168
  210. package/dist/shims/cache-flight-arguments.d.ts +22 -0
  211. package/dist/shims/cache-flight-arguments.js +56 -0
  212. package/dist/shims/cache-request-state.d.ts +18 -14
  213. package/dist/shims/cache-request-state.js +33 -21
  214. package/dist/shims/cache-runtime.d.ts +6 -19
  215. package/dist/shims/cache-runtime.js +168 -226
  216. package/dist/shims/cache.d.ts +3 -2
  217. package/dist/shims/cache.js +8 -5
  218. package/dist/shims/cacheability-classification.d.ts +8 -1
  219. package/dist/shims/cacheability-classification.js +2 -2
  220. package/dist/shims/cdn-cache.d.ts +10 -1
  221. package/dist/shims/cdn-cache.js +16 -9
  222. package/dist/shims/error-boundary-navigation.d.ts +7 -0
  223. package/dist/shims/error-boundary-navigation.js +19 -1
  224. package/dist/shims/error-boundary.d.ts +10 -10
  225. package/dist/shims/error-boundary.js +26 -7
  226. package/dist/shims/error.d.ts +4 -18
  227. package/dist/shims/error.js +17 -10
  228. package/dist/shims/error.react-server.d.ts +4 -6
  229. package/dist/shims/error.react-server.js +4 -1
  230. package/dist/shims/fetch-cache.d.ts +6 -0
  231. package/dist/shims/fetch-cache.js +23 -4
  232. package/dist/shims/form.js +1 -1
  233. package/dist/shims/headers.d.ts +19 -69
  234. package/dist/shims/headers.js +82 -183
  235. package/dist/shims/internal/app-page-props-cache-key.d.ts +5 -2
  236. package/dist/shims/internal/app-page-props-cache-key.js +24 -15
  237. package/dist/shims/internal/cache-context-accessor.d.ts +16 -0
  238. package/dist/shims/internal/cache-context-accessor.js +15 -0
  239. package/dist/shims/internal/headers-state.d.ts +73 -0
  240. package/dist/shims/internal/headers-state.js +157 -0
  241. package/dist/shims/metadata.js +3 -3
  242. package/dist/shims/navigation-errors.d.ts +6 -0
  243. package/dist/shims/navigation-errors.js +11 -1
  244. package/dist/shims/navigation.d.ts +7 -0
  245. package/dist/shims/navigation.js +16 -6
  246. package/dist/shims/request-context.d.ts +9 -0
  247. package/dist/shims/request-state-types.d.ts +2 -1
  248. package/dist/shims/root-params.js +1 -1
  249. package/dist/shims/server.js +52 -2
  250. package/dist/shims/thenable-params.d.ts +4 -0
  251. package/dist/shims/thenable-params.js +25 -3
  252. package/dist/shims/unified-request-context.d.ts +1 -1
  253. package/dist/shims/unified-request-context.js +1 -0
  254. package/dist/utils/encode-cache-tag.d.ts +1 -0
  255. package/dist/utils/encode-cache-tag.js +11 -1
  256. package/dist/utils/mdx-scan.d.ts +5 -0
  257. package/dist/utils/mdx-scan.js +8 -1
  258. package/dist/utils/regex-safety.d.ts +10 -0
  259. package/dist/utils/regex-safety.js +70 -18
  260. package/dist/utils/vite-cli-invocation.js +8 -1
  261. package/package.json +8 -4
  262. package/dist/server/metadata-route-cache-policy.d.ts +0 -5
  263. package/dist/server/metadata-route-cache-policy.js +0 -6
@@ -4,23 +4,28 @@ import { decodeMatchedParams } from "../routing/utils.js";
4
4
  import { getRequestExecutionContext } from "../shims/request-context.js";
5
5
  import { VINEXT_METADATA_ROUTE_CACHE_HEADER } from "./headers.js";
6
6
  import { notFoundResponse } from "./http-error-responses.js";
7
- import { beginRouteCacheability, markRouteCacheabilityExplicitResponsePolicy } from "../shims/cacheability-classification.js";
8
- import { hasCdnResponsePolicy, hasExplicitNonCacheableResponsePolicy } from "./cache-control.js";
7
+ import { beginRouteCacheability, markRouteCacheabilityExplicitResponsePolicy, markRouteCacheabilityResponseBodyComplete, recordRouteCacheability } from "../shims/cacheability-classification.js";
8
+ import { NEVER_CACHE_CONTROL, applyCdnResponseHeaders, hasCdnResponsePolicy } from "./cache-control.js";
9
+ import { buildAppRouteMissIsrCacheControl } from "./isr-decision.js";
9
10
  import { isrCacheControl } from "./isr-cache.js";
10
11
  import { resolveClientStaleTimeSeconds } from "../utils/cache-control-metadata.js";
11
- import { _consumeRequestScopedCacheLife, cacheLifeProfiles } from "../shims/cache-request-state.js";
12
+ import { isRenderDynamicLatched } from "../shims/internal/headers-state.js";
13
+ import { getActiveDraftModeState, getHeadersContext, hasDraftModeCookieHeader, replaceHeadersContext } from "../shims/headers.js";
14
+ import { _consumeRequestScopedCacheLife } from "../shims/cache-request-state.js";
12
15
  import { ensureFetchPatch, getCollectedFetchTags, setCurrentFetchSoftTags } from "../shims/fetch-cache.js";
13
16
  import { isValidMetadataImageId, manifestToJson, matchMetadataRoutePattern, robotsToText, sitemapToXml } from "./metadata-routes.js";
14
17
  import { applyPrerenderCacheLifeHeader, applyPrerenderCacheTagsHeader } from "./prerender-cache-life-header.js";
15
18
  import { canonicalizeAppPageParams } from "./app-page-segment-state.js";
16
19
  import { buildPageCacheTags } from "./implicit-tags.js";
17
20
  import { buildAppRouteCacheValue, buildRouteHandlerCachedResponse } from "./app-route-handler-response.js";
18
- import { isMetadataResponseCacheable } from "./metadata-route-cache-policy.js";
21
+ import { createStaticGenerationHeadersContext } from "./app-static-generation.js";
22
+ import { completeAppRouteHandlerResponse } from "./app-route-handler-execution.js";
19
23
  //#region src/server/metadata-route-response.ts
20
24
  function isOuterMetadataCacheEnabled() {
21
25
  return process.env.NODE_ENV !== "development";
22
26
  }
23
27
  const routeFunctionCache = /* @__PURE__ */ new WeakMap();
28
+ const userMetadataResponses = /* @__PURE__ */ new WeakSet();
24
29
  const USE_CACHE_FUNCTION_SYMBOL = Symbol.for("vinext.useCacheFunction");
25
30
  const CACHE_HEADERS = {
26
31
  noCache: "no-cache, no-store",
@@ -53,17 +58,19 @@ function metadataRouteCacheHeader(route) {
53
58
  if (route.isDynamic && isImageMetadataRoute(route) && process.env.NODE_ENV === "development") return CACHE_HEADERS.noCache;
54
59
  return CACHE_HEADERS.revalidate;
55
60
  }
56
- function withMetadataRouteCacheHeader(response, route) {
57
- if (hasCdnResponsePolicy(response.headers) && !hasExplicitNonCacheableResponsePolicy(response.headers)) markRouteCacheabilityExplicitResponsePolicy();
58
- const headers = new Headers(response.headers);
59
- if (!headers.has("Cache-Control")) headers.set("Cache-Control", metadataRouteCacheHeader(route));
60
- return new Response(response.body, {
61
- headers,
62
- status: response.status,
63
- statusText: response.statusText
64
- });
65
- }
66
61
  function beginMetadataRouteCacheability(route) {
62
+ const dynamic = route.module?.dynamic;
63
+ if (dynamic === "force-static" || dynamic === "error") {
64
+ const current = getHeadersContext();
65
+ const staticContext = createStaticGenerationHeadersContext({
66
+ dynamicConfig: dynamic,
67
+ routeKind: "route",
68
+ routePattern: route.servedUrl,
69
+ draftModeEnabled: getActiveDraftModeState() ?? void 0,
70
+ draftModeSecret: current?.draftModeSecret
71
+ });
72
+ replaceHeadersContext(staticContext);
73
+ }
67
74
  beginRouteCacheability("app-route", route.patternParts ? `/${route.patternParts.join("/")}` : route.servedUrl);
68
75
  }
69
76
  function getMetadataRouteFunctions(route) {
@@ -137,29 +144,68 @@ function markMetadataRouteCacheValue(value) {
137
144
  function buildMetadataRouteTags(route, cleanPathname, collectedTags) {
138
145
  return buildPageCacheTags(cleanPathname, collectedTags, route.routeSegments ?? [], "route");
139
146
  }
140
- function captureRenderedMetadataRoute(response) {
141
- const cacheLife = _consumeRequestScopedCacheLife();
147
+ async function captureRenderedMetadataRoute(response, route, cleanPathname) {
148
+ const isUserResponse = userMetadataResponses.has(response);
149
+ let completed = false;
150
+ if (isOuterMetadataCacheEnabled()) {
151
+ const result = await completeAppRouteHandlerResponse(response);
152
+ response = result.response;
153
+ completed = result.completed;
154
+ if (completed) markRouteCacheabilityResponseBodyComplete();
155
+ }
156
+ const browserCacheControl = response.headers.get("Cache-Control") ?? void 0;
157
+ const hasUserPolicy = isUserResponse && hasCdnResponsePolicy(response.headers);
158
+ if (hasUserPolicy) markRouteCacheabilityExplicitResponsePolicy();
159
+ const observedCacheLife = _consumeRequestScopedCacheLife();
142
160
  const collectedTags = getCollectedFetchTags();
161
+ const revalidate = typeof route.module?.revalidate === "number" ? route.module.revalidate : Infinity;
162
+ const frameworkRevalidate = Math.min(observedCacheLife?.revalidate ?? Infinity, revalidate);
163
+ const cacheLife = {
164
+ ...observedCacheLife,
165
+ revalidate: frameworkRevalidate
166
+ };
167
+ const cacheable = (response.status < 400 || response.status === 404) && completed && !isRenderDynamicLatched() && !getActiveDraftModeState() && !hasDraftModeCookieHeader() && route.module?.dynamic !== "force-dynamic" && frameworkRevalidate > 0 && !response.headers.has("set-cookie");
168
+ const cacheControl = cacheable ? buildAppRouteMissIsrCacheControl(frameworkRevalidate, cacheLife?.expire) : NEVER_CACHE_CONTROL;
169
+ if (isUserResponse && !hasUserPolicy) {
170
+ response = new Response(response.body, {
171
+ headers: new Headers(response.headers),
172
+ status: response.status,
173
+ statusText: response.statusText
174
+ });
175
+ applyCdnResponseHeaders(response.headers, { cacheControl: isOuterMetadataCacheEnabled() ? cacheControl : metadataRouteCacheHeader(route) });
176
+ }
177
+ recordRouteCacheability(cacheable ? {
178
+ cacheable: true,
179
+ cacheControl,
180
+ tags: buildMetadataRouteTags(route, cleanPathname, collectedTags)
181
+ } : {
182
+ cacheable: false,
183
+ dynamicUsage: true
184
+ });
143
185
  if (process.env.VINEXT_PRERENDER === "1") {
144
- applyPrerenderCacheLifeHeader(response.headers, cacheLife);
186
+ applyPrerenderCacheLifeHeader(response.headers, {
187
+ ...cacheLife,
188
+ revalidate: cacheable ? frameworkRevalidate : 0
189
+ });
145
190
  applyPrerenderCacheTagsHeader(response.headers, collectedTags);
146
191
  }
147
192
  return {
193
+ browserCacheControl,
148
194
  cacheLife,
195
+ cacheable,
149
196
  collectedTags,
150
197
  response
151
198
  };
152
199
  }
153
200
  async function writeRenderedMetadataRoute(options, route, routeKey, rendered, previousEntry) {
154
- if (!options.isrSet || !rendered.response.ok || !isMetadataResponseCacheable(rendered.response)) return;
201
+ if (!options.isrSet || !rendered.cacheable) return;
155
202
  const previousCacheControl = previousEntry?.value.cacheControl;
156
- const defaultCacheLife = cacheLifeProfiles.default;
157
- const revalidate = rendered.cacheLife?.revalidate ?? previousCacheControl?.revalidate ?? defaultCacheLife.revalidate ?? 900;
158
- const expire = rendered.cacheLife?.expire ?? previousCacheControl?.expire ?? defaultCacheLife.expire;
203
+ const revalidate = rendered.cacheLife?.revalidate ?? previousCacheControl?.revalidate ?? (typeof route.module?.revalidate === "number" ? route.module.revalidate : false);
204
+ const expire = rendered.cacheLife?.expire ?? previousCacheControl?.expire;
159
205
  const stale = resolveClientStaleTimeSeconds(rendered.cacheLife) ?? previousCacheControl?.stale;
160
- const value = markMetadataRouteCacheValue(await buildAppRouteCacheValue(rendered.response));
206
+ const value = markMetadataRouteCacheValue(await buildAppRouteCacheValue(rendered.response, rendered.browserCacheControl));
161
207
  await options.isrSet(routeKey, value, {
162
- cacheControl: isrCacheControl(revalidate, {
208
+ cacheControl: isrCacheControl(revalidate === Infinity ? false : revalidate, {
163
209
  ...typeof expire === "number" ? { expireSeconds: expire } : {},
164
210
  ...typeof stale === "number" ? { staleSeconds: stale } : {}
165
211
  }),
@@ -171,16 +217,21 @@ async function runMetadataRouteRegeneration(options, route, render, routeKey, pr
171
217
  try {
172
218
  await runWithRequestContext(requestContext, async () => {
173
219
  ensureFetchPatch();
220
+ beginMetadataRouteCacheability(route);
174
221
  setCurrentFetchSoftTags(buildMetadataRouteTags(route, options.cleanPathname, []));
175
222
  const rendered = await render();
176
- if (rendered) await writeRenderedMetadataRoute(options, route, routeKey, rendered, previousEntry);
223
+ if (rendered) try {
224
+ await writeRenderedMetadataRoute(options, route, routeKey, rendered, previousEntry);
225
+ } finally {
226
+ rendered.response.body?.cancel().catch(() => {});
227
+ }
177
228
  });
178
229
  } finally {
179
230
  await closeAfterResponse(requestContext);
180
231
  }
181
232
  }
182
- async function readMatchedPrerenderedMetadataRouteResponse(options, route, functions, render) {
183
- if (!isOuterMetadataCacheEnabled() || !options.isrGet || !options.isrRouteKey) return {
233
+ async function readMatchedPrerenderedMetadataRouteResponse(options, route, render) {
234
+ if (!isOuterMetadataCacheEnabled() || !options.isrGet || !options.isrRouteKey || getActiveDraftModeState() || hasDraftModeCookieHeader() || route.module?.dynamic === "force-dynamic" || route.module?.revalidate === 0) return {
184
235
  cachedEntry: null,
185
236
  response: null
186
237
  };
@@ -214,7 +265,7 @@ async function readMatchedPrerenderedMetadataRouteResponse(options, route, funct
214
265
  revalidateSeconds
215
266
  })
216
267
  };
217
- if (isUseCacheFunction(functions.defaultExport) && options.isrSet && options.scheduleBackgroundRegeneration) {
268
+ if (options.isrSet && options.scheduleBackgroundRegeneration) {
218
269
  const executionContext = getRequestExecutionContext();
219
270
  options.scheduleBackgroundRegeneration(routeKey, () => runMetadataRouteRegeneration(options, route, render, routeKey, cached, executionContext), {
220
271
  routerKind: "App Router",
@@ -330,7 +381,10 @@ async function handleGeneratedSitemap(route, cleanPathname, functions) {
330
381
  const matchedId = findGeneratedSitemapId(await functions.generateSitemaps({}), rawId);
331
382
  if (!matchedId) return notFoundResponse();
332
383
  const result = await functions.defaultExport({ id: makeThenableMetadataRouteId(matchedId) });
333
- if (result instanceof Response) return withMetadataRouteCacheHeader(result, route);
384
+ if (result instanceof Response) {
385
+ userMetadataResponses.add(result);
386
+ return result;
387
+ }
334
388
  if (!isSitemapEntries(result)) throw new TypeError("Metadata sitemap routes must return an array.");
335
389
  return markFullyBufferedBody(new Response(sitemapToXml(result), { headers: {
336
390
  "Content-Type": route.contentType,
@@ -367,7 +421,10 @@ async function callDynamicMetadataRoute(route, match, makeThenableParams, functi
367
421
  id: makeThenableMetadataRouteId(matchedImageId)
368
422
  });
369
423
  } else result = await functions.defaultExport({ params: paramsThenable });
370
- if (result instanceof Response) return withMetadataRouteCacheHeader(result, route);
424
+ if (result instanceof Response) {
425
+ userMetadataResponses.add(result);
426
+ return result;
427
+ }
371
428
  let body;
372
429
  if (route.type === "sitemap") {
373
430
  if (!isSitemapEntries(result)) throw new TypeError("Metadata sitemap routes must return an array.");
@@ -410,8 +467,8 @@ function isMetadataRouteRequestPath(metadataRoutes, cleanPathname) {
410
467
  }
411
468
  return false;
412
469
  }
413
- async function writeMetadataRouteMiss(options, route, functions, rendered, cachedEntry) {
414
- if (process.env.VINEXT_PRERENDER === "1" || !isOuterMetadataCacheEnabled() || !rendered.response.ok || !isMetadataResponseCacheable(rendered.response) || !isUseCacheFunction(functions.defaultExport) || !options.isrRouteKey || !options.isrSet) return;
470
+ async function writeMetadataRouteMiss(options, route, rendered, cachedEntry) {
471
+ if (process.env.VINEXT_PRERENDER === "1" || !isOuterMetadataCacheEnabled() || !rendered.cacheable || !options.isrRouteKey || !options.isrSet) return;
415
472
  try {
416
473
  await writeRenderedMetadataRoute(options, route, options.isrRouteKey(options.cleanPathname), {
417
474
  ...rendered,
@@ -433,13 +490,13 @@ async function handleMetadataRouteRequest(options) {
433
490
  const render = async () => {
434
491
  setCurrentFetchSoftTags(buildMetadataRouteTags(route, options.cleanPathname, []));
435
492
  const response = await handleGeneratedSitemap(route, options.cleanPathname, functions);
436
- return response ? captureRenderedMetadataRoute(response) : null;
493
+ return response ? captureRenderedMetadataRoute(response, route, options.cleanPathname) : null;
437
494
  };
438
- const cached = await readMatchedPrerenderedMetadataRouteResponse(options, route, functions, render);
495
+ const cached = await readMatchedPrerenderedMetadataRouteResponse(options, route, render);
439
496
  if (cached.response) return cached.response;
440
497
  const rendered = await render();
441
498
  if (rendered) {
442
- await writeMetadataRouteMiss(options, route, functions, rendered, cached.cachedEntry);
499
+ await writeMetadataRouteMiss(options, route, rendered, cached.cachedEntry);
443
500
  return rendered.response;
444
501
  }
445
502
  }
@@ -471,12 +528,12 @@ async function handleMetadataRouteRequest(options) {
471
528
  }
472
529
  const render = async () => {
473
530
  setCurrentFetchSoftTags(buildMetadataRouteTags(route, options.cleanPathname, []));
474
- return captureRenderedMetadataRoute(route.isDynamic ? await callDynamicMetadataRoute(route, match, options.makeThenableParams, functions) : serveStaticMetadataRoute(route));
531
+ return captureRenderedMetadataRoute(route.isDynamic ? await callDynamicMetadataRoute(route, match, options.makeThenableParams, functions) : serveStaticMetadataRoute(route), route, options.cleanPathname);
475
532
  };
476
- const cached = await readMatchedPrerenderedMetadataRouteResponse(options, route, functions, render);
533
+ const cached = await readMatchedPrerenderedMetadataRouteResponse(options, route, render);
477
534
  if (cached.response) return cached.response;
478
535
  const rendered = await render();
479
- await writeMetadataRouteMiss(options, route, functions, rendered, cached.cachedEntry);
536
+ await writeMetadataRouteMiss(options, route, rendered, cached.cachedEntry);
480
537
  return rendered.response;
481
538
  }
482
539
  return null;
@@ -1,4 +1,4 @@
1
- import { analyzeRegexSafety, regexAtomsMayOverlap } from "../utils/regex-safety.js";
1
+ import { analyzeRegexSafety, mayHaveNamedGroups, regexAtomsMayOverlap } from "../utils/regex-safety.js";
2
2
  import { middlewarePathTokensToRegExp, normalizeMiddlewarePathTokens, parseMiddlewarePath } from "./middleware-path-to-regexp.js";
3
3
  //#region src/server/middleware-matcher-pattern.ts
4
4
  const MATCHER_OBJECT_KEYS = /* @__PURE__ */ new Set([
@@ -138,8 +138,11 @@ function hasOverlappingSequentialRepetition(pattern) {
138
138
  }
139
139
  return false;
140
140
  }
141
- function unsafeTokenReason(token) {
142
- const regexSafetyIssue = analyzeRegexSafety(token.pattern, { ignoreCase: true });
141
+ function unsafeTokenReason(token, namedGroups) {
142
+ const regexSafetyIssue = analyzeRegexSafety(token.pattern, {
143
+ ignoreCase: true,
144
+ namedGroups
145
+ });
143
146
  if (regexSafetyIssue) {
144
147
  if (regexSafetyIssue === "analysis budget exceeded") return `parameter "${token.name}" exceeds the regex analysis budget`;
145
148
  return `parameter "${token.name}" contains ${regexSafetyIssue}`;
@@ -150,9 +153,10 @@ function unsafeTokenReason(token) {
150
153
  return null;
151
154
  }
152
155
  function validateTokens(tokens) {
156
+ const namedGroups = tokens.some((token) => typeof token !== "string" && mayHaveNamedGroups(token.pattern));
153
157
  for (const token of tokens) {
154
158
  if (typeof token === "string") continue;
155
- const reason = unsafeTokenReason(token);
159
+ const reason = unsafeTokenReason(token, namedGroups);
156
160
  if (reason) return reason;
157
161
  }
158
162
  return null;
@@ -1,3 +1,4 @@
1
+ import "./headers.js";
1
2
  //#region src/server/middleware-response-headers.ts
2
3
  const ADDITIVE_RESPONSE_HEADER_NAMES = /* @__PURE__ */ new Set(["set-cookie", "vary"]);
3
4
  function mergeVaryHeader(target, value) {
@@ -28,6 +29,7 @@ function mergeMiddlewareResponseHeaders(target, middlewareHeaders) {
28
29
  if (!middlewareHeaders) return;
29
30
  for (const [key, value] of middlewareHeaders) {
30
31
  const lowerName = key.toLowerCase();
32
+ if (lowerName === "x-vinext-special-error-status") continue;
31
33
  if (lowerName === "link" && !value) continue;
32
34
  if (lowerName === "vary") {
33
35
  mergeVaryHeader(target, value);
@@ -21,6 +21,14 @@ export type VinextResponseStageDispatchOptions = {
21
21
  * opt into completed-response admission and honor core's `no-store` policy.
22
22
  */
23
23
  cache: "shared" | "bypass";
24
+ /**
25
+ * Render an App page document whole before its first byte, as Next.js
26
+ * renders a static page, so its streamed metadata is in `<head>`. An adapter
27
+ * sets this for a render no client is waiting on, such as a regeneration or
28
+ * a deployment warm-up. It is not a cache selector: a transport must not
29
+ * partition its lookup or stored entry by it.
30
+ */
31
+ renderWholeDocument?: boolean;
24
32
  /**
25
33
  * Query-free identity of a shared App page GET/HEAD dispatch, supplied only
26
34
  * to adapters that declare `responseStageCacheIdentity: "query-free"` and
@@ -1,8 +1,8 @@
1
1
  import { splitPathnameForRouteMatch } from "../routing/utils.js";
2
2
  import { stripBasePath } from "../utils/base-path.js";
3
+ import { matchRoutePattern, matchRoutePatternPrefix, matchRoutePatternWithOptionalDynamicSegments } from "../routing/route-pattern.js";
3
4
  import { AppElementsWire, compareAppElementsSlotIds } from "./app-elements-wire.js";
4
5
  import "./app-elements.js";
5
- import { matchRoutePattern, matchRoutePatternPrefix, matchRoutePatternWithOptionalDynamicSegments } from "../routing/route-pattern.js";
6
6
  import { resolveHardNavigationTargetFromRscResponse, resolveRscCompatibilityNavigationDecision } from "./app-rsc-cache-busting.js";
7
7
  import { resolveRscRedirectLifecycleHop, resolveStreamedRscRedirectLifecycleHop } from "./app-browser-rsc-redirect.js";
8
8
  import { NavigationTraceReasonCodes, createNavigationLifecycleTraceFields, createNavigationTrace } from "./navigation-trace.js";
@@ -0,0 +1,16 @@
1
+ import { Writable } from "node:stream";
2
+ //#region src/server/node-response-signal.d.ts
3
+ export declare class ResponseAborted extends Error {
4
+ readonly name = "ResponseAborted";
5
+ }
6
+ /**
7
+ * Creates an AbortSignal tied to the closing of a ServerResponse.
8
+ *
9
+ * The incoming request cannot be used: once its body has been fully read the
10
+ * readable side is done, so a later client disconnect fires nothing on it
11
+ * (and its `close` event also fires on normal requests). If `finish` fires
12
+ * first, `res.end()` completed and the following `close` is our own teardown;
13
+ * if `close` fires first, the client disconnected before we finished.
14
+ */
15
+ export declare function signalFromNodeResponse(response: Writable): AbortSignal;
16
+ //#endregion
@@ -0,0 +1,26 @@
1
+ //#region src/server/node-response-signal.ts
2
+ const ResponseAbortedName = "ResponseAborted";
3
+ var ResponseAborted = class extends Error {
4
+ name = ResponseAbortedName;
5
+ };
6
+ /**
7
+ * Creates an AbortSignal tied to the closing of a ServerResponse.
8
+ *
9
+ * The incoming request cannot be used: once its body has been fully read the
10
+ * readable side is done, so a later client disconnect fires nothing on it
11
+ * (and its `close` event also fires on normal requests). If `finish` fires
12
+ * first, `res.end()` completed and the following `close` is our own teardown;
13
+ * if `close` fires first, the client disconnected before we finished.
14
+ */
15
+ function signalFromNodeResponse(response) {
16
+ const { errored, destroyed } = response;
17
+ if (errored || destroyed) return AbortSignal.abort(errored ?? new ResponseAborted());
18
+ const controller = new AbortController();
19
+ response.once("close", () => {
20
+ if (response.writableFinished) return;
21
+ controller.abort(new ResponseAborted());
22
+ });
23
+ return controller.signal;
24
+ }
25
+ //#endregion
26
+ export { ResponseAborted, signalFromNodeResponse };
@@ -8,4 +8,51 @@
8
8
  * to resolve a `Location` containing the pathname as protocol-relative.
9
9
  */
10
10
  export declare function isOpenRedirectShaped(rawPathname: string): boolean;
11
+ /**
12
+ * What a request entry answers for a raw path containing a backslash or a
13
+ * repeated slash: Next.js's 308, or a 404 when the collapsed path would still
14
+ * be open-redirect shaped.
15
+ */
16
+ export type RepeatedSlashRedirect = {
17
+ status: 308;
18
+ location: string;
19
+ } | {
20
+ status: 404;
21
+ };
22
+ /**
23
+ * Returns Next.js's redirect for a raw request path containing a backslash or
24
+ * a repeated slash, or `null` when the path needs no change.
25
+ *
26
+ * Ported from Next.js's request entry (`base-server.ts` / `resolve-routes.ts`,
27
+ * via `normalizeRepeatedSlashes` in `shared/lib/utils.ts`): backslashes in the
28
+ * path become `/`, runs of slashes collapse to one and the query is kept.
29
+ * `next start` then builds the Location with `url.format(parseUrl(...))` in
30
+ * `router-server.ts`, so the result is serialized through a WHATWG URL here
31
+ * too (`/a/%2e%2e//b` → `/b`, `?q="x"` → `?q=%22x%22`, as observed). It runs
32
+ * before basePath handling, so `//` → `/`, `/docs//` → `/docs/` and
33
+ * `//evil.com` → `/evil.com`. Percent-encoded `%2F`/`%5C` are not touched and
34
+ * keep falling through to `isOpenRedirectShaped` (404), as in Next.js.
35
+ *
36
+ * The Location is always same-origin. Absolute-form request targets
37
+ * (`GET http://host//x`) are left alone. A collapsed path that still starts
38
+ * with an encoded slash or backslash (`//%2Fevil.com`, `/.//%2Fevil.com`) is
39
+ * never echoed into a Location or routed: it gets a 404 instead.
40
+ *
41
+ * @param rawUrl - The raw request target: pathname plus optional `?query`
42
+ */
43
+ export declare function getRepeatedSlashRedirect(rawUrl: string): RepeatedSlashRedirect | null;
44
+ /**
45
+ * Build the Response for a raw request path containing a backslash or a
46
+ * repeated slash. See `getRepeatedSlashRedirect`.
47
+ */
48
+ export declare function repeatedSlashRedirectResponse(rawUrl: string): Response | null;
49
+ /**
50
+ * Node `ServerResponse` counterpart of `repeatedSlashRedirectResponse`, for
51
+ * entries that see the raw request target (dev and `vinext start`). Returns
52
+ * true when a response was sent.
53
+ */
54
+ export declare function sendRepeatedSlashRedirect(rawUrl: string, res: {
55
+ writeHead(status: number, headers?: Record<string, string>): unknown;
56
+ end(body: string): unknown;
57
+ }): boolean;
11
58
  //#endregion
@@ -1,3 +1,4 @@
1
+ import { notFoundResponse } from "./http-error-responses.js";
1
2
  //#region src/server/open-redirect.ts
2
3
  /**
3
4
  * Returns true if a request pathname looks like a protocol-relative open
@@ -17,5 +18,77 @@ function isOpenRedirectShaped(rawPathname) {
17
18
  }
18
19
  return false;
19
20
  }
21
+ const REPEATED_SLASH_OR_BACKSLASH_RE = /\\|\/\//;
22
+ const REDIRECT_LOCATION_BASE = "http://vinext.invalid";
23
+ /**
24
+ * Returns Next.js's redirect for a raw request path containing a backslash or
25
+ * a repeated slash, or `null` when the path needs no change.
26
+ *
27
+ * Ported from Next.js's request entry (`base-server.ts` / `resolve-routes.ts`,
28
+ * via `normalizeRepeatedSlashes` in `shared/lib/utils.ts`): backslashes in the
29
+ * path become `/`, runs of slashes collapse to one and the query is kept.
30
+ * `next start` then builds the Location with `url.format(parseUrl(...))` in
31
+ * `router-server.ts`, so the result is serialized through a WHATWG URL here
32
+ * too (`/a/%2e%2e//b` → `/b`, `?q="x"` → `?q=%22x%22`, as observed). It runs
33
+ * before basePath handling, so `//` → `/`, `/docs//` → `/docs/` and
34
+ * `//evil.com` → `/evil.com`. Percent-encoded `%2F`/`%5C` are not touched and
35
+ * keep falling through to `isOpenRedirectShaped` (404), as in Next.js.
36
+ *
37
+ * The Location is always same-origin. Absolute-form request targets
38
+ * (`GET http://host//x`) are left alone. A collapsed path that still starts
39
+ * with an encoded slash or backslash (`//%2Fevil.com`, `/.//%2Fevil.com`) is
40
+ * never echoed into a Location or routed: it gets a 404 instead.
41
+ *
42
+ * @param rawUrl - The raw request target: pathname plus optional `?query`
43
+ */
44
+ function getRepeatedSlashRedirect(rawUrl) {
45
+ if (!rawUrl.startsWith("/")) return null;
46
+ const urlParts = rawUrl.split("?");
47
+ const pathname = urlParts[0];
48
+ if (!REPEATED_SLASH_OR_BACKSLASH_RE.test(pathname)) return null;
49
+ const cleanUrl = pathname.replaceAll("\\", "/").replace(/\/\/+/g, "/") + (urlParts[1] ? `?${urlParts.slice(1).join("?")}` : "");
50
+ const parsed = new URL(REDIRECT_LOCATION_BASE + cleanUrl);
51
+ const location = parsed.pathname + parsed.search + parsed.hash;
52
+ return isOpenRedirectShaped(location) ? { status: 404 } : {
53
+ status: 308,
54
+ location
55
+ };
56
+ }
57
+ function repeatedSlashRedirectHeaders(location) {
58
+ return {
59
+ Location: location,
60
+ Refresh: `0;url=${location}`
61
+ };
62
+ }
63
+ /**
64
+ * Build the Response for a raw request path containing a backslash or a
65
+ * repeated slash. See `getRepeatedSlashRedirect`.
66
+ */
67
+ function repeatedSlashRedirectResponse(rawUrl) {
68
+ const redirect = getRepeatedSlashRedirect(rawUrl);
69
+ if (!redirect) return null;
70
+ if (redirect.status === 404) return notFoundResponse();
71
+ return new Response(redirect.location, {
72
+ status: 308,
73
+ headers: repeatedSlashRedirectHeaders(redirect.location)
74
+ });
75
+ }
76
+ /**
77
+ * Node `ServerResponse` counterpart of `repeatedSlashRedirectResponse`, for
78
+ * entries that see the raw request target (dev and `vinext start`). Returns
79
+ * true when a response was sent.
80
+ */
81
+ function sendRepeatedSlashRedirect(rawUrl, res) {
82
+ const redirect = getRepeatedSlashRedirect(rawUrl);
83
+ if (!redirect) return false;
84
+ if (redirect.status === 404) {
85
+ res.writeHead(404);
86
+ res.end("This page could not be found");
87
+ return true;
88
+ }
89
+ res.writeHead(308, repeatedSlashRedirectHeaders(redirect.location));
90
+ res.end(redirect.location);
91
+ return true;
92
+ }
20
93
  //#endregion
21
- export { isOpenRedirectShaped };
94
+ export { getRepeatedSlashRedirect, isOpenRedirectShaped, repeatedSlashRedirectResponse, sendRepeatedSlashRedirect };
@@ -3,6 +3,8 @@ import "./server-globals.js";
3
3
  import { NextRequest } from "../shims/server.js";
4
4
  import { internalServerErrorResponse } from "./http-error-responses.js";
5
5
  import { cloneRequestWithUrl } from "./request-pipeline.js";
6
+ import { markRouteCacheabilityExplicitResponsePolicy } from "../shims/cacheability-classification.js";
7
+ import { hasCdnResponsePolicy } from "./cache-control.js";
6
8
  import { mergeRouteParamsIntoQuery, parseQueryString, urlQueryToSearchParams } from "../utils/query.js";
7
9
  import { tracePagesApiHandler } from "./pages-execution-tracing.js";
8
10
  import { resolveBodyParserConfig } from "./pages-body-parser-config.js";
@@ -29,8 +31,12 @@ function isNodeApiRouteModule(module) {
29
31
  return typeof module.default === "function" && !isEdgeApiRuntime(resolveModuleRuntime(module));
30
32
  }
31
33
  async function handlePagesApiRoute(options) {
32
- if (options.ctx) return runWithExecutionContext(options.ctx, () => _handlePagesApiRoute(options));
33
- return _handlePagesApiRoute(options);
34
+ const run = async () => {
35
+ const response = await _handlePagesApiRoute(options);
36
+ if (hasCdnResponsePolicy(response.headers)) markRouteCacheabilityExplicitResponsePolicy();
37
+ return response;
38
+ };
39
+ return options.ctx ? runWithExecutionContext(options.ctx, run) : run();
34
40
  }
35
41
  async function _handlePagesApiRoute(options) {
36
42
  if (!options.match) return new Response("404 - API route not found", { status: 404 });
@@ -9,6 +9,7 @@
9
9
  * unit-testable and lives in a normal typed module rather than a codegen
10
10
  * template string.
11
11
  */
12
+ export declare function getSharedChunkFiles(manifest: Record<string, string[]>): string[];
12
13
  /**
13
14
  * Resolve the effective SSR manifest: prefer the caller-supplied object and
14
15
  * fall back to the registered client build metadata.