vinext 1.0.0-beta.7 → 1.0.0-beta.9
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 +43 -0
- package/dist/build/prerender-paths.d.ts +50 -1
- package/dist/build/prerender-paths.js +627 -88
- package/dist/build/prerender.d.ts +4 -2
- package/dist/build/prerender.js +40 -23
- package/dist/build/report.d.ts +16 -1
- package/dist/build/report.js +64 -3
- package/dist/build/static-export.d.ts +1 -1
- package/dist/cache/cache-adapters-virtual.d.ts +47 -1
- package/dist/cache/cache-adapters-virtual.js +10 -1
- package/dist/cli.js +7 -0
- package/dist/client/vinext-next-data.d.ts +2 -0
- package/dist/config/config-matchers.d.ts +3 -3
- package/dist/config/config-matchers.js +8 -3
- package/dist/config/prerender.d.ts +2 -2
- package/dist/config/prerender.js +2 -2
- package/dist/entries/app-browser-entry.d.ts +1 -1
- package/dist/entries/app-browser-entry.js +4 -1
- package/dist/entries/app-rsc-entry.d.ts +2 -0
- package/dist/entries/app-rsc-entry.js +28 -4
- package/dist/entries/app-rsc-manifest.js +1 -0
- package/dist/entries/pages-server-entry.d.ts +1 -1
- package/dist/entries/pages-server-entry.js +4 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +146 -28
- package/dist/init-cloudflare.d.ts +5 -3
- package/dist/init-cloudflare.js +58 -25
- package/dist/plugins/import-meta-url.js +7 -6
- package/dist/routing/app-router.d.ts +3 -1
- package/dist/routing/app-router.js +5 -1
- package/dist/server/app-browser-entry.js +41 -13
- package/dist/server/app-browser-navigation-controller.js +1 -1
- package/dist/server/app-browser-state.js +1 -1
- package/dist/server/app-browser-visible-commit.js +1 -1
- package/dist/server/app-middleware.d.ts +8 -0
- package/dist/server/app-middleware.js +28 -2
- package/dist/server/app-optimistic-routing.d.ts +6 -1
- package/dist/server/app-optimistic-routing.js +13 -1
- package/dist/server/app-page-boundary.js +2 -1
- package/dist/server/app-page-cache-finalizer.d.ts +16 -1
- package/dist/server/app-page-cache-finalizer.js +73 -4
- package/dist/server/app-page-cache.js +2 -2
- package/dist/server/app-page-dispatch.d.ts +2 -0
- package/dist/server/app-page-dispatch.js +12 -4
- package/dist/server/app-page-execution.js +1 -1
- package/dist/server/app-page-render.d.ts +1 -0
- package/dist/server/app-page-render.js +27 -9
- package/dist/server/app-page-response.js +2 -2
- package/dist/server/app-page-stream.js +2 -1
- package/dist/server/app-pages-bridge.js +2 -0
- package/dist/server/app-prerender-endpoints.js +23 -11
- package/dist/server/app-prerender-static-params.js +12 -5
- package/dist/server/app-route-handler-dispatch.js +13 -9
- package/dist/server/app-route-handler-execution.d.ts +1 -0
- package/dist/server/app-route-handler-execution.js +127 -14
- package/dist/server/app-route-handler-policy.d.ts +15 -3
- package/dist/server/app-route-handler-policy.js +19 -4
- package/dist/server/app-route-tree-prefetch.js +2 -2
- package/dist/server/app-router-entry.js +52 -8
- package/dist/server/app-rsc-cache-busting.d.ts +22 -4
- package/dist/server/app-rsc-cache-busting.js +68 -18
- package/dist/server/app-rsc-handler.d.ts +2 -0
- package/dist/server/app-rsc-handler.js +95 -30
- package/dist/server/app-rsc-response-finalizer.js +24 -7
- package/dist/server/app-rsc-route-matching.d.ts +1 -0
- package/dist/server/app-rsc-route-matching.js +1 -0
- package/dist/server/app-segment-config.d.ts +3 -1
- package/dist/server/app-segment-config.js +23 -11
- package/dist/server/app-server-action-execution.js +4 -4
- package/dist/server/app-ssr-entry.js +4 -4
- package/dist/server/cache-control.d.ts +8 -2
- package/dist/server/cache-control.js +35 -4
- package/dist/server/cacheability-limits.d.ts +9 -0
- package/dist/server/cacheability-limits.js +9 -0
- package/dist/server/cacheability-manifest.d.ts +42 -0
- package/dist/server/cacheability-manifest.js +191 -0
- package/dist/server/cacheability-request.d.ts +20 -0
- package/dist/server/cacheability-request.js +457 -0
- package/dist/server/config-headers.js +23 -3
- package/dist/server/headers.d.ts +16 -3
- package/dist/server/headers.js +29 -3
- package/dist/server/middleware-matcher.d.ts +9 -1
- package/dist/server/middleware-matcher.js +22 -1
- package/dist/server/middleware-runtime.d.ts +6 -0
- package/dist/server/middleware-runtime.js +18 -3
- package/dist/server/normalize-path.js +1 -1
- package/dist/server/pages-api-route.js +1 -1
- package/dist/server/pages-i18n.js +3 -2
- package/dist/server/pages-page-handler.js +22 -9
- package/dist/server/pages-request-pipeline.d.ts +2 -0
- package/dist/server/pages-request-pipeline.js +17 -6
- package/dist/server/pages-router-entry.js +56 -9
- package/dist/server/prerender-route-params.js +1 -0
- package/dist/server/prerender-work-unit-setup.d.ts +3 -2
- package/dist/server/prerender-work-unit-setup.js +34 -11
- package/dist/server/prod-server.js +5 -5
- package/dist/server/request-pipeline.d.ts +1 -5
- package/dist/server/request-pipeline.js +2 -9
- package/dist/server/revalidation-host.js +1 -0
- package/dist/server/server-globals.js +13 -0
- package/dist/server/static-file-signal.d.ts +13 -0
- package/dist/server/static-file-signal.js +33 -0
- package/dist/server/worker-prerender-discovery.d.ts +19 -0
- package/dist/server/worker-prerender-discovery.js +55 -0
- package/dist/server/worker-utils.js +2 -3
- package/dist/shims/cache-runtime.js +140 -102
- package/dist/shims/cache.js +11 -1
- package/dist/shims/cacheability-classification.d.ts +85 -0
- package/dist/shims/cacheability-classification.js +146 -0
- package/dist/shims/cdn-cache.d.ts +30 -1
- package/dist/shims/cdn-cache.js +25 -1
- package/dist/shims/error.js +1 -1
- package/dist/shims/fetch-cache.js +8 -3
- package/dist/shims/headers.d.ts +3 -1
- package/dist/shims/headers.js +9 -5
- package/dist/shims/internal/app-prefetch-rsc-request.d.ts +25 -0
- package/dist/shims/internal/app-prefetch-rsc-request.js +21 -0
- package/dist/shims/internal/app-route-prefetch-policy.d.ts +2 -0
- package/dist/shims/internal/app-route-prefetch-policy.js +2 -0
- package/dist/shims/internal/hybrid-client-route-owner-direct.d.ts +2 -2
- package/dist/shims/internal/hybrid-client-route-owner-direct.js +29 -11
- package/dist/shims/internal/hybrid-client-route-owner.js +2 -2
- package/dist/shims/internal/make-hanging-promise.d.ts +7 -1
- package/dist/shims/internal/make-hanging-promise.js +13 -2
- package/dist/shims/internal/work-unit-async-storage.d.ts +2 -0
- package/dist/shims/link.js +25 -14
- package/dist/shims/navigation.js +31 -11
- package/dist/shims/navigation.react-server.js +1 -1
- package/dist/shims/request-context.d.ts +2 -0
- package/dist/shims/server.js +1 -1
- package/dist/shims/url-utils.d.ts +8 -1
- package/dist/shims/url-utils.js +10 -1
- package/dist/utils/prerender-output-paths.d.ts +7 -7
- package/dist/utils/prerender-output-paths.js +16 -9
- package/dist/utils/react-compiler-support.d.ts +20 -0
- package/dist/utils/react-compiler-support.js +29 -0
- package/package.json +10 -2
|
@@ -1,21 +1,42 @@
|
|
|
1
1
|
import path, { toSlash } from "../deps/.pnpm/pathslash@0.1.0/deps/pathslash/dist/index.js";
|
|
2
2
|
import { findDir } from "../utils/project.js";
|
|
3
|
-
import { apiRouter, pagesRouter } from "../routing/pages-router.js";
|
|
3
|
+
import { apiRouter, matchRoute, pagesRouter } from "../routing/pages-router.js";
|
|
4
4
|
import { VINEXT_PRERENDER_SECRET_HEADER } from "../utils/protocol-headers.js";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import "../server/headers.js";
|
|
6
|
+
import { matchHeaders, matchesRewriteSource } from "../config/config-matchers.js";
|
|
7
|
+
import { extractLocaleFromUrl, normalizeDefaultLocalePathname } from "../server/pages-i18n.js";
|
|
8
|
+
import { classifyAppRoute, classifyAppRouteHandler, classifyPagesRoute, extractExportConstString, getAppRouteRenderEntryPath } from "./report.js";
|
|
9
|
+
import { appRouteHasMainTreeLoadingBoundary, appRouter, matchAppRoute } from "../routing/app-router.js";
|
|
10
|
+
import { normalizeStaticPathname, normalizeStaticPathsEntry } from "../routing/route-pattern.js";
|
|
11
|
+
import { buildPagesDataHref } from "../shims/internal/pages-data-url.js";
|
|
12
|
+
import { normalizePathTrailingSlash } from "../shims/url-utils.js";
|
|
8
13
|
import { BLOCKED_PAGES, PHASE_PRODUCTION_BUILD } from "../shims/constants.js";
|
|
9
14
|
import { loadNextConfig, resolveNextConfig } from "../config/next-config.js";
|
|
15
|
+
import { CACHEABILITY_POLICY_HEADERS } from "../shims/cacheability-classification.js";
|
|
16
|
+
import { pagesRouteHasPriorityOverAppRoute } from "../server/hybrid-route-priority.js";
|
|
10
17
|
import { readPrerenderSecret } from "./server-manifest.js";
|
|
11
18
|
import { startProdServer } from "../server/prod-server.js";
|
|
12
19
|
import { enterPrerenderPhase } from "./prerender-phase.js";
|
|
13
20
|
import { buildUrlFromParams, resolveParentParams } from "./prerender.js";
|
|
21
|
+
import { resolveAppPageDynamicConfig } from "../server/app-segment-config.js";
|
|
14
22
|
import fs from "node:fs";
|
|
15
23
|
//#region src/build/prerender-paths.ts
|
|
16
24
|
const PRERENDER_PATH_DISCOVERY_ENV = "__VINEXT_PRERENDER_PATH_DISCOVERY";
|
|
17
25
|
const PRERENDER_PATHS_MANIFEST = "vinext-prerender-paths.json";
|
|
18
26
|
const PATH_DISCOVERY_FETCH_TIMEOUT_MS = 3e4;
|
|
27
|
+
const DEFAULT_REMOTE_PATH_DISCOVERY_RETRY_DELAY_MS = 1e3;
|
|
28
|
+
const DEFAULT_REMOTE_PATH_DISCOVERY_PHASE_TIMEOUT_MS = 12e4;
|
|
29
|
+
function readDiscoveryUserFailure(response, text) {
|
|
30
|
+
if (response.status !== 500 || !response.headers.get("content-type")?.includes("application/json")) return null;
|
|
31
|
+
try {
|
|
32
|
+
const value = JSON.parse(text);
|
|
33
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
34
|
+
const error = value.error;
|
|
35
|
+
return typeof error === "string" && error.length > 0 ? error : null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
19
40
|
function readBuiltBuildId(serverDir) {
|
|
20
41
|
try {
|
|
21
42
|
const buildId = fs.readFileSync(path.join(serverDir, "BUILD_ID"), "utf-8").trim();
|
|
@@ -25,40 +46,153 @@ function readBuiltBuildId(serverDir) {
|
|
|
25
46
|
throw error;
|
|
26
47
|
}
|
|
27
48
|
}
|
|
49
|
+
function readBuiltRscBuildId(serverDir) {
|
|
50
|
+
try {
|
|
51
|
+
const buildId = fs.readFileSync(path.join(serverDir, "RSC_BUILD_ID"), "utf-8").trim();
|
|
52
|
+
return buildId.length > 0 ? buildId : null;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return null;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
28
58
|
function addPath(paths, seen, pathname) {
|
|
29
59
|
if (seen.has(pathname)) return;
|
|
30
60
|
seen.add(pathname);
|
|
31
61
|
paths.push(pathname);
|
|
32
62
|
}
|
|
33
|
-
function
|
|
63
|
+
function throwDiscoveryFailure(route, error) {
|
|
34
64
|
const message = error instanceof Error ? error.message : String(error);
|
|
35
|
-
|
|
65
|
+
if (/cloudflare:|ERR_UNSUPPORTED_ESM_URL_SCHEME/i.test(message)) throw new Error(`Failed to discover warmup path(s) for ${route}: Cloudflare runtime bindings cannot execute in the local Node prerender server. Use \`vinext-cloudflare deploy --experimental-warm-cdn-cache\` so path discovery runs against the staged Worker version.`, { cause: error });
|
|
66
|
+
throw new Error(`Failed to discover warmup path(s) for ${route}: ${message}`, { cause: error });
|
|
36
67
|
}
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
function validatePagesStaticPathsResult(value, route) {
|
|
69
|
+
const expected = "Expected { paths: [], fallback: boolean | 'blocking' }.";
|
|
70
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid value returned from getStaticPaths for ${route}. ${expected}`);
|
|
71
|
+
const result = value;
|
|
72
|
+
const extraKeys = Object.keys(result).filter((key) => key !== "paths" && key !== "fallback");
|
|
73
|
+
if (extraKeys.length > 0) throw new Error(`Extra key(s) returned from getStaticPaths for ${route}: ${extraKeys.join(", ")}. ${expected}`);
|
|
74
|
+
if (typeof result.fallback !== "boolean" && result.fallback !== "blocking") throw new Error(`Invalid fallback returned from getStaticPaths for ${route}. ${expected}`);
|
|
75
|
+
if (!Array.isArray(result.paths)) throw new Error(`Invalid paths returned from getStaticPaths for ${route}; paths must be an array.`);
|
|
76
|
+
return {
|
|
77
|
+
fallback: result.fallback,
|
|
78
|
+
paths: result.paths
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function hasUnsafeRawUrlPathCharacter(value) {
|
|
82
|
+
for (let index = 0; index < value.length; index++) {
|
|
83
|
+
const code = value.charCodeAt(index);
|
|
84
|
+
if (code === 92 || code <= 31 || code === 127) return true;
|
|
53
85
|
}
|
|
86
|
+
return false;
|
|
54
87
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
88
|
+
function getDynamicPatternParams(pattern) {
|
|
89
|
+
return pattern.split("/").filter((segment) => segment.startsWith(":")).map((segment) => ({
|
|
90
|
+
name: segment.slice(1, segment.endsWith("+") || segment.endsWith("*") ? -1 : void 0),
|
|
91
|
+
optional: segment.endsWith("*"),
|
|
92
|
+
repeat: segment.endsWith("+") || segment.endsWith("*")
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
function validateDiscoveredParams(value, pattern, source) {
|
|
96
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${source} must return parameter objects for ${pattern}.`);
|
|
97
|
+
const params = { ...value };
|
|
98
|
+
for (const { name, optional, repeat } of getDynamicPatternParams(pattern)) {
|
|
99
|
+
const hasValue = Object.prototype.hasOwnProperty.call(params, name);
|
|
100
|
+
let paramValue = params[name];
|
|
101
|
+
if (optional && hasValue && (paramValue === null || paramValue === void 0 || paramValue === false)) {
|
|
102
|
+
paramValue = [];
|
|
103
|
+
params[name] = paramValue;
|
|
104
|
+
}
|
|
105
|
+
if (!(repeat ? Array.isArray(paramValue) && paramValue.every((entry) => typeof entry === "string") : typeof paramValue === "string")) throw new Error(`Parameter ${name} from ${source} for ${pattern} must be ${repeat ? "an array of strings" : "a string"}.`);
|
|
106
|
+
if ((Array.isArray(paramValue) ? paramValue : [paramValue]).some((entry) => entry === "." || entry === "..")) throw new Error(`Parameter ${name} from ${source} for ${pattern} must not contain dot path segments.`);
|
|
107
|
+
}
|
|
108
|
+
return params;
|
|
109
|
+
}
|
|
110
|
+
function validatePagesStaticPathsEntry(entry, pattern) {
|
|
111
|
+
if (typeof entry === "string") {
|
|
112
|
+
if (!entry.startsWith("/") || entry.includes("//") || entry.includes("?") || entry.includes("#") || hasUnsafeRawUrlPathCharacter(entry)) throw new Error(`The provided path \`${entry}\` from getStaticPaths does not match the route pattern \`${pattern}\`.`);
|
|
113
|
+
let decodedSegments;
|
|
114
|
+
try {
|
|
115
|
+
decodedSegments = entry.split("/").map((segment) => decodeURIComponent(segment));
|
|
116
|
+
} catch {
|
|
117
|
+
throw new Error(`The provided path \`${entry}\` from getStaticPaths contains malformed percent-encoding.`);
|
|
118
|
+
}
|
|
119
|
+
if (decodedSegments.some((segment) => segment === "." || segment === "..")) throw new Error(`The provided path \`${entry}\` from getStaticPaths contains a dot path segment.`);
|
|
120
|
+
return entry;
|
|
61
121
|
}
|
|
122
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return entry;
|
|
123
|
+
const extraKeys = Object.keys(entry).filter((key) => key !== "params" && key !== "locale");
|
|
124
|
+
if (extraKeys.length > 0) throw new Error(`Additional key(s) returned from getStaticPaths for ${pattern}: ${extraKeys.join(", ")}.`);
|
|
125
|
+
if (entry.locale !== void 0 && typeof entry.locale !== "string") throw new Error(`Invalid locale returned from getStaticPaths for ${pattern}.`);
|
|
126
|
+
return {
|
|
127
|
+
...entry,
|
|
128
|
+
params: validateDiscoveredParams(entry.params, pattern, "getStaticPaths")
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
async function fetchDiscoveryEndpoint(url, headers, retryOptions = {}) {
|
|
132
|
+
const hasSharedPhaseDeadline = retryOptions.deadlineAt !== void 0;
|
|
133
|
+
const retryDelayMs = Math.max(0, retryOptions.retryDelayMs ?? (hasSharedPhaseDeadline ? 1e3 : 0));
|
|
134
|
+
const phaseTimeoutMs = Math.max(1, retryOptions.phaseTimeoutMs ?? 12e4);
|
|
135
|
+
const retryBudgetDelayMs = retryDelayMs > 0 ? retryDelayMs : DEFAULT_REMOTE_PATH_DISCOVERY_RETRY_DELAY_MS;
|
|
136
|
+
const retries = Math.max(0, retryOptions.retries ?? (hasSharedPhaseDeadline ? Math.ceil(phaseTimeoutMs / retryBudgetDelayMs) : 0));
|
|
137
|
+
const deadlineAt = retryOptions.deadlineAt ?? Date.now() + phaseTimeoutMs;
|
|
138
|
+
let lastError;
|
|
139
|
+
let attemptsRun = 0;
|
|
140
|
+
let lastTransientStatus;
|
|
141
|
+
const attemptSummary = () => ` after ${attemptsRun} attempt(s)${lastTransientStatus === void 0 ? "" : `; last transient status was HTTP ${lastTransientStatus}`}`;
|
|
142
|
+
const phaseTimeoutError = () => /* @__PURE__ */ new Error(`remote path discovery exceeded its ${phaseTimeoutMs}ms phase deadline${attemptSummary()}`);
|
|
143
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
144
|
+
const remainingMs = deadlineAt - Date.now();
|
|
145
|
+
if (remainingMs <= 0) throw phaseTimeoutError();
|
|
146
|
+
attemptsRun++;
|
|
147
|
+
const controller = new AbortController();
|
|
148
|
+
const attemptTimeoutMs = Math.min(PATH_DISCOVERY_FETCH_TIMEOUT_MS, remainingMs);
|
|
149
|
+
let timeout;
|
|
150
|
+
let shouldRetry = true;
|
|
151
|
+
try {
|
|
152
|
+
const request = (async () => {
|
|
153
|
+
const res = await fetch(url, {
|
|
154
|
+
headers,
|
|
155
|
+
signal: controller.signal
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
res,
|
|
159
|
+
text: await res.text()
|
|
160
|
+
};
|
|
161
|
+
})();
|
|
162
|
+
const timedOut = new Promise((_resolve, reject) => {
|
|
163
|
+
timeout = setTimeout(() => {
|
|
164
|
+
controller.abort();
|
|
165
|
+
reject(new DOMException(`Timed out after ${attemptTimeoutMs}ms`, "AbortError"));
|
|
166
|
+
}, attemptTimeoutMs);
|
|
167
|
+
});
|
|
168
|
+
const { res, text } = await Promise.race([request, timedOut]);
|
|
169
|
+
if (res.ok) {
|
|
170
|
+
if (res.status === 204) return null;
|
|
171
|
+
return text;
|
|
172
|
+
}
|
|
173
|
+
const userFailure = readDiscoveryUserFailure(res, text);
|
|
174
|
+
const detail = userFailure ?? (/cloudflare:|ERR_UNSUPPORTED_ESM_URL_SCHEME/i.test(text) ? text.trim() : "");
|
|
175
|
+
lastError = /* @__PURE__ */ new Error(`path discovery returned HTTP ${res.status}${detail ? `: ${detail}` : ""}`);
|
|
176
|
+
if (!(res.status === 404 || res.status >= 500 && res.status <= 599 && userFailure === null)) {
|
|
177
|
+
shouldRetry = false;
|
|
178
|
+
throw lastError;
|
|
179
|
+
}
|
|
180
|
+
lastTransientStatus = res.status;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (Date.now() >= deadlineAt) throw phaseTimeoutError();
|
|
183
|
+
lastError = error instanceof Error && error.name === "AbortError" ? /* @__PURE__ */ new Error(`path discovery timed out after ${attemptTimeoutMs}ms`) : error;
|
|
184
|
+
if (!shouldRetry) throw lastError;
|
|
185
|
+
} finally {
|
|
186
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
187
|
+
}
|
|
188
|
+
if (attempt < retries && retryDelayMs > 0) {
|
|
189
|
+
const delayMs = Math.min(retryDelayMs, Math.max(0, deadlineAt - Date.now()));
|
|
190
|
+
if (delayMs <= 0) throw phaseTimeoutError();
|
|
191
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
195
|
+
throw new Error(`${message}${attemptSummary()}`, { cause: lastError });
|
|
62
196
|
}
|
|
63
197
|
function resolveConfiguredRouteDirs(root, routeRootConfig) {
|
|
64
198
|
if (!routeRootConfig) return {
|
|
@@ -83,16 +217,12 @@ function resolveConfiguredRouteDirs(root, routeRootConfig) {
|
|
|
83
217
|
pagesDir: fs.existsSync(pagesDir) ? pagesDir : null
|
|
84
218
|
};
|
|
85
219
|
}
|
|
86
|
-
function appRouteMayHaveGenerateStaticParams(route) {
|
|
87
|
-
if (fileHasNamedExport(route.pagePath, "generateStaticParams")) return true;
|
|
88
|
-
return route.layouts.some((layoutPath) => fileHasNamedExport(layoutPath, "generateStaticParams"));
|
|
89
|
-
}
|
|
90
220
|
async function shouldStartPathDiscoveryServer(options) {
|
|
91
221
|
if (options.appDir) {
|
|
92
|
-
if ((await appRouter(options.appDir, options.pageExtensions)).some((route) => route.isDynamic
|
|
222
|
+
if ((await appRouter(options.appDir, options.pageExtensions)).some((route) => route.isDynamic)) return true;
|
|
93
223
|
}
|
|
94
224
|
if (options.pagesDir) {
|
|
95
|
-
if ((await pagesRouter(options.pagesDir, options.pageExtensions)).some((route) => route.isDynamic
|
|
225
|
+
if ((await pagesRouter(options.pagesDir, options.pageExtensions)).some((route) => route.isDynamic)) return true;
|
|
96
226
|
}
|
|
97
227
|
return false;
|
|
98
228
|
}
|
|
@@ -113,56 +243,154 @@ async function collectPagesPaths(options) {
|
|
|
113
243
|
const apiPatterns = new Set(apiRoutes.map((route) => route.pattern));
|
|
114
244
|
const paths = [];
|
|
115
245
|
const seen = /* @__PURE__ */ new Set();
|
|
246
|
+
const dataPaths = [];
|
|
247
|
+
const seenDataPaths = /* @__PURE__ */ new Set();
|
|
248
|
+
const fallbackRoutePatterns = [];
|
|
116
249
|
for (const route of pageRoutes) {
|
|
117
250
|
if (apiPatterns.has(route.pattern)) continue;
|
|
118
251
|
if (BLOCKED_PAGES.includes(route.pattern)) continue;
|
|
119
252
|
if (route.pattern === "/404" || route.pattern === "/500" || route.pattern === "/_error") continue;
|
|
120
|
-
const { type } = classifyPagesRoute(route.filePath);
|
|
121
|
-
if (type === "api"
|
|
253
|
+
const { hasServerSideProps, hasStaticProps, type } = classifyPagesRoute(route.filePath);
|
|
254
|
+
if (type === "api") continue;
|
|
122
255
|
if (!route.isDynamic) {
|
|
123
|
-
|
|
256
|
+
if (options.i18n) for (const locale of options.i18n.locales) {
|
|
257
|
+
const pathname = localizePagesPath(route.pattern, locale, options.i18n);
|
|
258
|
+
addPath(paths, seen, pathname);
|
|
259
|
+
if (hasStaticProps || hasServerSideProps) addPath(dataPaths, seenDataPaths, pathname);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
addPath(paths, seen, route.pattern);
|
|
263
|
+
if (hasStaticProps || hasServerSideProps) addPath(dataPaths, seenDataPaths, route.pattern);
|
|
264
|
+
}
|
|
124
265
|
continue;
|
|
125
266
|
}
|
|
126
|
-
if (
|
|
267
|
+
if (type === "ssr") continue;
|
|
127
268
|
if (!options.baseUrl) continue;
|
|
128
269
|
try {
|
|
129
270
|
const search = new URLSearchParams({ pattern: route.pattern });
|
|
130
|
-
|
|
271
|
+
if (options.i18n) {
|
|
272
|
+
search.set("locales", JSON.stringify(options.i18n.locales));
|
|
273
|
+
search.set("defaultLocale", options.i18n.defaultLocale);
|
|
274
|
+
}
|
|
275
|
+
const text = await fetchDiscoveryEndpoint(`${options.baseUrl}/__vinext/prerender/pages-static-paths?${search}`, options.secretHeaders, options.retryOptions);
|
|
131
276
|
if (text === null) continue;
|
|
132
|
-
const pathsResult = JSON.parse(text);
|
|
133
|
-
|
|
134
|
-
|
|
277
|
+
const pathsResult = validatePagesStaticPathsResult(JSON.parse(text), route.pattern);
|
|
278
|
+
if (pathsResult.fallback !== false) fallbackRoutePatterns.push({
|
|
279
|
+
kind: "pages-page",
|
|
280
|
+
pattern: route.pattern
|
|
281
|
+
});
|
|
282
|
+
for (const item of pathsResult.paths) {
|
|
283
|
+
const validatedItem = validatePagesStaticPathsEntry(item, route.pattern);
|
|
284
|
+
let itemToNormalize = validatedItem;
|
|
285
|
+
let locale = options.i18n?.defaultLocale;
|
|
286
|
+
if (options.i18n && typeof validatedItem === "string") itemToNormalize = extractPagesStaticPathLocale(validatedItem, options.i18n).url;
|
|
287
|
+
else if (options.i18n && validatedItem && typeof validatedItem === "object" && validatedItem.locale) {
|
|
288
|
+
if (!options.i18n.locales.includes(validatedItem.locale)) throw new Error(`Invalid locale returned from getStaticPaths for ${route.pattern}: ${validatedItem.locale}`);
|
|
289
|
+
locale = validatedItem.locale;
|
|
290
|
+
}
|
|
291
|
+
const normalized = normalizeStaticPathsEntry(itemToNormalize, route.pattern);
|
|
135
292
|
if ("error" in normalized) throw new Error(normalized.error);
|
|
136
|
-
|
|
293
|
+
const pathname = typeof validatedItem === "string" ? normalizeStaticPathname(validatedItem) : localizePagesPath(buildUrlFromParams(route.pattern, normalized.params), locale, options.i18n);
|
|
294
|
+
addPath(paths, seen, pathname);
|
|
295
|
+
if (hasStaticProps) addPath(dataPaths, seenDataPaths, pathname);
|
|
137
296
|
}
|
|
138
297
|
} catch (error) {
|
|
139
|
-
|
|
298
|
+
throwDiscoveryFailure(route.pattern, error);
|
|
140
299
|
}
|
|
141
300
|
}
|
|
142
|
-
return
|
|
301
|
+
return {
|
|
302
|
+
dataPaths,
|
|
303
|
+
fallbackRoutePatterns,
|
|
304
|
+
paths
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
async function excludePagesApiWarmPaths(options) {
|
|
308
|
+
const apiRoutes = await apiRouter(options.pagesDir, options.pageExtensions);
|
|
309
|
+
return options.paths.filter((pathname) => {
|
|
310
|
+
const pagesPathname = options.i18n ? extractLocaleFromUrl(pathname, options.i18n).url : pathname;
|
|
311
|
+
if (pagesPathname !== "/api" && !pagesPathname.startsWith("/api/")) return true;
|
|
312
|
+
return matchRoute(pagesPathname, apiRoutes) === null;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
async function resolvePagesWarmRoutePatterns(options) {
|
|
316
|
+
const pageRoutes = await pagesRouter(options.pagesDir, options.pageExtensions);
|
|
317
|
+
return Object.fromEntries(options.paths.flatMap((pathname) => {
|
|
318
|
+
const match = matchRoute(options.i18n ? extractLocaleFromUrl(pathname, options.i18n).url : pathname, pageRoutes);
|
|
319
|
+
return match ? [[pathname, {
|
|
320
|
+
kind: "pages-page",
|
|
321
|
+
pattern: match.route.pattern
|
|
322
|
+
}]] : [];
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
function localizePagesPath(pathname, locale, i18n) {
|
|
326
|
+
if (!i18n || !locale || locale === i18n.defaultLocale) return pathname;
|
|
327
|
+
return pathname === "/" ? `/${locale}` : `/${locale}${pathname}`;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Next.js data URLs always carry the locale segment, including the default
|
|
331
|
+
* locale that is omitted from the corresponding public HTML pathname.
|
|
332
|
+
*
|
|
333
|
+
* Ported from Next.js:
|
|
334
|
+
* packages/next/src/shared/lib/router/utils/format-next-pathname-info.ts
|
|
335
|
+
*/
|
|
336
|
+
function localizePagesDataPath(pathname, i18n) {
|
|
337
|
+
if (!i18n) return pathname;
|
|
338
|
+
if (extractPagesStaticPathLocale(pathname, i18n).explicitLocalePrefix) return pathname;
|
|
339
|
+
return pathname === "/" ? `/${i18n.defaultLocale}` : `/${i18n.defaultLocale}${pathname}`;
|
|
340
|
+
}
|
|
341
|
+
function extractPagesStaticPathLocale(url, i18n) {
|
|
342
|
+
const queryIndex = url.indexOf("?");
|
|
343
|
+
const pathname = queryIndex === -1 ? url : url.slice(0, queryIndex);
|
|
344
|
+
const query = queryIndex === -1 ? "" : url.slice(queryIndex);
|
|
345
|
+
const parts = pathname.split("/").filter(Boolean);
|
|
346
|
+
const locale = parts.length > 0 ? i18n.locales.find((candidate) => candidate.toLowerCase() === parts[0].toLowerCase()) : void 0;
|
|
347
|
+
if (!locale) return extractLocaleFromUrl(url, i18n);
|
|
348
|
+
const rest = `/${parts.slice(1).join("/")}`;
|
|
349
|
+
return {
|
|
350
|
+
explicitLocalePrefix: parts[0],
|
|
351
|
+
locale,
|
|
352
|
+
url: `${rest || "/"}${query}`
|
|
353
|
+
};
|
|
143
354
|
}
|
|
144
355
|
async function collectAppPaths(options) {
|
|
145
356
|
const routes = await appRouter(options.appDir, options.pageExtensions);
|
|
146
357
|
const paths = [];
|
|
147
358
|
const seen = /* @__PURE__ */ new Set();
|
|
359
|
+
const loadingShellPaths = [];
|
|
360
|
+
const seenLoadingShellPaths = /* @__PURE__ */ new Set();
|
|
361
|
+
const routeHandlerPaths = [];
|
|
362
|
+
const seenRouteHandlerPaths = /* @__PURE__ */ new Set();
|
|
363
|
+
const fallbackRoutePatterns = [];
|
|
148
364
|
const staticParamsCache = /* @__PURE__ */ new Map();
|
|
365
|
+
let requireNonEmptyStaticParams = false;
|
|
149
366
|
const staticParamsMap = new Proxy({}, {
|
|
150
367
|
get(_target, pattern) {
|
|
151
368
|
return async ({ params }) => {
|
|
152
369
|
if (!options.baseUrl) return null;
|
|
153
370
|
const cacheKey = `${pattern}\0${JSON.stringify(params)}`;
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
371
|
+
let request = staticParamsCache.get(cacheKey);
|
|
372
|
+
if (request === void 0) {
|
|
373
|
+
request = (async () => {
|
|
374
|
+
const search = new URLSearchParams({ pattern });
|
|
375
|
+
if (Object.keys(params).length > 0) search.set("parentParams", JSON.stringify(params));
|
|
376
|
+
const text = await fetchDiscoveryEndpoint(`${options.baseUrl}/__vinext/prerender/static-params?${search}`, options.secretHeaders, options.retryOptions);
|
|
377
|
+
if (text === null) return null;
|
|
378
|
+
const value = JSON.parse(text);
|
|
379
|
+
if (!Array.isArray(value)) throw new Error(`generateStaticParams must return an array for ${pattern}.`);
|
|
380
|
+
return value.map((entry) => {
|
|
381
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`generateStaticParams must return parameter objects for ${pattern}.`);
|
|
382
|
+
return validateDiscoveredParams({
|
|
383
|
+
...params,
|
|
384
|
+
...entry
|
|
385
|
+
}, pattern, "generateStaticParams");
|
|
386
|
+
});
|
|
387
|
+
})();
|
|
388
|
+
request.catch(() => staticParamsCache.delete(cacheKey));
|
|
389
|
+
staticParamsCache.set(cacheKey, request);
|
|
390
|
+
}
|
|
391
|
+
const value = await request;
|
|
392
|
+
if (requireNonEmptyStaticParams && value?.length === 0) throw new Error("When using Cache Components, all `generateStaticParams` functions must return at least one result. This is to ensure that we can perform build-time validation that there is no other dynamic accesses that would cause a runtime error.\n\nLearn more: https://nextjs.org/docs/messages/empty-generate-static-params");
|
|
393
|
+
return value;
|
|
166
394
|
};
|
|
167
395
|
},
|
|
168
396
|
has() {
|
|
@@ -170,16 +398,30 @@ async function collectAppPaths(options) {
|
|
|
170
398
|
}
|
|
171
399
|
});
|
|
172
400
|
for (const route of routes) {
|
|
173
|
-
const
|
|
401
|
+
const isRouteHandler = route.routePath !== null && route.pagePath === null;
|
|
402
|
+
const renderEntryPath = isRouteHandler ? route.routePath : getAppRouteRenderEntryPath(route);
|
|
174
403
|
if (!renderEntryPath) continue;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
404
|
+
if (isRouteHandler) {
|
|
405
|
+
const classification = classifyAppRouteHandler(route.routePath);
|
|
406
|
+
if (!classification.hasGet || !classification.staticGenerationEnabled) continue;
|
|
407
|
+
} else {
|
|
408
|
+
const { type } = classifyAppRoute(renderEntryPath, route.routePath, route.isDynamic);
|
|
409
|
+
if (type === "api") continue;
|
|
410
|
+
}
|
|
411
|
+
const hasMainTreeLoadingBoundary = appRouteHasMainTreeLoadingBoundary(route);
|
|
412
|
+
const addDiscoveredPath = (pathname) => {
|
|
413
|
+
if (isRouteHandler) {
|
|
414
|
+
addPath(routeHandlerPaths, seenRouteHandlerPaths, pathname);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
addPath(paths, seen, pathname);
|
|
418
|
+
if (hasMainTreeLoadingBoundary) addPath(loadingShellPaths, seenLoadingShellPaths, pathname);
|
|
419
|
+
};
|
|
178
420
|
if (!route.isDynamic) {
|
|
179
|
-
|
|
421
|
+
addDiscoveredPath(route.pattern);
|
|
180
422
|
continue;
|
|
181
423
|
}
|
|
182
|
-
|
|
424
|
+
requireNonEmptyStaticParams = options.cacheComponents && !isRouteHandler;
|
|
183
425
|
try {
|
|
184
426
|
const generateStaticParams = staticParamsMap[route.pattern];
|
|
185
427
|
if (typeof generateStaticParams !== "function") continue;
|
|
@@ -200,18 +442,208 @@ async function collectAppPaths(options) {
|
|
|
200
442
|
}
|
|
201
443
|
} else {
|
|
202
444
|
const results = await generateStaticParams({ params: {} });
|
|
203
|
-
|
|
445
|
+
if (results === null) {
|
|
446
|
+
const layoutParamSets = await resolveParentParams(route, staticParamsMap, { includeLastDynamicSegment: true });
|
|
447
|
+
paramSets = layoutParamSets.length > 0 ? layoutParamSets : null;
|
|
448
|
+
} else paramSets = Array.isArray(results) ? results : [];
|
|
449
|
+
}
|
|
450
|
+
if (!paramSets?.length) {
|
|
451
|
+
if (isRouteHandler) {
|
|
452
|
+
const dynamicConfig = extractExportConstString(fs.readFileSync(renderEntryPath, "utf8"), "dynamic");
|
|
453
|
+
if (paramSets !== null || dynamicConfig === "force-static" || dynamicConfig === "error") fallbackRoutePatterns.push({
|
|
454
|
+
kind: "app-route",
|
|
455
|
+
pattern: route.pattern
|
|
456
|
+
});
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const parallelSegments = route.parallelSlots.flatMap((slot) => [
|
|
460
|
+
slot.layoutPath,
|
|
461
|
+
...slot.configLayoutPaths ?? [],
|
|
462
|
+
slot.pagePath ?? slot.defaultPath
|
|
463
|
+
].filter((filePath) => typeof filePath === "string"));
|
|
464
|
+
const hasDynamicSegment = [
|
|
465
|
+
...route.layouts,
|
|
466
|
+
renderEntryPath,
|
|
467
|
+
...parallelSegments
|
|
468
|
+
].map((filePath) => classifyAppRoute(filePath, null, false)).some((classification) => classification.type === "ssr");
|
|
469
|
+
const readDynamicConfig = (filePath) => {
|
|
470
|
+
const dynamic = extractExportConstString(fs.readFileSync(filePath, "utf8"), "dynamic");
|
|
471
|
+
return dynamic === null ? {} : { dynamic };
|
|
472
|
+
};
|
|
473
|
+
const dynamicConfig = resolveAppPageDynamicConfig({
|
|
474
|
+
layouts: route.layouts.map(readDynamicConfig),
|
|
475
|
+
page: readDynamicConfig(renderEntryPath),
|
|
476
|
+
parallelSegments: parallelSegments.map(readDynamicConfig)
|
|
477
|
+
});
|
|
478
|
+
if ((paramSets !== null || dynamicConfig === "force-static" || dynamicConfig === "error") && !hasDynamicSegment) fallbackRoutePatterns.push({
|
|
479
|
+
kind: "app-page",
|
|
480
|
+
pattern: route.pattern
|
|
481
|
+
});
|
|
482
|
+
continue;
|
|
204
483
|
}
|
|
205
|
-
if (!paramSets?.length) continue;
|
|
206
484
|
for (const params of paramSets) {
|
|
207
485
|
if (params === null || params === void 0) continue;
|
|
208
|
-
|
|
486
|
+
addDiscoveredPath(buildUrlFromParams(route.pattern, params));
|
|
209
487
|
}
|
|
210
488
|
} catch (error) {
|
|
211
|
-
|
|
489
|
+
throwDiscoveryFailure(route.pattern, error);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
fallbackRoutePatterns,
|
|
494
|
+
loadingShellPaths,
|
|
495
|
+
paths,
|
|
496
|
+
routeHandlerPaths
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
async function resolveAppWarmPaths(options) {
|
|
500
|
+
const appRoutes = await appRouter(options.appDir, options.pageExtensions);
|
|
501
|
+
const routeHandlerClassifications = new Map(appRoutes.flatMap((route) => route.routePath && !route.pagePath ? [[route.routePath, classifyAppRouteHandler(route.routePath)]] : []));
|
|
502
|
+
const [pageRoutes, apiRoutes] = options.pagesDir ? await Promise.all([pagesRouter(options.pagesDir, options.pageExtensions), apiRouter(options.pagesDir, options.pageExtensions)]) : [[], []];
|
|
503
|
+
const rscPaths = [];
|
|
504
|
+
const appPaths = [];
|
|
505
|
+
const appRoutePaths = [];
|
|
506
|
+
const htmlPaths = [];
|
|
507
|
+
const loadingShellPaths = [];
|
|
508
|
+
const pagesPaths = [];
|
|
509
|
+
const routePatterns = {};
|
|
510
|
+
for (const pathname of options.paths) {
|
|
511
|
+
const appMatch = matchAppRoute(pathname, appRoutes);
|
|
512
|
+
const pagesPathname = options.i18n ? extractLocaleFromUrl(pathname, options.i18n).url : pathname;
|
|
513
|
+
const isPagesApiRequest = pathname === "/api" || pathname.startsWith("/api/");
|
|
514
|
+
const pagesMatch = matchRoute(pagesPathname, isPagesApiRequest ? apiRoutes : pageRoutes);
|
|
515
|
+
if (pagesMatch && (!appMatch || pagesRouteHasPriorityOverAppRoute(pagesMatch.route, appMatch.route))) {
|
|
516
|
+
if (!isPagesApiRequest) {
|
|
517
|
+
htmlPaths.push(pathname);
|
|
518
|
+
pagesPaths.push(pathname);
|
|
519
|
+
routePatterns[pathname] = {
|
|
520
|
+
kind: "pages-page",
|
|
521
|
+
pattern: pagesMatch.route.pattern
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
if (!appMatch) continue;
|
|
527
|
+
const matchedAppRoute = appMatch.route;
|
|
528
|
+
if (matchedAppRoute.routePath && !matchedAppRoute.pagePath) {
|
|
529
|
+
const classification = routeHandlerClassifications.get(matchedAppRoute.routePath);
|
|
530
|
+
if (classification?.hasGet && classification.staticGenerationEnabled) {
|
|
531
|
+
appRoutePaths.push(pathname);
|
|
532
|
+
routePatterns[pathname] = {
|
|
533
|
+
kind: "app-route",
|
|
534
|
+
pattern: matchedAppRoute.pattern
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
continue;
|
|
212
538
|
}
|
|
539
|
+
const appRenderEntryPath = getAppRouteRenderEntryPath(matchedAppRoute);
|
|
540
|
+
if (!appRenderEntryPath) continue;
|
|
541
|
+
if (classifyAppRoute(appRenderEntryPath, matchedAppRoute.routePath, matchedAppRoute.isDynamic).type === "api") continue;
|
|
542
|
+
appPaths.push(pathname);
|
|
543
|
+
htmlPaths.push(pathname);
|
|
544
|
+
rscPaths.push(pathname);
|
|
545
|
+
routePatterns[pathname] = {
|
|
546
|
+
kind: "app-page",
|
|
547
|
+
pattern: matchedAppRoute.pattern
|
|
548
|
+
};
|
|
549
|
+
if (appRouteHasMainTreeLoadingBoundary(matchedAppRoute)) loadingShellPaths.push(pathname);
|
|
213
550
|
}
|
|
214
|
-
return
|
|
551
|
+
return {
|
|
552
|
+
appPaths,
|
|
553
|
+
appRoutePaths,
|
|
554
|
+
htmlPaths,
|
|
555
|
+
loadingShellPaths,
|
|
556
|
+
pagesPaths,
|
|
557
|
+
routePatterns,
|
|
558
|
+
rscPaths
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
const CACHEABILITY_POLICY_HEADER_NAMES = new Set(CACHEABILITY_POLICY_HEADERS);
|
|
562
|
+
function cachePolicyRuleMatchesWarmPath(pathname, rule, config) {
|
|
563
|
+
const canonicalPathname = normalizePathTrailingSlash(pathname, config.trailingSlash);
|
|
564
|
+
return [void 0, ...config.i18n?.domains?.map((domain) => domain.domain) ?? []].some((hostname) => {
|
|
565
|
+
const matchPathname = normalizeDefaultLocalePathname(canonicalPathname, config.i18n, { hostname });
|
|
566
|
+
let sourceMatched = false;
|
|
567
|
+
matchHeaders(matchPathname, [rule], {
|
|
568
|
+
cookies: {},
|
|
569
|
+
headers: new Headers(),
|
|
570
|
+
host: hostname ?? "",
|
|
571
|
+
query: new URLSearchParams()
|
|
572
|
+
}, {
|
|
573
|
+
basePath: config.basePath,
|
|
574
|
+
hadBasePath: true
|
|
575
|
+
}, () => {
|
|
576
|
+
sourceMatched = true;
|
|
577
|
+
});
|
|
578
|
+
return sourceMatched;
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
function cachePolicyRuleSourceMatchesWarmPath(pathname, rule, config) {
|
|
582
|
+
return cachePolicyRuleMatchesWarmPath(pathname, {
|
|
583
|
+
...rule,
|
|
584
|
+
has: void 0,
|
|
585
|
+
missing: void 0
|
|
586
|
+
}, config);
|
|
587
|
+
}
|
|
588
|
+
function staticConfigPatternSegments(pattern) {
|
|
589
|
+
const segments = [];
|
|
590
|
+
for (const segment of pattern.split("/").filter(Boolean)) {
|
|
591
|
+
if (/[:*()[\]{}]/.test(segment)) break;
|
|
592
|
+
segments.push(segment);
|
|
593
|
+
}
|
|
594
|
+
return segments;
|
|
595
|
+
}
|
|
596
|
+
function routePatternCouldIntersectCachePolicyRule(routePattern, rule, basePath) {
|
|
597
|
+
let ruleSource = rule.source;
|
|
598
|
+
if (rule.basePath !== false && basePath && (ruleSource === basePath || ruleSource.startsWith(`${basePath}/`))) ruleSource = ruleSource.slice(basePath.length) || "/";
|
|
599
|
+
const routeSegments = staticConfigPatternSegments(routePattern);
|
|
600
|
+
const ruleSegments = staticConfigPatternSegments(ruleSource);
|
|
601
|
+
const sharedLength = Math.min(routeSegments.length, ruleSegments.length);
|
|
602
|
+
for (let index = 0; index < sharedLength; index++) if (routeSegments[index] !== ruleSegments[index]) return false;
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Certify only route-config bailouts that cannot hide a path- or
|
|
607
|
+
* request-specific next.config cache policy. The final Worker still evaluates
|
|
608
|
+
* every concrete response before emitting public cache headers.
|
|
609
|
+
*/
|
|
610
|
+
function annotateCacheabilityProbeSafety(routePatterns, config) {
|
|
611
|
+
const cachePolicyRules = config.headers.filter((rule) => rule.headers.some((header) => CACHEABILITY_POLICY_HEADER_NAMES.has(header.key.toLowerCase())));
|
|
612
|
+
const matchingPolicyRules = new Map(Object.keys(routePatterns).map((pathname) => [pathname, new Set(cachePolicyRules.filter((rule) => cachePolicyRuleMatchesWarmPath(pathname, rule, config)))]));
|
|
613
|
+
const pathsByPattern = /* @__PURE__ */ new Map();
|
|
614
|
+
for (const [pathname, route] of Object.entries(routePatterns)) {
|
|
615
|
+
const key = `${route.kind}\0${route.pattern}`;
|
|
616
|
+
const paths = pathsByPattern.get(key) ?? [];
|
|
617
|
+
paths.push(pathname);
|
|
618
|
+
pathsByPattern.set(key, paths);
|
|
619
|
+
}
|
|
620
|
+
const canPrunePatterns = /* @__PURE__ */ new Map();
|
|
621
|
+
for (const [patternKey, patternPaths] of pathsByPattern) {
|
|
622
|
+
const routePattern = routePatterns[patternPaths[0]].pattern;
|
|
623
|
+
const relevantRules = cachePolicyRules.filter((rule) => patternPaths.some((path) => cachePolicyRuleSourceMatchesWarmPath(path, rule, config)) || routePatternCouldIntersectCachePolicyRule(routePattern, rule, config.basePath));
|
|
624
|
+
canPrunePatterns.set(patternKey, relevantRules.every((rule) => !rule.has?.length && !rule.missing?.length && patternPaths.every((path) => matchingPolicyRules.get(path)?.has(rule) === true)));
|
|
625
|
+
}
|
|
626
|
+
return Object.fromEntries(Object.entries(routePatterns).map(([pathname, route]) => {
|
|
627
|
+
const patternKey = `${route.kind}\0${route.pattern}`;
|
|
628
|
+
const canPrunePattern = canPrunePatterns.get(patternKey) ?? false;
|
|
629
|
+
return [pathname, {
|
|
630
|
+
...route,
|
|
631
|
+
cacheabilityProbe: { canPrunePattern }
|
|
632
|
+
}];
|
|
633
|
+
}));
|
|
634
|
+
}
|
|
635
|
+
function configuredRouteAffectsWarmPath(pathname, config) {
|
|
636
|
+
const canonicalPathname = normalizePathTrailingSlash(pathname, config.trailingSlash);
|
|
637
|
+
const hostnames = [void 0, ...config.i18n?.domains?.map((domain) => domain.domain) ?? []];
|
|
638
|
+
const matchPathnames = new Set(hostnames.map((hostname) => normalizeDefaultLocalePathname(canonicalPathname, config.i18n, { hostname })));
|
|
639
|
+
return [...[
|
|
640
|
+
...config.rewrites.beforeFiles,
|
|
641
|
+
...config.rewrites.afterFiles,
|
|
642
|
+
...config.rewrites.fallback
|
|
643
|
+
], ...config.redirects].some((rule) => Array.from(matchPathnames).some((matchPathname) => matchesRewriteSource(matchPathname, rule, {
|
|
644
|
+
basePath: config.basePath,
|
|
645
|
+
hadBasePath: true
|
|
646
|
+
})));
|
|
215
647
|
}
|
|
216
648
|
async function startPathDiscoveryServer(options) {
|
|
217
649
|
return startProdServer({
|
|
@@ -237,16 +669,29 @@ async function emitPrerenderPathManifest(options) {
|
|
|
237
669
|
const manifestDir = path.join(root, "dist", "server");
|
|
238
670
|
const config = options.nextConfig ? { ...options.nextConfig } : { ...await resolveNextConfig(await loadNextConfig(root, PHASE_PRODUCTION_BUILD), root) };
|
|
239
671
|
const builtBuildId = readBuiltBuildId(manifestDir) ?? readBuiltBuildId(bundleServerDir);
|
|
672
|
+
const rscBuildId = readBuiltRscBuildId(manifestDir) ?? readBuiltRscBuildId(bundleServerDir);
|
|
240
673
|
if (builtBuildId) config.buildId = builtBuildId;
|
|
241
674
|
const paths = [];
|
|
242
675
|
const seen = /* @__PURE__ */ new Set();
|
|
676
|
+
const discoveredPagesPaths = [];
|
|
677
|
+
const seenPagesPaths = /* @__PURE__ */ new Set();
|
|
678
|
+
const discoveredPagesDataPaths = [];
|
|
679
|
+
const seenPagesDataPaths = /* @__PURE__ */ new Set();
|
|
680
|
+
const discoveredAppPaths = [];
|
|
681
|
+
const seenAppPaths = /* @__PURE__ */ new Set();
|
|
682
|
+
const discoveredRouteHandlerPaths = [];
|
|
683
|
+
const seenRouteHandlerPaths = /* @__PURE__ */ new Set();
|
|
684
|
+
const discoveredLoadingShellPaths = [];
|
|
685
|
+
const seenLoadingShellPaths = /* @__PURE__ */ new Set();
|
|
686
|
+
const fallbackRoutePatterns = [];
|
|
243
687
|
await withPrerenderEndpoints(async () => {
|
|
244
688
|
let prodServer = null;
|
|
245
|
-
|
|
689
|
+
const needsServer = await shouldStartPathDiscoveryServer({
|
|
246
690
|
appDir,
|
|
247
691
|
pagesDir,
|
|
248
692
|
pageExtensions: config.pageExtensions
|
|
249
|
-
})
|
|
693
|
+
});
|
|
694
|
+
if (needsServer && !options.pathDiscoveryTarget) try {
|
|
250
695
|
prodServer = await startPathDiscoveryServer({
|
|
251
696
|
serverDir: bundleServerDir,
|
|
252
697
|
pagesBundlePath: !appDir && pagesDir ? pagesBundlePath : void 0,
|
|
@@ -254,37 +699,131 @@ async function emitPrerenderPathManifest(options) {
|
|
|
254
699
|
});
|
|
255
700
|
} catch (error) {
|
|
256
701
|
const message = error instanceof Error ? error.message : String(error);
|
|
257
|
-
|
|
702
|
+
throw new Error(`Failed to start prerender path discovery server: ${message}`, { cause: error });
|
|
258
703
|
}
|
|
259
|
-
const baseUrl = prodServer ? `http://127.0.0.1:${prodServer.port}` : null;
|
|
704
|
+
const baseUrl = options.pathDiscoveryTarget?.baseUrl ? new URL(options.pathDiscoveryTarget.baseUrl).origin : prodServer ? `http://127.0.0.1:${prodServer.port}` : null;
|
|
260
705
|
const prerenderSecret = readPrerenderSecret(bundleServerDir) ?? readPrerenderSecret(manifestDir);
|
|
261
|
-
|
|
706
|
+
if (needsServer && options.pathDiscoveryTarget && !prerenderSecret) throw new Error("Cannot discover warmup paths from the staged Worker because dist/server/vinext-server.json does not contain a prerender secret. Rebuild the app before deploying.");
|
|
707
|
+
const secretHeaders = Object.fromEntries(new Headers(options.pathDiscoveryTarget?.headers));
|
|
708
|
+
if (prerenderSecret) secretHeaders[VINEXT_PRERENDER_SECRET_HEADER] = prerenderSecret;
|
|
709
|
+
const pathDiscoveryRetryOptions = options.pathDiscoveryTarget ? {
|
|
710
|
+
deadlineAt: Date.now() + Math.max(1, options.pathDiscoveryTarget.phaseTimeoutMs ?? 12e4),
|
|
711
|
+
phaseTimeoutMs: options.pathDiscoveryTarget.phaseTimeoutMs,
|
|
712
|
+
retries: options.pathDiscoveryTarget.retries,
|
|
713
|
+
retryDelayMs: options.pathDiscoveryTarget.retryDelayMs
|
|
714
|
+
} : void 0;
|
|
262
715
|
try {
|
|
263
|
-
if (appDir)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
716
|
+
if (appDir) {
|
|
717
|
+
const appPathResult = await collectAppPaths({
|
|
718
|
+
appDir,
|
|
719
|
+
baseUrl,
|
|
720
|
+
cacheComponents: config.cacheComponents,
|
|
721
|
+
pageExtensions: config.pageExtensions,
|
|
722
|
+
retryOptions: pathDiscoveryRetryOptions,
|
|
723
|
+
secretHeaders
|
|
724
|
+
});
|
|
725
|
+
for (const pathname of appPathResult.paths) {
|
|
726
|
+
addPath(paths, seen, pathname);
|
|
727
|
+
addPath(discoveredAppPaths, seenAppPaths, pathname);
|
|
728
|
+
}
|
|
729
|
+
for (const pathname of appPathResult.loadingShellPaths) addPath(discoveredLoadingShellPaths, seenLoadingShellPaths, pathname);
|
|
730
|
+
for (const pathname of appPathResult.routeHandlerPaths) addPath(discoveredRouteHandlerPaths, seenRouteHandlerPaths, pathname);
|
|
731
|
+
fallbackRoutePatterns.push(...appPathResult.fallbackRoutePatterns);
|
|
732
|
+
}
|
|
733
|
+
if (pagesDir) {
|
|
734
|
+
const pagesPathResult = await collectPagesPaths({
|
|
735
|
+
baseUrl,
|
|
736
|
+
i18n: config.i18n,
|
|
737
|
+
pagesDir,
|
|
738
|
+
pageExtensions: config.pageExtensions,
|
|
739
|
+
retryOptions: pathDiscoveryRetryOptions,
|
|
740
|
+
secretHeaders
|
|
741
|
+
});
|
|
742
|
+
for (const pathname of pagesPathResult.paths) {
|
|
743
|
+
addPath(paths, seen, pathname);
|
|
744
|
+
addPath(discoveredPagesPaths, seenPagesPaths, pathname);
|
|
745
|
+
}
|
|
746
|
+
for (const pathname of pagesPathResult.dataPaths) addPath(discoveredPagesDataPaths, seenPagesDataPaths, pathname);
|
|
747
|
+
fallbackRoutePatterns.push(...pagesPathResult.fallbackRoutePatterns);
|
|
748
|
+
}
|
|
275
749
|
} finally {
|
|
276
750
|
if (prodServer) await new Promise((resolve) => prodServer.server.close(() => resolve()));
|
|
277
751
|
}
|
|
278
752
|
});
|
|
753
|
+
const excludedWarmPathSet = new Set(options.responseVary ? [...paths, ...discoveredRouteHandlerPaths].filter((pathname) => configuredRouteAffectsWarmPath(pathname, config)) : []);
|
|
754
|
+
const configuredPagesWarmPaths = discoveredPagesPaths.filter((pathname) => !excludedWarmPathSet.has(pathname));
|
|
755
|
+
const resolvedPagesWarmPaths = !appDir && pagesDir ? await excludePagesApiWarmPaths({
|
|
756
|
+
i18n: config.i18n,
|
|
757
|
+
pagesDir,
|
|
758
|
+
pageExtensions: config.pageExtensions,
|
|
759
|
+
paths: configuredPagesWarmPaths
|
|
760
|
+
}) : configuredPagesWarmPaths;
|
|
761
|
+
const discoveredPagesDataPathSet = new Set(discoveredPagesDataPaths);
|
|
762
|
+
const configuredCandidatePaths = paths.filter((pathname) => !excludedWarmPathSet.has(pathname));
|
|
763
|
+
const configuredRouteHandlerPaths = discoveredRouteHandlerPaths.filter((pathname) => !excludedWarmPathSet.has(pathname));
|
|
764
|
+
const appOwnedWarmPaths = appDir ? await resolveAppWarmPaths({
|
|
765
|
+
appDir,
|
|
766
|
+
i18n: config.i18n,
|
|
767
|
+
pagesDir,
|
|
768
|
+
pageExtensions: config.pageExtensions,
|
|
769
|
+
paths: [...configuredCandidatePaths, ...configuredRouteHandlerPaths]
|
|
770
|
+
}) : {
|
|
771
|
+
appPaths: [],
|
|
772
|
+
appRoutePaths: [],
|
|
773
|
+
htmlPaths: discoveredAppPaths,
|
|
774
|
+
loadingShellPaths: discoveredLoadingShellPaths,
|
|
775
|
+
pagesPaths: resolvedPagesWarmPaths,
|
|
776
|
+
routePatterns: pagesDir ? await resolvePagesWarmRoutePatterns({
|
|
777
|
+
i18n: config.i18n,
|
|
778
|
+
pagesDir,
|
|
779
|
+
pageExtensions: config.pageExtensions,
|
|
780
|
+
paths: resolvedPagesWarmPaths
|
|
781
|
+
}) : {},
|
|
782
|
+
rscPaths: discoveredAppPaths
|
|
783
|
+
};
|
|
784
|
+
const warmPaths = appDir ? appOwnedWarmPaths.htmlPaths : resolvedPagesWarmPaths;
|
|
785
|
+
const pagesOwnedWarmPaths = appDir ? appOwnedWarmPaths.pagesPaths : resolvedPagesWarmPaths;
|
|
786
|
+
const resolvedPagesDataWarmPaths = pagesOwnedWarmPaths.filter((pathname) => discoveredPagesDataPathSet.has(pathname));
|
|
787
|
+
const pagesDataPaths = resolvedPagesDataWarmPaths.map((pathname) => buildPagesDataHref(config.basePath, config.buildId, localizePagesDataPath(pathname, config.i18n), ""));
|
|
788
|
+
const routePatterns = annotateCacheabilityProbeSafety(appOwnedWarmPaths.routePatterns, config);
|
|
789
|
+
for (let index = 0; index < resolvedPagesDataWarmPaths.length; index++) {
|
|
790
|
+
const route = routePatterns[resolvedPagesDataWarmPaths[index]];
|
|
791
|
+
if (route) {
|
|
792
|
+
const htmlPathname = resolvedPagesDataWarmPaths[index] === "/" ? config.basePath || "/" : `${config.basePath}${resolvedPagesDataWarmPaths[index]}`;
|
|
793
|
+
routePatterns[pagesDataPaths[index]] = {
|
|
794
|
+
...route,
|
|
795
|
+
cacheabilityProbe: {
|
|
796
|
+
...route.cacheabilityProbe,
|
|
797
|
+
concretePathname: htmlPathname
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
}
|
|
279
802
|
const manifest = {
|
|
803
|
+
...appDir ? { appPaths: appOwnedWarmPaths.appPaths } : {},
|
|
804
|
+
...config.basePath ? { basePath: config.basePath } : {},
|
|
280
805
|
buildId: config.buildId,
|
|
806
|
+
...rscBuildId && options.buildIdentity === "response-header" ? { buildIdentity: rscBuildId } : {},
|
|
807
|
+
...config.deploymentId ? { deploymentId: config.deploymentId } : {},
|
|
808
|
+
...pagesDir ? {
|
|
809
|
+
pagesDataPaths,
|
|
810
|
+
pagesPaths: pagesOwnedWarmPaths
|
|
811
|
+
} : {},
|
|
812
|
+
...excludedWarmPathSet.size > 0 ? { excludedWarmPaths: Array.from(excludedWarmPathSet) } : {},
|
|
813
|
+
...fallbackRoutePatterns.length > 0 ? { fallbackRoutePatterns } : {},
|
|
814
|
+
...rscBuildId ? { rscBuildId } : {},
|
|
815
|
+
...options.responseVary ? { responseVary: options.responseVary } : {},
|
|
816
|
+
...options.responseVary ? { rscPaths: appOwnedWarmPaths.rscPaths } : {},
|
|
817
|
+
...Object.keys(routePatterns).length > 0 ? { routePatterns } : {},
|
|
818
|
+
...appOwnedWarmPaths.appRoutePaths.length > 0 ? { routeHandlerPaths: appOwnedWarmPaths.appRoutePaths } : {},
|
|
819
|
+
...options.responseVary ? { loadingShellPaths: appOwnedWarmPaths.loadingShellPaths } : {},
|
|
281
820
|
trailingSlash: config.trailingSlash,
|
|
282
|
-
paths
|
|
821
|
+
paths: warmPaths
|
|
283
822
|
};
|
|
284
823
|
fs.mkdirSync(manifestDir, { recursive: true });
|
|
285
824
|
fs.writeFileSync(path.join(manifestDir, PRERENDER_PATHS_MANIFEST), JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
286
|
-
console.log(` Discovered ${
|
|
825
|
+
console.log(` Discovered ${warmPaths.length + appOwnedWarmPaths.appRoutePaths.length} CDN warmup path(s).`);
|
|
287
826
|
return manifest;
|
|
288
827
|
}
|
|
289
828
|
//#endregion
|
|
290
|
-
export { PRERENDER_PATHS_MANIFEST, PRERENDER_PATH_DISCOVERY_ENV, emitPrerenderPathManifest };
|
|
829
|
+
export { DEFAULT_REMOTE_PATH_DISCOVERY_PHASE_TIMEOUT_MS, DEFAULT_REMOTE_PATH_DISCOVERY_RETRY_DELAY_MS, PRERENDER_PATHS_MANIFEST, PRERENDER_PATH_DISCOVERY_ENV, emitPrerenderPathManifest };
|