vinext 1.0.0-beta.5 → 1.0.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/README.md +20 -3
  2. package/dist/build/action-owner-manifest.d.ts +42 -0
  3. package/dist/build/action-owner-manifest.js +110 -0
  4. package/dist/build/clean-output.js +1 -4
  5. package/dist/build/client-build-config.js +10 -11
  6. package/dist/build/prerender-paths.js +3 -4
  7. package/dist/build/prerender-phase.d.ts +15 -0
  8. package/dist/build/prerender-phase.js +37 -0
  9. package/dist/build/prerender-server-entry.js +3 -3
  10. package/dist/build/prerender-server-pool.js +2 -0
  11. package/dist/build/prerender.d.ts +7 -3
  12. package/dist/build/prerender.js +103 -21
  13. package/dist/build/report.d.ts +1 -1
  14. package/dist/build/route-classification-manifest.d.ts +1 -1
  15. package/dist/build/run-prerender.js +9 -6
  16. package/dist/build/static-export.d.ts +1 -1
  17. package/dist/client/app-nav-failure-handler.js +1 -0
  18. package/dist/client/navigation-runtime.d.ts +5 -3
  19. package/dist/client/navigation-runtime.js +4 -8
  20. package/dist/client/vinext-next-data.d.ts +2 -0
  21. package/dist/config/next-config.js +24 -7
  22. package/dist/deps/.pnpm/image-size@2.0.2/deps/image-size/dist/index.js +808 -0
  23. package/dist/entries/app-browser-entry.d.ts +1 -1
  24. package/dist/entries/app-browser-entry.js +2 -1
  25. package/dist/entries/app-rsc-entry.d.ts +2 -1
  26. package/dist/entries/app-rsc-entry.js +136 -8
  27. package/dist/entries/pages-client-entry.d.ts +1 -1
  28. package/dist/entries/pages-client-entry.js +8 -3
  29. package/dist/entries/pages-server-entry.d.ts +1 -1
  30. package/dist/index.js +190 -181
  31. package/dist/plugins/action-owner-manifest.d.ts +14 -0
  32. package/dist/plugins/action-owner-manifest.js +84 -0
  33. package/dist/plugins/ast-utils.d.ts +30 -1
  34. package/dist/plugins/ast-utils.js +76 -1
  35. package/dist/plugins/css-module-imports.d.ts +2 -2
  36. package/dist/plugins/css-module-imports.js +5 -13
  37. package/dist/plugins/dynamic-preload-metadata.js +13 -34
  38. package/dist/plugins/environment.d.ts +11 -0
  39. package/dist/plugins/environment.js +7 -0
  40. package/dist/plugins/extensionless-dynamic-import.js +11 -35
  41. package/dist/plugins/fonts.js +8 -12
  42. package/dist/plugins/ignore-dynamic-requests.d.ts +2 -4
  43. package/dist/plugins/ignore-dynamic-requests.js +29 -59
  44. package/dist/plugins/import-meta-url.d.ts +15 -9
  45. package/dist/plugins/import-meta-url.js +309 -85
  46. package/dist/plugins/instrumentation-client.js +4 -5
  47. package/dist/plugins/middleware-export-validation.js +5 -17
  48. package/dist/plugins/middleware-server-only.js +2 -2
  49. package/dist/plugins/og-asset-ownership.js +18 -26
  50. package/dist/plugins/og-assets.js +2 -4
  51. package/dist/plugins/optimize-imports.js +2 -4
  52. package/dist/plugins/pages-node-externals.d.ts +23 -0
  53. package/dist/plugins/pages-node-externals.js +179 -0
  54. package/dist/plugins/remove-console.js +2 -4
  55. package/dist/plugins/require-condition-resolution.d.ts +16 -0
  56. package/dist/plugins/require-condition-resolution.js +167 -0
  57. package/dist/plugins/require-context.js +11 -46
  58. package/dist/plugins/sass.js +1 -1
  59. package/dist/plugins/server-externals-manifest.js +1 -23
  60. package/dist/plugins/strip-server-exports.js +13 -19
  61. package/dist/plugins/styled-jsx.js +17 -19
  62. package/dist/plugins/transform-result.d.ts +10 -0
  63. package/dist/plugins/transform-result.js +10 -0
  64. package/dist/plugins/transitive-externals.d.ts +24 -0
  65. package/dist/plugins/transitive-externals.js +107 -0
  66. package/dist/plugins/typeof-window.d.ts +10 -5
  67. package/dist/plugins/typeof-window.js +142 -24
  68. package/dist/plugins/use-cache-callable.d.ts +12 -0
  69. package/dist/plugins/use-cache-callable.js +199 -0
  70. package/dist/plugins/wasm-module-import.js +1 -1
  71. package/dist/plugins/worker-image-imports.d.ts +12 -0
  72. package/dist/plugins/worker-image-imports.js +136 -0
  73. package/dist/routing/app-route-graph.d.ts +3 -2
  74. package/dist/routing/app-route-graph.js +1 -1
  75. package/dist/routing/route-pattern.d.ts +8 -1
  76. package/dist/routing/route-pattern.js +28 -1
  77. package/dist/routing/route-validation.js +7 -1
  78. package/dist/server/accept-encoding.d.ts +1 -2
  79. package/dist/server/accept-encoding.js +1 -4
  80. package/dist/server/api-handler.js +1 -1
  81. package/dist/server/app-action-forwarding.d.ts +18 -0
  82. package/dist/server/app-action-forwarding.js +140 -0
  83. package/dist/server/app-browser-entry.js +236 -51
  84. package/dist/server/app-browser-error.d.ts +1 -2
  85. package/dist/server/app-browser-error.js +1 -5
  86. package/dist/server/app-browser-history-controller.d.ts +16 -1
  87. package/dist/server/app-browser-history-controller.js +135 -18
  88. package/dist/server/app-browser-navigation-abort.d.ts +16 -0
  89. package/dist/server/app-browser-navigation-abort.js +29 -0
  90. package/dist/server/app-browser-navigation-controller.d.ts +4 -0
  91. package/dist/server/app-browser-navigation-controller.js +22 -6
  92. package/dist/server/app-browser-navigation-response.d.ts +11 -0
  93. package/dist/server/app-browser-navigation-response.js +8 -0
  94. package/dist/server/app-browser-popstate.d.ts +6 -1
  95. package/dist/server/app-browser-popstate.js +6 -2
  96. package/dist/server/app-browser-server-action-client.js +1 -1
  97. package/dist/server/app-browser-state.d.ts +2 -2
  98. package/dist/server/app-browser-state.js +3 -3
  99. package/dist/server/app-browser-stream.d.ts +0 -2
  100. package/dist/server/app-browser-supplemental-refresh.d.ts +49 -0
  101. package/dist/server/app-browser-supplemental-refresh.js +190 -0
  102. package/dist/server/app-browser-visible-commit.js +23 -12
  103. package/dist/server/app-elements-wire.d.ts +2 -0
  104. package/dist/server/app-elements-wire.js +7 -0
  105. package/dist/server/app-history-state.d.ts +22 -1
  106. package/dist/server/app-history-state.js +93 -9
  107. package/dist/server/app-optimistic-routing.d.ts +22 -1
  108. package/dist/server/app-optimistic-routing.js +137 -12
  109. package/dist/server/app-page-boundary-render.js +4 -7
  110. package/dist/server/app-page-cache-finalizer.d.ts +4 -1
  111. package/dist/server/app-page-cache-finalizer.js +11 -15
  112. package/dist/server/app-page-cache-render.d.ts +1 -0
  113. package/dist/server/app-page-cache-render.js +3 -1
  114. package/dist/server/app-page-cache.d.ts +4 -2
  115. package/dist/server/app-page-cache.js +13 -6
  116. package/dist/server/app-page-dispatch.d.ts +4 -1
  117. package/dist/server/app-page-dispatch.js +13 -3
  118. package/dist/server/app-page-element-builder.d.ts +1 -0
  119. package/dist/server/app-page-element-builder.js +1 -0
  120. package/dist/server/app-page-execution.js +19 -6
  121. package/dist/server/app-page-ppr-runtime.js +3 -2
  122. package/dist/server/app-page-render.d.ts +2 -1
  123. package/dist/server/app-page-render.js +10 -4
  124. package/dist/server/app-page-request.d.ts +2 -33
  125. package/dist/server/app-page-request.js +1 -27
  126. package/dist/server/app-page-response.js +7 -14
  127. package/dist/server/app-page-route-wiring.d.ts +1 -0
  128. package/dist/server/app-page-route-wiring.js +4 -0
  129. package/dist/server/app-page-segment-state.d.ts +2 -2
  130. package/dist/server/app-page-segment-state.js +14 -8
  131. package/dist/server/app-page-stream.d.ts +12 -0
  132. package/dist/server/app-page-stream.js +10 -3
  133. package/dist/server/app-ppr-fallback-shell.d.ts +2 -2
  134. package/dist/server/app-ppr-fallback-shell.js +3 -6
  135. package/dist/server/app-prerender-endpoints.d.ts +1 -0
  136. package/dist/server/app-prerender-endpoints.js +12 -0
  137. package/dist/server/app-response-header-provenance.d.ts +13 -0
  138. package/dist/server/app-response-header-provenance.js +26 -0
  139. package/dist/server/app-route-handler-cache.d.ts +1 -0
  140. package/dist/server/app-route-handler-cache.js +2 -2
  141. package/dist/server/app-route-handler-dispatch.js +8 -3
  142. package/dist/server/app-route-handler-execution.js +11 -7
  143. package/dist/server/app-route-handler-policy.d.ts +1 -0
  144. package/dist/server/app-route-handler-response.d.ts +3 -1
  145. package/dist/server/app-route-handler-response.js +15 -5
  146. package/dist/server/app-route-handler-runtime.d.ts +1 -1
  147. package/dist/server/app-route-handler-runtime.js +218 -84
  148. package/dist/server/app-route-request-built-ins.d.ts +16 -0
  149. package/dist/server/app-route-request-built-ins.js +41 -0
  150. package/dist/server/app-router-entry.js +2 -2
  151. package/dist/server/app-rsc-cache-busting.d.ts +4 -1
  152. package/dist/server/app-rsc-cache-busting.js +51 -17
  153. package/dist/server/app-rsc-handler.d.ts +6 -2
  154. package/dist/server/app-rsc-handler.js +68 -18
  155. package/dist/server/app-rsc-request-normalization.d.ts +2 -0
  156. package/dist/server/app-rsc-request-normalization.js +5 -1
  157. package/dist/server/app-rsc-response-finalizer.d.ts +2 -0
  158. package/dist/server/app-rsc-response-finalizer.js +4 -1
  159. package/dist/server/app-rsc-route-matching.d.ts +4 -3
  160. package/dist/server/app-rsc-route-matching.js +14 -42
  161. package/dist/server/app-rsc-state-fingerprint.d.ts +14 -0
  162. package/dist/server/app-rsc-state-fingerprint.js +13 -0
  163. package/dist/server/app-server-action-execution.d.ts +3 -0
  164. package/dist/server/app-server-action-execution.js +79 -4
  165. package/dist/server/app-ssr-entry.d.ts +6 -0
  166. package/dist/server/app-ssr-entry.js +21 -9
  167. package/dist/server/app-ssr-stream.d.ts +7 -2
  168. package/dist/server/app-ssr-stream.js +30 -6
  169. package/dist/server/app-visited-response-cache.d.ts +6 -1
  170. package/dist/server/app-visited-response-cache.js +18 -2
  171. package/dist/server/cache-control.d.ts +11 -7
  172. package/dist/server/cache-control.js +23 -12
  173. package/dist/server/config-headers.d.ts +4 -0
  174. package/dist/server/config-headers.js +19 -3
  175. package/dist/server/dev-route-files.js +2 -5
  176. package/dist/server/dev-server.d.ts +5 -13
  177. package/dist/server/dev-server.js +49 -32
  178. package/dist/server/dev-stack-sourcemap.d.ts +1 -2
  179. package/dist/server/dev-stack-sourcemap.js +1 -4
  180. package/dist/server/headers.d.ts +11 -1
  181. package/dist/server/headers.js +13 -2
  182. package/dist/server/http-error-responses.d.ts +1 -1
  183. package/dist/server/http-error-responses.js +7 -2
  184. package/dist/server/isr-cache.d.ts +6 -14
  185. package/dist/server/isr-cache.js +6 -27
  186. package/dist/server/metadata-route-build-data.js +1 -1
  187. package/dist/server/metadata-route-cache-policy.d.ts +6 -0
  188. package/dist/server/metadata-route-cache-policy.js +6 -0
  189. package/dist/server/metadata-route-response.d.ts +26 -1
  190. package/dist/server/metadata-route-response.js +219 -6
  191. package/dist/server/metadata-routes.js +1 -1
  192. package/dist/server/middleware-response-headers.js +4 -2
  193. package/dist/server/middleware.d.ts +3 -16
  194. package/dist/server/middleware.js +2 -19
  195. package/dist/server/navigation-planner.d.ts +1 -0
  196. package/dist/server/navigation-planner.js +57 -9
  197. package/dist/server/navigation-trace.d.ts +1 -0
  198. package/dist/server/navigation-trace.js +1 -0
  199. package/dist/server/pages-data-route.d.ts +1 -10
  200. package/dist/server/pages-data-route.js +1 -12
  201. package/dist/server/pages-dev-hydration.js +11 -4
  202. package/dist/server/pages-node-compat.d.ts +1 -4
  203. package/dist/server/pages-node-compat.js +1 -7
  204. package/dist/server/pages-page-handler.d.ts +3 -1
  205. package/dist/server/pages-page-handler.js +4 -15
  206. package/dist/server/pages-page-response.js +10 -5
  207. package/dist/server/pages-readiness.js +4 -4
  208. package/dist/server/prerender-cache-life-header.d.ts +11 -0
  209. package/dist/server/prerender-cache-life-header.js +18 -0
  210. package/dist/server/prerender-manifest.d.ts +5 -2
  211. package/dist/server/prerender-manifest.js +4 -1
  212. package/dist/server/prerender-route-params.d.ts +1 -2
  213. package/dist/server/prerender-route-params.js +1 -4
  214. package/dist/server/prod-server.js +9 -11
  215. package/dist/server/request-pipeline.js +2 -0
  216. package/dist/server/seed-cache.d.ts +4 -1
  217. package/dist/server/seed-cache.js +28 -4
  218. package/dist/server/worker-utils.js +1 -1
  219. package/dist/shims/cache-callable-runtime.d.ts +11 -0
  220. package/dist/shims/cache-callable-runtime.js +30 -0
  221. package/dist/shims/cache-request-state.d.ts +4 -1
  222. package/dist/shims/cache-request-state.js +6 -1
  223. package/dist/shims/cache-runtime.d.ts +6 -1
  224. package/dist/shims/cache-runtime.js +137 -30
  225. package/dist/shims/cache.d.ts +2 -2
  226. package/dist/shims/cache.js +2 -2
  227. package/dist/shims/cdn-cache.d.ts +18 -6
  228. package/dist/shims/cdn-cache.js +4 -2
  229. package/dist/shims/dynamic.d.ts +3 -3
  230. package/dist/shims/dynamic.js +4 -6
  231. package/dist/shims/error.js +1 -1
  232. package/dist/shims/fetch-cache.d.ts +3 -1
  233. package/dist/shims/fetch-cache.js +27 -7
  234. package/dist/shims/font-local.js +3 -3
  235. package/dist/shims/head.d.ts +16 -1
  236. package/dist/shims/head.js +70 -8
  237. package/dist/shims/internal/app-route-prefetch-policy.d.ts +8 -7
  238. package/dist/shims/internal/app-route-prefetch-policy.js +13 -6
  239. package/dist/shims/internal/pages-data-target.d.ts +1 -1
  240. package/dist/shims/internal/pages-data-target.js +18 -5
  241. package/dist/shims/link.js +50 -55
  242. package/dist/shims/navigation-context-state.d.ts +4 -0
  243. package/dist/shims/navigation.d.ts +39 -3
  244. package/dist/shims/navigation.js +139 -18
  245. package/dist/shims/navigation.react-server.js +1 -1
  246. package/dist/shims/request-state-types.d.ts +1 -1
  247. package/dist/shims/root-params.d.ts +3 -1
  248. package/dist/shims/root-params.js +7 -1
  249. package/dist/shims/router.js +252 -80
  250. package/dist/shims/server.js +2 -1
  251. package/dist/shims/unified-request-context.d.ts +1 -1
  252. package/dist/shims/unified-request-context.js +1 -0
  253. package/dist/utils/built-asset-url.d.ts +1 -1
  254. package/dist/utils/built-asset-url.js +4 -2
  255. package/dist/utils/encode-cache-tag.d.ts +1 -1
  256. package/dist/utils/encode-cache-tag.js +1 -1
  257. package/dist/utils/package-name.d.ts +11 -0
  258. package/dist/utils/package-name.js +18 -0
  259. package/dist/utils/path.d.ts +8 -1
  260. package/dist/utils/path.js +21 -1
  261. package/dist/utils/prerender-output-paths.d.ts +3 -1
  262. package/dist/utils/prerender-output-paths.js +6 -1
  263. package/package.json +5 -4
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Run Next.js applications on Vite, with Cloudflare Workers as the primary deployment target.
4
4
 
5
+ **Website:** [vinext.dev](https://vinext.dev)
6
+
5
7
  > **Read the announcement:** [How we rebuilt Next.js with AI in one week](https://blog.cloudflare.com/vinext/)
6
8
 
7
9
  > **Under active development.** vinext supports substantial Next.js applications today, but it is not yet a drop-in replacement for every application or production workload. Expect compatibility gaps, especially in newer App Router features, and evaluate it against your own application before adopting it.
@@ -516,12 +518,12 @@ These are deployed to Cloudflare Workers and updated on every push to `main`:
516
518
  | App Router (minimal) | Minimal App Router on Workers | [app-router-cloudflare.vinext.workers.dev](https://app-router-cloudflare.vinext.workers.dev) |
517
519
  | Pages Router (minimal) | Minimal Pages Router on Workers | [pages-router-cloudflare.vinext.workers.dev](https://pages-router-cloudflare.vinext.workers.dev) |
518
520
  | RealWorld API | REST API routes example | [realworld-api-rest.vinext.workers.dev](https://realworld-api-rest.vinext.workers.dev) |
519
- | Benchmarks Dashboard | Build performance tracking over time (D1-backed) | [vinext-web.vinext.workers.dev/benchmarks](https://vinext-web.vinext.workers.dev/benchmarks) |
521
+ | Benchmarks Dashboard | Build performance tracking over time (D1-backed) | [vinext.dev/benchmarks](https://vinext.dev/benchmarks) |
520
522
  | App Router + Nitro | App Router deployed via Nitro (multi-platform) | [examples/app-router-nitro](examples/app-router-nitro) |
521
523
 
522
524
  ## API coverage
523
525
 
524
- ~94% of the Next.js 16 API surface has full or partial support. The remaining gaps are intentional stubs for deprecated features and Partial Prerendering (which Next.js 16 reworked into `"use cache"` — that directive is fully supported).
526
+ ~94% of the Next.js 16 API surface has full or partial support. The remaining gaps are intentional stubs for deprecated features, plus Partial Prerendering and Cache Components. Next.js 16 reworked PPR into `"use cache"`; vinext implements that directive for file-level and function-level caching, but full `cacheComponents` behavior is still incomplete — see [Known gaps we're working on](#known-gaps-were-working-on).
525
527
 
526
528
  > ✅ = full implementation | 🟡 = partial (runtime behavior correct, some build-time optimizations missing) | ⬜ = intentional stub/no-op
527
529
 
@@ -643,16 +645,21 @@ The cache is pluggable. The default `MemoryCacheHandler` works out of the box. S
643
645
  Instead of wiring up cache handlers imperatively from a worker entry, you can declare them in the `vinext()` plugin config. The `@vinext/cloudflare` package ships Cloudflare adapters for this:
644
646
 
645
647
  - **`kvDataAdapter()`** (`@vinext/cloudflare/cache/kv-data-adapter`) — backs the `"use cache"` data cache with a Workers KV namespace.
648
+ - **`cdnAdapter()`** (`@vinext/cloudflare/cache/cdn-adapter`) — serves page-level ISR from the Cloudflare Workers Cache (`ctx.cache`) instead of from the origin.
649
+
650
+ The two fill different slots and can be used together:
646
651
 
647
652
  ```ts
648
653
  import { defineConfig } from "vite";
649
654
  import vinext from "vinext";
655
+ import { cdnAdapter } from "@vinext/cloudflare/cache/cdn-adapter";
650
656
  import { kvDataAdapter } from "@vinext/cloudflare/cache/kv-data-adapter";
651
657
 
652
658
  export default defineConfig({
653
659
  plugins: [
654
660
  vinext({
655
661
  cache: {
662
+ cdn: cdnAdapter(),
656
663
  data: kvDataAdapter(),
657
664
  },
658
665
  }),
@@ -670,6 +677,16 @@ The KV data adapter reads `env[binding]` at runtime, so add the matching KV name
670
677
 
671
678
  `binding` defaults to `VINEXT_KV_CACHE`, so `kvDataAdapter()` with no options works as long as that's your binding name. Other options: `appPrefix` (namespace cache keys to isolate multiple apps in one KV namespace), `ttlSeconds` (default KV `expirationTtl`, default 30 days), and `tagCacheTtlMs` (in-memory tag-invalidation cache TTL, default 5s).
672
679
 
680
+ `cdnAdapter()` takes no options, but the Workers Cache only exposes `ctx.cache` when `cache.enabled` is set in `wrangler.jsonc`:
681
+
682
+ ```jsonc
683
+ {
684
+ "cache": { "enabled": true },
685
+ }
686
+ ```
687
+
688
+ While the data adapter can store entries and serve HIT/STALE itself, the CDN adapter delegates serving to Cloudflare's edge: the origin renders fresh responses and tags them with `Cache-Tag`, and `revalidateTag()` / `revalidatePath()` purge the edge through `ctx.cache.purge({ tags })`. See [examples/workers-cache](examples/workers-cache) for both adapters wired up together.
689
+
673
690
  Each builder returns a plain, serializable `{ adapter, options }` descriptor — **it never touches the Workers runtime**, so nothing throws at build or dev time when bindings aren't available. The actual adapter (and its `env` binding lookup) is instantiated lazily on the first request.
674
691
 
675
692
  Registration is wired into **every router and runtime** — App Router and Pages Router, on Cloudflare Workers as well as the Node.js server (`vinext start`) and dev. It self-guards (instantiated once per isolate) and is resilient: if an adapter can't initialize on a given runtime (e.g. a KV binding doesn't exist on the Node server), vinext logs a warning and falls back to the default handler instead of failing requests.
@@ -713,7 +730,7 @@ We measure three things:
713
730
  - **Client bundle size** — gzipped output of each build.
714
731
  - **Dev server cold start** — 10 runs, randomized execution order. Vite's dependency optimizer cache is cleared before each run.
715
732
 
716
- Benchmarks run on GitHub CI runners (2-core Ubuntu) on every merge to `main`. See the launch numbers in the [announcement blog post](https://blog.cloudflare.com/vinext/) and the latest results at **[vinext-web.vinext.workers.dev/benchmarks](https://vinext-web.vinext.workers.dev/benchmarks)**.
733
+ Benchmarks run on GitHub CI runners (2-core Ubuntu) on every merge to `main`. See the launch numbers in the [announcement blog post](https://blog.cloudflare.com/vinext/) and the latest results at **[vinext.dev/benchmarks](https://vinext.dev/benchmarks)**.
717
734
 
718
735
  <details>
719
736
  <summary>Why the bundle size difference?</summary>
@@ -0,0 +1,42 @@
1
+ import { AppRoute } from "../routing/app-route-graph.js";
2
+ import { Rollup } from "vite";
3
+ import { RscPluginManager } from "@vitejs/plugin-rsc";
4
+ //#region src/build/action-owner-manifest.d.ts
5
+ type ActionOwnerRoute = Pick<AppRoute, "errorPath" | "errorPaths" | "forbiddenPath" | "forbiddenPaths" | "layoutErrorPaths" | "layouts" | "loadingPath" | "loadingPaths" | "notFoundPath" | "notFoundPaths" | "pagePath" | "parallelSlots" | "pattern" | "siblingIntercepts" | "templates" | "unauthorizedPath" | "unauthorizedPaths">;
6
+ type ActionOwnerRouteReachability = Map<string, {
7
+ clientReferenceImportIds: Set<string>;
8
+ serverReferenceIds: Set<string>;
9
+ }>;
10
+ type ActionOwnerModuleInfo = Pick<Rollup.ModuleInfo, "dynamicallyImportedIds" | "importedIds">;
11
+ type ActionOwnerReferenceMaps = {
12
+ clientReferenceMetaMap: RscPluginManager["clientReferenceMetaMap"];
13
+ serverReferenceMetaMap: RscPluginManager["serverReferences"]["metaMap"];
14
+ };
15
+ declare function actionOwnerRouteEntryIds(route: ActionOwnerRoute): string[];
16
+ declare function collectReachableActionReferences(options: {
17
+ canonicalizeModuleId?: (id: string) => string;
18
+ getModuleInfo: (id: string) => ActionOwnerModuleInfo | null;
19
+ roots: readonly string[];
20
+ } & ActionOwnerReferenceMaps): {
21
+ clientReferenceImportIds: Set<string>;
22
+ serverReferenceIds: Set<string>;
23
+ };
24
+ declare function collectRscActionReachability(options: {
25
+ canonicalizeModuleId?: (id: string) => string;
26
+ getModuleInfo: (id: string) => ActionOwnerModuleInfo | null;
27
+ routes: readonly ActionOwnerRoute[];
28
+ sharedRoots?: readonly string[];
29
+ } & ActionOwnerReferenceMaps): ActionOwnerRouteReachability;
30
+ declare function resolveClientReferenceImportIds(options: {
31
+ canonicalizeModuleId?: (id: string) => string;
32
+ resolveId: (id: string) => Promise<string | null>;
33
+ routeReachability: ActionOwnerRouteReachability;
34
+ }): Promise<void>;
35
+ declare function addClientActionReachability(options: {
36
+ canonicalizeModuleId?: (id: string) => string;
37
+ getModuleInfo: (id: string) => ActionOwnerModuleInfo | null;
38
+ routeReachability: ActionOwnerRouteReachability;
39
+ } & ActionOwnerReferenceMaps): void;
40
+ declare function buildActionOwnerManifest(routeReachability: ActionOwnerRouteReachability): Record<string, string[]>;
41
+ //#endregion
42
+ export { ActionOwnerRouteReachability, actionOwnerRouteEntryIds, addClientActionReachability, buildActionOwnerManifest, collectReachableActionReferences, collectRscActionReachability, resolveClientReferenceImportIds };
@@ -0,0 +1,110 @@
1
+ //#region src/build/action-owner-manifest.ts
2
+ function actionOwnerRouteEntryIds(route) {
3
+ return [
4
+ route.pagePath,
5
+ ...route.layouts,
6
+ ...route.templates,
7
+ route.loadingPath,
8
+ ...route.loadingPaths ?? [],
9
+ route.errorPath,
10
+ ...route.layoutErrorPaths ?? [],
11
+ ...route.errorPaths ?? [],
12
+ route.notFoundPath,
13
+ ...route.notFoundPaths ?? [],
14
+ route.forbiddenPath,
15
+ ...route.forbiddenPaths ?? [],
16
+ route.unauthorizedPath,
17
+ ...route.unauthorizedPaths ?? [],
18
+ ...route.parallelSlots.flatMap((slot) => [
19
+ slot.pagePath,
20
+ slot.defaultPath,
21
+ slot.layoutPath,
22
+ ...slot.configLayoutPaths ?? [],
23
+ slot.loadingPath,
24
+ ...slot.loadingPaths ?? [],
25
+ slot.errorPath,
26
+ slot.notFoundPath,
27
+ ...slot.interceptingRoutes.flatMap((intercept) => [
28
+ intercept.pagePath,
29
+ ...intercept.layoutPaths,
30
+ ...intercept.loadingPaths ?? [],
31
+ intercept.notFoundPath
32
+ ])
33
+ ]),
34
+ ...route.siblingIntercepts.flatMap((intercept) => [
35
+ intercept.pagePath,
36
+ ...intercept.layoutPaths,
37
+ ...intercept.loadingPaths ?? [],
38
+ intercept.notFoundPath
39
+ ])
40
+ ].filter((value) => typeof value === "string");
41
+ }
42
+ function collectReachableActionReferences(options) {
43
+ const canonicalizeModuleId = options.canonicalizeModuleId ?? ((id) => id);
44
+ const clientReferenceImportIds = /* @__PURE__ */ new Set();
45
+ const serverReferenceIds = /* @__PURE__ */ new Set();
46
+ const visited = /* @__PURE__ */ new Set();
47
+ const queue = options.roots.map(canonicalizeModuleId);
48
+ for (let index = 0; index < queue.length; index++) {
49
+ const id = queue[index];
50
+ if (visited.has(id)) continue;
51
+ visited.add(id);
52
+ const clientReference = options.clientReferenceMetaMap[id];
53
+ if (clientReference) clientReferenceImportIds.add(clientReference.importId);
54
+ const serverReference = options.serverReferenceMetaMap.get(id);
55
+ if (serverReference) for (const exportName of serverReference.exportNames) serverReferenceIds.add(`${serverReference.referenceKey}#${exportName}`);
56
+ const info = options.getModuleInfo(id);
57
+ for (const importedId of [...info?.importedIds ?? [], ...info?.dynamicallyImportedIds ?? []]) if (!visited.has(importedId)) queue.push(importedId);
58
+ }
59
+ return {
60
+ clientReferenceImportIds,
61
+ serverReferenceIds
62
+ };
63
+ }
64
+ function collectRscActionReachability(options) {
65
+ const routeReachability = new Map(options.routes.map((route) => [route.pattern, collectReachableActionReferences({
66
+ canonicalizeModuleId: options.canonicalizeModuleId,
67
+ clientReferenceMetaMap: options.clientReferenceMetaMap,
68
+ getModuleInfo: options.getModuleInfo,
69
+ roots: actionOwnerRouteEntryIds(route),
70
+ serverReferenceMetaMap: options.serverReferenceMetaMap
71
+ })]));
72
+ if (options.sharedRoots?.length) routeReachability.set("*", collectReachableActionReferences({
73
+ canonicalizeModuleId: options.canonicalizeModuleId,
74
+ clientReferenceMetaMap: options.clientReferenceMetaMap,
75
+ getModuleInfo: options.getModuleInfo,
76
+ roots: options.sharedRoots,
77
+ serverReferenceMetaMap: options.serverReferenceMetaMap
78
+ }));
79
+ return routeReachability;
80
+ }
81
+ async function resolveClientReferenceImportIds(options) {
82
+ const canonicalizeModuleId = options.canonicalizeModuleId ?? ((id) => id);
83
+ for (const reachability of options.routeReachability.values()) {
84
+ const resolvedImportIds = /* @__PURE__ */ new Set();
85
+ for (const importId of reachability.clientReferenceImportIds) {
86
+ const resolvedId = await options.resolveId(importId) ?? importId;
87
+ resolvedImportIds.add(canonicalizeModuleId(resolvedId));
88
+ }
89
+ reachability.clientReferenceImportIds = resolvedImportIds;
90
+ }
91
+ }
92
+ function addClientActionReachability(options) {
93
+ for (const reachability of options.routeReachability.values()) {
94
+ const clientReachability = collectReachableActionReferences({
95
+ canonicalizeModuleId: options.canonicalizeModuleId,
96
+ clientReferenceMetaMap: options.clientReferenceMetaMap,
97
+ getModuleInfo: options.getModuleInfo,
98
+ roots: [...reachability.clientReferenceImportIds],
99
+ serverReferenceMetaMap: options.serverReferenceMetaMap
100
+ });
101
+ for (const actionId of clientReachability.serverReferenceIds) reachability.serverReferenceIds.add(actionId);
102
+ }
103
+ }
104
+ function buildActionOwnerManifest(routeReachability) {
105
+ const manifest = {};
106
+ for (const [pattern, reachability] of routeReachability) for (const actionId of reachability.serverReferenceIds) (manifest[actionId] ??= []).push(pattern);
107
+ return manifest;
108
+ }
109
+ //#endregion
110
+ export { actionOwnerRouteEntryIds, addClientActionReachability, buildActionOwnerManifest, collectReachableActionReferences, collectRscActionReachability, resolveClientReferenceImportIds };
@@ -1,4 +1,5 @@
1
1
  import path from "../deps/.pnpm/pathslash@0.1.0/deps/pathslash/dist/index.js";
2
+ import { isPathInsideOrEqual } from "../utils/path.js";
2
3
  import fs from "node:fs";
3
4
  //#region src/build/clean-output.ts
4
5
  function resolveOutDir(root, outDir) {
@@ -6,10 +7,6 @@ function resolveOutDir(root, outDir) {
6
7
  if (!outDir) return path.join(resolvedRoot, "dist");
7
8
  return path.isAbsolute(outDir) ? outDir : path.resolve(resolvedRoot, outDir);
8
9
  }
9
- function isPathInsideOrEqual(parent, child) {
10
- const relative = path.relative(parent, child);
11
- return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
12
- }
13
10
  function shouldCleanBuildOutput(options) {
14
11
  if (options.emptyOutDir === false) return false;
15
12
  if (options.emptyOutDir === true) return true;
@@ -86,11 +86,20 @@ function getPackageName(id) {
86
86
  */
87
87
  function createClientManualChunks(shimsDir, preserveRouteBoundaries = false) {
88
88
  return function clientManualChunks(id) {
89
+ const slashedId = toSlash(id);
90
+ if (slashedId.startsWith(shimsDir)) {
91
+ if (preserveRouteBoundaries) {
92
+ const relativeId = slashedId.slice(shimsDir.length).split("?", 1)[0] ?? "";
93
+ const extensionIndex = relativeId.lastIndexOf(".");
94
+ const shimName = extensionIndex === -1 ? relativeId : relativeId.slice(0, extensionIndex);
95
+ if (ROUTE_OWNED_CLIENT_SHIMS.has(shimName)) return void 0;
96
+ }
97
+ return "vinext";
98
+ }
89
99
  if (id.includes("node_modules")) {
90
100
  const pkg = getPackageName(id);
91
101
  if (!pkg) return void 0;
92
102
  if (pkg === "react-dom") {
93
- const slashedId = toSlash(id);
94
103
  const sub = slashedId.slice(slashedId.lastIndexOf("react-dom/") + 10);
95
104
  if (sub.startsWith("server.") || sub.startsWith("static.") || sub.startsWith("cjs/react-dom-server")) return "react-dom-server";
96
105
  return "framework";
@@ -98,16 +107,6 @@ function createClientManualChunks(shimsDir, preserveRouteBoundaries = false) {
98
107
  if (pkg === "react" || pkg === "scheduler") return "framework";
99
108
  return;
100
109
  }
101
- const slashedId = toSlash(id);
102
- if (slashedId.startsWith(shimsDir)) {
103
- if (preserveRouteBoundaries) {
104
- const relativeId = slashedId.slice(shimsDir.length).split("?", 1)[0] ?? "";
105
- const extensionIndex = relativeId.lastIndexOf(".");
106
- const shimName = extensionIndex === -1 ? relativeId : relativeId.slice(0, extensionIndex);
107
- if (ROUTE_OWNED_CLIENT_SHIMS.has(shimName)) return void 0;
108
- }
109
- return "vinext";
110
- }
111
110
  };
112
111
  }
113
112
  function createClientFileNameConfig(assetsDir) {
@@ -9,6 +9,7 @@ import { BLOCKED_PAGES, PHASE_PRODUCTION_BUILD } from "../shims/constants.js";
9
9
  import { loadNextConfig, resolveNextConfig } from "../config/next-config.js";
10
10
  import { readPrerenderSecret } from "./server-manifest.js";
11
11
  import { startProdServer } from "../server/prod-server.js";
12
+ import { enterPrerenderPhase } from "./prerender-phase.js";
12
13
  import { buildUrlFromParams, resolveParentParams } from "./prerender.js";
13
14
  import fs from "node:fs";
14
15
  //#region src/build/prerender-paths.ts
@@ -96,15 +97,13 @@ async function shouldStartPathDiscoveryServer(options) {
96
97
  return false;
97
98
  }
98
99
  async function withPrerenderEndpoints(fn) {
99
- const previousPrerenderFlag = process.env.VINEXT_PRERENDER;
100
+ const restorePrerenderPhase = enterPrerenderPhase();
100
101
  const previousPathDiscoveryFlag = process.env[PRERENDER_PATH_DISCOVERY_ENV];
101
- process.env.VINEXT_PRERENDER = "1";
102
102
  process.env[PRERENDER_PATH_DISCOVERY_ENV] = "1";
103
103
  try {
104
104
  return await fn();
105
105
  } finally {
106
- if (previousPrerenderFlag === void 0) delete process.env.VINEXT_PRERENDER;
107
- else process.env.VINEXT_PRERENDER = previousPrerenderFlag;
106
+ restorePrerenderPhase();
108
107
  if (previousPathDiscoveryFlag === void 0) delete process.env[PRERENDER_PATH_DISCOVERY_ENV];
109
108
  else process.env[PRERENDER_PATH_DISCOVERY_ENV] = previousPathDiscoveryFlag;
110
109
  }
@@ -0,0 +1,15 @@
1
+ //#region src/build/prerender-phase.d.ts
2
+ type PrerenderPhaseEnv = {
3
+ NEXT_PHASE?: string;
4
+ NODE_ENV?: string;
5
+ VINEXT_PRERENDER?: string;
6
+ };
7
+ /**
8
+ * Enter the same process environment phase that Next.js exposes while its
9
+ * static workers execute application code. The returned cleanup is nest-safe:
10
+ * direct prerender helpers can run inside the CLI's broader build scope without
11
+ * overwriting the caller's prior phase when they finish.
12
+ */
13
+ declare function enterPrerenderPhase(env?: PrerenderPhaseEnv): () => void;
14
+ //#endregion
15
+ export { enterPrerenderPhase };
@@ -0,0 +1,37 @@
1
+ import { PHASE_PRODUCTION_BUILD } from "../shims/constants.js";
2
+ //#region src/build/prerender-phase.ts
3
+ const activePrerenderPhases = /* @__PURE__ */ new WeakMap();
4
+ /**
5
+ * Enter the same process environment phase that Next.js exposes while its
6
+ * static workers execute application code. The returned cleanup is nest-safe:
7
+ * direct prerender helpers can run inside the CLI's broader build scope without
8
+ * overwriting the caller's prior phase when they finish.
9
+ */
10
+ function enterPrerenderPhase(env = process.env) {
11
+ let state = activePrerenderPhases.get(env);
12
+ if (state) state.depth += 1;
13
+ else {
14
+ state = {
15
+ depth: 1,
16
+ previousPrerenderFlag: env.VINEXT_PRERENDER,
17
+ previousNextPhase: env.NEXT_PHASE
18
+ };
19
+ activePrerenderPhases.set(env, state);
20
+ }
21
+ let restored = false;
22
+ env.VINEXT_PRERENDER = "1";
23
+ env.NEXT_PHASE = PHASE_PRODUCTION_BUILD;
24
+ return () => {
25
+ if (restored) return;
26
+ restored = true;
27
+ state.depth -= 1;
28
+ if (state.depth > 0) return;
29
+ activePrerenderPhases.delete(env);
30
+ if (state.previousPrerenderFlag === void 0) delete env.VINEXT_PRERENDER;
31
+ else env.VINEXT_PRERENDER = state.previousPrerenderFlag;
32
+ if (state.previousNextPhase === void 0) delete env.NEXT_PHASE;
33
+ else env.NEXT_PHASE = state.previousNextPhase;
34
+ };
35
+ }
36
+ //#endregion
37
+ export { enterPrerenderPhase };
@@ -11,9 +11,9 @@ import { startProdServer } from "../server/prod-server.js";
11
11
  * vinext production server on an ephemeral port and reports it to the parent
12
12
  * over IPC. The parent then load-balances per-route fetches across the pool.
13
13
  *
14
- * `VINEXT_PRERENDER` / `VINEXT_PRERENDER_OUTDIR` are passed via the fork env so
15
- * they are set before any module loads (some server modules read the flag at
16
- * import time).
14
+ * `VINEXT_PRERENDER` / `NEXT_PHASE` / `VINEXT_PRERENDER_OUTDIR` are passed via
15
+ * the fork env so they are set before any module loads (some server and user
16
+ * modules read the phase at import time).
17
17
  */
18
18
  async function main() {
19
19
  const outDir = process.env.VINEXT_PRERENDER_OUTDIR;
@@ -1,4 +1,5 @@
1
1
  import path from "../deps/.pnpm/pathslash@0.1.0/deps/pathslash/dist/index.js";
2
+ import { PHASE_PRODUCTION_BUILD } from "../shims/constants.js";
2
3
  import fs from "node:fs";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import os from "node:os";
@@ -105,6 +106,7 @@ async function startPrerenderServerPool(outDir, size, entry = WORKER_ENTRY) {
105
106
  env: {
106
107
  ...process.env,
107
108
  VINEXT_PRERENDER: "1",
109
+ NEXT_PHASE: PHASE_PRODUCTION_BUILD,
108
110
  VINEXT_PRERENDER_OUTDIR: outDir
109
111
  },
110
112
  stdio: [
@@ -1,6 +1,6 @@
1
+ import { AppRoute } from "../routing/app-route-graph.js";
1
2
  import { ResolvedNextConfig } from "../config/next-config.js";
2
3
  import { Route } from "../routing/pages-router.js";
3
- import { AppRoute } from "../routing/app-route-graph.js";
4
4
  import { MetadataFileRoute } from "../server/metadata-routes.js";
5
5
  import { readPrerenderSecret } from "./server-manifest.js";
6
6
  import { Server } from "node:http";
@@ -27,11 +27,15 @@ type PrerenderRouteResult = {
27
27
  */
28
28
  path?: string;
29
29
  /** Which router produced this route. Used by cache seeding. */
30
- router: "app" | "pages";
30
+ router: "app" | "pages" | "metadata";
31
31
  /** Response headers that must be replayed with the prerendered artifact. */
32
- headers?: Record<string, string>;
32
+ headers?: Record<string, string | string[]>;
33
+ /** Original HTTP status for prerendered App Route-style responses. */
34
+ responseStatus?: number;
33
35
  /** Cache tags collected while rendering this route. */
34
36
  tags?: string[];
37
+ /** Raw app-tree segments used to derive App Route implicit tags. */
38
+ routeSegments?: string[];
35
39
  /** Set to true when this is a PPR fallback shell. */
36
40
  fallback?: boolean;
37
41
  } | {
@@ -1,7 +1,7 @@
1
1
  import path from "../deps/.pnpm/pathslash@0.1.0/deps/pathslash/dist/index.js";
2
2
  import { createValidFileMatcher, findFileWithExtensions } from "../routing/file-matcher.js";
3
3
  import { VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER, VINEXT_PRERENDER_SPECULATIVE_HEADER } from "../utils/protocol-headers.js";
4
- import { NEXT_CACHE_TAGS_HEADER, VINEXT_PRERENDER_CACHE_LIFE_HEADER } from "../server/headers.js";
4
+ import { NEXT_CACHE_TAGS_HEADER, VINEXT_METADATA_ROUTE_CACHE_HEADER, VINEXT_PRERENDER_CACHE_LIFE_HEADER, VINEXT_PRERENDER_METADATA_ROUTES_PATH, VINEXT_PRERENDER_RENDER_ERROR_HEADER } from "../server/headers.js";
5
5
  import { classifyAppRoute, classifyPagesRoute, getAppRouteRenderEntryPath } from "./report.js";
6
6
  import { resolveClientStaleTimeSeconds } from "../utils/cache-control-metadata.js";
7
7
  import { NoOpCacheHandler, getCacheHandler, setCacheHandler } from "../shims/cache-handler.js";
@@ -15,9 +15,12 @@ import { navigationRuntimeRscBootstrapExpression } from "../server/app-ssr-strea
15
15
  import { encodePrerenderRouteParams, serializePrerenderRouteParamsHeader } from "../server/prerender-route-params.js";
16
16
  import { readPrerenderSecret } from "./server-manifest.js";
17
17
  import { createAppPprFallbackShells, markAppPprDynamicFallbackShellHtml } from "../server/app-ppr-fallback-shell.js";
18
- import { getOutputPath, getRscOutputPath } from "../utils/prerender-output-paths.js";
18
+ import { getAppRouteOutputPath, getOutputPath, getRscOutputPath } from "../utils/prerender-output-paths.js";
19
19
  import { startProdServer } from "../server/prod-server.js";
20
20
  import { prerenderPoolAvailable, resolvePrerenderPoolSize, startPrerenderServerPool } from "./prerender-server-pool.js";
21
+ import { enterPrerenderPhase } from "./prerender-phase.js";
22
+ import { buildAppRouteCacheValue } from "../server/app-route-handler-response.js";
23
+ import { isMetadataResponseCacheable } from "../server/metadata-route-cache-policy.js";
21
24
  import fs from "node:fs";
22
25
  import os from "node:os";
23
26
  //#region src/build/prerender.ts
@@ -265,8 +268,7 @@ async function prerenderPages({ routes, apiRoutes, pagesDir, outDir, config, mod
265
268
  });
266
269
  const previousHandler = getCacheHandler();
267
270
  setCacheHandler(new NoOpCacheHandler());
268
- const previousPrerenderFlag = process.env.VINEXT_PRERENDER;
269
- process.env.VINEXT_PRERENDER = "1";
271
+ const restorePrerenderPhase = enterPrerenderPhase();
270
272
  let ownedProdServerHandle = null;
271
273
  let renderPool = null;
272
274
  try {
@@ -497,11 +499,13 @@ async function prerenderPages({ routes, apiRoutes, pagesDir, outDir, config, mod
497
499
  });
498
500
  return { routes: results };
499
501
  } finally {
500
- if (renderPool) await renderPool.close();
501
- setCacheHandler(previousHandler);
502
- if (previousPrerenderFlag === void 0) delete process.env.VINEXT_PRERENDER;
503
- else process.env.VINEXT_PRERENDER = previousPrerenderFlag;
504
- if (ownedProdServerHandle) await new Promise((resolve) => ownedProdServerHandle.server.close(() => resolve()));
502
+ try {
503
+ if (renderPool) await renderPool.close();
504
+ setCacheHandler(previousHandler);
505
+ if (ownedProdServerHandle) await new Promise((resolve) => ownedProdServerHandle.server.close(() => resolve()));
506
+ } finally {
507
+ restorePrerenderPhase();
508
+ }
505
509
  }
506
510
  }
507
511
  /**
@@ -530,8 +534,7 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
530
534
  fs.mkdirSync(outDir, { recursive: true });
531
535
  const previousHandler = getCacheHandler();
532
536
  setCacheHandler(new NoOpCacheHandler());
533
- const previousPrerenderFlag = process.env.VINEXT_PRERENDER;
534
- process.env.VINEXT_PRERENDER = "1";
537
+ const restorePrerenderPhase = enterPrerenderPhase();
535
538
  const serverDir = path.dirname(rscBundlePath);
536
539
  let rscHandler;
537
540
  let staticParamsMap = {};
@@ -743,14 +746,85 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
743
746
  isSpeculative: false
744
747
  });
745
748
  }
749
+ if (metadataRoutes.length > 0) {
750
+ const response = await fetch(`${baseUrl}${VINEXT_PRERENDER_METADATA_ROUTES_PATH}`, { headers: secretHeaders });
751
+ const text = await response.text();
752
+ if (!response.ok) throw new Error(`[vinext] Failed to enumerate prerenderable metadata routes: ${parsePrerenderEndpointError(text)}`);
753
+ const metadataEntries = JSON.parse(text);
754
+ if (!Array.isArray(metadataEntries)) throw new Error("[vinext] Invalid prerender metadata route response");
755
+ const queuedMetadataPaths = /* @__PURE__ */ new Set();
756
+ for (const metadataEntry of metadataEntries) {
757
+ if (typeof metadataEntry !== "object" || metadataEntry === null || typeof Reflect.get(metadataEntry, "path") !== "string" || typeof Reflect.get(metadataEntry, "routePattern") !== "string" || !Array.isArray(Reflect.get(metadataEntry, "routeSegments"))) continue;
758
+ const { path: metadataPath, routePattern, routeSegments } = metadataEntry;
759
+ if (!metadataPath.startsWith("/") || !routePattern.startsWith("/") || routeSegments.some((segment) => typeof segment !== "string") || queuedMetadataPaths.has(metadataPath)) continue;
760
+ queuedMetadataPaths.add(metadataPath);
761
+ urlsToRender.push({
762
+ kind: "metadata",
763
+ metadataRouteSegments: routeSegments,
764
+ urlPath: metadataPath,
765
+ routePattern,
766
+ prerenderRouteParams: null,
767
+ revalidate: false,
768
+ isSpeculative: false
769
+ });
770
+ }
771
+ }
746
772
  /**
747
773
  * Render a single URL and return its result.
748
774
  * `onProgress` is intentionally not called here; the outer loop calls it
749
775
  * exactly once per URL after this function returns, keeping the callback
750
776
  * at a single, predictable call site.
751
777
  */
752
- async function renderUrl({ urlPath, routePattern, prerenderRouteParams, revalidate, isSpeculative, isFallback }) {
778
+ async function renderUrl({ kind, metadataRouteSegments, urlPath, routePattern, prerenderRouteParams, revalidate, isSpeculative, isFallback }) {
753
779
  try {
780
+ if (kind === "metadata") {
781
+ const request = new Request(`http://localhost${config.basePath ?? ""}${urlPath}`);
782
+ const response = await runWithHeadersContext(headersContextFromRequest(request), () => rscHandler(request));
783
+ if (!response.ok) {
784
+ const fatal = response.headers.get(VINEXT_PRERENDER_RENDER_ERROR_HEADER) === "1";
785
+ await response.body?.cancel();
786
+ return {
787
+ route: routePattern,
788
+ status: "error",
789
+ error: `Metadata route returned ${response.status}`,
790
+ ...fatal ? { fatal: true } : {}
791
+ };
792
+ }
793
+ const cacheControl = response.headers.get("cache-control") ?? "";
794
+ if (!isMetadataResponseCacheable(response)) {
795
+ await response.body?.cancel();
796
+ return {
797
+ route: routePattern,
798
+ status: "skipped",
799
+ reason: "dynamic"
800
+ };
801
+ }
802
+ const requestCacheLife = readPrerenderCacheLifeHeader(response.headers);
803
+ const collectedTags = readPrerenderCacheTagsHeader(response.headers);
804
+ const cacheValue = await buildAppRouteCacheValue(response);
805
+ cacheValue.headers[VINEXT_METADATA_ROUTE_CACHE_HEADER] = "1";
806
+ const outputPath = getAppRouteOutputPath(urlPath);
807
+ const fullPath = path.join(outDir, outputPath);
808
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
809
+ fs.writeFileSync(fullPath, new Uint8Array(cacheValue.body));
810
+ const renderedCacheControl = resolveRenderedCacheControl(requestCacheLife ?? {}, cacheControl, config.expireTime);
811
+ const renderedRevalidate = renderedCacheControl.revalidate ?? false;
812
+ const renderedStale = resolveClientStaleTimeSeconds(requestCacheLife ?? void 0);
813
+ return {
814
+ route: routePattern,
815
+ ...urlPath === routePattern ? {} : { path: urlPath },
816
+ status: "rendered",
817
+ outputFiles: [outputPath],
818
+ revalidate: renderedRevalidate,
819
+ ...typeof renderedRevalidate === "number" ? { expire: renderedCacheControl.expire } : {},
820
+ ...renderedStale === void 0 ? {} : { stale: renderedStale },
821
+ router: "metadata",
822
+ headers: cacheValue.headers,
823
+ responseStatus: cacheValue.status,
824
+ routeSegments: metadataRouteSegments ?? [],
825
+ tags: collectedTags
826
+ };
827
+ }
754
828
  const prerenderRouteParamsHeader = serializePrerenderRouteParamsHeader(prerenderRouteParams);
755
829
  const htmlHeaders = new Headers();
756
830
  if (prerenderRouteParamsHeader !== null) htmlHeaders.set(VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, prerenderRouteParamsHeader);
@@ -762,6 +836,7 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
762
836
  const linkHeader = response.headers.get("link");
763
837
  const responseCacheLife = readPrerenderCacheLifeHeader(response.headers);
764
838
  const cacheTags = readPrerenderCacheTagsHeader(response.headers);
839
+ const fatal = response.headers.get(VINEXT_PRERENDER_RENDER_ERROR_HEADER) === "1";
765
840
  if (!response.ok || cacheControl.includes("no-store")) {
766
841
  await response.body?.cancel();
767
842
  return {
@@ -771,7 +846,8 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
771
846
  ok: response.ok,
772
847
  requestCacheLife: null,
773
848
  tags: [],
774
- status: response.status
849
+ status: response.status,
850
+ fatal
775
851
  };
776
852
  }
777
853
  const html = await response.text();
@@ -783,12 +859,13 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
783
859
  ok: true,
784
860
  requestCacheLife: responseCacheLife ?? processCacheLife,
785
861
  status: response.status,
786
- tags: cacheTags
862
+ tags: cacheTags,
863
+ fatal: false
787
864
  };
788
865
  });
789
866
  const htmlCacheControl = htmlRender.cacheControl;
790
867
  if (!htmlRender.ok) {
791
- if (isSpeculative) return {
868
+ if (isSpeculative && !htmlRender.fatal) return {
792
869
  route: routePattern,
793
870
  status: "skipped",
794
871
  reason: "dynamic"
@@ -796,7 +873,8 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
796
873
  return {
797
874
  route: routePattern,
798
875
  status: "error",
799
- error: `RSC handler returned ${htmlRender.status}`
876
+ error: `RSC handler returned ${htmlRender.status}`,
877
+ ...htmlRender.fatal ? { fatal: true } : {}
800
878
  };
801
879
  }
802
880
  if (htmlCacheControl.includes("no-store")) return {
@@ -919,11 +997,13 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
919
997
  ...outputFiles.length > 0 ? { outputFiles } : {}
920
998
  };
921
999
  } finally {
922
- if (renderPool) await renderPool.close();
923
- setCacheHandler(previousHandler);
924
- if (previousPrerenderFlag === void 0) delete process.env.VINEXT_PRERENDER;
925
- else process.env.VINEXT_PRERENDER = previousPrerenderFlag;
926
- if (ownedProdServerHandle) await new Promise((resolve) => ownedProdServerHandle.server.close(() => resolve()));
1000
+ try {
1001
+ if (renderPool) await renderPool.close();
1002
+ setCacheHandler(previousHandler);
1003
+ if (ownedProdServerHandle) await new Promise((resolve) => ownedProdServerHandle.server.close(() => resolve()));
1004
+ } finally {
1005
+ restorePrerenderPhase();
1006
+ }
927
1007
  }
928
1008
  }
929
1009
  function resolveRenderedCacheControl(requestCacheLife, cacheControl, fallbackExpireSeconds) {
@@ -991,7 +1071,9 @@ function writePrerenderIndex(routes, outDir, options) {
991
1071
  ...typeof r.stale === "number" ? { stale: r.stale } : {},
992
1072
  router: r.router,
993
1073
  ...r.tags && r.tags.length > 0 ? { tags: r.tags } : {},
1074
+ ...r.routeSegments ? { routeSegments: r.routeSegments } : {},
994
1075
  ...r.headers ? { headers: r.headers } : {},
1076
+ ...typeof r.responseStatus === "number" ? { responseStatus: r.responseStatus } : {},
995
1077
  ...r.path ? { path: r.path } : {},
996
1078
  ...r.fallback ? { fallback: true } : {}
997
1079
  };
@@ -1,6 +1,6 @@
1
+ import { AppRoute } from "../routing/app-route-graph.js";
1
2
  import { LayoutBuildClassification } from "./layout-classification-types.js";
2
3
  import { Route } from "../routing/pages-router.js";
3
- import { AppRoute } from "../routing/app-route-graph.js";
4
4
  import { PrerenderResult } from "./prerender.js";
5
5
  //#region src/build/report.d.ts
6
6
  type RouteType = "static" | "isr" | "ssr" | "unknown" | "api";
@@ -1,5 +1,5 @@
1
- import { ClassificationReason, ModuleGraphStaticReason } from "./layout-classification-types.js";
2
1
  import { AppRoute } from "../routing/app-route-graph.js";
2
+ import { ClassificationReason, ModuleGraphStaticReason } from "./layout-classification-types.js";
3
3
  //#region src/build/route-classification-manifest.d.ts
4
4
  type Layer1Class = "static" | "dynamic";
5
5
  type RouteManifestEntry = {