vinext 0.2.0 → 1.0.0-beta.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 (168) hide show
  1. package/README.md +50 -29
  2. package/dist/build/client-build-config.d.ts +13 -92
  3. package/dist/build/client-build-config.js +17 -95
  4. package/dist/build/css-url-assets.d.ts +3 -1
  5. package/dist/build/css-url-assets.js +18 -1
  6. package/dist/build/inject-pregenerated-paths.d.ts +3 -0
  7. package/dist/build/inject-pregenerated-paths.js +4 -1
  8. package/dist/build/prerender-paths.d.ts +23 -0
  9. package/dist/build/prerender-paths.js +296 -0
  10. package/dist/build/prerender-server-entry.d.ts +1 -0
  11. package/dist/build/prerender-server-entry.js +49 -0
  12. package/dist/build/prerender-server-pool.d.ts +44 -0
  13. package/dist/build/prerender-server-pool.js +194 -0
  14. package/dist/build/prerender.d.ts +12 -1
  15. package/dist/build/prerender.js +87 -23
  16. package/dist/build/run-prerender.js +2 -1
  17. package/dist/cache/cache-adapters-virtual.d.ts +7 -1
  18. package/dist/cache/cache-adapters-virtual.js +26 -2
  19. package/dist/check.d.ts +4 -6
  20. package/dist/check.js +13 -9
  21. package/dist/cli.js +34 -54
  22. package/dist/client/vinext-next-data.d.ts +4 -1
  23. package/dist/config/config-matchers.js +33 -11
  24. package/dist/config/next-config.d.ts +36 -6
  25. package/dist/config/next-config.js +17 -4
  26. package/dist/config/prerender.d.ts +11 -1
  27. package/dist/config/prerender.js +19 -1
  28. package/dist/config/tsconfig-paths.js +5 -1
  29. package/dist/entries/app-browser-entry.js +5 -1
  30. package/dist/entries/app-rsc-entry.d.ts +3 -2
  31. package/dist/entries/app-rsc-entry.js +4 -0
  32. package/dist/entries/pages-client-entry.js +14 -3
  33. package/dist/entries/pages-server-entry.js +18 -4
  34. package/dist/image/image-adapters-virtual.js +1 -0
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.js +549 -152
  37. package/dist/init-cloudflare.js +12 -3
  38. package/dist/init-platform.d.ts +10 -1
  39. package/dist/init-platform.js +78 -12
  40. package/dist/init.d.ts +11 -3
  41. package/dist/init.js +129 -31
  42. package/dist/plugins/fonts.js +1 -1
  43. package/dist/plugins/ignore-dynamic-requests.js +1 -1
  44. package/dist/plugins/og-assets.js +2 -1
  45. package/dist/plugins/optimize-imports.js +1 -1
  46. package/dist/plugins/rsc-reference-validation-normalizer.d.ts +12 -0
  47. package/dist/plugins/rsc-reference-validation-normalizer.js +48 -0
  48. package/dist/plugins/sass.d.ts +17 -1
  49. package/dist/plugins/sass.js +74 -1
  50. package/dist/plugins/styled-jsx.d.ts +16 -0
  51. package/dist/plugins/styled-jsx.js +149 -0
  52. package/dist/routing/app-route-graph.js +22 -16
  53. package/dist/routing/file-matcher.d.ts +8 -1
  54. package/dist/routing/file-matcher.js +15 -3
  55. package/dist/server/app-browser-entry.js +44 -14
  56. package/dist/server/app-browser-navigation-controller.js +5 -2
  57. package/dist/server/app-browser-server-action-client.js +5 -3
  58. package/dist/server/app-browser-state.d.ts +1 -0
  59. package/dist/server/app-browser-state.js +1 -1
  60. package/dist/server/app-layout-param-observation.d.ts +1 -1
  61. package/dist/server/app-page-boundary.js +2 -1
  62. package/dist/server/app-page-cache-finalizer.d.ts +1 -0
  63. package/dist/server/app-page-cache-finalizer.js +3 -3
  64. package/dist/server/app-page-cache-render.d.ts +1 -1
  65. package/dist/server/app-page-cache.js +23 -6
  66. package/dist/server/app-page-dispatch.d.ts +2 -1
  67. package/dist/server/app-page-dispatch.js +27 -10
  68. package/dist/server/app-page-element-builder.js +1 -0
  69. package/dist/server/app-page-execution.js +2 -1
  70. package/dist/server/app-page-render-identity.d.ts +1 -0
  71. package/dist/server/app-page-render-identity.js +2 -1
  72. package/dist/server/app-page-render.d.ts +3 -1
  73. package/dist/server/app-page-render.js +67 -16
  74. package/dist/server/app-page-response.d.ts +7 -0
  75. package/dist/server/app-page-response.js +16 -4
  76. package/dist/server/app-page-route-wiring.js +3 -3
  77. package/dist/server/app-page-stream.d.ts +2 -1
  78. package/dist/server/app-page-stream.js +1 -1
  79. package/dist/server/app-route-handler-execution.d.ts +1 -1
  80. package/dist/server/app-route-tree-prefetch.d.ts +43 -0
  81. package/dist/server/app-route-tree-prefetch.js +187 -0
  82. package/dist/server/app-router-entry.js +1 -1
  83. package/dist/server/app-rsc-cache-busting.d.ts +2 -1
  84. package/dist/server/app-rsc-cache-busting.js +9 -5
  85. package/dist/server/app-rsc-handler.d.ts +6 -0
  86. package/dist/server/app-rsc-handler.js +88 -10
  87. package/dist/server/app-rsc-render-mode.d.ts +3 -5
  88. package/dist/server/app-rsc-render-mode.js +5 -12
  89. package/dist/server/app-rsc-request-normalization.d.ts +3 -4
  90. package/dist/server/app-rsc-request-normalization.js +4 -5
  91. package/dist/server/app-server-action-execution.js +6 -5
  92. package/dist/server/app-ssr-entry.js +1 -1
  93. package/dist/server/cache-control.d.ts +3 -1
  94. package/dist/server/cache-control.js +13 -1
  95. package/dist/server/dev-module-runner.js +1 -1
  96. package/dist/server/dev-origin-check.d.ts +2 -2
  97. package/dist/server/dev-origin-check.js +2 -2
  98. package/dist/server/dev-server.d.ts +11 -1
  99. package/dist/server/dev-server.js +82 -24
  100. package/dist/server/headers.d.ts +6 -2
  101. package/dist/server/headers.js +11 -5
  102. package/dist/server/image-optimization.d.ts +12 -1
  103. package/dist/server/image-optimization.js +13 -1
  104. package/dist/server/isr-cache.d.ts +13 -4
  105. package/dist/server/isr-cache.js +8 -4
  106. package/dist/server/pages-data-route.d.ts +4 -2
  107. package/dist/server/pages-data-route.js +18 -4
  108. package/dist/server/pages-dev-module-url.d.ts +2 -1
  109. package/dist/server/pages-dev-module-url.js +6 -3
  110. package/dist/server/pages-node-compat.d.ts +12 -1
  111. package/dist/server/pages-node-compat.js +50 -1
  112. package/dist/server/pages-page-data.d.ts +9 -0
  113. package/dist/server/pages-page-data.js +18 -10
  114. package/dist/server/pages-page-handler.js +19 -7
  115. package/dist/server/pages-page-response.d.ts +1 -0
  116. package/dist/server/pages-page-response.js +5 -4
  117. package/dist/server/pages-request-pipeline.d.ts +8 -6
  118. package/dist/server/pages-request-pipeline.js +31 -7
  119. package/dist/server/pages-router-entry.js +1 -1
  120. package/dist/server/prerender-manifest.d.ts +15 -1
  121. package/dist/server/prerender-manifest.js +29 -1
  122. package/dist/server/prod-server.d.ts +10 -7
  123. package/dist/server/prod-server.js +43 -28
  124. package/dist/server/request-pipeline.js +1 -1
  125. package/dist/server/static-file-cache.js +1 -1
  126. package/dist/shims/cache-handler.js +8 -1
  127. package/dist/shims/cache.d.ts +1 -1
  128. package/dist/shims/cache.js +3 -0
  129. package/dist/shims/cdn-cache.d.ts +2 -7
  130. package/dist/shims/cdn-cache.js +2 -14
  131. package/dist/shims/fetch-cache.d.ts +3 -1
  132. package/dist/shims/fetch-cache.js +77 -52
  133. package/dist/shims/internal/app-prefetch-fetch-queue.d.ts +10 -0
  134. package/dist/shims/internal/app-prefetch-fetch-queue.js +61 -0
  135. package/dist/shims/internal/hybrid-client-route-owner.d.ts +2 -1
  136. package/dist/shims/internal/hybrid-client-route-owner.js +34 -1
  137. package/dist/shims/internal/pages-data-fetch-dedup.d.ts +2 -1
  138. package/dist/shims/internal/pages-data-fetch-dedup.js +13 -2
  139. package/dist/shims/internal/pages-data-target.d.ts +8 -3
  140. package/dist/shims/internal/pages-data-target.js +9 -4
  141. package/dist/shims/link.d.ts +1 -1
  142. package/dist/shims/link.js +179 -42
  143. package/dist/shims/navigation.d.ts +15 -3
  144. package/dist/shims/navigation.js +202 -39
  145. package/dist/shims/request-context.js +18 -0
  146. package/dist/shims/request-state-types.d.ts +2 -2
  147. package/dist/shims/router.js +31 -15
  148. package/dist/shims/unified-request-context.d.ts +1 -1
  149. package/dist/shims/unified-request-context.js +1 -0
  150. package/dist/typegen.js +1 -1
  151. package/dist/utils/middleware-request-headers.js +1 -1
  152. package/dist/utils/project.d.ts +1 -1
  153. package/dist/utils/protocol-headers.d.ts +7 -1
  154. package/dist/utils/protocol-headers.js +7 -1
  155. package/dist/utils/vite-version.d.ts +4 -12
  156. package/dist/utils/vite-version.js +39 -21
  157. package/package.json +31 -13
  158. package/dist/cloudflare/index.d.ts +0 -3
  159. package/dist/cloudflare/index.js +0 -3
  160. package/dist/packages/cloudflare/src/cache/cdn-adapter.runtime.js +0 -102
  161. package/dist/packages/cloudflare/src/cache/kv-data-adapter.runtime.d.ts +0 -126
  162. package/dist/packages/cloudflare/src/cache/kv-data-adapter.runtime.js +0 -435
  163. package/dist/packages/cloudflare/src/deploy-config.js +0 -150
  164. package/dist/packages/cloudflare/src/deploy-help.js +0 -55
  165. package/dist/packages/cloudflare/src/deploy.js +0 -276
  166. package/dist/packages/cloudflare/src/tpr.d.ts +0 -45
  167. package/dist/packages/cloudflare/src/tpr.js +0 -561
  168. package/dist/packages/cloudflare/src/utils/cache-control-metadata.js +0 -20
@@ -0,0 +1,296 @@
1
+ import { findDir } from "../utils/project.js";
2
+ import { normalizePathSeparators } from "../utils/path.js";
3
+ import { apiRouter, pagesRouter } from "../routing/pages-router.js";
4
+ import { VINEXT_PRERENDER_SECRET_HEADER } from "../utils/protocol-headers.js";
5
+ import { classifyAppRoute, classifyPagesRoute, getAppRouteRenderEntryPath, hasNamedExport } from "./report.js";
6
+ import { appRouter } from "../routing/app-router.js";
7
+ import { normalizeStaticPathsEntry } from "../routing/route-pattern.js";
8
+ import { BLOCKED_PAGES, PHASE_PRODUCTION_BUILD } from "../shims/constants.js";
9
+ import { loadNextConfig, resolveNextConfig } from "../config/next-config.js";
10
+ import { readPrerenderSecret } from "./server-manifest.js";
11
+ import { startProdServer } from "../server/prod-server.js";
12
+ import { buildUrlFromParams, resolveParentParams } from "./prerender.js";
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ //#region src/build/prerender-paths.ts
16
+ const PRERENDER_PATH_DISCOVERY_ENV = "__VINEXT_PRERENDER_PATH_DISCOVERY";
17
+ const PRERENDER_PATHS_MANIFEST = "vinext-prerender-paths.json";
18
+ const PATH_DISCOVERY_FETCH_TIMEOUT_MS = 3e4;
19
+ function readBuiltBuildId(serverDir) {
20
+ try {
21
+ const buildId = fs.readFileSync(path.join(serverDir, "BUILD_ID"), "utf-8").trim();
22
+ return buildId.length > 0 ? buildId : null;
23
+ } catch (error) {
24
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return null;
25
+ throw error;
26
+ }
27
+ }
28
+ function addPath(paths, seen, pathname) {
29
+ if (seen.has(pathname)) return;
30
+ seen.add(pathname);
31
+ paths.push(pathname);
32
+ }
33
+ function warnDiscoveryFailure(route, error) {
34
+ const message = error instanceof Error ? error.message : String(error);
35
+ console.warn(`[vinext] Warning: failed to discover warmup path(s) for ${route}: ${message}`);
36
+ }
37
+ async function fetchDiscoveryEndpoint(url, headers) {
38
+ const controller = new AbortController();
39
+ const timeout = setTimeout(() => controller.abort(), PATH_DISCOVERY_FETCH_TIMEOUT_MS);
40
+ try {
41
+ const res = await fetch(url, {
42
+ headers,
43
+ signal: controller.signal
44
+ });
45
+ const text = await res.text();
46
+ if (!res.ok || text === "null") return null;
47
+ return text;
48
+ } catch (error) {
49
+ if (error instanceof Error && error.name === "AbortError") throw new Error(`path discovery timed out after ${PATH_DISCOVERY_FETCH_TIMEOUT_MS}ms`);
50
+ throw error;
51
+ } finally {
52
+ clearTimeout(timeout);
53
+ }
54
+ }
55
+ function fileHasNamedExport(filePath, name) {
56
+ if (!filePath) return false;
57
+ try {
58
+ return hasNamedExport(fs.readFileSync(filePath, "utf-8"), name);
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+ function resolveConfiguredRouteDirs(root, routeRootConfig) {
64
+ if (!routeRootConfig) return {
65
+ appDir: findDir(root, "app", "src/app"),
66
+ pagesDir: findDir(root, "pages", "src/pages")
67
+ };
68
+ let baseDir;
69
+ if (routeRootConfig.appDir) {
70
+ baseDir = path.isAbsolute(routeRootConfig.appDir) ? routeRootConfig.appDir : path.resolve(root, routeRootConfig.appDir);
71
+ baseDir = normalizePathSeparators(baseDir);
72
+ } else {
73
+ const hasRootApp = fs.existsSync(path.posix.join(root, "app"));
74
+ const hasRootPages = fs.existsSync(path.posix.join(root, "pages"));
75
+ const hasSrcApp = fs.existsSync(path.posix.join(root, "src", "app"));
76
+ const hasSrcPages = fs.existsSync(path.posix.join(root, "src", "pages"));
77
+ baseDir = hasRootApp || hasRootPages ? root : hasSrcApp || hasSrcPages ? path.posix.join(root, "src") : root;
78
+ }
79
+ const appDir = path.posix.join(baseDir, "app");
80
+ const pagesDir = path.posix.join(baseDir, "pages");
81
+ return {
82
+ appDir: !routeRootConfig.disableAppRouter && fs.existsSync(appDir) ? appDir : null,
83
+ pagesDir: fs.existsSync(pagesDir) ? pagesDir : null
84
+ };
85
+ }
86
+ function appRouteMayHaveGenerateStaticParams(route) {
87
+ if (fileHasNamedExport(route.pagePath, "generateStaticParams")) return true;
88
+ return route.layouts.some((layoutPath) => fileHasNamedExport(layoutPath, "generateStaticParams"));
89
+ }
90
+ async function shouldStartPathDiscoveryServer(options) {
91
+ if (options.appDir) {
92
+ if ((await appRouter(options.appDir, options.pageExtensions)).some((route) => route.isDynamic && appRouteMayHaveGenerateStaticParams(route))) return true;
93
+ }
94
+ if (options.pagesDir) {
95
+ if ((await pagesRouter(options.pagesDir, options.pageExtensions)).some((route) => route.isDynamic && fileHasNamedExport(route.filePath, "getStaticPaths"))) return true;
96
+ }
97
+ return false;
98
+ }
99
+ async function withPrerenderEndpoints(fn) {
100
+ const previousPrerenderFlag = process.env.VINEXT_PRERENDER;
101
+ const previousPathDiscoveryFlag = process.env[PRERENDER_PATH_DISCOVERY_ENV];
102
+ process.env.VINEXT_PRERENDER = "1";
103
+ process.env[PRERENDER_PATH_DISCOVERY_ENV] = "1";
104
+ try {
105
+ return await fn();
106
+ } finally {
107
+ if (previousPrerenderFlag === void 0) delete process.env.VINEXT_PRERENDER;
108
+ else process.env.VINEXT_PRERENDER = previousPrerenderFlag;
109
+ if (previousPathDiscoveryFlag === void 0) delete process.env[PRERENDER_PATH_DISCOVERY_ENV];
110
+ else process.env[PRERENDER_PATH_DISCOVERY_ENV] = previousPathDiscoveryFlag;
111
+ }
112
+ }
113
+ async function collectPagesPaths(options) {
114
+ const [pageRoutes, apiRoutes] = await Promise.all([pagesRouter(options.pagesDir, options.pageExtensions), apiRouter(options.pagesDir, options.pageExtensions)]);
115
+ const apiPatterns = new Set(apiRoutes.map((route) => route.pattern));
116
+ const paths = [];
117
+ const seen = /* @__PURE__ */ new Set();
118
+ for (const route of pageRoutes) {
119
+ if (apiPatterns.has(route.pattern)) continue;
120
+ if (BLOCKED_PAGES.includes(route.pattern)) continue;
121
+ if (route.pattern === "/404" || route.pattern === "/500" || route.pattern === "/_error") continue;
122
+ const { type } = classifyPagesRoute(route.filePath);
123
+ if (type === "api" || type === "ssr") continue;
124
+ if (!route.isDynamic) {
125
+ addPath(paths, seen, route.pattern);
126
+ continue;
127
+ }
128
+ if (!fileHasNamedExport(route.filePath, "getStaticPaths")) continue;
129
+ if (!options.baseUrl) continue;
130
+ try {
131
+ const search = new URLSearchParams({ pattern: route.pattern });
132
+ const text = await fetchDiscoveryEndpoint(`${options.baseUrl}/__vinext/prerender/pages-static-paths?${search}`, options.secretHeaders);
133
+ if (text === null) continue;
134
+ const pathsResult = JSON.parse(text);
135
+ for (const item of pathsResult.paths ?? []) {
136
+ const normalized = normalizeStaticPathsEntry(item, route.pattern);
137
+ if ("error" in normalized) throw new Error(normalized.error);
138
+ addPath(paths, seen, buildUrlFromParams(route.pattern, normalized.params));
139
+ }
140
+ } catch (error) {
141
+ warnDiscoveryFailure(route.pattern, error);
142
+ }
143
+ }
144
+ return paths;
145
+ }
146
+ async function collectAppPaths(options) {
147
+ const routes = await appRouter(options.appDir, options.pageExtensions);
148
+ const paths = [];
149
+ const seen = /* @__PURE__ */ new Set();
150
+ const staticParamsCache = /* @__PURE__ */ new Map();
151
+ const staticParamsMap = new Proxy({}, {
152
+ get(_target, pattern) {
153
+ return async ({ params }) => {
154
+ if (!options.baseUrl) return null;
155
+ const cacheKey = `${pattern}\0${JSON.stringify(params)}`;
156
+ const cached = staticParamsCache.get(cacheKey);
157
+ if (cached !== void 0) return cached;
158
+ const request = (async () => {
159
+ const search = new URLSearchParams({ pattern });
160
+ if (Object.keys(params).length > 0) search.set("parentParams", JSON.stringify(params));
161
+ const text = await fetchDiscoveryEndpoint(`${options.baseUrl}/__vinext/prerender/static-params?${search}`, options.secretHeaders);
162
+ if (text === null) return null;
163
+ return JSON.parse(text);
164
+ })();
165
+ request.catch(() => staticParamsCache.delete(cacheKey));
166
+ staticParamsCache.set(cacheKey, request);
167
+ return request;
168
+ };
169
+ },
170
+ has() {
171
+ return false;
172
+ }
173
+ });
174
+ for (const route of routes) {
175
+ const renderEntryPath = getAppRouteRenderEntryPath(route);
176
+ if (!renderEntryPath) continue;
177
+ const { type } = classifyAppRoute(renderEntryPath, route.routePath, route.isDynamic);
178
+ if (type === "api") continue;
179
+ if (type === "ssr" && !route.isDynamic) continue;
180
+ if (!route.isDynamic) {
181
+ addPath(paths, seen, route.pattern);
182
+ continue;
183
+ }
184
+ if (!appRouteMayHaveGenerateStaticParams(route)) continue;
185
+ try {
186
+ const generateStaticParams = staticParamsMap[route.pattern];
187
+ if (typeof generateStaticParams !== "function") continue;
188
+ const parentParamSets = await resolveParentParams(route, staticParamsMap);
189
+ let paramSets;
190
+ if (parentParamSets.length > 0) {
191
+ paramSets = [];
192
+ for (const parentParams of parentParamSets) {
193
+ const childResults = await generateStaticParams({ params: parentParams });
194
+ if (childResults === null) {
195
+ paramSets = null;
196
+ break;
197
+ }
198
+ if (Array.isArray(childResults)) for (const childParams of childResults) paramSets.push({
199
+ ...parentParams,
200
+ ...childParams
201
+ });
202
+ }
203
+ } else {
204
+ const results = await generateStaticParams({ params: {} });
205
+ paramSets = Array.isArray(results) || results === null ? results : [];
206
+ }
207
+ if (!paramSets?.length) continue;
208
+ for (const params of paramSets) {
209
+ if (params === null || params === void 0) continue;
210
+ addPath(paths, seen, buildUrlFromParams(route.pattern, params));
211
+ }
212
+ } catch (error) {
213
+ warnDiscoveryFailure(route.pattern, error);
214
+ }
215
+ }
216
+ return paths;
217
+ }
218
+ async function startPathDiscoveryServer(options) {
219
+ return startProdServer({
220
+ port: 0,
221
+ host: "127.0.0.1",
222
+ outDir: options.pagesBundlePath ? path.dirname(path.dirname(options.pagesBundlePath)) : path.dirname(options.serverDir),
223
+ rscEntryPath: options.rscBundlePath,
224
+ serverEntryPath: options.pagesBundlePath,
225
+ noCompression: true,
226
+ purpose: "prerender"
227
+ });
228
+ }
229
+ async function emitPrerenderPathManifest(options) {
230
+ const { root } = options;
231
+ const configuredRouteDirs = resolveConfiguredRouteDirs(root, options.routeRootConfig);
232
+ const appDir = options.appDir !== void 0 ? options.appDir : configuredRouteDirs.appDir;
233
+ const pagesDir = options.pagesDir !== void 0 ? options.pagesDir : configuredRouteDirs.pagesDir;
234
+ if (!appDir && !pagesDir) return null;
235
+ const defaultRscBundlePath = options.routeRootConfig?.rscOutDir ? path.join(path.resolve(root, options.routeRootConfig.rscOutDir), "index.js") : path.join(root, "dist", "server", "index.js");
236
+ const rscBundlePath = options.rscBundlePath ?? defaultRscBundlePath;
237
+ const pagesBundlePath = options.pagesBundlePath ?? path.join(root, "dist", "server", "entry.js");
238
+ const bundleServerDir = fs.existsSync(rscBundlePath) ? path.dirname(rscBundlePath) : path.dirname(pagesBundlePath);
239
+ const manifestDir = path.join(root, "dist", "server");
240
+ const loadedConfig = await resolveNextConfig(await loadNextConfig(root, PHASE_PRODUCTION_BUILD), root);
241
+ const config = options.nextConfigOverride ? {
242
+ ...loadedConfig,
243
+ ...options.nextConfigOverride
244
+ } : { ...loadedConfig };
245
+ const builtBuildId = readBuiltBuildId(manifestDir) ?? readBuiltBuildId(bundleServerDir);
246
+ if (builtBuildId) config.buildId = builtBuildId;
247
+ const paths = [];
248
+ const seen = /* @__PURE__ */ new Set();
249
+ await withPrerenderEndpoints(async () => {
250
+ let prodServer = null;
251
+ if (await shouldStartPathDiscoveryServer({
252
+ appDir,
253
+ pagesDir,
254
+ pageExtensions: config.pageExtensions
255
+ })) try {
256
+ prodServer = await startPathDiscoveryServer({
257
+ serverDir: bundleServerDir,
258
+ pagesBundlePath: !appDir && pagesDir ? pagesBundlePath : void 0,
259
+ rscBundlePath: appDir ? rscBundlePath : void 0
260
+ });
261
+ } catch (error) {
262
+ const message = error instanceof Error ? error.message : String(error);
263
+ console.warn(`[vinext] Warning: failed to start prerender path discovery server: ${message}`);
264
+ }
265
+ const baseUrl = prodServer ? `http://127.0.0.1:${prodServer.port}` : null;
266
+ const prerenderSecret = readPrerenderSecret(bundleServerDir) ?? readPrerenderSecret(manifestDir);
267
+ const secretHeaders = prerenderSecret ? { [VINEXT_PRERENDER_SECRET_HEADER]: prerenderSecret } : {};
268
+ try {
269
+ if (appDir) for (const pathname of await collectAppPaths({
270
+ appDir,
271
+ baseUrl,
272
+ pageExtensions: config.pageExtensions,
273
+ secretHeaders
274
+ })) addPath(paths, seen, pathname);
275
+ if (pagesDir) for (const pathname of await collectPagesPaths({
276
+ baseUrl,
277
+ pagesDir,
278
+ pageExtensions: config.pageExtensions,
279
+ secretHeaders
280
+ })) addPath(paths, seen, pathname);
281
+ } finally {
282
+ if (prodServer) await new Promise((resolve) => prodServer.server.close(() => resolve()));
283
+ }
284
+ });
285
+ const manifest = {
286
+ buildId: config.buildId,
287
+ trailingSlash: config.trailingSlash,
288
+ paths
289
+ };
290
+ fs.mkdirSync(manifestDir, { recursive: true });
291
+ fs.writeFileSync(path.join(manifestDir, PRERENDER_PATHS_MANIFEST), JSON.stringify(manifest, null, 2) + "\n", "utf-8");
292
+ console.log(` Discovered ${paths.length} CDN warmup path(s).`);
293
+ return manifest;
294
+ }
295
+ //#endregion
296
+ export { PRERENDER_PATHS_MANIFEST, PRERENDER_PATH_DISCOVERY_ENV, emitPrerenderPathManifest };
@@ -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 };
@@ -135,6 +135,17 @@ type PrerenderAppOptionsInternal = PrerenderAppOptions & {
135
135
  * all '<' and '>' in the embedded JSON, preventing false </script> matches.
136
136
  */
137
137
  declare function extractRscPayloadFromPrerenderedHtml(html: string): Uint8Array | null;
138
+ /**
139
+ * Build a URL path from a route pattern and params.
140
+ * "/posts/:id" + { id: "42" } → "/posts/42"
141
+ * "/docs/:slug+" + { slug: ["a", "b"] } → "/docs/a/b"
142
+ *
143
+ * Throws a descriptive error rather than a cryptic `Cannot read properties of
144
+ * undefined` if `params` itself is missing or required keys are absent — the
145
+ * caller (prerenderPages / prerenderApp) catches this and surfaces it as a
146
+ * per-route error result.
147
+ */
148
+ declare function buildUrlFromParams(pattern: string, params: Record<string, string | string[]> | undefined | null): string;
138
149
  /** Map of route patterns to generateStaticParams functions (or null/undefined). */
139
150
  type StaticParamsMap = Record<string, ((opts: {
140
151
  params: Record<string, string | string[]>;
@@ -207,4 +218,4 @@ declare function writePrerenderIndex(routes: PrerenderRouteResult[], outDir: str
207
218
  trailingSlash?: boolean;
208
219
  }): void;
209
220
  //#endregion
210
- export { PrerenderResult, PrerenderRouteResult, StaticParamsMap, extractRscPayloadFromPrerenderedHtml, prerenderApp, prerenderPages, readPrerenderSecret, resolveParentParams, writePrerenderIndex };
221
+ export { PrerenderResult, PrerenderRouteResult, StaticParamsMap, buildUrlFromParams, extractRscPayloadFromPrerenderedHtml, prerenderApp, prerenderPages, readPrerenderSecret, resolveParentParams, writePrerenderIndex };