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.
- package/README.md +50 -29
- package/dist/build/client-build-config.d.ts +13 -92
- package/dist/build/client-build-config.js +17 -95
- package/dist/build/css-url-assets.d.ts +3 -1
- package/dist/build/css-url-assets.js +18 -1
- package/dist/build/inject-pregenerated-paths.d.ts +3 -0
- package/dist/build/inject-pregenerated-paths.js +4 -1
- package/dist/build/prerender-paths.d.ts +23 -0
- package/dist/build/prerender-paths.js +296 -0
- package/dist/build/prerender-server-entry.d.ts +1 -0
- package/dist/build/prerender-server-entry.js +49 -0
- package/dist/build/prerender-server-pool.d.ts +44 -0
- package/dist/build/prerender-server-pool.js +194 -0
- package/dist/build/prerender.d.ts +12 -1
- package/dist/build/prerender.js +87 -23
- package/dist/build/run-prerender.js +2 -1
- package/dist/cache/cache-adapters-virtual.d.ts +7 -1
- package/dist/cache/cache-adapters-virtual.js +26 -2
- package/dist/check.d.ts +4 -6
- package/dist/check.js +13 -9
- package/dist/cli.js +34 -54
- package/dist/client/vinext-next-data.d.ts +4 -1
- package/dist/config/config-matchers.js +33 -11
- package/dist/config/next-config.d.ts +36 -6
- package/dist/config/next-config.js +17 -4
- package/dist/config/prerender.d.ts +11 -1
- package/dist/config/prerender.js +19 -1
- package/dist/config/tsconfig-paths.js +5 -1
- package/dist/entries/app-browser-entry.js +5 -1
- package/dist/entries/app-rsc-entry.d.ts +3 -2
- package/dist/entries/app-rsc-entry.js +4 -0
- package/dist/entries/pages-client-entry.js +14 -3
- package/dist/entries/pages-server-entry.js +18 -4
- package/dist/image/image-adapters-virtual.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +549 -152
- package/dist/init-cloudflare.js +12 -3
- package/dist/init-platform.d.ts +10 -1
- package/dist/init-platform.js +78 -12
- package/dist/init.d.ts +11 -3
- package/dist/init.js +129 -31
- package/dist/plugins/fonts.js +1 -1
- package/dist/plugins/ignore-dynamic-requests.js +1 -1
- package/dist/plugins/og-assets.js +2 -1
- package/dist/plugins/optimize-imports.js +1 -1
- package/dist/plugins/rsc-reference-validation-normalizer.d.ts +12 -0
- package/dist/plugins/rsc-reference-validation-normalizer.js +48 -0
- package/dist/plugins/sass.d.ts +17 -1
- package/dist/plugins/sass.js +74 -1
- package/dist/plugins/styled-jsx.d.ts +16 -0
- package/dist/plugins/styled-jsx.js +149 -0
- package/dist/routing/app-route-graph.js +22 -16
- package/dist/routing/file-matcher.d.ts +8 -1
- package/dist/routing/file-matcher.js +15 -3
- package/dist/server/app-browser-entry.js +44 -14
- package/dist/server/app-browser-navigation-controller.js +5 -2
- package/dist/server/app-browser-server-action-client.js +5 -3
- package/dist/server/app-browser-state.d.ts +1 -0
- package/dist/server/app-browser-state.js +1 -1
- package/dist/server/app-layout-param-observation.d.ts +1 -1
- package/dist/server/app-page-boundary.js +2 -1
- package/dist/server/app-page-cache-finalizer.d.ts +1 -0
- package/dist/server/app-page-cache-finalizer.js +3 -3
- package/dist/server/app-page-cache-render.d.ts +1 -1
- package/dist/server/app-page-cache.js +23 -6
- package/dist/server/app-page-dispatch.d.ts +2 -1
- package/dist/server/app-page-dispatch.js +27 -10
- package/dist/server/app-page-element-builder.js +1 -0
- package/dist/server/app-page-execution.js +2 -1
- package/dist/server/app-page-render-identity.d.ts +1 -0
- package/dist/server/app-page-render-identity.js +2 -1
- package/dist/server/app-page-render.d.ts +3 -1
- package/dist/server/app-page-render.js +67 -16
- package/dist/server/app-page-response.d.ts +7 -0
- package/dist/server/app-page-response.js +16 -4
- package/dist/server/app-page-route-wiring.js +3 -3
- package/dist/server/app-page-stream.d.ts +2 -1
- package/dist/server/app-page-stream.js +1 -1
- package/dist/server/app-route-handler-execution.d.ts +1 -1
- package/dist/server/app-route-tree-prefetch.d.ts +43 -0
- package/dist/server/app-route-tree-prefetch.js +187 -0
- package/dist/server/app-router-entry.js +1 -1
- package/dist/server/app-rsc-cache-busting.d.ts +2 -1
- package/dist/server/app-rsc-cache-busting.js +9 -5
- package/dist/server/app-rsc-handler.d.ts +6 -0
- package/dist/server/app-rsc-handler.js +88 -10
- package/dist/server/app-rsc-render-mode.d.ts +3 -5
- package/dist/server/app-rsc-render-mode.js +5 -12
- package/dist/server/app-rsc-request-normalization.d.ts +3 -4
- package/dist/server/app-rsc-request-normalization.js +4 -5
- package/dist/server/app-server-action-execution.js +6 -5
- package/dist/server/app-ssr-entry.js +1 -1
- package/dist/server/cache-control.d.ts +3 -1
- package/dist/server/cache-control.js +13 -1
- package/dist/server/dev-module-runner.js +1 -1
- package/dist/server/dev-origin-check.d.ts +2 -2
- package/dist/server/dev-origin-check.js +2 -2
- package/dist/server/dev-server.d.ts +11 -1
- package/dist/server/dev-server.js +82 -24
- package/dist/server/headers.d.ts +6 -2
- package/dist/server/headers.js +11 -5
- package/dist/server/image-optimization.d.ts +12 -1
- package/dist/server/image-optimization.js +13 -1
- package/dist/server/isr-cache.d.ts +13 -4
- package/dist/server/isr-cache.js +8 -4
- package/dist/server/pages-data-route.d.ts +4 -2
- package/dist/server/pages-data-route.js +18 -4
- package/dist/server/pages-dev-module-url.d.ts +2 -1
- package/dist/server/pages-dev-module-url.js +6 -3
- package/dist/server/pages-node-compat.d.ts +12 -1
- package/dist/server/pages-node-compat.js +50 -1
- package/dist/server/pages-page-data.d.ts +9 -0
- package/dist/server/pages-page-data.js +18 -10
- package/dist/server/pages-page-handler.js +19 -7
- package/dist/server/pages-page-response.d.ts +1 -0
- package/dist/server/pages-page-response.js +5 -4
- package/dist/server/pages-request-pipeline.d.ts +8 -6
- package/dist/server/pages-request-pipeline.js +31 -7
- package/dist/server/pages-router-entry.js +1 -1
- package/dist/server/prerender-manifest.d.ts +15 -1
- package/dist/server/prerender-manifest.js +29 -1
- package/dist/server/prod-server.d.ts +10 -7
- package/dist/server/prod-server.js +43 -28
- package/dist/server/request-pipeline.js +1 -1
- package/dist/server/static-file-cache.js +1 -1
- package/dist/shims/cache-handler.js +8 -1
- package/dist/shims/cache.d.ts +1 -1
- package/dist/shims/cache.js +3 -0
- package/dist/shims/cdn-cache.d.ts +2 -7
- package/dist/shims/cdn-cache.js +2 -14
- package/dist/shims/fetch-cache.d.ts +3 -1
- package/dist/shims/fetch-cache.js +77 -52
- package/dist/shims/internal/app-prefetch-fetch-queue.d.ts +10 -0
- package/dist/shims/internal/app-prefetch-fetch-queue.js +61 -0
- package/dist/shims/internal/hybrid-client-route-owner.d.ts +2 -1
- package/dist/shims/internal/hybrid-client-route-owner.js +34 -1
- package/dist/shims/internal/pages-data-fetch-dedup.d.ts +2 -1
- package/dist/shims/internal/pages-data-fetch-dedup.js +13 -2
- package/dist/shims/internal/pages-data-target.d.ts +8 -3
- package/dist/shims/internal/pages-data-target.js +9 -4
- package/dist/shims/link.d.ts +1 -1
- package/dist/shims/link.js +179 -42
- package/dist/shims/navigation.d.ts +15 -3
- package/dist/shims/navigation.js +202 -39
- package/dist/shims/request-context.js +18 -0
- package/dist/shims/request-state-types.d.ts +2 -2
- package/dist/shims/router.js +31 -15
- package/dist/shims/unified-request-context.d.ts +1 -1
- package/dist/shims/unified-request-context.js +1 -0
- package/dist/typegen.js +1 -1
- package/dist/utils/middleware-request-headers.js +1 -1
- package/dist/utils/project.d.ts +1 -1
- package/dist/utils/protocol-headers.d.ts +7 -1
- package/dist/utils/protocol-headers.js +7 -1
- package/dist/utils/vite-version.d.ts +4 -12
- package/dist/utils/vite-version.js +39 -21
- package/package.json +31 -13
- package/dist/cloudflare/index.d.ts +0 -3
- package/dist/cloudflare/index.js +0 -3
- package/dist/packages/cloudflare/src/cache/cdn-adapter.runtime.js +0 -102
- package/dist/packages/cloudflare/src/cache/kv-data-adapter.runtime.d.ts +0 -126
- package/dist/packages/cloudflare/src/cache/kv-data-adapter.runtime.js +0 -435
- package/dist/packages/cloudflare/src/deploy-config.js +0 -150
- package/dist/packages/cloudflare/src/deploy-help.js +0 -55
- package/dist/packages/cloudflare/src/deploy.js +0 -276
- package/dist/packages/cloudflare/src/tpr.d.ts +0 -45
- package/dist/packages/cloudflare/src/tpr.js +0 -561
- 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 };
|