vinext 1.0.0-beta.2 → 1.0.0-beta.4
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 +6 -9
- package/dist/build/client-build-config.js +7 -1
- package/dist/build/precompress.js +1 -1
- package/dist/build/prerender.d.ts +2 -1
- package/dist/build/prerender.js +12 -2
- package/dist/check.js +4 -0
- package/dist/cli-args.d.ts +2 -1
- package/dist/cli-args.js +10 -0
- package/dist/cli.js +14 -8
- package/dist/client/navigation-runtime.d.ts +6 -0
- package/dist/client/navigation-runtime.js +1 -1
- package/dist/config/config-matchers.js +4 -1
- package/dist/config/dotenv.d.ts +3 -4
- package/dist/config/dotenv.js +30 -22
- package/dist/config/next-config.d.ts +16 -5
- package/dist/config/next-config.js +7 -0
- package/dist/entries/app-browser-entry.d.ts +4 -10
- package/dist/entries/app-browser-entry.js +20 -4
- package/dist/entries/app-rsc-entry.js +21 -3
- package/dist/entries/app-rsc-manifest.js +19 -0
- package/dist/entries/pages-client-entry.js +0 -1
- package/dist/entries/pages-server-entry.js +13 -3
- package/dist/index.js +122 -49
- package/dist/init-cloudflare.d.ts +3 -1
- package/dist/init-cloudflare.js +59 -5
- package/dist/plugins/client-reference-dedup.js +7 -5
- package/dist/plugins/extensionless-dynamic-import.js +23 -18
- package/dist/plugins/ignore-dynamic-requests.js +3 -1
- package/dist/plugins/import-meta-url.js +17 -6
- package/dist/plugins/require-context.js +20 -15
- package/dist/plugins/sass.js +3 -4
- package/dist/plugins/transform-cache.d.ts +19 -0
- package/dist/plugins/transform-cache.js +36 -0
- package/dist/routing/app-route-graph.d.ts +14 -6
- package/dist/routing/app-route-graph.js +106 -16
- package/dist/server/api-handler.d.ts +2 -0
- package/dist/server/api-handler.js +7 -6
- package/dist/server/app-bfcache-identity.js +28 -2
- package/dist/server/app-browser-entry.js +175 -75
- package/dist/server/app-browser-navigation-controller.d.ts +23 -21
- package/dist/server/app-browser-navigation-controller.js +30 -5
- package/dist/server/app-browser-prefetch-response.d.ts +9 -0
- package/dist/server/app-browser-prefetch-response.js +32 -0
- package/dist/server/app-browser-state.d.ts +8 -3
- package/dist/server/app-browser-state.js +9 -3
- package/dist/server/app-page-cache-finalizer.js +1 -1
- package/dist/server/app-page-cache.d.ts +1 -1
- package/dist/server/app-page-cache.js +16 -2
- package/dist/server/app-page-dispatch.d.ts +9 -1
- package/dist/server/app-page-dispatch.js +37 -8
- package/dist/server/app-page-element-builder.d.ts +4 -1
- package/dist/server/app-page-element-builder.js +34 -7
- package/dist/server/app-page-head.d.ts +3 -3
- package/dist/server/app-page-head.js +58 -13
- package/dist/server/app-page-http-access-fallback-metadata.d.ts +2 -2
- package/dist/server/app-page-http-access-fallback-metadata.js +9 -1
- package/dist/server/app-page-probe.d.ts +4 -0
- package/dist/server/app-page-probe.js +6 -2
- package/dist/server/app-page-render.d.ts +1 -1
- package/dist/server/app-page-render.js +13 -3
- package/dist/server/app-page-request.d.ts +3 -1
- package/dist/server/app-page-request.js +4 -13
- package/dist/server/app-page-response.d.ts +2 -0
- package/dist/server/app-page-response.js +7 -2
- package/dist/server/app-page-route-wiring.d.ts +10 -2
- package/dist/server/app-page-route-wiring.js +269 -59
- package/dist/server/app-pages-bridge.d.ts +2 -1
- package/dist/server/app-pages-bridge.js +3 -1
- package/dist/server/app-route-handler-cache.js +4 -0
- package/dist/server/app-route-handler-dispatch.js +9 -3
- package/dist/server/app-route-handler-response.js +1 -1
- package/dist/server/app-route-module-loader.d.ts +6 -0
- package/dist/server/app-route-module-loader.js +3 -0
- package/dist/server/app-router-entry.js +6 -3
- package/dist/server/app-rsc-cache-busting.d.ts +7 -0
- package/dist/server/app-rsc-cache-busting.js +6 -0
- package/dist/server/app-rsc-errors.js +10 -0
- package/dist/server/app-rsc-handler.d.ts +2 -0
- package/dist/server/app-rsc-handler.js +30 -13
- package/dist/server/app-rsc-render-mode.d.ts +3 -2
- package/dist/server/app-rsc-render-mode.js +4 -1
- package/dist/server/app-rsc-request-normalization.js +65 -4
- package/dist/server/app-rsc-route-matching.d.ts +9 -0
- package/dist/server/app-rsc-route-matching.js +6 -0
- package/dist/server/app-server-action-execution.d.ts +1 -1
- package/dist/server/app-ssr-entry.js +4 -4
- package/dist/server/app-visited-response-cache.d.ts +6 -1
- package/dist/server/app-visited-response-cache.js +49 -1
- package/dist/server/cache-control.d.ts +2 -2
- package/dist/server/cache-control.js +2 -1
- package/dist/server/dev-server.d.ts +3 -1
- package/dist/server/dev-server.js +204 -348
- package/dist/server/edge-api-runtime.d.ts +10 -1
- package/dist/server/edge-api-runtime.js +29 -1
- package/dist/server/headers.d.ts +4 -2
- package/dist/server/headers.js +6 -3
- package/dist/server/isr-cache.d.ts +10 -33
- package/dist/server/isr-cache.js +33 -43
- package/dist/server/isr-decision.d.ts +2 -2
- package/dist/server/pages-api-route.d.ts +4 -0
- package/dist/server/pages-api-route.js +6 -3
- package/dist/server/pages-asset-tags.d.ts +1 -0
- package/dist/server/pages-asset-tags.js +6 -4
- package/dist/server/pages-dev-hydration.js +0 -1
- package/dist/server/pages-document-asset-props.d.ts +26 -0
- package/dist/server/pages-document-asset-props.js +69 -0
- package/dist/server/pages-get-initial-props.d.ts +1 -1
- package/dist/server/pages-get-initial-props.js +1 -4
- package/dist/server/pages-i18n.js +64 -16
- package/dist/server/pages-node-compat.d.ts +2 -0
- package/dist/server/pages-node-compat.js +10 -6
- package/dist/server/pages-page-data.d.ts +61 -9
- package/dist/server/pages-page-data.js +334 -74
- package/dist/server/pages-page-handler.d.ts +6 -1
- package/dist/server/pages-page-handler.js +119 -20
- package/dist/server/pages-page-response.d.ts +7 -3
- package/dist/server/pages-page-response.js +41 -33
- package/dist/server/pages-request-pipeline.d.ts +7 -0
- package/dist/server/pages-request-pipeline.js +5 -1
- package/dist/server/pages-revalidate.d.ts +1 -1
- package/dist/server/pages-revalidate.js +54 -8
- package/dist/server/pages-router-entry.d.ts +1 -0
- package/dist/server/pages-router-entry.js +10 -4
- package/dist/server/prerender-manifest.d.ts +1 -0
- package/dist/server/prod-server.d.ts +2 -1
- package/dist/server/prod-server.js +38 -13
- package/dist/server/revalidation-host.d.ts +11 -0
- package/dist/server/revalidation-host.js +19 -0
- package/dist/server/seed-cache.js +1 -1
- package/dist/server/worker-revalidation-context.d.ts +13 -0
- package/dist/server/worker-revalidation-context.js +30 -0
- package/dist/shims/app-router-scroll-state.d.ts +2 -1
- package/dist/shims/app-router-scroll-state.js +7 -2
- package/dist/shims/cache-handler.d.ts +1 -1
- package/dist/shims/cache-handler.js +10 -7
- package/dist/shims/cache-runtime.d.ts +6 -0
- package/dist/shims/cache-runtime.js +4 -1
- package/dist/shims/cache.js +1 -1
- package/dist/shims/document.js +15 -2
- package/dist/shims/error.js +1 -1
- package/dist/shims/fetch-cache.js +1 -1
- package/dist/shims/font-google-base.js +17 -8
- package/dist/shims/font-local.js +41 -10
- package/dist/shims/hash-scroll.d.ts +1 -1
- package/dist/shims/hash-scroll.js +2 -1
- package/dist/shims/headers.d.ts +10 -13
- package/dist/shims/headers.js +85 -36
- package/dist/shims/internal/cookie-serialize.d.ts +10 -9
- package/dist/shims/internal/cookie-serialize.js +9 -8
- package/dist/shims/internal/interpolate-as.d.ts +15 -1
- package/dist/shims/internal/interpolate-as.js +27 -2
- package/dist/shims/link.d.ts +2 -0
- package/dist/shims/link.js +77 -24
- package/dist/shims/metadata.d.ts +18 -34
- package/dist/shims/metadata.js +98 -56
- package/dist/shims/navigation.d.ts +18 -2
- package/dist/shims/navigation.js +70 -21
- package/dist/shims/request-context.d.ts +6 -2
- package/dist/shims/router.js +18 -7
- package/dist/shims/script.js +1 -1
- package/dist/shims/server.d.ts +7 -6
- package/dist/shims/server.js +156 -57
- package/dist/shims/slot.d.ts +2 -1
- package/dist/shims/slot.js +6 -2
- package/dist/shims/streamed-icons.d.ts +9 -0
- package/dist/shims/streamed-icons.js +33 -0
- package/dist/shims/unified-request-context.d.ts +21 -2
- package/dist/shims/unified-request-context.js +82 -1
- package/dist/utils/cache-control-metadata.d.ts +2 -1
- package/dist/utils/cache-control-metadata.js +5 -1
- package/dist/utils/html-limited-bots.js +1 -1
- package/dist/utils/protocol-headers.d.ts +7 -1
- package/dist/utils/protocol-headers.js +7 -1
- package/package.json +1 -1
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
import { createRequestContext, runWithRequestContext } from "../shims/unified-request-context.js";
|
|
1
|
+
import { closeAfterResponse, closeAfterResponseWithBody, createRequestContext, runWithRequestContext } from "../shims/unified-request-context.js";
|
|
2
2
|
import { patternToNextFormat } from "../routing/route-validation.js";
|
|
3
3
|
import { getRequestExecutionContext } from "../shims/request-context.js";
|
|
4
|
-
import {
|
|
4
|
+
import { PRERENDER_REVALIDATE_HEADER } from "../utils/protocol-headers.js";
|
|
5
|
+
import { NEXTJS_CACHE_HEADER, NEXTJS_DEPLOYMENT_ID_HEADER, VINEXT_CACHE_HEADER } from "./headers.js";
|
|
5
6
|
import { reportRequestError } from "./instrumentation.js";
|
|
6
7
|
import { BROWSER_REVALIDATE_CACHE_CONTROL, NEVER_CACHE_CONTROL, applyCdnResponseHeaders, shouldUseNextDeployCacheControl } from "./cache-control.js";
|
|
7
8
|
import { buildMissIsrCacheControl } from "./isr-decision.js";
|
|
8
|
-
import {
|
|
9
|
+
import { coalesceOnDemandRevalidation, isOnDemandRevalidateRequest, isrCacheKey, isrGet, isrSet, triggerBackgroundRegeneration } from "./isr-cache.js";
|
|
10
|
+
import { encodeCacheTag } from "../utils/encode-cache-tag.js";
|
|
9
11
|
import { ensureFetchPatch } from "../shims/fetch-cache.js";
|
|
10
|
-
import { appendAssetDeploymentIdQuery } from "../utils/deployment-id.js";
|
|
11
12
|
import { mergeRouteParamsIntoQuery, parseQueryString } from "../utils/query.js";
|
|
12
13
|
import { getScriptNonceFromHeaderSources } from "./csp.js";
|
|
13
14
|
import { extractLocaleFromUrl, resolvePagesI18nRequest } from "./pages-i18n.js";
|
|
14
15
|
import { buildDefaultPagesNotFoundResponse } from "./pages-default-404.js";
|
|
15
16
|
import { buildPagesReadinessNextData } from "./pages-readiness.js";
|
|
16
17
|
import { resolvePagesPageMethodResponse } from "./pages-page-method.js";
|
|
18
|
+
import { setCacheStateHeaders } from "./cache-headers.js";
|
|
17
19
|
import { renderPagesPageResponse } from "./pages-page-response.js";
|
|
18
20
|
import { hasPagesGetInitialProps } from "./pages-get-initial-props.js";
|
|
19
21
|
import { buildNextDataNotFoundResponse, buildNextDataPropsJsonResponse, normalizePagesDataRequest, parseNextDataPathname } from "./pages-data-route.js";
|
|
@@ -25,7 +27,12 @@ import { createPagesReqRes } from "./pages-node-compat.js";
|
|
|
25
27
|
function finalizePagesPreviewResponse(response, preview) {
|
|
26
28
|
if (preview.data === false && !preview.shouldClear) return response;
|
|
27
29
|
const headers = new Headers(response.headers);
|
|
28
|
-
if (preview.data !== false)
|
|
30
|
+
if (preview.data !== false) {
|
|
31
|
+
headers.set("Cache-Control", PAGES_PREVIEW_CACHE_CONTROL);
|
|
32
|
+
headers.delete("CDN-Cache-Control");
|
|
33
|
+
headers.delete("Cloudflare-CDN-Cache-Control");
|
|
34
|
+
headers.delete("Cache-Tag");
|
|
35
|
+
}
|
|
29
36
|
if (preview.shouldClear) appendPagesPreviewClearCookies(headers);
|
|
30
37
|
return new Response(response.body, {
|
|
31
38
|
headers,
|
|
@@ -33,6 +40,41 @@ function finalizePagesPreviewResponse(response, preview) {
|
|
|
33
40
|
statusText: response.statusText
|
|
34
41
|
});
|
|
35
42
|
}
|
|
43
|
+
function withPagesCacheState(response, state) {
|
|
44
|
+
const headers = new Headers(response.headers);
|
|
45
|
+
if (state === "REVALIDATED") {
|
|
46
|
+
headers.set(NEXTJS_CACHE_HEADER, state);
|
|
47
|
+
headers.delete(VINEXT_CACHE_HEADER);
|
|
48
|
+
} else setCacheStateHeaders(headers, state);
|
|
49
|
+
return new Response(response.body, {
|
|
50
|
+
headers,
|
|
51
|
+
status: response.status,
|
|
52
|
+
statusText: response.statusText
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function applyPagesErrorCachePolicy(response, revalidateSeconds, expireSeconds, cacheTagPathname) {
|
|
56
|
+
const headers = new Headers(response.headers);
|
|
57
|
+
const browserPolicy = headers.get("Cache-Control");
|
|
58
|
+
const sharedPolicies = [headers.get("CDN-Cache-Control"), headers.get("Cloudflare-CDN-Cache-Control")];
|
|
59
|
+
const hasCacheableSharedPolicy = sharedPolicies.some((value) => value && /(?:^|,)\s*s-maxage\s*=/i.test(value));
|
|
60
|
+
if (sharedPolicies.some((value) => value && /(?:private|no-store|no-cache)/i.test(value)) || !hasCacheableSharedPolicy && browserPolicy && /(?:private|no-store|no-cache)/i.test(browserPolicy)) return response;
|
|
61
|
+
headers.delete("CDN-Cache-Control");
|
|
62
|
+
headers.delete("Cloudflare-CDN-Cache-Control");
|
|
63
|
+
headers.delete("Cache-Tag");
|
|
64
|
+
if (revalidateSeconds === void 0) applyCdnResponseHeaders(headers, { cacheControl: NEVER_CACHE_CONTROL });
|
|
65
|
+
else {
|
|
66
|
+
const stem = cacheTagPathname.endsWith("/") ? cacheTagPathname.slice(0, -1) : cacheTagPathname;
|
|
67
|
+
applyCdnResponseHeaders(headers, {
|
|
68
|
+
cacheControl: buildMissIsrCacheControl(revalidateSeconds, expireSeconds),
|
|
69
|
+
tags: [encodeCacheTag(`_N_T_${stem || "/"}`)]
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return new Response(response.body, {
|
|
73
|
+
headers,
|
|
74
|
+
status: response.status,
|
|
75
|
+
statusText: response.statusText
|
|
76
|
+
});
|
|
77
|
+
}
|
|
36
78
|
function shouldEmitPagesClientTraceMetadata(pageModule, appComponent) {
|
|
37
79
|
if (typeof pageModule.getServerSideProps === "function") return true;
|
|
38
80
|
if (typeof pageModule.getStaticProps === "function") return false;
|
|
@@ -122,6 +164,7 @@ function createPagesPageHandler(opts) {
|
|
|
122
164
|
route: options.__forcedRoute,
|
|
123
165
|
params: {}
|
|
124
166
|
} : matchRoute(routeUrl, pageRoutes);
|
|
167
|
+
let isRouteMissErrorRender = false;
|
|
125
168
|
let renderStatusCodeOverride = statusCode;
|
|
126
169
|
let renderAsPath = asPath;
|
|
127
170
|
if (!match) {
|
|
@@ -133,6 +176,7 @@ function createPagesPageHandler(opts) {
|
|
|
133
176
|
route: notFoundRoute,
|
|
134
177
|
params: {}
|
|
135
178
|
};
|
|
179
|
+
isRouteMissErrorRender = true;
|
|
136
180
|
renderStatusCodeOverride = 404;
|
|
137
181
|
renderAsPath = routeUrl;
|
|
138
182
|
} else return buildDefaultPagesNotFoundResponse();
|
|
@@ -141,16 +185,43 @@ function createPagesPageHandler(opts) {
|
|
|
141
185
|
const pageModule = route.module;
|
|
142
186
|
const isStaticPropsRoute = typeof pageModule.getStaticProps === "function";
|
|
143
187
|
const isStaticPropsRender = isStaticPropsRoute && typeof pageModule.getServerSideProps !== "function";
|
|
188
|
+
if (!options?.__skipOnDemandCoalesce && !options?.__forcedRoute && isStaticPropsRoute && isOnDemandRevalidateRequest(request.headers.get("x-prerender-revalidate"))) {
|
|
189
|
+
const snapshot = await coalesceOnDemandRevalidation(pageIsrCacheKey("pages", routeUrl.split("?")[0]), async () => {
|
|
190
|
+
const response = await renderPage(request, url, manifest, middlewareHeaders, {
|
|
191
|
+
...options,
|
|
192
|
+
__skipOnDemandCoalesce: true
|
|
193
|
+
});
|
|
194
|
+
return {
|
|
195
|
+
body: request.method === "HEAD" || response.status === 204 || response.status === 304 ? null : new Uint8Array(await response.arrayBuffer()),
|
|
196
|
+
headers: [...response.headers.entries()],
|
|
197
|
+
status: response.status,
|
|
198
|
+
statusText: response.statusText
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
return new Response(snapshot.body?.slice() ?? null, {
|
|
202
|
+
headers: snapshot.headers,
|
|
203
|
+
status: snapshot.status,
|
|
204
|
+
statusText: snapshot.statusText
|
|
205
|
+
});
|
|
206
|
+
}
|
|
144
207
|
const renderRouteUrl = isStaticPropsRender ? routeUrl.split("?")[0] : routeUrl;
|
|
145
208
|
const routerAsPathSource = isStaticPropsRender ? renderRouteUrl : renderAsPath ?? renderRouteUrl;
|
|
146
209
|
const routerAsPath = i18nConfig ? extractLocaleFromUrl(routerAsPathSource, i18nConfig, locale).url : routerAsPathSource;
|
|
147
|
-
|
|
210
|
+
const uCtx = createRequestContext({ executionContext: getRequestExecutionContext() });
|
|
211
|
+
return closeAfterResponseWithBody(await runWithRequestContext(uCtx, async () => {
|
|
148
212
|
ensureFetchPatch();
|
|
149
213
|
try {
|
|
150
214
|
const routePattern = patternToNextFormat(route.pattern);
|
|
151
215
|
const renderStatusCode = renderStatusCodeOverride ?? (routePattern === "/404" ? 404 : void 0);
|
|
216
|
+
const isrCachePathname = isStaticPropsRender && (routePattern === "/404" || routePattern === "/500" || routePattern === "/_error") ? routePattern : renderRouteUrl.split("?")[0];
|
|
217
|
+
const isNotFoundErrorRender = routePattern === "/404" || routePattern === "/_error" && renderStatusCode === 404;
|
|
218
|
+
const isStatusErrorRender = isNotFoundErrorRender || routePattern === "/500" || routePattern === "/_error" && renderStatusCode === 500;
|
|
219
|
+
const errorPageRevalidateSeconds = isNotFoundErrorRender ? options?.__notFoundRevalidateSeconds : void 0;
|
|
220
|
+
const errorPageExpireSeconds = isNotFoundErrorRender ? options?.__notFoundExpireSeconds : void 0;
|
|
221
|
+
const errorResponseCachePathname = options?.__notFoundCachePathname ?? isrCachePathname;
|
|
152
222
|
const query = mergeRouteParamsIntoQuery(parseQueryString(renderRouteUrl), params);
|
|
153
223
|
const isOnDemandRevalidate = isOnDemandRevalidateRequest(request.headers.get(PRERENDER_REVALIDATE_HEADER));
|
|
224
|
+
const revalidateOnlyGenerated = isOnDemandRevalidate && request.headers.has("x-prerender-revalidate-if-generated");
|
|
154
225
|
const preview = isStaticPropsRoute || typeof pageModule.getServerSideProps === "function" ? getPagesPreviewState(request.headers.get("cookie"), { isOnDemandRevalidate }) : {
|
|
155
226
|
data: false,
|
|
156
227
|
shouldClear: false
|
|
@@ -211,11 +282,12 @@ function createPagesPageHandler(opts) {
|
|
|
211
282
|
}
|
|
212
283
|
};
|
|
213
284
|
const scriptNonce = getScriptNonceFromHeaderSources(request.headers, middlewareHeaders);
|
|
285
|
+
const shouldApplyErrorResponsePolicy = previewData === false && !scriptNonce && isStatusErrorRender && (isStaticPropsRoute || isRouteMissErrorRender || options?.__forcedRoute !== void 0);
|
|
214
286
|
let fontLinkHeader = "";
|
|
215
287
|
let allFontPreloads = [];
|
|
216
288
|
try {
|
|
217
289
|
allFontPreloads = getFontPreloads();
|
|
218
|
-
if (allFontPreloads.length > 0) fontLinkHeader = allFontPreloads.map((p) => "<" +
|
|
290
|
+
if (allFontPreloads.length > 0) fontLinkHeader = allFontPreloads.map((p) => "<" + p.href + ">; rel=preload; as=font; type=" + p.type + "; crossorigin").join(", ");
|
|
219
291
|
} catch {}
|
|
220
292
|
const pagesResolvedUrl = (new URL(routeUrl, originalRequestUrl).pathname || "/") + originalRequestUrl.search;
|
|
221
293
|
const createPageReqRes = () => {
|
|
@@ -232,6 +304,7 @@ function createPagesPageHandler(opts) {
|
|
|
232
304
|
isDataReq,
|
|
233
305
|
err: err instanceof Error ? err : void 0,
|
|
234
306
|
applyRequestContexts: applySSRContext,
|
|
307
|
+
basePath: vinextConfig.basePath,
|
|
235
308
|
buildId,
|
|
236
309
|
deploymentId: process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID,
|
|
237
310
|
htmlLimitedBots: vinextConfig.htmlLimitedBots,
|
|
@@ -253,6 +326,7 @@ function createPagesPageHandler(opts) {
|
|
|
253
326
|
isBuildTimePrerendering: typeof process !== "undefined" && process.env && process.env.VINEXT_PRERENDER === "1",
|
|
254
327
|
validatePropsSerialization: process.env.NODE_ENV !== "production" || process.env.VINEXT_PRERENDER === "1",
|
|
255
328
|
isOnDemandRevalidate,
|
|
329
|
+
revalidateOnlyGenerated,
|
|
256
330
|
previewData,
|
|
257
331
|
pageModule,
|
|
258
332
|
AppComponent,
|
|
@@ -265,10 +339,16 @@ function createPagesPageHandler(opts) {
|
|
|
265
339
|
route: { isDynamic: route.isDynamic },
|
|
266
340
|
routePattern,
|
|
267
341
|
routeUrl: renderRouteUrl,
|
|
342
|
+
isrCachePathname,
|
|
268
343
|
runInFreshUnifiedContext(callback) {
|
|
269
|
-
|
|
344
|
+
const revalCtx = createRequestContext({ executionContext: null });
|
|
345
|
+
return runWithRequestContext(revalCtx, async () => {
|
|
270
346
|
ensureFetchPatch();
|
|
271
|
-
|
|
347
|
+
try {
|
|
348
|
+
return await callback();
|
|
349
|
+
} finally {
|
|
350
|
+
await closeAfterResponse(revalCtx);
|
|
351
|
+
}
|
|
272
352
|
});
|
|
273
353
|
},
|
|
274
354
|
safeJsonStringify,
|
|
@@ -284,15 +364,26 @@ function createPagesPageHandler(opts) {
|
|
|
284
364
|
});
|
|
285
365
|
if (pageDataResult.kind === "notFound") {
|
|
286
366
|
const notFoundRoute = findNotFoundRoute();
|
|
287
|
-
|
|
367
|
+
let notFoundResponse;
|
|
368
|
+
if (notFoundRoute && routePattern !== "/404" && routePattern !== "/_error") notFoundResponse = await renderPage(request, url, manifest, middlewareHeaders, {
|
|
288
369
|
statusCode: 404,
|
|
289
370
|
asPath: routerAsPath,
|
|
290
371
|
renderErrorPageOnMiss: false,
|
|
291
|
-
__forcedRoute: notFoundRoute
|
|
292
|
-
|
|
293
|
-
|
|
372
|
+
__forcedRoute: notFoundRoute,
|
|
373
|
+
__notFoundRevalidateSeconds: pageDataResult.revalidateSeconds,
|
|
374
|
+
__notFoundExpireSeconds: pageDataResult.expireSeconds,
|
|
375
|
+
__notFoundCachePathname: isrCachePathname
|
|
376
|
+
});
|
|
377
|
+
else notFoundResponse = buildDefaultPagesNotFoundResponse();
|
|
378
|
+
if (isOnDemandRevalidate) notFoundResponse = withPagesCacheState(notFoundResponse, "REVALIDATED");
|
|
379
|
+
else if (pageDataResult.cacheState) notFoundResponse = withPagesCacheState(notFoundResponse, pageDataResult.cacheState);
|
|
380
|
+
return finalizePagesPreviewResponse(notFoundResponse, preview);
|
|
381
|
+
}
|
|
382
|
+
if (pageDataResult.kind === "response") {
|
|
383
|
+
let response = isOnDemandRevalidate && pageDataResult.onDemandRevalidateSuccess !== false ? withPagesCacheState(pageDataResult.response, "REVALIDATED") : pageDataResult.response;
|
|
384
|
+
if (shouldApplyErrorResponsePolicy) response = applyPagesErrorCachePolicy(response, errorPageRevalidateSeconds, errorPageExpireSeconds, errorResponseCachePathname);
|
|
385
|
+
return finalizePagesPreviewResponse(response, preview);
|
|
294
386
|
}
|
|
295
|
-
if (pageDataResult.kind === "response") return finalizePagesPreviewResponse(pageDataResult.response, preview);
|
|
296
387
|
let pageProps = pageDataResult.pageProps;
|
|
297
388
|
let renderProps = pageDataResult.props;
|
|
298
389
|
if (previewData !== false) renderProps = {
|
|
@@ -312,6 +403,7 @@ function createPagesPageHandler(opts) {
|
|
|
312
403
|
const gsspRes = pageDataResult.gsspRes;
|
|
313
404
|
const documentReqRes = serializedPagesNextData.autoExport === true ? null : pageDataResult.documentReqRes ?? createPageReqRes();
|
|
314
405
|
const isrRevalidateSeconds = pageDataResult.isrRevalidateSeconds;
|
|
406
|
+
const isrExpireSeconds = pageDataResult.isrExpireSeconds;
|
|
315
407
|
const isFallbackRender = pageDataResult.isFallback === true;
|
|
316
408
|
if (isFallbackRender) applySSRContext({
|
|
317
409
|
query: {},
|
|
@@ -337,7 +429,7 @@ function createPagesPageHandler(opts) {
|
|
|
337
429
|
}
|
|
338
430
|
if (!hasUserCacheControl) init.headers["Cache-Control"] = NEVER_CACHE_CONTROL;
|
|
339
431
|
} else if (isStaticPropsRoute) {
|
|
340
|
-
if (isrRevalidateSeconds) {
|
|
432
|
+
if (isrRevalidateSeconds !== null) {
|
|
341
433
|
const headers = new Headers(init.headers);
|
|
342
434
|
applyCdnResponseHeaders(headers, { cacheControl: buildMissIsrCacheControl(isrRevalidateSeconds, vinextConfig.expireTime) });
|
|
343
435
|
for (const [key, value] of headers) init.headers[key] = value;
|
|
@@ -352,7 +444,7 @@ function createPagesPageHandler(opts) {
|
|
|
352
444
|
const pageModuleIds = [];
|
|
353
445
|
if (appAssetPath) pageModuleIds.push(appAssetPath);
|
|
354
446
|
if (route.filePath) pageModuleIds.push(route.filePath);
|
|
355
|
-
|
|
447
|
+
let pageResponse = await renderPagesPageResponse({
|
|
356
448
|
assetTags: collectAssetTags({
|
|
357
449
|
manifest,
|
|
358
450
|
moduleIds: pageModuleIds,
|
|
@@ -360,7 +452,8 @@ function createPagesPageHandler(opts) {
|
|
|
360
452
|
disableOptimizedLoading: vinextConfig.disableOptimizedLoading,
|
|
361
453
|
basePath: vinextConfig.basePath,
|
|
362
454
|
assetPrefix: vinextConfig.assetPrefix,
|
|
363
|
-
deploymentId: process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID
|
|
455
|
+
deploymentId: process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID,
|
|
456
|
+
crossOrigin: vinextConfig.crossOrigin
|
|
364
457
|
}),
|
|
365
458
|
buildId,
|
|
366
459
|
clearSsrContext() {
|
|
@@ -386,8 +479,10 @@ function createPagesPageHandler(opts) {
|
|
|
386
479
|
documentReqRes,
|
|
387
480
|
gsspRes,
|
|
388
481
|
isrCacheKey: pageIsrCacheKey,
|
|
389
|
-
|
|
482
|
+
isrCachePathname,
|
|
483
|
+
expireSeconds: isrExpireSeconds,
|
|
390
484
|
isrRevalidateSeconds,
|
|
485
|
+
isOnDemandRevalidate,
|
|
391
486
|
isStaticPropsRoute,
|
|
392
487
|
isrSet,
|
|
393
488
|
i18n: buildI18nRenderContext(i18nConfig, locale, currentDefaultLocale, domainLocales),
|
|
@@ -406,12 +501,16 @@ function createPagesPageHandler(opts) {
|
|
|
406
501
|
routeUrl: renderRouteUrl,
|
|
407
502
|
safeJsonStringify,
|
|
408
503
|
scriptNonce,
|
|
504
|
+
crossOrigin: vinextConfig.crossOrigin,
|
|
505
|
+
disableOptimizedLoading: vinextConfig.disableOptimizedLoading,
|
|
409
506
|
statusCode: renderStatusCode,
|
|
410
507
|
nextData: serializedPagesNextData,
|
|
411
508
|
userAgent: request.headers.get("user-agent") ?? void 0,
|
|
412
509
|
ifNoneMatch: request.headers.get("if-none-match") ?? void 0,
|
|
413
510
|
requestCacheControl: request.headers.get("cache-control") ?? void 0
|
|
414
|
-
})
|
|
511
|
+
});
|
|
512
|
+
if (shouldApplyErrorResponsePolicy) pageResponse = applyPagesErrorCachePolicy(pageResponse, errorPageRevalidateSeconds, errorPageExpireSeconds, errorResponseCachePathname);
|
|
513
|
+
return finalizePagesPreviewResponse(pageResponse, preview);
|
|
415
514
|
} catch (e) {
|
|
416
515
|
console.error("[vinext] SSR error:", e);
|
|
417
516
|
reportRequestError(e instanceof Error ? e : new Error(String(e)), {
|
|
@@ -445,7 +544,7 @@ function createPagesPageHandler(opts) {
|
|
|
445
544
|
}
|
|
446
545
|
return new Response("Internal Server Error", { status: 500 });
|
|
447
546
|
}
|
|
448
|
-
});
|
|
547
|
+
}), uCtx);
|
|
449
548
|
}
|
|
450
549
|
return renderPage;
|
|
451
550
|
}
|
|
@@ -88,11 +88,13 @@ type RenderPagesPageResponseOptions = {
|
|
|
88
88
|
setDocumentInitialHead?: ((head: ReactNode[]) => void) | undefined;
|
|
89
89
|
documentReqRes?: PagesDocumentReqRes | null;
|
|
90
90
|
gsspRes: PagesGsspResponse | null;
|
|
91
|
-
isrCacheKey: (router: string, pathname: string) => string;
|
|
91
|
+
isrCacheKey: (router: string, pathname: string) => string; /** Filesystem-route identity used for ISR persistence and cache tags. */
|
|
92
|
+
isrCachePathname?: string;
|
|
92
93
|
expireSeconds?: number;
|
|
93
|
-
isrRevalidateSeconds: number | null;
|
|
94
|
+
isrRevalidateSeconds: number | false | null; /** Synchronous `res.revalidate()` render; cache persistence must finish before returning. */
|
|
95
|
+
isOnDemandRevalidate?: boolean;
|
|
94
96
|
isStaticPropsRoute?: boolean;
|
|
95
|
-
isrSet: (key: string, data: CachedPagesValue, revalidateSeconds: number, tags?: string[], expireSeconds?: number) => Promise<void>;
|
|
97
|
+
isrSet: (key: string, data: CachedPagesValue, revalidateSeconds: number | false, tags?: string[], expireSeconds?: number) => Promise<void>;
|
|
96
98
|
i18n: PagesI18nRenderContext;
|
|
97
99
|
/**
|
|
98
100
|
* True when rendering a `getStaticPaths` fallback shell for a path that
|
|
@@ -112,6 +114,8 @@ type RenderPagesPageResponseOptions = {
|
|
|
112
114
|
routeUrl: string;
|
|
113
115
|
safeJsonStringify: (value: unknown) => string;
|
|
114
116
|
scriptNonce?: string;
|
|
117
|
+
crossOrigin?: string;
|
|
118
|
+
disableOptimizedLoading: boolean;
|
|
115
119
|
statusCode?: number;
|
|
116
120
|
vinext?: VinextNextData["__vinext"];
|
|
117
121
|
nextData?: PagesNextDataExtras;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getRequestExecutionContext } from "../shims/request-context.js";
|
|
2
|
+
import { NEXTJS_CACHE_HEADER } from "./headers.js";
|
|
2
3
|
import { reportRequestError } from "./instrumentation.js";
|
|
3
|
-
import { setCacheStateHeaders } from "./cache-headers.js";
|
|
4
4
|
import { BROWSER_REVALIDATE_CACHE_CONTROL, NEVER_CACHE_CONTROL, NO_STORE_CACHE_CONTROL, applyCdnResponseHeaders, shouldUseNextDeployCacheControl } from "./cache-control.js";
|
|
5
5
|
import { buildMissIsrCacheControl } from "./isr-decision.js";
|
|
6
6
|
import { fnv1a52 } from "../utils/hash.js";
|
|
@@ -8,30 +8,21 @@ import { encodeCacheTag } from "../utils/encode-cache-tag.js";
|
|
|
8
8
|
import { appendAssetDeploymentIdQuery } from "../utils/deployment-id.js";
|
|
9
9
|
import { withScriptNonce } from "../shims/script-nonce-context.js";
|
|
10
10
|
import { createNonceAttribute, escapeHtmlAttr } from "./html.js";
|
|
11
|
+
import { applyDocumentAssetProps, extractDocumentAssetProps } from "./pages-document-asset-props.js";
|
|
11
12
|
import { getClientTraceMetadataHTML } from "./client-trace-metadata.js";
|
|
13
|
+
import { setCacheStateHeaders } from "./cache-headers.js";
|
|
12
14
|
import { readStreamAsText } from "../utils/text-stream.js";
|
|
13
15
|
import { loadUserDocumentInitialProps, runDocumentRenderPage } from "./pages-document-initial-props.js";
|
|
14
16
|
import { callDocumentGetInitialProps } from "./document-initial-head.js";
|
|
17
|
+
import { isBotUserAgent } from "../utils/html-limited-bots.js";
|
|
15
18
|
import React from "react";
|
|
16
19
|
//#region src/server/pages-page-response.ts
|
|
17
20
|
/**
|
|
18
|
-
* Crawlers that cannot handle streamed HTML: they read metadata only from
|
|
19
|
-
* the first network chunk, so streaming would give them an incomplete <head>.
|
|
20
|
-
* Pattern sourced from Next.js html-bots.ts (updated to match the canary).
|
|
21
|
-
*/
|
|
22
|
-
const HTML_LIMITED_BOT_UA_RE = /[\w-]+-Google|Google-[\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight/i;
|
|
23
|
-
/**
|
|
24
|
-
* Googlebot (the main search crawler) executes JavaScript via a headless
|
|
25
|
-
* browser, so it too cannot safely handle mid-stream HTML mutations.
|
|
26
|
-
* Matches "Googlebot" but NOT suffixed variants like "Googlebot-Image".
|
|
27
|
-
*/
|
|
28
|
-
const HEADLESS_BROWSER_BOT_UA_RE = /Googlebot(?!-)|Googlebot$/i;
|
|
29
|
-
/**
|
|
30
21
|
* Returns true when the User-Agent belongs to a bot or crawler that cannot
|
|
31
22
|
* reliably consume a streamed HTML response.
|
|
32
23
|
*/
|
|
33
24
|
function isPagesStreamingBot(userAgent) {
|
|
34
|
-
return
|
|
25
|
+
return isBotUserAgent(userAgent);
|
|
35
26
|
}
|
|
36
27
|
function generatePagesETag(payload) {
|
|
37
28
|
return "\"" + fnv1a52(payload).toString(36) + payload.length.toString(36) + "\"";
|
|
@@ -64,7 +55,7 @@ function buildPagesFontHeadHtml(fontLinks, fontPreloads, fontStyles, scriptNonce
|
|
|
64
55
|
let html = "";
|
|
65
56
|
const nonceAttr = createNonceAttribute(scriptNonce);
|
|
66
57
|
for (const link of fontLinks) html += `<link rel="stylesheet"${nonceAttr} href="${escapeHtmlAttr(appendAssetDeploymentIdQuery(link))}" />\n `;
|
|
67
|
-
for (const preload of fontPreloads) html += `<link rel="preload"${nonceAttr} href="${escapeHtmlAttr(
|
|
58
|
+
for (const preload of fontPreloads) html += `<link rel="preload"${nonceAttr} href="${escapeHtmlAttr(preload.href)}" as="font" type="${escapeHtmlAttr(preload.type)}" crossorigin="anonymous" />\n `;
|
|
68
59
|
if (fontStyles.length > 0) html += `<style data-vinext-fonts${nonceAttr}>${fontStyles.join("\n")}</style>\n `;
|
|
69
60
|
return html;
|
|
70
61
|
}
|
|
@@ -95,19 +86,27 @@ async function buildPagesShellHtml(bodyMarker, fontHeadHTML, nextDataScript, opt
|
|
|
95
86
|
if (options.DocumentComponent) {
|
|
96
87
|
const docProps = options.resolvedDocProps ?? await loadUserDocumentInitialProps(options.DocumentComponent);
|
|
97
88
|
const docElement = docProps ? React.createElement(options.DocumentComponent, docProps) : React.createElement(options.DocumentComponent);
|
|
98
|
-
|
|
89
|
+
const renderedDocument = extractDocumentAssetProps(await options.renderDocumentToString(docElement));
|
|
90
|
+
let html = renderedDocument.html;
|
|
91
|
+
const generatedAssetTags = applyDocumentAssetProps(options.assetTags, renderedDocument.props, {
|
|
92
|
+
configuredCrossOrigin: options.crossOrigin,
|
|
93
|
+
scriptOwner: options.disableOptimizedLoading ? "next-script" : "head"
|
|
94
|
+
});
|
|
95
|
+
const generatedNextDataScript = applyDocumentAssetProps(nextDataScript, renderedDocument.props, { configuredCrossOrigin: options.crossOrigin });
|
|
99
96
|
html = html.replace("__NEXT_MAIN__", bodyMarker);
|
|
100
|
-
if (options.ssrHeadHTML ||
|
|
101
|
-
html = html.replace("<!-- __NEXT_SCRIPTS__ -->",
|
|
102
|
-
if (!html.includes("__NEXT_DATA__")) html = html.replace("</body>", ` ${
|
|
97
|
+
if (options.ssrHeadHTML || generatedAssetTags || fontHeadHTML) html = html.replace("</head>", ` ${fontHeadHTML}${options.ssrHeadHTML}\n ${generatedAssetTags}\n</head>`);
|
|
98
|
+
html = html.replace("<!-- __NEXT_SCRIPTS__ -->", generatedNextDataScript);
|
|
99
|
+
if (!html.includes("__NEXT_DATA__")) html = html.replace("</body>", ` ${generatedNextDataScript}\n</body>`);
|
|
103
100
|
return html;
|
|
104
101
|
}
|
|
102
|
+
const generatedAssetTags = applyDocumentAssetProps(options.assetTags, {}, { configuredCrossOrigin: options.crossOrigin });
|
|
103
|
+
const generatedNextDataScript = applyDocumentAssetProps(nextDataScript, {}, { configuredCrossOrigin: options.crossOrigin });
|
|
105
104
|
return `<!DOCTYPE html>
|
|
106
105
|
<html>
|
|
107
106
|
<head>
|
|
108
|
-
${fontHeadHTML}${options.ssrHeadHTML}\n ${
|
|
107
|
+
${fontHeadHTML}${options.ssrHeadHTML}\n ${generatedAssetTags}\n</head>
|
|
109
108
|
<body>
|
|
110
|
-
<div id="__next">${bodyMarker}</div>\n ${
|
|
109
|
+
<div id="__next">${bodyMarker}</div>\n ${generatedNextDataScript}\n</body>
|
|
111
110
|
</html>`;
|
|
112
111
|
}
|
|
113
112
|
async function buildPagesCompositeStream(bodyStream, shellPrefix, shellSuffix) {
|
|
@@ -142,14 +141,18 @@ async function reportPagesIsrCacheWriteError(error, cacheKey, routePattern) {
|
|
|
142
141
|
});
|
|
143
142
|
} catch {}
|
|
144
143
|
}
|
|
145
|
-
function
|
|
146
|
-
const
|
|
144
|
+
async function writePagesIsrCache(options) {
|
|
145
|
+
const bodyHtml = await readStreamAsText(options.stream);
|
|
146
|
+
await options.setCache(options.cacheKey, {
|
|
147
147
|
kind: "PAGES",
|
|
148
148
|
html: options.shellPrefix + bodyHtml + options.shellSuffix,
|
|
149
149
|
pageData: options.pageData,
|
|
150
150
|
headers: void 0,
|
|
151
151
|
status: options.status
|
|
152
|
-
}, options.revalidateSeconds, void 0, options.expireSeconds)
|
|
152
|
+
}, options.revalidateSeconds, void 0, options.expireSeconds);
|
|
153
|
+
}
|
|
154
|
+
function schedulePagesIsrCacheWrite(options) {
|
|
155
|
+
const cacheWritePromise = writePagesIsrCache(options).catch((error) => reportPagesIsrCacheWriteError(error, options.cacheKey, options.routePattern));
|
|
153
156
|
getRequestExecutionContext()?.waitUntil(cacheWritePromise);
|
|
154
157
|
}
|
|
155
158
|
function applyGsspHeaders(headers, gsspRes, statusCode) {
|
|
@@ -223,10 +226,12 @@ async function renderPagesPageResponse(options) {
|
|
|
223
226
|
if (documentRenderPage.status === "rendered" && documentRenderPage.stylesHTML) ssrHeadHTML += `\n ${documentRenderPage.stylesHTML}`;
|
|
224
227
|
const shellHtml = await buildPagesShellHtml(bodyMarker, fontHeadHTML, nextDataScript, {
|
|
225
228
|
assetTags: options.assetTags,
|
|
229
|
+
disableOptimizedLoading: options.disableOptimizedLoading,
|
|
226
230
|
DocumentComponent: options.DocumentComponent,
|
|
227
231
|
renderDocumentToString: options.renderDocumentToString,
|
|
228
232
|
ssrHeadHTML,
|
|
229
|
-
resolvedDocProps: documentRenderPage.status === "skipped" ? null : documentRenderPage.docProps
|
|
233
|
+
resolvedDocProps: documentRenderPage.status === "skipped" ? null : documentRenderPage.docProps,
|
|
234
|
+
crossOrigin: options.crossOrigin
|
|
230
235
|
});
|
|
231
236
|
options.clearSsrContext();
|
|
232
237
|
const markerIndex = shellHtml.indexOf(bodyMarker);
|
|
@@ -235,15 +240,15 @@ async function renderPagesPageResponse(options) {
|
|
|
235
240
|
const responseHeaders = new Headers({ "Content-Type": "text/html; charset=utf-8" });
|
|
236
241
|
const finalStatus = applyGsspHeaders(responseHeaders, options.gsspRes ?? options.documentReqRes?.res ?? null, options.statusCode);
|
|
237
242
|
let responseBodyStream = bodyStream;
|
|
238
|
-
if (!options.scriptNonce && options.isrRevalidateSeconds !== null && options.isrRevalidateSeconds > 0) {
|
|
243
|
+
if (!options.scriptNonce && options.isrRevalidateSeconds !== null && (options.isrRevalidateSeconds === false || options.isrRevalidateSeconds > 0)) {
|
|
239
244
|
const cacheBodyStreamPair = bodyStream.tee();
|
|
240
245
|
responseBodyStream = cacheBodyStreamPair[0];
|
|
241
246
|
const cacheBodyStream = cacheBodyStreamPair[1];
|
|
242
|
-
const isrPathname = options.routeUrl.split("?")[0];
|
|
243
|
-
|
|
247
|
+
const isrPathname = options.isrCachePathname ?? options.routeUrl.split("?")[0];
|
|
248
|
+
const cacheWriteOptions = {
|
|
244
249
|
cacheKey: options.isrCacheKey("pages", isrPathname),
|
|
245
250
|
expireSeconds: options.expireSeconds,
|
|
246
|
-
pageData: options.pageProps,
|
|
251
|
+
pageData: options.props ?? { pageProps: options.pageProps },
|
|
247
252
|
revalidateSeconds: options.isrRevalidateSeconds,
|
|
248
253
|
routePattern: options.routePattern,
|
|
249
254
|
setCache: options.isrSet,
|
|
@@ -251,19 +256,22 @@ async function renderPagesPageResponse(options) {
|
|
|
251
256
|
shellSuffix,
|
|
252
257
|
status: finalStatus,
|
|
253
258
|
stream: cacheBodyStream
|
|
254
|
-
}
|
|
259
|
+
};
|
|
260
|
+
if (options.isOnDemandRevalidate) await writePagesIsrCache(cacheWriteOptions);
|
|
261
|
+
else schedulePagesIsrCacheWrite(cacheWriteOptions);
|
|
255
262
|
}
|
|
256
263
|
const compositeStream = await buildPagesCompositeStream(responseBodyStream, shellPrefix, shellSuffix);
|
|
257
264
|
const userSetCacheControl = responseHeaders.has("Cache-Control");
|
|
258
265
|
if (options.scriptNonce) responseHeaders.set("Cache-Control", NO_STORE_CACHE_CONTROL);
|
|
259
|
-
else if (options.isrRevalidateSeconds) {
|
|
260
|
-
const isrPathname = options.routeUrl.split("?")[0];
|
|
266
|
+
else if (options.isrRevalidateSeconds !== null) {
|
|
267
|
+
const isrPathname = options.isrCachePathname ?? options.routeUrl.split("?")[0];
|
|
261
268
|
const stem = isrPathname.endsWith("/") ? isrPathname.slice(0, -1) : isrPathname;
|
|
262
269
|
applyCdnResponseHeaders(responseHeaders, {
|
|
263
270
|
cacheControl: buildMissIsrCacheControl(options.isrRevalidateSeconds, options.expireSeconds),
|
|
264
271
|
tags: [encodeCacheTag(`_N_T_${stem || "/"}`)]
|
|
265
272
|
});
|
|
266
|
-
|
|
273
|
+
if (options.isOnDemandRevalidate) responseHeaders.set(NEXTJS_CACHE_HEADER, "REVALIDATED");
|
|
274
|
+
else setCacheStateHeaders(responseHeaders, "MISS");
|
|
267
275
|
} else if (options.isStaticPropsRoute && shouldUseNextDeployCacheControl()) responseHeaders.set("Cache-Control", BROWSER_REVALIDATE_CACHE_CONTROL);
|
|
268
276
|
else if (options.gsspRes && !userSetCacheControl) responseHeaders.set("Cache-Control", NEVER_CACHE_CONTROL);
|
|
269
277
|
if (options.fontLinkHeader) responseHeaders.set("Link", options.fontLinkHeader);
|
|
@@ -45,6 +45,13 @@ type PagesPipelineDeps = {
|
|
|
45
45
|
ctx?: unknown;
|
|
46
46
|
rawSearch?: string;
|
|
47
47
|
configMatchPathname?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Validate an on-demand revalidation credential using the runtime's
|
|
50
|
+
* authoritative build secret. Production adapters must inject the verifier
|
|
51
|
+
* exported by the generated server entry so this early middleware decision
|
|
52
|
+
* uses the same baked secret as the eventual page renderer.
|
|
53
|
+
*/
|
|
54
|
+
authorizeOnDemandRevalidate?: (headerValue: string | null) => boolean;
|
|
48
55
|
matchPageRoute?: ((pathname: string, request: Request) => PageRouteMatch | null) | null;
|
|
49
56
|
runMiddleware?: ((request: Request, ctx: unknown, opts: {
|
|
50
57
|
isDataRequest: boolean;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { addBasePathToPathname, hasBasePath } from "../utils/base-path.js";
|
|
2
2
|
import { patternToNextFormat } from "../routing/route-validation.js";
|
|
3
|
+
import { PRERENDER_REVALIDATE_HEADER } from "../utils/protocol-headers.js";
|
|
3
4
|
import { requestContextFromRequest } from "../config/request-context.js";
|
|
4
5
|
import { isExternalUrl } from "../utils/external-url.js";
|
|
5
6
|
import { applyMiddlewareRequestHeaders, matchRedirect, matchRewrite, preserveRedirectDestinationQuery, proxyExternalRequest, sanitizeDestination } from "../config/config-matchers.js";
|
|
6
7
|
import { cloneRequestWithUrl, normalizeTrailingSlash } from "./request-pipeline.js";
|
|
8
|
+
import { isOnDemandRevalidateRequest } from "./isr-cache.js";
|
|
7
9
|
import { mergeRewriteQuery } from "../utils/query.js";
|
|
8
10
|
import { normalizeDefaultLocalePathname, stripI18nLocaleForApiRoute } from "./pages-i18n.js";
|
|
9
11
|
import { buildMiddlewarePrefetchSkipResponse } from "./pages-data-route.js";
|
|
@@ -56,6 +58,8 @@ async function runPagesRequest(request, deps) {
|
|
|
56
58
|
const url = new URL(request.url);
|
|
57
59
|
let pathname = url.pathname;
|
|
58
60
|
const search = url.search;
|
|
61
|
+
const revalidateHeader = request.headers.get(PRERENDER_REVALIDATE_HEADER);
|
|
62
|
+
const isOnDemandRevalidate = deps.authorizeOnDemandRevalidate ? deps.authorizeOnDemandRevalidate(revalidateHeader) : isOnDemandRevalidateRequest(revalidateHeader);
|
|
59
63
|
const requestConfigPathname = deps.configMatchPathname ?? pathname;
|
|
60
64
|
const basePathState = {
|
|
61
65
|
basePath,
|
|
@@ -98,7 +102,7 @@ async function runPagesRequest(request, deps) {
|
|
|
98
102
|
};
|
|
99
103
|
return served ? { type: "handled" } : null;
|
|
100
104
|
};
|
|
101
|
-
if (typeof deps.runMiddleware === "function") {
|
|
105
|
+
if (!isOnDemandRevalidate && typeof deps.runMiddleware === "function") {
|
|
102
106
|
const result = await deps.runMiddleware(request, deps.ctx ?? null, { isDataRequest });
|
|
103
107
|
if (result.waitUntilPromises && result.waitUntilPromises.length > 0) {
|
|
104
108
|
const ctx = deps.ctx;
|
|
@@ -10,6 +10,6 @@ type RevalidateOptions = {
|
|
|
10
10
|
*/
|
|
11
11
|
unstable_onlyGenerated?: boolean;
|
|
12
12
|
};
|
|
13
|
-
declare function performOnDemandRevalidate(source: IncomingMessage | Headers, urlPath: string, opts?: RevalidateOptions): Promise<void>;
|
|
13
|
+
declare function performOnDemandRevalidate(source: IncomingMessage | Headers, urlPath: string, opts?: RevalidateOptions, trustedOrigin?: string, allowedRevalidateHeaderKeys?: readonly string[], dev?: boolean): Promise<void>;
|
|
14
14
|
//#endregion
|
|
15
15
|
export { RevalidateOptions, performOnDemandRevalidate };
|
|
@@ -1,19 +1,65 @@
|
|
|
1
|
+
import { getRequestExecutionContext } from "../shims/request-context.js";
|
|
2
|
+
import { PRERENDER_REVALIDATE_HEADER, PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER, VINEXT_REVALIDATE_HOST_HEADER } from "../utils/protocol-headers.js";
|
|
1
3
|
import "./headers.js";
|
|
2
|
-
import {
|
|
4
|
+
import { getRevalidateSecret, isOnDemandRevalidateRequest } from "./isr-cache.js";
|
|
5
|
+
import { normalizeDomainHostname } from "../utils/domain-locale.js";
|
|
3
6
|
import { resolveRequestHost, resolveRequestProtocol } from "./proxy-trust.js";
|
|
4
7
|
//#region src/server/pages-revalidate.ts
|
|
5
|
-
async function performOnDemandRevalidate(source, urlPath, opts = {}) {
|
|
8
|
+
async function performOnDemandRevalidate(source, urlPath, opts = {}, trustedOrigin, allowedRevalidateHeaderKeys = [], dev = false) {
|
|
6
9
|
if (typeof urlPath !== "string" || !urlPath.startsWith("/")) throw new Error(`Invalid urlPath provided to revalidate(), must be a path e.g. /blog/post-1, received ${urlPath}`);
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
+
if (isSourceRevalidationRequest(source)) throw new Error(`Cannot revalidate ${urlPath} from an internal revalidation request`);
|
|
11
|
+
const executionContext = getRequestExecutionContext();
|
|
12
|
+
const dispatchRevalidate = executionContext?.dispatchPagesRevalidate;
|
|
13
|
+
const target = createRevalidateTarget(source, urlPath, trustedOrigin);
|
|
10
14
|
const headers = { [PRERENDER_REVALIDATE_HEADER]: getRevalidateSecret() };
|
|
15
|
+
if (trustedOrigin && !dispatchRevalidate) {
|
|
16
|
+
const logicalHostname = normalizeDomainHostname(resolveRequestHost(source, "localhost"));
|
|
17
|
+
if (logicalHostname) headers[VINEXT_REVALIDATE_HOST_HEADER] = logicalHostname;
|
|
18
|
+
}
|
|
11
19
|
if (opts.unstable_onlyGenerated) headers[PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER] = "1";
|
|
12
|
-
const
|
|
20
|
+
const allowedHeaders = new Set(allowedRevalidateHeaderKeys.map((key) => key.toLowerCase()));
|
|
21
|
+
if (dev) allowedHeaders.add("cookie");
|
|
22
|
+
for (const key of allowedHeaders) {
|
|
23
|
+
const value = readSourceHeader(source, key);
|
|
24
|
+
if (value !== void 0) headers[key] = value;
|
|
25
|
+
}
|
|
26
|
+
if (dispatchRevalidate && executionContext.isInternalPagesRevalidation) throw new Error(`Cannot revalidate ${urlPath} from an internal revalidation request`);
|
|
27
|
+
const res = await fetchRevalidateTarget(target, headers, dispatchRevalidate);
|
|
28
|
+
if (!((res.headers.get("x-vercel-cache") ?? res.headers.get("x-nextjs-cache"))?.toUpperCase() === "REVALIDATED" || res.status === 200 || res.status === 404 && opts.unstable_onlyGenerated === true)) throw new Error(`Failed to revalidate ${urlPath}: ${res.status}`);
|
|
29
|
+
}
|
|
30
|
+
function readSourceHeader(source, key) {
|
|
31
|
+
if (isWebHeaders(source)) return source.get(key) ?? void 0;
|
|
32
|
+
const value = source.headers[key];
|
|
33
|
+
return Array.isArray(value) ? value.join(", ") : value;
|
|
34
|
+
}
|
|
35
|
+
function isSourceRevalidationRequest(source) {
|
|
36
|
+
return isOnDemandRevalidateRequest(isWebHeaders(source) ? source.get(PRERENDER_REVALIDATE_HEADER) : source.headers[PRERENDER_REVALIDATE_HEADER]);
|
|
37
|
+
}
|
|
38
|
+
function isWebHeaders(source) {
|
|
39
|
+
return typeof source.get === "function";
|
|
40
|
+
}
|
|
41
|
+
async function fetchRevalidateTarget(initialTarget, headers, dispatchRevalidate) {
|
|
42
|
+
const request = new Request(initialTarget, {
|
|
13
43
|
method: "HEAD",
|
|
14
|
-
headers
|
|
44
|
+
headers,
|
|
45
|
+
redirect: "manual"
|
|
15
46
|
});
|
|
16
|
-
|
|
47
|
+
return dispatchRevalidate ? await dispatchRevalidate(request) : await fetch(request);
|
|
48
|
+
}
|
|
49
|
+
function createRevalidateTarget(source, urlPath, trustedOrigin) {
|
|
50
|
+
const origin = resolveRevalidateOrigin(source, trustedOrigin);
|
|
51
|
+
const target = new URL(`${origin}${urlPath}`);
|
|
52
|
+
if (target.origin !== origin) throw new Error(`Invalid urlPath provided to revalidate(), resolved outside application origin`);
|
|
53
|
+
return target;
|
|
54
|
+
}
|
|
55
|
+
function resolveRevalidateOrigin(source, trustedOrigin) {
|
|
56
|
+
if (trustedOrigin) return normalizeRevalidateOrigin(trustedOrigin);
|
|
57
|
+
return normalizeRevalidateOrigin(`${resolveRequestProtocol(source)}://${resolveRequestHost(source, "localhost")}`);
|
|
58
|
+
}
|
|
59
|
+
function normalizeRevalidateOrigin(origin) {
|
|
60
|
+
const parsed = new URL(origin);
|
|
61
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`Invalid revalidate origin protocol: ${parsed.protocol}`);
|
|
62
|
+
return parsed.origin;
|
|
17
63
|
}
|
|
18
64
|
//#endregion
|
|
19
65
|
export { performOnDemandRevalidate };
|
|
@@ -22,6 +22,7 @@ type PagesWorkerEnv = {
|
|
|
22
22
|
type PagesWorkerExecutionContext = {
|
|
23
23
|
waitUntil?(promise: Promise<unknown>): void;
|
|
24
24
|
passThroughOnException?(): void;
|
|
25
|
+
cache?: unknown;
|
|
25
26
|
};
|
|
26
27
|
declare const _default: {
|
|
27
28
|
fetch(request: Request, env?: PagesWorkerEnv, ctx?: PagesWorkerExecutionContext): Promise<Response>;
|