vinext 0.2.0 → 0.2.1

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 (128) hide show
  1. package/dist/build/css-url-assets.d.ts +3 -1
  2. package/dist/build/css-url-assets.js +18 -1
  3. package/dist/build/inject-pregenerated-paths.d.ts +3 -0
  4. package/dist/build/inject-pregenerated-paths.js +4 -1
  5. package/dist/build/prerender-server-entry.d.ts +1 -0
  6. package/dist/build/prerender-server-entry.js +49 -0
  7. package/dist/build/prerender-server-pool.d.ts +44 -0
  8. package/dist/build/prerender-server-pool.js +194 -0
  9. package/dist/build/prerender.js +86 -22
  10. package/dist/build/run-prerender.js +2 -1
  11. package/dist/check.d.ts +4 -6
  12. package/dist/check.js +8 -9
  13. package/dist/cli.js +1 -1
  14. package/dist/client/vinext-next-data.d.ts +4 -1
  15. package/dist/config/config-matchers.js +33 -11
  16. package/dist/config/next-config.d.ts +36 -6
  17. package/dist/config/next-config.js +16 -2
  18. package/dist/entries/app-browser-entry.js +5 -1
  19. package/dist/entries/app-rsc-entry.d.ts +3 -2
  20. package/dist/entries/app-rsc-entry.js +4 -0
  21. package/dist/entries/pages-client-entry.js +14 -3
  22. package/dist/entries/pages-server-entry.js +18 -4
  23. package/dist/index.js +320 -25
  24. package/dist/packages/cloudflare/src/cache/kv-data-adapter.runtime.js +8 -1
  25. package/dist/packages/cloudflare/src/deploy.js +1 -5
  26. package/dist/packages/cloudflare/src/tpr.js +26 -5
  27. package/dist/plugins/fonts.js +1 -1
  28. package/dist/plugins/ignore-dynamic-requests.js +1 -1
  29. package/dist/plugins/og-assets.js +2 -1
  30. package/dist/plugins/optimize-imports.js +1 -1
  31. package/dist/plugins/rsc-reference-validation-normalizer.d.ts +12 -0
  32. package/dist/plugins/rsc-reference-validation-normalizer.js +48 -0
  33. package/dist/plugins/sass.d.ts +17 -1
  34. package/dist/plugins/sass.js +74 -1
  35. package/dist/plugins/styled-jsx.d.ts +16 -0
  36. package/dist/plugins/styled-jsx.js +149 -0
  37. package/dist/routing/app-route-graph.js +22 -16
  38. package/dist/routing/file-matcher.d.ts +8 -1
  39. package/dist/routing/file-matcher.js +15 -3
  40. package/dist/server/app-browser-entry.js +46 -14
  41. package/dist/server/app-browser-navigation-controller.js +1 -1
  42. package/dist/server/app-browser-server-action-client.js +5 -3
  43. package/dist/server/app-browser-state.d.ts +1 -0
  44. package/dist/server/app-browser-state.js +2 -2
  45. package/dist/server/app-browser-visible-commit.js +1 -1
  46. package/dist/server/app-page-boundary.js +2 -1
  47. package/dist/server/app-page-cache-finalizer.d.ts +1 -0
  48. package/dist/server/app-page-cache-finalizer.js +3 -3
  49. package/dist/server/app-page-cache.js +23 -6
  50. package/dist/server/app-page-dispatch.d.ts +1 -0
  51. package/dist/server/app-page-dispatch.js +25 -8
  52. package/dist/server/app-page-element-builder.js +1 -0
  53. package/dist/server/app-page-execution.js +2 -1
  54. package/dist/server/app-page-render-identity.d.ts +1 -0
  55. package/dist/server/app-page-render-identity.js +2 -1
  56. package/dist/server/app-page-render.d.ts +2 -0
  57. package/dist/server/app-page-render.js +67 -16
  58. package/dist/server/app-page-response.d.ts +7 -0
  59. package/dist/server/app-page-response.js +16 -4
  60. package/dist/server/app-page-stream.d.ts +2 -1
  61. package/dist/server/app-page-stream.js +1 -1
  62. package/dist/server/app-route-handler-execution.js +1 -1
  63. package/dist/server/app-route-tree-prefetch.d.ts +43 -0
  64. package/dist/server/app-route-tree-prefetch.js +187 -0
  65. package/dist/server/app-router-entry.js +1 -1
  66. package/dist/server/app-rsc-cache-busting.d.ts +2 -1
  67. package/dist/server/app-rsc-cache-busting.js +9 -4
  68. package/dist/server/app-rsc-handler.d.ts +6 -0
  69. package/dist/server/app-rsc-handler.js +89 -11
  70. package/dist/server/app-rsc-render-mode.d.ts +3 -2
  71. package/dist/server/app-rsc-render-mode.js +4 -1
  72. package/dist/server/app-rsc-request-normalization.d.ts +3 -4
  73. package/dist/server/app-rsc-request-normalization.js +4 -5
  74. package/dist/server/app-server-action-execution.js +4 -3
  75. package/dist/server/app-ssr-entry.js +3 -3
  76. package/dist/server/cache-control.d.ts +3 -1
  77. package/dist/server/cache-control.js +13 -1
  78. package/dist/server/dev-server.d.ts +11 -1
  79. package/dist/server/dev-server.js +78 -20
  80. package/dist/server/headers.d.ts +6 -2
  81. package/dist/server/headers.js +11 -5
  82. package/dist/server/isr-cache.d.ts +2 -1
  83. package/dist/server/isr-cache.js +7 -3
  84. package/dist/server/pages-data-route.d.ts +4 -2
  85. package/dist/server/pages-data-route.js +18 -4
  86. package/dist/server/pages-dev-module-url.d.ts +2 -1
  87. package/dist/server/pages-dev-module-url.js +6 -3
  88. package/dist/server/pages-node-compat.d.ts +12 -1
  89. package/dist/server/pages-node-compat.js +50 -1
  90. package/dist/server/pages-page-data.d.ts +9 -0
  91. package/dist/server/pages-page-data.js +17 -9
  92. package/dist/server/pages-page-handler.js +19 -7
  93. package/dist/server/pages-page-response.d.ts +1 -0
  94. package/dist/server/pages-page-response.js +3 -2
  95. package/dist/server/pages-request-pipeline.d.ts +8 -6
  96. package/dist/server/pages-request-pipeline.js +30 -6
  97. package/dist/server/pages-router-entry.js +1 -1
  98. package/dist/server/prod-server.d.ts +4 -2
  99. package/dist/server/prod-server.js +38 -22
  100. package/dist/server/request-pipeline.js +1 -1
  101. package/dist/shims/cache-handler.js +8 -1
  102. package/dist/shims/cache.js +3 -0
  103. package/dist/shims/fetch-cache.d.ts +3 -1
  104. package/dist/shims/fetch-cache.js +77 -52
  105. package/dist/shims/form.d.ts +1 -1
  106. package/dist/shims/internal/app-prefetch-fetch-queue.d.ts +10 -0
  107. package/dist/shims/internal/app-prefetch-fetch-queue.js +61 -0
  108. package/dist/shims/internal/hybrid-client-route-owner.d.ts +2 -1
  109. package/dist/shims/internal/hybrid-client-route-owner.js +34 -1
  110. package/dist/shims/internal/pages-data-fetch-dedup.d.ts +2 -1
  111. package/dist/shims/internal/pages-data-fetch-dedup.js +13 -2
  112. package/dist/shims/internal/pages-data-target.d.ts +8 -3
  113. package/dist/shims/internal/pages-data-target.js +9 -4
  114. package/dist/shims/link.d.ts +1 -1
  115. package/dist/shims/link.js +179 -42
  116. package/dist/shims/navigation.d.ts +15 -3
  117. package/dist/shims/navigation.js +202 -39
  118. package/dist/shims/navigation.react-server.js +1 -1
  119. package/dist/shims/request-context.js +18 -0
  120. package/dist/shims/router.js +31 -15
  121. package/dist/shims/unified-request-context.js +1 -0
  122. package/dist/typegen.js +1 -1
  123. package/dist/utils/middleware-request-headers.js +1 -1
  124. package/dist/utils/protocol-headers.d.ts +7 -1
  125. package/dist/utils/protocol-headers.js +7 -1
  126. package/dist/utils/vite-version.d.ts +2 -0
  127. package/dist/utils/vite-version.js +11 -1
  128. package/package.json +2 -2
@@ -13,6 +13,8 @@ type EmitRestoredCssUrlAsset = (asset: {
13
13
  * safe to run on every build-environment stylesheet, vendored CSS included.
14
14
  */
15
15
  declare function markCssUrlAssetReferences(code: string, id: string): string | null;
16
+ /** Rebase relative asset URLs when Sass inlines a partial into an entry file. */
17
+ declare function rebaseCssUrlAssetReferences(code: string, sourceDirectory: string, targetDirectory: string): string | null;
16
18
  /**
17
19
  * Mutates emitted CSS assets in place so byte-identical `url()` dependencies
18
20
  * keep their distinct Next-compatible filenames. Sibling files are produced via
@@ -26,4 +28,4 @@ declare function markCssUrlAssetReferences(code: string, id: string): string | n
26
28
  */
27
29
  declare function restoreDedupedCssAssetReferences(bundle: CssUrlAssetBundle, emitRestoredAsset: EmitRestoredCssUrlAsset): void;
28
30
  //#endregion
29
- export { markCssUrlAssetReferences, restoreDedupedCssAssetReferences };
31
+ export { markCssUrlAssetReferences, rebaseCssUrlAssetReferences, restoreDedupedCssAssetReferences };
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  //#region src/build/css-url-assets.ts
2
3
  /**
3
4
  * Next-parity for CSS `url()` asset dependencies.
@@ -128,6 +129,22 @@ function markCssUrlAssetReferences(code, id) {
128
129
  });
129
130
  });
130
131
  }
132
+ /** Rebase relative asset URLs when Sass inlines a partial into an entry file. */
133
+ function rebaseCssUrlAssetReferences(code, sourceDirectory, targetDirectory) {
134
+ return rewriteCssUrls(code, (rawUrl) => {
135
+ const parts = splitUrl(rawUrl.trim());
136
+ if (!isRelativeAssetUrl(parts.path)) return null;
137
+ const lower = parts.path.toLowerCase();
138
+ if (!CSS_ASSET_EXT_RE.test(lower) || lower.endsWith(".css")) return null;
139
+ const absolutePath = path.resolve(sourceDirectory, parts.path);
140
+ let rebasedPath = path.relative(targetDirectory, absolutePath).replaceAll("\\", "/");
141
+ if (!rebasedPath.startsWith(".")) rebasedPath = `./${rebasedPath}`;
142
+ return joinUrl({
143
+ ...parts,
144
+ path: rebasedPath
145
+ });
146
+ });
147
+ }
131
148
  function isCssFileName(fileName) {
132
149
  return fileName.toLowerCase().endsWith(".css");
133
150
  }
@@ -210,4 +227,4 @@ function stripChunkMarkers(code) {
210
227
  return code.replace(CSS_URL_MARKER_PARAM_RE, (_m, leadingQuestion) => leadingQuestion ? leadingQuestion : "");
211
228
  }
212
229
  //#endregion
213
- export { markCssUrlAssetReferences, restoreDedupedCssAssetReferences };
230
+ export { markCssUrlAssetReferences, rebaseCssUrlAssetReferences, restoreDedupedCssAssetReferences };
@@ -1,4 +1,7 @@
1
1
  //#region src/build/inject-pregenerated-paths.d.ts
2
+ declare global {
3
+ var __VINEXT_PREGENERATED_CONCRETE_PATHS: unknown;
4
+ }
2
5
  declare function injectPregeneratedConcretePaths(root: string): void;
3
6
  //#endregion
4
7
  export { injectPregeneratedConcretePaths };
@@ -11,7 +11,10 @@ function injectPregeneratedConcretePaths(root) {
11
11
  if (!fs.existsSync(workerEntry)) return;
12
12
  let code = fs.readFileSync(workerEntry, "utf-8").replace(VINEXT_PREGEN_RE, "");
13
13
  const table = readPrerenderManifest(path.join(root, "dist", "server", "vinext-prerender.json"))?.pregeneratedConcretePaths ?? [];
14
- if (table.length > 0) code = `${VINEXT_PREGEN_START}\nglobalThis.__VINEXT_PREGENERATED_CONCRETE_PATHS = ${JSON.stringify(table)};\n${VINEXT_PREGEN_END}\n` + code;
14
+ if (table.length > 0) {
15
+ globalThis.__VINEXT_PREGENERATED_CONCRETE_PATHS = table;
16
+ code = `${VINEXT_PREGEN_START}\nglobalThis.__VINEXT_PREGENERATED_CONCRETE_PATHS = ${JSON.stringify(table)};\n${VINEXT_PREGEN_END}\n` + code;
17
+ } else delete globalThis.__VINEXT_PREGENERATED_CONCRETE_PATHS;
15
18
  fs.writeFileSync(workerEntry, code);
16
19
  }
17
20
  //#endregion
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,49 @@
1
+ import { NoOpCacheHandler, setCacheHandler } from "../shims/cache-handler.js";
2
+ import { startProdServer } from "../server/prod-server.js";
3
+ //#region src/build/prerender-server-entry.ts
4
+ /**
5
+ * Child-process entry for a parallel prerender render server.
6
+ *
7
+ * Forked by `prerender-server-pool.ts`. React SSR/RSC rendering is CPU-bound
8
+ * JS, so a single-process promise pool serializes every render on one core.
9
+ * This entry lets the prerender phase fan rendering out across N OS processes
10
+ * (one per core, like Next.js's static worker pool): each child starts a normal
11
+ * vinext production server on an ephemeral port and reports it to the parent
12
+ * over IPC. The parent then load-balances per-route fetches across the pool.
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).
17
+ */
18
+ async function main() {
19
+ const outDir = process.env.VINEXT_PRERENDER_OUTDIR;
20
+ if (!outDir) throw new Error("[vinext] prerender server worker: VINEXT_PRERENDER_OUTDIR not set");
21
+ setCacheHandler(new NoOpCacheHandler());
22
+ const { port } = await startProdServer({
23
+ port: 0,
24
+ host: "127.0.0.1",
25
+ outDir,
26
+ noCompression: true,
27
+ purpose: "prerender",
28
+ silent: true
29
+ });
30
+ if (typeof process.send === "function") process.send({
31
+ type: "ready",
32
+ port
33
+ });
34
+ else throw new Error("[vinext] prerender server worker: no IPC channel to parent");
35
+ }
36
+ process.on("disconnect", () => process.exit(0));
37
+ main().catch((err) => {
38
+ const message = err instanceof Error ? err.stack ?? err.message : String(err);
39
+ if (typeof process.send === "function") {
40
+ process.send({
41
+ type: "error",
42
+ error: message
43
+ }, () => process.exit(1));
44
+ return;
45
+ }
46
+ process.exit(1);
47
+ });
48
+ //#endregion
49
+ export {};
@@ -0,0 +1,44 @@
1
+ //#region src/build/prerender-server-pool.d.ts
2
+ /**
3
+ * Whether the forkable worker entry exists as a runnable `.js` sibling. False
4
+ * when vinext runs from source (e.g. the test suite transpiles `.ts` on the
5
+ * fly and a forked plain-node child can't load `.ts`); callers fall back to the
6
+ * single in-process server. True in the published/built package.
7
+ */
8
+ declare function prerenderPoolAvailable(): boolean;
9
+ type PrerenderServerPool = {
10
+ /** Ports of the forked render servers, for round-robin per-route fetches. */ports: number[];
11
+ /**
12
+ * Throw if any worker exited unexpectedly (i.e. not via `close()`). The
13
+ * caller MUST call this after the render loop: a crashed worker makes every
14
+ * route routed to it fail with a connection error, which the per-route
15
+ * handler records as a (non-fatal) error — without this check the build
16
+ * would emit partial output and still exit 0. Mirrors the single-process
17
+ * design, where a server crash takes the whole build down loudly.
18
+ */
19
+ assertHealthy: () => void;
20
+ /**
21
+ * Record a failed fetch to a render worker. This closes the tiny ordering gap
22
+ * where the request can fail before Node has delivered the child `exit` event.
23
+ */
24
+ recordRenderError: (err: unknown) => void; /** Kill every forked server. Safe to call more than once. */
25
+ close: () => Promise<void>;
26
+ };
27
+ /**
28
+ * Choose how many render processes to fork for `routeCount` routes.
29
+ *
30
+ * Returns 1 to mean "don't fork — render in-process as before" (small apps,
31
+ * single/dual-core, low memory, or an explicit `--prerender-concurrency 1`).
32
+ * `maxOverride` comes from `--prerender-concurrency`, which caps both the
33
+ * number of in-flight route fetches and the worker count (the default is capped
34
+ * at `min(cores, 8)`).
35
+ */
36
+ declare function resolvePrerenderPoolSize(routeCount: number, maxOverride?: number): number;
37
+ /**
38
+ * Fork `size` production servers against `outDir` and resolve once all are
39
+ * listening. Each child reports its port over IPC. Rejects (and tears down any
40
+ * already-started children) if any child fails to come up.
41
+ */
42
+ declare function startPrerenderServerPool(outDir: string, size: number, entry?: string): Promise<PrerenderServerPool>;
43
+ //#endregion
44
+ export { PrerenderServerPool, prerenderPoolAvailable, resolvePrerenderPoolSize, startPrerenderServerPool };
@@ -0,0 +1,194 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import os from "node:os";
5
+ import { fork } from "node:child_process";
6
+ //#region src/build/prerender-server-pool.ts
7
+ /**
8
+ * Parallel prerender server pool.
9
+ *
10
+ * React SSR/RSC rendering during prerender is CPU-bound JS. vinext renders
11
+ * every route by fetching it from a single in-process production server, so the
12
+ * promise pool in `prerender.ts` only overlaps I/O — the actual rendering
13
+ * serializes on one core. This module forks a pool of OS processes (one vinext
14
+ * production server each, on its own ephemeral port) so the prerender phase can
15
+ * spread rendering across cores, mirroring Next.js's static worker pool. The
16
+ * caller load-balances per-route fetches across `ports`.
17
+ *
18
+ * child_process (not worker_threads) is deliberate: worker threads share the
19
+ * process and contend badly for CPU on this workload (measured ~2× slower
20
+ * per route and non-scaling), which is also why Next.js uses processes.
21
+ */
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ const WORKER_ENTRY = path.join(__dirname, "prerender-server-entry.js");
24
+ /**
25
+ * Whether the forkable worker entry exists as a runnable `.js` sibling. False
26
+ * when vinext runs from source (e.g. the test suite transpiles `.ts` on the
27
+ * fly and a forked plain-node child can't load `.ts`); callers fall back to the
28
+ * single in-process server. True in the published/built package.
29
+ */
30
+ function prerenderPoolAvailable() {
31
+ return fs.existsSync(WORKER_ENTRY);
32
+ }
33
+ /** Hard cap on render processes. Beyond this the main thread (which collects
34
+ * HTML and writes files) becomes the bottleneck, so more workers don't help. */
35
+ const MAX_POOL_SIZE = 8;
36
+ /** Don't fork a dedicated process unless a worker gets at least this many
37
+ * routes — below this the fork + bundle-import startup cost outweighs the
38
+ * parallel-render saving (measured: a 2-worker pool only reliably beats the
39
+ * single server from ~50 routes/worker up). */
40
+ const MIN_ROUTES_PER_WORKER = 48;
41
+ /** Rough memory budget reserved for the main build process plus its in-process
42
+ * prerender server. */
43
+ const APPROX_MAIN_PROCESS_BYTES = 768 * 1024 * 1024;
44
+ /** Rough memory budget per worker (a worker loads the whole server bundle).
45
+ * Used with total system memory as a coarse upper bound on constrained machines
46
+ * so K render processes don't OOM. Deliberately under-forks rather than risking
47
+ * OOM; measured mid-size apps are usually lower (~260-380 MB/worker). */
48
+ const APPROX_BYTES_PER_WORKER = 768 * 1024 * 1024;
49
+ /** Per-worker readiness timeout. */
50
+ const WORKER_READY_TIMEOUT_MS = 6e4;
51
+ /** Best-effort close timeout so build cleanup is not blocked forever by a wedged child. */
52
+ const WORKER_CLOSE_TIMEOUT_MS = 5e3;
53
+ const PRERENDER_WORKER_TRANSPORT_ERROR_CODES = new Set([
54
+ "ECONNREFUSED",
55
+ "ECONNRESET",
56
+ "EPIPE",
57
+ "UND_ERR_SOCKET"
58
+ ]);
59
+ /**
60
+ * Choose how many render processes to fork for `routeCount` routes.
61
+ *
62
+ * Returns 1 to mean "don't fork — render in-process as before" (small apps,
63
+ * single/dual-core, low memory, or an explicit `--prerender-concurrency 1`).
64
+ * `maxOverride` comes from `--prerender-concurrency`, which caps both the
65
+ * number of in-flight route fetches and the worker count (the default is capped
66
+ * at `min(cores, 8)`).
67
+ */
68
+ function resolvePrerenderPoolSize(routeCount, maxOverride) {
69
+ const cores = Math.max(1, os.availableParallelism());
70
+ const byCores = Math.max(1, Math.min(cores - 1, MAX_POOL_SIZE));
71
+ const memoryForWorkers = Math.max(0, os.totalmem() - APPROX_MAIN_PROCESS_BYTES);
72
+ const byMemory = Math.max(1, Math.floor(memoryForWorkers / APPROX_BYTES_PER_WORKER));
73
+ const cap = maxOverride && maxOverride > 0 ? Math.min(maxOverride, byCores) : byCores;
74
+ const byRoutes = Math.floor(routeCount / MIN_ROUTES_PER_WORKER);
75
+ return Math.max(1, Math.min(cap, byMemory, byRoutes));
76
+ }
77
+ function getErrorCauseCode(err) {
78
+ if (!(err instanceof Error)) return void 0;
79
+ const cause = err.cause;
80
+ return typeof cause?.code === "string" ? cause.code : void 0;
81
+ }
82
+ function isWorkerTransportError(err) {
83
+ const code = getErrorCauseCode(err);
84
+ if (code && PRERENDER_WORKER_TRANSPORT_ERROR_CODES.has(code)) return true;
85
+ return err instanceof TypeError && err.message === "fetch failed";
86
+ }
87
+ /**
88
+ * Fork `size` production servers against `outDir` and resolve once all are
89
+ * listening. Each child reports its port over IPC. Rejects (and tears down any
90
+ * already-started children) if any child fails to come up.
91
+ */
92
+ async function startPrerenderServerPool(outDir, size, entry = WORKER_ENTRY) {
93
+ const children = [];
94
+ let shuttingDown = false;
95
+ let crash = null;
96
+ let renderTransportError = null;
97
+ const close = async () => {
98
+ shuttingDown = true;
99
+ await Promise.all(children.map(closeChild));
100
+ };
101
+ try {
102
+ const readies = Array.from({ length: size }, () => {
103
+ const child = fork(entry, [], {
104
+ cwd: process.cwd(),
105
+ env: {
106
+ ...process.env,
107
+ VINEXT_PRERENDER: "1",
108
+ VINEXT_PRERENDER_OUTDIR: outDir
109
+ },
110
+ stdio: [
111
+ "ignore",
112
+ "inherit",
113
+ "inherit",
114
+ "ipc"
115
+ ]
116
+ });
117
+ children.push(child);
118
+ return new Promise((resolve, reject) => {
119
+ const timer = setTimeout(() => {
120
+ reject(/* @__PURE__ */ new Error("[vinext] prerender render server did not start within 60s"));
121
+ }, WORKER_READY_TIMEOUT_MS);
122
+ const cleanupStartupListeners = () => {
123
+ clearTimeout(timer);
124
+ child.off("message", onMessage);
125
+ child.off("error", onError);
126
+ child.off("exit", onStartupExit);
127
+ };
128
+ const onMessage = (msg) => {
129
+ cleanupStartupListeners();
130
+ if (msg?.type === "ready" && typeof msg.port === "number") {
131
+ child.once("exit", (code, signal) => {
132
+ if (!shuttingDown && !crash) crash = {
133
+ port: msg.port,
134
+ code,
135
+ signal
136
+ };
137
+ });
138
+ resolve(msg.port);
139
+ } else reject(/* @__PURE__ */ new Error(`[vinext] prerender render server failed: ${msg?.error ?? "unknown"}`));
140
+ };
141
+ const onError = (err) => {
142
+ cleanupStartupListeners();
143
+ reject(err);
144
+ };
145
+ const onStartupExit = (code, signal) => {
146
+ if (code !== null || signal !== null) {
147
+ cleanupStartupListeners();
148
+ reject(/* @__PURE__ */ new Error(`[vinext] prerender render server exited during startup (code ${code}, signal ${signal})`));
149
+ }
150
+ };
151
+ child.once("message", onMessage);
152
+ child.once("error", onError);
153
+ child.once("exit", onStartupExit);
154
+ });
155
+ });
156
+ const ports = await Promise.all(readies);
157
+ const recordRenderError = (err) => {
158
+ if (!renderTransportError && isWorkerTransportError(err)) renderTransportError = err instanceof Error ? err : new Error(String(err));
159
+ };
160
+ const assertHealthy = () => {
161
+ if (crash) throw new Error(`[vinext] A prerender render worker (port ${crash.port}) exited unexpectedly (code ${crash.code}, signal ${crash.signal}) during the build. Prerender output is incomplete; failing the build. This is often an out-of-memory kill — try a lower --prerender-concurrency.`);
162
+ if (renderTransportError) {
163
+ const causeCode = getErrorCauseCode(renderTransportError);
164
+ throw new Error(`[vinext] A prerender render worker request failed` + (causeCode ? ` (${causeCode})` : "") + " before the worker exit was observed. Prerender output is incomplete; failing the build. This is often an out-of-memory kill — try a lower --prerender-concurrency.");
165
+ }
166
+ };
167
+ return {
168
+ ports,
169
+ assertHealthy,
170
+ recordRenderError,
171
+ close
172
+ };
173
+ } catch (err) {
174
+ await close();
175
+ throw err;
176
+ }
177
+ }
178
+ async function closeChild(child) {
179
+ if (child.exitCode !== null || child.signalCode !== null) return;
180
+ const exited = new Promise((resolve) => {
181
+ const timer = setTimeout(resolve, WORKER_CLOSE_TIMEOUT_MS);
182
+ timer.unref?.();
183
+ const finish = () => {
184
+ clearTimeout(timer);
185
+ resolve();
186
+ };
187
+ child.once("exit", finish);
188
+ if (child.exitCode !== null || child.signalCode !== null) finish();
189
+ });
190
+ if (!child.killed) child.kill("SIGKILL");
191
+ await exited;
192
+ }
193
+ //#endregion
194
+ export { prerenderPoolAvailable, resolvePrerenderPoolSize, startPrerenderServerPool };
@@ -1,5 +1,6 @@
1
1
  import { createValidFileMatcher, findFileWithExtensions } from "../routing/file-matcher.js";
2
- import { VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER } from "../utils/protocol-headers.js";
2
+ import { VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER, VINEXT_PRERENDER_SPECULATIVE_HEADER } from "../utils/protocol-headers.js";
3
+ import { VINEXT_PRERENDER_CACHE_LIFE_HEADER } from "../server/headers.js";
3
4
  import { classifyAppRoute, classifyPagesRoute, getAppRouteRenderEntryPath } from "./report.js";
4
5
  import { NoOpCacheHandler, getCacheHandler, setCacheHandler } from "../shims/cache-handler.js";
5
6
  import { headersContextFromRequest, runWithHeadersContext } from "../shims/headers.js";
@@ -14,6 +15,7 @@ import { readPrerenderSecret } from "./server-manifest.js";
14
15
  import { createAppPprFallbackShells, markAppPprDynamicFallbackShellHtml } from "../server/app-ppr-fallback-shell.js";
15
16
  import { getOutputPath, getRscOutputPath } from "../utils/prerender-output-paths.js";
16
17
  import { startProdServer } from "../server/prod-server.js";
18
+ import { prerenderPoolAvailable, resolvePrerenderPoolSize, startPrerenderServerPool } from "./prerender-server-pool.js";
17
19
  import fs from "node:fs";
18
20
  import path from "node:path";
19
21
  import os from "node:os";
@@ -42,6 +44,14 @@ function isExperimentalPprFallbackShellGenerationEnabled(env = process.env) {
42
44
  function getErrorMessageWithStack(err) {
43
45
  return err.stack || err.message;
44
46
  }
47
+ async function startOptionalPrerenderServerPool(outDir, poolSize) {
48
+ try {
49
+ return await startPrerenderServerPool(outDir, poolSize);
50
+ } catch (e) {
51
+ console.warn(`[vinext] prerender render pool failed to start; falling back to single-process render: ${e instanceof Error ? e.message : String(e)}`);
52
+ return null;
53
+ }
54
+ }
45
55
  var PrerenderUserFunctionError = class extends Error {};
46
56
  function parsePrerenderEndpointError(text) {
47
57
  try {
@@ -257,11 +267,12 @@ async function prerenderPages({ routes, apiRoutes, pagesDir, outDir, config, mod
257
267
  const previousPrerenderFlag = process.env.VINEXT_PRERENDER;
258
268
  process.env.VINEXT_PRERENDER = "1";
259
269
  let ownedProdServerHandle = null;
270
+ let renderPool = null;
260
271
  try {
261
272
  let prerenderSecret = options._prerenderSecret;
262
273
  if (!prerenderSecret && pagesBundlePath) prerenderSecret = readPrerenderSecret(path.dirname(pagesBundlePath));
263
274
  if (!prerenderSecret) console.warn("[vinext] Warning: prerender secret not found. /__vinext/prerender/* endpoints will return 403 and dynamic routes will produce no paths. Run `vinext build` to regenerate the secret.");
264
- const baseUrl = `http://127.0.0.1:${(options._prodServer ? options._prodServer : await (async () => {
275
+ const prodServer = options._prodServer ? options._prodServer : await (async () => {
265
276
  const srv = await startProdServer({
266
277
  port: 0,
267
278
  host: "127.0.0.1",
@@ -271,12 +282,18 @@ async function prerenderPages({ routes, apiRoutes, pagesDir, outDir, config, mod
271
282
  });
272
283
  ownedProdServerHandle = srv;
273
284
  return srv;
274
- })()).port}`;
285
+ })();
286
+ const baseUrl = `http://127.0.0.1:${prodServer.port}`;
275
287
  const secretHeaders = prerenderSecret ? { [VINEXT_PRERENDER_SECRET_HEADER]: prerenderSecret } : {};
276
- const renderPage = (urlPath) => fetch(`${baseUrl}${urlPath}`, {
277
- headers: secretHeaders,
278
- redirect: "manual"
279
- });
288
+ let renderPorts = [prodServer.port];
289
+ let renderRoundRobin = 0;
290
+ const renderPage = (urlPath) => {
291
+ const port = renderPorts[renderRoundRobin++ % renderPorts.length];
292
+ return fetch(`http://127.0.0.1:${port}${urlPath}`, {
293
+ headers: secretHeaders,
294
+ redirect: "manual"
295
+ });
296
+ };
280
297
  const bundlePageRoutes = routes.map((r) => ({
281
298
  pattern: r.pattern,
282
299
  isDynamic: r.isDynamic ?? false,
@@ -397,6 +414,13 @@ async function prerenderPages({ routes, apiRoutes, pagesDir, outDir, config, mod
397
414
  revalidate
398
415
  });
399
416
  }
417
+ if (!options._prodServer && pagesBundlePath && prerenderPoolAvailable()) {
418
+ const poolSize = resolvePrerenderPoolSize(pagesToRender.length, concurrency);
419
+ if (poolSize > 1) {
420
+ renderPool = await startOptionalPrerenderServerPool(path.dirname(path.dirname(pagesBundlePath)), poolSize);
421
+ if (renderPool) renderPorts = renderPool.ports;
422
+ }
423
+ }
400
424
  let completed = 0;
401
425
  const pageResults = await runWithConcurrency(pagesToRender, concurrency, async ({ route, urlPath, revalidate }) => {
402
426
  let result;
@@ -427,6 +451,7 @@ async function prerenderPages({ routes, apiRoutes, pagesDir, outDir, config, mod
427
451
  ...urlPath !== route.pattern ? { path: urlPath } : {}
428
452
  };
429
453
  } catch (e) {
454
+ renderPool?.recordRenderError(e);
430
455
  const err = e;
431
456
  result = {
432
457
  route: route.pattern,
@@ -443,6 +468,7 @@ async function prerenderPages({ routes, apiRoutes, pagesDir, outDir, config, mod
443
468
  return result;
444
469
  });
445
470
  results.push(...pageResults);
471
+ renderPool?.assertHealthy();
446
472
  const hasCustom404 = findFileWithExtensions(path.join(pagesDir, "404"), fileMatcher);
447
473
  const hasErrorPage = findFileWithExtensions(path.join(pagesDir, "_error"), fileMatcher);
448
474
  if (hasCustom404 || hasErrorPage) try {
@@ -460,13 +486,17 @@ async function prerenderPages({ routes, apiRoutes, pagesDir, outDir, config, mod
460
486
  router: "pages"
461
487
  });
462
488
  }
463
- } catch {}
489
+ } catch (e) {
490
+ renderPool?.recordRenderError(e);
491
+ }
492
+ renderPool?.assertHealthy();
464
493
  if (!skipManifest) writePrerenderIndex(results, manifestDir, {
465
494
  buildId: config.buildId,
466
495
  trailingSlash: config.trailingSlash
467
496
  });
468
497
  return { routes: results };
469
498
  } finally {
499
+ if (renderPool) await renderPool.close();
470
500
  setCacheHandler(previousHandler);
471
501
  if (previousPrerenderFlag === void 0) delete process.env.VINEXT_PRERENDER;
472
502
  else process.env.VINEXT_PRERENDER = previousPrerenderFlag;
@@ -505,10 +535,13 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
505
535
  let rscHandler;
506
536
  let staticParamsMap = {};
507
537
  let ownedProdServerHandle = null;
538
+ let renderPool = null;
539
+ let renderPorts = [];
540
+ let renderRoundRobin = 0;
508
541
  try {
509
542
  const prerenderSecret = readPrerenderSecret(serverDir);
510
543
  if (!prerenderSecret) console.warn("[vinext] Warning: prerender secret not found. /__vinext/prerender/* endpoints will return 403 and generateStaticParams will not be called. Run `vinext build` to regenerate the secret.");
511
- const baseUrl = `http://127.0.0.1:${(options._prodServer ? options._prodServer : await (async () => {
544
+ const prodServer = options._prodServer ? options._prodServer : await (async () => {
512
545
  const srv = await startProdServer({
513
546
  port: 0,
514
547
  host: "127.0.0.1",
@@ -518,11 +551,13 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
518
551
  });
519
552
  ownedProdServerHandle = srv;
520
553
  return srv;
521
- })()).port}`;
554
+ })();
555
+ const baseUrl = `http://127.0.0.1:${prodServer.port}`;
556
+ renderPorts = [prodServer.port];
522
557
  const secretHeaders = prerenderSecret ? { [VINEXT_PRERENDER_SECRET_HEADER]: prerenderSecret } : {};
523
558
  rscHandler = (req) => {
524
559
  const parsed = new URL(req.url);
525
- const url = `${baseUrl}${parsed.pathname}${parsed.search}`;
560
+ const url = `http://127.0.0.1:${renderPorts[renderRoundRobin++ % renderPorts.length]}${parsed.pathname}${parsed.search}`;
526
561
  return fetch(url, {
527
562
  method: req.method,
528
563
  headers: {
@@ -718,12 +753,14 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
718
753
  const prerenderRouteParamsHeader = serializePrerenderRouteParamsHeader(prerenderRouteParams);
719
754
  const htmlHeaders = new Headers();
720
755
  if (prerenderRouteParamsHeader !== null) htmlHeaders.set(VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, prerenderRouteParamsHeader);
756
+ if (isSpeculative) htmlHeaders.set(VINEXT_PRERENDER_SPECULATIVE_HEADER, "1");
721
757
  const htmlRequest = new Request(`http://localhost${urlPath}`, { headers: htmlHeaders });
722
758
  const htmlRender = await runWithHeadersContext(headersContextFromRequest(htmlRequest), async () => {
723
759
  const response = await rscHandler(htmlRequest);
724
760
  const cacheControl = response.headers.get("cache-control") ?? "";
725
761
  const linkHeader = response.headers.get("link");
726
- if (!response.ok || isSpeculative && cacheControl.includes("no-store")) {
762
+ const responseCacheLife = readPrerenderCacheLifeHeader(response.headers);
763
+ if (!response.ok || cacheControl.includes("no-store")) {
727
764
  await response.body?.cancel();
728
765
  return {
729
766
  cacheControl,
@@ -734,12 +771,14 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
734
771
  status: response.status
735
772
  };
736
773
  }
774
+ const html = await response.text();
775
+ const processCacheLife = _consumeRequestScopedCacheLife();
737
776
  return {
738
777
  cacheControl,
739
778
  linkHeader,
740
- html: await response.text(),
779
+ html,
741
780
  ok: true,
742
- requestCacheLife: _consumeRequestScopedCacheLife(),
781
+ requestCacheLife: responseCacheLife ?? processCacheLife,
743
782
  status: response.status
744
783
  };
745
784
  });
@@ -756,13 +795,11 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
756
795
  error: `RSC handler returned ${htmlRender.status}`
757
796
  };
758
797
  }
759
- if (isSpeculative) {
760
- if (htmlCacheControl.includes("no-store")) return {
761
- route: routePattern,
762
- status: "skipped",
763
- reason: "dynamic"
764
- };
765
- }
798
+ if (htmlCacheControl.includes("no-store")) return {
799
+ route: routePattern,
800
+ status: "skipped",
801
+ reason: "dynamic"
802
+ };
766
803
  if (htmlRender.html === null) return {
767
804
  route: routePattern,
768
805
  status: "error",
@@ -776,6 +813,7 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
776
813
  RSC: "1"
777
814
  });
778
815
  if (prerenderRouteParamsHeader !== null) rscHeaders.set(VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, prerenderRouteParamsHeader);
816
+ if (isSpeculative) rscHeaders.set(VINEXT_PRERENDER_SPECULATIVE_HEADER, "1");
779
817
  const rscRequest = new Request(`http://localhost${urlPath}`, { headers: rscHeaders });
780
818
  const rscRes = await runWithHeadersContext(headersContextFromRequest(rscRequest), () => rscHandler(rscRequest));
781
819
  if (!rscRes.ok) {
@@ -809,6 +847,7 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
809
847
  ...isFallback ? { fallback: true } : {}
810
848
  };
811
849
  } catch (e) {
850
+ renderPool?.recordRenderError(e);
812
851
  if (isSpeculative) return {
813
852
  route: routePattern,
814
853
  status: "skipped",
@@ -823,6 +862,13 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
823
862
  };
824
863
  }
825
864
  }
865
+ if (!options._prodServer && prerenderPoolAvailable()) {
866
+ const poolSize = resolvePrerenderPoolSize(urlsToRender.length, concurrency);
867
+ if (poolSize > 1) {
868
+ renderPool = await startOptionalPrerenderServerPool(path.dirname(serverDir), poolSize);
869
+ if (renderPool) renderPorts = renderPool.ports;
870
+ }
871
+ }
826
872
  let completedApp = 0;
827
873
  const appResults = await runWithConcurrency(urlsToRender, concurrency, async (urlToRender) => {
828
874
  const result = await renderUrl(urlToRender);
@@ -835,6 +881,7 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
835
881
  return result;
836
882
  });
837
883
  results.push(...appResults);
884
+ renderPool?.assertHealthy();
838
885
  const outputFiles = mode === "export" && metadataRoutes.length > 0 ? emitStaticMetadataFiles(metadataRoutes, outDir) : [];
839
886
  try {
840
887
  const notFoundRequest = new Request(`http://localhost${NOT_FOUND_SENTINEL_PATH}`);
@@ -852,7 +899,10 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
852
899
  router: "app"
853
900
  });
854
901
  }
855
- } catch {}
902
+ } catch (e) {
903
+ renderPool?.recordRenderError(e);
904
+ }
905
+ renderPool?.assertHealthy();
856
906
  if (!skipManifest) writePrerenderIndex(results, manifestDir, {
857
907
  buildId: config.buildId,
858
908
  trailingSlash: config.trailingSlash
@@ -862,6 +912,7 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
862
912
  ...outputFiles.length > 0 ? { outputFiles } : {}
863
913
  };
864
914
  } finally {
915
+ if (renderPool) await renderPool.close();
865
916
  setCacheHandler(previousHandler);
866
917
  if (previousPrerenderFlag === void 0) delete process.env.VINEXT_PRERENDER;
867
918
  else process.env.VINEXT_PRERENDER = previousPrerenderFlag;
@@ -881,6 +932,19 @@ function resolveRenderedCacheControl(requestCacheLife, cacheControl, fallbackExp
881
932
  ...revalidate === void 0 ? {} : { revalidate }
882
933
  };
883
934
  }
935
+ function readPrerenderCacheLifeHeader(headers) {
936
+ const value = headers.get(VINEXT_PRERENDER_CACHE_LIFE_HEADER);
937
+ if (!value) return null;
938
+ try {
939
+ const parsed = JSON.parse(value);
940
+ const cacheLife = {};
941
+ if (typeof parsed.revalidate === "number" && Number.isFinite(parsed.revalidate)) cacheLife.revalidate = parsed.revalidate;
942
+ if (typeof parsed.expire === "number" && Number.isFinite(parsed.expire)) cacheLife.expire = parsed.expire;
943
+ return cacheLife.revalidate === void 0 && cacheLife.expire === void 0 ? null : cacheLife;
944
+ } catch {
945
+ return null;
946
+ }
947
+ }
884
948
  function resolveRenderedExpireSeconds(options) {
885
949
  const { fallbackExpireSeconds, sMaxage, staleWhileRevalidate } = options;
886
950
  if (sMaxage === void 0 || staleWhileRevalidate === void 0) return fallbackExpireSeconds;
@@ -4,7 +4,7 @@ import { appRouter } from "../routing/app-router.js";
4
4
  import { scanMetadataFiles } from "../server/metadata-routes.js";
5
5
  import { loadNextConfig, resolveNextConfig } from "../config/next-config.js";
6
6
  import { readPrerenderSecret } from "./server-manifest.js";
7
- import { startProdServer } from "../server/prod-server.js";
7
+ import { rememberCurrentServerEntryImportMtime, startProdServer } from "../server/prod-server.js";
8
8
  import { prerenderApp, prerenderPages, writePrerenderIndex } from "./prerender.js";
9
9
  import { injectPregeneratedConcretePaths } from "./inject-pregenerated-paths.js";
10
10
  import fs from "node:fs";
@@ -224,6 +224,7 @@ async function runPrerender(options) {
224
224
  throw new Error(`Static export failed: ${errors} route${errors !== 1 ? "s" : ""} cannot be statically exported.\n${errorRoutes}\n\nRemove server-side data fetching (getServerSideProps, force-dynamic, revalidate) from these routes, or remove \`output: "export"\` from next.config.js.`);
225
225
  }
226
226
  injectPregeneratedConcretePaths(root);
227
+ if (fs.existsSync(rscBundlePath)) rememberCurrentServerEntryImportMtime(rscBundlePath);
227
228
  return {
228
229
  routes: allRoutes,
229
230
  ...allOutputFiles.length > 0 ? { outputFiles: allOutputFiles } : {}