vinext 0.1.0 → 0.1.2
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 +2 -5
- package/dist/build/assets-ignore.d.ts +32 -0
- package/dist/build/assets-ignore.js +48 -0
- package/dist/build/client-build-config.d.ts +33 -1
- package/dist/build/client-build-config.js +66 -1
- package/dist/check.js +4 -3
- package/dist/cli.js +2 -0
- package/dist/client/navigation-runtime.d.ts +11 -2
- package/dist/client/navigation-runtime.js +1 -1
- package/dist/client/vinext-next-data.d.ts +2 -1
- package/dist/client/window-next.d.ts +6 -4
- package/dist/config/config-matchers.d.ts +31 -5
- package/dist/config/config-matchers.js +50 -3
- package/dist/config/next-config.d.ts +29 -3
- package/dist/config/next-config.js +32 -2
- package/dist/deploy.js +47 -304
- package/dist/entries/app-rsc-entry.d.ts +8 -2
- package/dist/entries/app-rsc-entry.js +61 -5
- package/dist/entries/app-rsc-manifest.js +20 -2
- package/dist/entries/pages-client-entry.js +1 -1
- package/dist/entries/pages-server-entry.js +16 -7
- package/dist/index.d.ts +0 -2
- package/dist/index.js +233 -280
- package/dist/plugins/dynamic-preload-metadata.d.ts +13 -0
- package/dist/plugins/dynamic-preload-metadata.js +415 -0
- package/dist/plugins/og-assets.js +2 -2
- package/dist/plugins/optimize-imports.d.ts +8 -4
- package/dist/plugins/optimize-imports.js +16 -12
- package/dist/plugins/postcss.js +18 -14
- package/dist/plugins/require-context.d.ts +6 -0
- package/dist/plugins/require-context.js +184 -0
- package/dist/plugins/sass.d.ts +53 -24
- package/dist/plugins/sass.js +249 -1
- package/dist/plugins/wasm-module-import.d.ts +15 -0
- package/dist/plugins/wasm-module-import.js +50 -0
- package/dist/routing/app-route-graph.d.ts +35 -2
- package/dist/routing/app-route-graph.js +179 -8
- package/dist/routing/file-matcher.js +1 -1
- package/dist/routing/route-pattern.d.ts +2 -1
- package/dist/routing/route-pattern.js +16 -1
- package/dist/server/api-handler.js +4 -0
- package/dist/server/app-browser-entry.js +155 -215
- package/dist/server/app-browser-error.d.ts +4 -1
- package/dist/server/app-browser-error.js +7 -1
- package/dist/server/app-browser-history-controller.d.ts +104 -0
- package/dist/server/app-browser-history-controller.js +210 -0
- package/dist/server/app-browser-interception-context.d.ts +2 -1
- package/dist/server/app-browser-interception-context.js +15 -2
- package/dist/server/app-browser-navigation-controller.d.ts +13 -2
- package/dist/server/app-browser-navigation-controller.js +83 -4
- package/dist/server/app-browser-popstate.d.ts +12 -3
- package/dist/server/app-browser-popstate.js +19 -4
- package/dist/server/app-browser-rsc-redirect.d.ts +11 -2
- package/dist/server/app-browser-rsc-redirect.js +30 -8
- package/dist/server/app-browser-state.d.ts +3 -0
- package/dist/server/app-browser-state.js +10 -10
- package/dist/server/app-browser-visible-commit.js +10 -8
- package/dist/server/app-fallback-renderer.d.ts +2 -1
- package/dist/server/app-fallback-renderer.js +3 -1
- package/dist/server/app-history-state.d.ts +45 -1
- package/dist/server/app-history-state.js +109 -1
- package/dist/server/app-middleware.js +1 -0
- package/dist/server/app-optimistic-routing.js +22 -1
- package/dist/server/app-page-boundary-render.d.ts +2 -1
- package/dist/server/app-page-boundary-render.js +45 -21
- package/dist/server/app-page-cache.js +9 -7
- package/dist/server/app-page-dispatch.d.ts +14 -0
- package/dist/server/app-page-dispatch.js +21 -6
- package/dist/server/app-page-element-builder.d.ts +23 -2
- package/dist/server/app-page-element-builder.js +58 -17
- package/dist/server/app-page-execution.d.ts +1 -1
- package/dist/server/app-page-execution.js +32 -17
- package/dist/server/app-page-render.d.ts +7 -1
- package/dist/server/app-page-render.js +11 -16
- package/dist/server/app-page-request.d.ts +9 -6
- package/dist/server/app-page-request.js +14 -10
- package/dist/server/app-page-response.d.ts +2 -2
- package/dist/server/app-page-response.js +2 -2
- package/dist/server/app-page-route-wiring.d.ts +3 -1
- package/dist/server/app-page-route-wiring.js +10 -8
- package/dist/server/app-page-stream.d.ts +37 -7
- package/dist/server/app-page-stream.js +36 -6
- package/dist/server/app-pages-bridge.d.ts +16 -0
- package/dist/server/app-pages-bridge.js +23 -3
- package/dist/server/app-route-handler-cache.d.ts +1 -0
- package/dist/server/app-route-handler-cache.js +1 -0
- package/dist/server/app-route-handler-dispatch.d.ts +1 -0
- package/dist/server/app-route-handler-dispatch.js +2 -0
- package/dist/server/app-route-handler-execution.d.ts +1 -0
- package/dist/server/app-route-handler-execution.js +1 -0
- package/dist/server/app-route-handler-response.js +11 -10
- package/dist/server/app-route-handler-runtime.d.ts +1 -0
- package/dist/server/app-route-handler-runtime.js +15 -3
- package/dist/server/app-rsc-handler.d.ts +1 -0
- package/dist/server/app-rsc-handler.js +5 -4
- package/dist/server/app-rsc-response-finalizer.js +1 -1
- package/dist/server/app-rsc-route-matching.d.ts +20 -1
- package/dist/server/app-rsc-route-matching.js +29 -4
- package/dist/server/app-server-action-execution.d.ts +22 -1
- package/dist/server/app-server-action-execution.js +73 -12
- package/dist/server/app-ssr-entry.d.ts +6 -0
- package/dist/server/app-ssr-entry.js +19 -3
- package/dist/server/app-ssr-stream.js +9 -1
- package/dist/server/dev-lockfile.js +2 -1
- package/dist/server/dev-server.d.ts +1 -1
- package/dist/server/dev-server.js +97 -43
- package/dist/server/headers.d.ts +8 -1
- package/dist/server/headers.js +8 -1
- package/dist/server/instrumentation-runtime.d.ts +6 -0
- package/dist/server/instrumentation-runtime.js +8 -0
- package/dist/server/isr-cache.d.ts +37 -1
- package/dist/server/isr-cache.js +85 -1
- package/dist/server/isr-decision.d.ts +79 -0
- package/dist/server/isr-decision.js +70 -0
- package/dist/server/metadata-route-response.js +5 -3
- package/dist/server/middleware-runtime.d.ts +13 -0
- package/dist/server/middleware-runtime.js +11 -7
- package/dist/server/middleware.js +1 -0
- package/dist/server/navigation-planner.d.ts +62 -1
- package/dist/server/navigation-planner.js +193 -3
- package/dist/server/navigation-trace.d.ts +12 -2
- package/dist/server/navigation-trace.js +11 -1
- package/dist/server/normalize-path.d.ts +0 -8
- package/dist/server/normalize-path.js +3 -1
- package/dist/server/otel-tracer-extension.d.ts +45 -0
- package/dist/server/otel-tracer-extension.js +89 -0
- package/dist/server/pages-api-route.d.ts +14 -3
- package/dist/server/pages-api-route.js +6 -1
- package/dist/server/pages-asset-tags.d.ts +15 -4
- package/dist/server/pages-asset-tags.js +18 -12
- package/dist/server/pages-data-route.js +5 -1
- package/dist/server/pages-node-compat.d.ts +5 -11
- package/dist/server/pages-node-compat.js +175 -118
- package/dist/server/pages-page-data.d.ts +38 -7
- package/dist/server/pages-page-data.js +64 -18
- package/dist/server/pages-page-handler.d.ts +10 -2
- package/dist/server/pages-page-handler.js +49 -20
- package/dist/server/pages-page-response.d.ts +55 -2
- package/dist/server/pages-page-response.js +74 -6
- package/dist/server/pages-readiness.d.ts +36 -0
- package/dist/server/pages-readiness.js +21 -0
- package/dist/server/pages-request-pipeline.d.ts +113 -0
- package/dist/server/pages-request-pipeline.js +230 -0
- package/dist/server/pages-revalidate.d.ts +15 -0
- package/dist/server/pages-revalidate.js +19 -0
- package/dist/server/prod-server.d.ts +45 -3
- package/dist/server/prod-server.js +182 -234
- package/dist/server/socket-error-backstop.d.ts +19 -1
- package/dist/server/socket-error-backstop.js +77 -4
- package/dist/shims/app-router-scroll.js +22 -4
- package/dist/shims/cache-runtime.js +39 -2
- package/dist/shims/dynamic-preload-chunks.d.ts +8 -0
- package/dist/shims/dynamic-preload-chunks.js +77 -0
- package/dist/shims/dynamic.d.ts +4 -0
- package/dist/shims/dynamic.js +4 -2
- package/dist/shims/error-boundary.d.ts +17 -7
- package/dist/shims/error-boundary.js +8 -1
- package/dist/shims/error.js +37 -11
- package/dist/shims/fetch-cache.d.ts +22 -1
- package/dist/shims/fetch-cache.js +28 -1
- package/dist/shims/hash-scroll.d.ts +1 -0
- package/dist/shims/hash-scroll.js +3 -1
- package/dist/shims/head.js +6 -1
- package/dist/shims/headers.d.ts +16 -2
- package/dist/shims/headers.js +37 -1
- package/dist/shims/image-config.js +7 -1
- package/dist/shims/internal/app-route-detection.d.ts +6 -3
- package/dist/shims/internal/app-route-detection.js +10 -6
- package/dist/shims/internal/app-router-context.d.ts +5 -0
- package/dist/shims/internal/link-status-registry.d.ts +43 -0
- package/dist/shims/internal/link-status-registry.js +42 -0
- package/dist/shims/internal/route-pattern-for-warning.d.ts +27 -0
- package/dist/shims/internal/route-pattern-for-warning.js +40 -0
- package/dist/shims/internal/utils.d.ts +1 -0
- package/dist/shims/link.js +20 -6
- package/dist/shims/metadata.d.ts +6 -2
- package/dist/shims/metadata.js +32 -14
- package/dist/shims/navigation.d.ts +9 -18
- package/dist/shims/navigation.js +96 -23
- package/dist/shims/router-state.d.ts +1 -0
- package/dist/shims/router-state.js +2 -0
- package/dist/shims/router.d.ts +6 -3
- package/dist/shims/router.js +156 -22
- package/dist/shims/script-nonce-context.d.ts +1 -1
- package/dist/shims/script-nonce-context.js +11 -3
- package/dist/shims/server.d.ts +17 -1
- package/dist/shims/server.js +31 -6
- package/dist/shims/slot.js +1 -1
- package/dist/shims/unified-request-context.js +1 -0
- package/dist/typegen.js +1 -0
- package/dist/utils/client-build-manifest.d.ts +8 -1
- package/dist/utils/client-build-manifest.js +41 -6
- package/dist/utils/client-entry-manifest.d.ts +11 -0
- package/dist/utils/client-entry-manifest.js +29 -0
- package/dist/utils/client-runtime-metadata.d.ts +45 -0
- package/dist/utils/client-runtime-metadata.js +63 -0
- package/dist/utils/hash.d.ts +17 -1
- package/dist/utils/hash.js +36 -1
- package/dist/utils/lazy-chunks.d.ts +27 -1
- package/dist/utils/lazy-chunks.js +65 -1
- package/dist/utils/manifest-paths.d.ts +20 -2
- package/dist/utils/manifest-paths.js +38 -3
- package/dist/utils/path.d.ts +2 -1
- package/dist/utils/path.js +5 -1
- package/package.json +6 -2
|
@@ -2,7 +2,7 @@ import { Route } from "../routing/pages-router.js";
|
|
|
2
2
|
import { VinextNextData } from "../client/vinext-next-data.js";
|
|
3
3
|
import { CachedPagesValue } from "../shims/cache.js";
|
|
4
4
|
import { ISRCacheEntry } from "./isr-cache.js";
|
|
5
|
-
import { PagesGsspResponse, PagesI18nRenderContext } from "./pages-page-response.js";
|
|
5
|
+
import { PagesGsspResponse, PagesI18nRenderContext, PagesNextDataExtras } from "./pages-page-response.js";
|
|
6
6
|
import { ReactNode } from "react";
|
|
7
7
|
|
|
8
8
|
//#region src/server/pages-page-data.d.ts
|
|
@@ -90,6 +90,7 @@ type RenderPagesIsrHtmlOptions = {
|
|
|
90
90
|
routePattern: string;
|
|
91
91
|
safeJsonStringify: (value: unknown) => string;
|
|
92
92
|
vinext?: VinextNextData["__vinext"];
|
|
93
|
+
nextData?: PagesNextDataExtras;
|
|
93
94
|
};
|
|
94
95
|
type ResolvePagesPageDataOptions = {
|
|
95
96
|
applyRequestContexts: () => void;
|
|
@@ -125,17 +126,25 @@ type ResolvePagesPageDataOptions = {
|
|
|
125
126
|
* When true, this dispatch was triggered by an on-demand revalidation
|
|
126
127
|
* request (e.g. `res.revalidate()` in a Pages Router API route, or an
|
|
127
128
|
* equivalent webhook). Maps to `revalidateReason: "on-demand"` when
|
|
128
|
-
* `getStaticProps` is invoked
|
|
129
|
+
* `getStaticProps` is invoked, and bypasses the fresh/stale cache-hit
|
|
130
|
+
* short-circuits so the entry is regenerated synchronously. Mirrors Next.js's
|
|
129
131
|
* `renderOpts.isOnDemandRevalidate` flag — see
|
|
130
132
|
* `.nextjs-ref/packages/next/src/server/render.tsx`.
|
|
131
133
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
134
|
+
* The page handler sets this only when the incoming request's
|
|
135
|
+
* `x-prerender-revalidate` header (`PRERENDER_REVALIDATE_HEADER`) *equals* the
|
|
136
|
+
* process revalidate secret that `res.revalidate()` attaches to its internal
|
|
137
|
+
* request (`isOnDemandRevalidateRequest`). It is never set on mere header
|
|
138
|
+
* presence — see the security note in `isr-cache.ts`.
|
|
137
139
|
*/
|
|
138
140
|
isOnDemandRevalidate?: boolean;
|
|
141
|
+
/**
|
|
142
|
+
* The deployment ID used for deployment-skew protection. When set, it is
|
|
143
|
+
* included as `x-nextjs-deployment-id` on all `_next/data` responses
|
|
144
|
+
* (success, redirect, notFound). Mirrors Next.js pages-handler.ts behavior.
|
|
145
|
+
* Typically sourced from `process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID`.
|
|
146
|
+
*/
|
|
147
|
+
deploymentId?: string;
|
|
139
148
|
pageModule: PagesPageModule;
|
|
140
149
|
params: Record<string, unknown>;
|
|
141
150
|
query: Record<string, unknown>;
|
|
@@ -155,6 +164,28 @@ type ResolvePagesPageDataOptions = {
|
|
|
155
164
|
}) => void;
|
|
156
165
|
renderIsrPassToStringAsync: (element: ReactNode) => Promise<string>;
|
|
157
166
|
vinext?: VinextNextData["__vinext"];
|
|
167
|
+
nextData?: PagesNextDataExtras;
|
|
168
|
+
/**
|
|
169
|
+
* The request's User-Agent string. When this matches a known crawler/bot
|
|
170
|
+
* pattern, ISR cache-HIT and cache-STALE responses receive an ETag header
|
|
171
|
+
* for consistency with the fresh-MISS path (which also attaches an ETag for
|
|
172
|
+
* bot UAs via `renderPagesPageResponse`). See the divergence note in
|
|
173
|
+
* `pages-page-response.ts` for why UA-gating is used instead of Next.js's
|
|
174
|
+
* `isDynamic` check.
|
|
175
|
+
*/
|
|
176
|
+
userAgent?: string;
|
|
177
|
+
/**
|
|
178
|
+
* The incoming request's `If-None-Match` header value. When the cached HTML
|
|
179
|
+
* ETag matches (weak-ETag semantics), the ISR cache-HIT or cache-STALE
|
|
180
|
+
* response is a `304 Not Modified` with no body.
|
|
181
|
+
*/
|
|
182
|
+
ifNoneMatch?: string;
|
|
183
|
+
/**
|
|
184
|
+
* The incoming request's `Cache-Control` header value. When it contains
|
|
185
|
+
* `no-cache`, the 304 short-circuit is skipped and a full response is
|
|
186
|
+
* returned — mirroring the `fresh` package used by Next.js.
|
|
187
|
+
*/
|
|
188
|
+
requestCacheControl?: string;
|
|
158
189
|
};
|
|
159
190
|
type ResolvePagesPageDataRenderResult = {
|
|
160
191
|
kind: "render";
|
|
@@ -1,22 +1,26 @@
|
|
|
1
|
+
import { NEXTJS_DEPLOYMENT_ID_HEADER } from "./headers.js";
|
|
1
2
|
import { normalizeStaticPathname } from "../routing/route-pattern.js";
|
|
2
3
|
import { buildCacheStateHeaders } from "./cache-headers.js";
|
|
4
|
+
import { applyCdnResponseHeaders } from "./cache-control.js";
|
|
5
|
+
import { decideIsr } from "./isr-decision.js";
|
|
3
6
|
import { buildPagesCacheValue } from "./isr-cache.js";
|
|
4
7
|
import { isSerializableProps } from "./pages-serializable-props.js";
|
|
5
8
|
import { hasPagesGetInitialProps, isResponseSent, loadPagesGetInitialProps } from "./pages-get-initial-props.js";
|
|
6
|
-
import { applyCdnResponseHeaders, buildCachedRevalidateCacheControl } from "./cache-control.js";
|
|
7
9
|
import { buildNextDataJsonResponse } from "./pages-data-route.js";
|
|
8
|
-
import { buildPagesNextDataScript } from "./pages-page-response.js";
|
|
10
|
+
import { buildPagesNextDataScript, etagMatches, generatePagesETag, isPagesStreamingBot, requestsNoCache } from "./pages-page-response.js";
|
|
9
11
|
//#region src/server/pages-page-data.ts
|
|
10
|
-
function buildPagesDataNotFoundResponse() {
|
|
12
|
+
function buildPagesDataNotFoundResponse(deploymentId) {
|
|
13
|
+
const headers = { "Content-Type": "application/json" };
|
|
14
|
+
if (deploymentId) headers[NEXTJS_DEPLOYMENT_ID_HEADER] = deploymentId;
|
|
11
15
|
return new Response("{}", {
|
|
12
16
|
status: 404,
|
|
13
|
-
headers
|
|
17
|
+
headers
|
|
14
18
|
});
|
|
15
19
|
}
|
|
16
20
|
function buildPagesNotFoundResult(options) {
|
|
17
21
|
if (options.isDataReq) return {
|
|
18
22
|
kind: "response",
|
|
19
|
-
response: buildPagesDataNotFoundResponse()
|
|
23
|
+
response: buildPagesDataNotFoundResponse(options.deploymentId)
|
|
20
24
|
};
|
|
21
25
|
return { kind: "notFound" };
|
|
22
26
|
}
|
|
@@ -46,10 +50,14 @@ function resolvePagesRedirectStatus(redirect) {
|
|
|
46
50
|
*/
|
|
47
51
|
function buildPagesRedirectResponse(redirect, options) {
|
|
48
52
|
const destination = options.sanitizeDestination(redirect.destination);
|
|
49
|
-
if (options.isDataReq)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
if (options.isDataReq) {
|
|
54
|
+
const init = { headers: {} };
|
|
55
|
+
if (options.deploymentId) init.headers[NEXTJS_DEPLOYMENT_ID_HEADER] = options.deploymentId;
|
|
56
|
+
return buildNextDataJsonResponse({
|
|
57
|
+
__N_REDIRECT: destination,
|
|
58
|
+
__N_REDIRECT_STATUS: resolvePagesRedirectStatus(redirect)
|
|
59
|
+
}, options.safeJsonStringify, init);
|
|
60
|
+
}
|
|
53
61
|
return new Response(null, {
|
|
54
62
|
status: resolvePagesRedirectStatus(redirect),
|
|
55
63
|
headers: { Location: destination }
|
|
@@ -81,19 +89,47 @@ function matchesPagesStaticPath(pathEntry, params, routeUrl) {
|
|
|
81
89
|
});
|
|
82
90
|
}
|
|
83
91
|
function buildPagesCacheResponse(html, cacheState, fontLinkHeader, revalidateSeconds, expireSeconds, cacheControl, status) {
|
|
84
|
-
const
|
|
85
|
-
|
|
92
|
+
const { cacheControl: cacheControlHeader } = decideIsr({
|
|
93
|
+
cacheState,
|
|
94
|
+
kind: "pages",
|
|
95
|
+
revalidateSeconds: revalidateSeconds ?? 60,
|
|
96
|
+
expireSeconds,
|
|
97
|
+
cacheControlMeta: cacheControl
|
|
98
|
+
});
|
|
86
99
|
const headers = new Headers({
|
|
87
100
|
"Content-Type": "text/html",
|
|
88
101
|
...buildCacheStateHeaders(cacheState)
|
|
89
102
|
});
|
|
90
|
-
applyCdnResponseHeaders(headers, { cacheControl:
|
|
103
|
+
applyCdnResponseHeaders(headers, { cacheControl: cacheControlHeader });
|
|
91
104
|
if (fontLinkHeader) headers.set("Link", fontLinkHeader);
|
|
92
105
|
return new Response(html, {
|
|
93
106
|
status: status ?? 200,
|
|
94
107
|
headers
|
|
95
108
|
});
|
|
96
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* For bot / crawler UAs, attach an ETag to a cached ISR response (HIT or
|
|
112
|
+
* STALE) so it is consistent with the fresh-MISS path, then check for a
|
|
113
|
+
* matching `If-None-Match`. When the check passes — and the request did NOT
|
|
114
|
+
* carry `Cache-Control: no-cache` — returns a 304 response; otherwise returns
|
|
115
|
+
* `null` so the caller can return the full response.
|
|
116
|
+
*
|
|
117
|
+
* Extracted to avoid duplicating the same three-line block across the HIT and
|
|
118
|
+
* STALE branches.
|
|
119
|
+
*/
|
|
120
|
+
function applyBotETagAndCheck(cachedResponse, html, options) {
|
|
121
|
+
if (!options.userAgent || !isPagesStreamingBot(options.userAgent)) return null;
|
|
122
|
+
const etag = generatePagesETag(html);
|
|
123
|
+
cachedResponse.headers.set("ETag", etag);
|
|
124
|
+
if (!requestsNoCache(options.requestCacheControl) && options.ifNoneMatch && etagMatches(etag, options.ifNoneMatch)) return {
|
|
125
|
+
kind: "response",
|
|
126
|
+
response: new Response(null, {
|
|
127
|
+
status: 304,
|
|
128
|
+
headers: cachedResponse.headers
|
|
129
|
+
})
|
|
130
|
+
};
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
97
133
|
function rewritePagesCachedHtml(cachedHtml, freshBody, nextDataScript) {
|
|
98
134
|
const bodyStart = cachedHtml.indexOf("<div id=\"__next\">");
|
|
99
135
|
const contentStart = bodyStart >= 0 ? bodyStart + 17 : -1;
|
|
@@ -117,6 +153,7 @@ async function renderPagesIsrHtml(options) {
|
|
|
117
153
|
params: options.params,
|
|
118
154
|
routePattern: options.routePattern,
|
|
119
155
|
safeJsonStringify: options.safeJsonStringify,
|
|
156
|
+
nextData: options.nextData,
|
|
120
157
|
vinext: options.vinext
|
|
121
158
|
});
|
|
122
159
|
return rewritePagesCachedHtml(options.cachedHtml, freshBody, nextDataScript);
|
|
@@ -174,11 +211,16 @@ async function resolvePagesPageData(options) {
|
|
|
174
211
|
const cacheKey = options.isrCacheKey("pages", pathname);
|
|
175
212
|
const cached = await options.isrGet(cacheKey);
|
|
176
213
|
const cachedValue = cached?.value.value;
|
|
177
|
-
if (cachedValue?.kind === "PAGES" && cached && !cached.isStale && !options.scriptNonce && !options.isDataReq)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
214
|
+
if (!options.isOnDemandRevalidate && cachedValue?.kind === "PAGES" && cached && !cached.isStale && !options.scriptNonce && !options.isDataReq) {
|
|
215
|
+
const hitResponse = buildPagesCacheResponse(cachedValue.html, "HIT", options.fontLinkHeader, void 0, options.expireSeconds, cached.value.cacheControl, cachedValue.status);
|
|
216
|
+
const hitBotResult = applyBotETagAndCheck(hitResponse, cachedValue.html, options);
|
|
217
|
+
if (hitBotResult) return hitBotResult;
|
|
218
|
+
return {
|
|
219
|
+
kind: "response",
|
|
220
|
+
response: hitResponse
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (!options.isOnDemandRevalidate && cachedValue?.kind === "PAGES" && cached && cached.isStale && !options.scriptNonce && !options.isDataReq) {
|
|
182
224
|
options.triggerBackgroundRegeneration(cacheKey, async function() {
|
|
183
225
|
return options.runInFreshUnifiedContext(async () => {
|
|
184
226
|
const freshResult = await options.pageModule.getStaticProps?.({
|
|
@@ -200,6 +242,7 @@ async function resolvePagesPageData(options) {
|
|
|
200
242
|
renderIsrPassToStringAsync: options.renderIsrPassToStringAsync,
|
|
201
243
|
routePattern: options.routePattern,
|
|
202
244
|
safeJsonStringify: options.safeJsonStringify,
|
|
245
|
+
nextData: options.nextData,
|
|
203
246
|
vinext: options.vinext
|
|
204
247
|
});
|
|
205
248
|
await options.isrSet(cacheKey, buildPagesCacheValue(freshHtml, freshResult.props, options.statusCode), freshResult.revalidate, void 0, options.expireSeconds);
|
|
@@ -210,9 +253,12 @@ async function resolvePagesPageData(options) {
|
|
|
210
253
|
routePath: options.routePattern,
|
|
211
254
|
routeType: "render"
|
|
212
255
|
});
|
|
256
|
+
const staleResponse = buildPagesCacheResponse(cachedValue.html, "STALE", options.fontLinkHeader, void 0, options.expireSeconds, cached.value.cacheControl, cachedValue.status);
|
|
257
|
+
const staleBotResult = applyBotETagAndCheck(staleResponse, cachedValue.html, options);
|
|
258
|
+
if (staleBotResult) return staleBotResult;
|
|
213
259
|
return {
|
|
214
260
|
kind: "response",
|
|
215
|
-
response:
|
|
261
|
+
response: staleResponse
|
|
216
262
|
};
|
|
217
263
|
}
|
|
218
264
|
const result = await options.pageModule.getStaticProps({
|
|
@@ -28,6 +28,7 @@ type I18nConfig = {
|
|
|
28
28
|
} | null;
|
|
29
29
|
type VinextConfigSubset = {
|
|
30
30
|
basePath: string;
|
|
31
|
+
assetPrefix: string;
|
|
31
32
|
trailingSlash: boolean;
|
|
32
33
|
expireTime?: number;
|
|
33
34
|
clientTraceMetadata?: readonly string[];
|
|
@@ -47,8 +48,15 @@ type CreatePagesPageHandlerOptions = {
|
|
|
47
48
|
vinextConfig: VinextConfigSubset; /** Build ID embedded at build time (or null in dev). */
|
|
48
49
|
buildId: string | null; /** Whether the app has user-defined middleware. */
|
|
49
50
|
hasMiddleware: boolean; /** Absolute file path of `pages/_app` (or null). Used for manifest lookup. */
|
|
50
|
-
appAssetPath: string | null; /**
|
|
51
|
-
|
|
51
|
+
appAssetPath: string | null; /** Whether next.config rewrites are configured (gates Pages router readiness). */
|
|
52
|
+
hasRewrites: boolean; /** `setSSRContext` from `next/router`. */
|
|
53
|
+
setSSRContext: ((ctx: Record<string, unknown> | null) => void) | null;
|
|
54
|
+
/**
|
|
55
|
+
* `getPagesNavigationIsReadyFromSerializedState` from `next/router`. Decides
|
|
56
|
+
* the initial `router.isReady` value for the Pages Router navigation
|
|
57
|
+
* compat hooks (mirrors Next.js's Pages adapter readiness gate).
|
|
58
|
+
*/
|
|
59
|
+
getPagesNavigationIsReadyFromSerializedState: ((routePattern: string | undefined, searchString: string, nextData?: Record<string, unknown>) => boolean) | null; /** `setI18nContext` from `vinext/i18n-context`. */
|
|
52
60
|
setI18nContext: ((ctx: Record<string, unknown>) => void) | null; /** `wrapWithRouterContext` from `next/router`. */
|
|
53
61
|
wrapWithRouterContext: ((element: ReactNode) => ReactNode) | null; /** `resetSSRHead` from `next/head`. */
|
|
54
62
|
resetSSRHead: (() => void) | undefined; /** `getSSRHeadHTML` from `next/head`. */
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { 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 { NEXTJS_DEPLOYMENT_ID_HEADER } from "./headers.js";
|
|
4
5
|
import { reportRequestError } from "./instrumentation.js";
|
|
5
|
-
import {
|
|
6
|
+
import { NEVER_CACHE_CONTROL } from "./cache-control.js";
|
|
7
|
+
import "./isr-decision.js";
|
|
8
|
+
import { PRERENDER_REVALIDATE_HEADER, isOnDemandRevalidateRequest, isrCacheKey, isrGet, isrSet, triggerBackgroundRegeneration } from "./isr-cache.js";
|
|
6
9
|
import { ensureFetchPatch } from "../shims/fetch-cache.js";
|
|
7
10
|
import { mergeRouteParamsIntoQuery, parseQueryString } from "../utils/query.js";
|
|
8
11
|
import { getScriptNonceFromHeaderSources } from "./csp.js";
|
|
9
12
|
import { resolvePagesI18nRequest } from "./pages-i18n.js";
|
|
10
13
|
import { buildDefaultPagesNotFoundResponse } from "./pages-default-404.js";
|
|
14
|
+
import { buildPagesReadinessNextData } from "./pages-readiness.js";
|
|
11
15
|
import { resolvePagesPageMethodResponse } from "./pages-page-method.js";
|
|
12
16
|
import { buildNextDataJsonResponse, buildNextDataNotFoundResponse, normalizePagesDataRequest } from "./pages-data-route.js";
|
|
13
17
|
import { createPagesReqRes } from "./pages-node-compat.js";
|
|
@@ -30,7 +34,7 @@ function buildI18nRenderContext(i18nConfig, locale, currentDefaultLocale, domain
|
|
|
30
34
|
* accepts the same options shape the generated entry always passed inline.
|
|
31
35
|
*/
|
|
32
36
|
function createPagesPageHandler(opts) {
|
|
33
|
-
const { pageRoutes, errorPageRoute, matchRoute, i18nConfig, vinextConfig, buildId, hasMiddleware, appAssetPath, setSSRContext, setI18nContext, wrapWithRouterContext, resetSSRHead, getSSRHeadHTML, setDocumentInitialHead, flushPreloads, getFontLinks, getFontStyles, getFontPreloads, renderToReadableStream, renderIsrPassToStringAsync, safeJsonStringify, sanitizeDestination, createPageElement, enhancePageElement, AppComponent, DocumentComponent } = opts;
|
|
37
|
+
const { pageRoutes, errorPageRoute, matchRoute, i18nConfig, vinextConfig, buildId, hasMiddleware, appAssetPath, hasRewrites, setSSRContext, getPagesNavigationIsReadyFromSerializedState, setI18nContext, wrapWithRouterContext, resetSSRHead, getSSRHeadHTML, setDocumentInitialHead, flushPreloads, getFontLinks, getFontStyles, getFontPreloads, renderToReadableStream, renderIsrPassToStringAsync, safeJsonStringify, sanitizeDestination, createPageElement, enhancePageElement, AppComponent, DocumentComponent } = opts;
|
|
34
38
|
function renderToStringAsync(element) {
|
|
35
39
|
return renderToReadableStream(element).then((stream) => new Response(stream).text());
|
|
36
40
|
}
|
|
@@ -102,15 +106,24 @@ function createPagesPageHandler(opts) {
|
|
|
102
106
|
const routePattern = patternToNextFormat(route.pattern);
|
|
103
107
|
const renderStatusCode = renderStatusCodeOverride ?? (routePattern === "/404" ? 404 : void 0);
|
|
104
108
|
const query = mergeRouteParamsIntoQuery(parseQueryString(routeUrl), params);
|
|
105
|
-
|
|
109
|
+
const pageModule = route.module;
|
|
110
|
+
const pagesNextData = buildPagesReadinessNextData({
|
|
111
|
+
pageModule,
|
|
112
|
+
appComponent: AppComponent,
|
|
113
|
+
hasRewrites
|
|
114
|
+
});
|
|
115
|
+
const navigationIsReady = typeof getPagesNavigationIsReadyFromSerializedState === "function" ? getPagesNavigationIsReadyFromSerializedState(routePattern, new URL(renderAsPath ?? routeUrl, "http://_").search, pagesNextData) : true;
|
|
116
|
+
function applySSRContext(extra) {
|
|
106
117
|
if (typeof setSSRContext === "function") setSSRContext({
|
|
107
118
|
pathname: routePattern,
|
|
108
119
|
query,
|
|
109
120
|
asPath: renderAsPath ?? routeUrl,
|
|
121
|
+
navigationIsReady,
|
|
110
122
|
locale,
|
|
111
123
|
locales: i18nConfig ? i18nConfig.locales : void 0,
|
|
112
124
|
defaultLocale: currentDefaultLocale,
|
|
113
|
-
domainLocales
|
|
125
|
+
domainLocales,
|
|
126
|
+
...extra
|
|
114
127
|
});
|
|
115
128
|
if (i18nConfig && typeof setI18nContext === "function") setI18nContext({
|
|
116
129
|
locale,
|
|
@@ -120,8 +133,7 @@ function createPagesPageHandler(opts) {
|
|
|
120
133
|
hostname: new URL(request.url).hostname
|
|
121
134
|
});
|
|
122
135
|
}
|
|
123
|
-
applySSRContext();
|
|
124
|
-
const pageModule = route.module;
|
|
136
|
+
applySSRContext({ nextData: pagesNextData });
|
|
125
137
|
const PageComponent = pageModule.default;
|
|
126
138
|
if (!PageComponent) return new Response("Page has no default export", { status: 500 });
|
|
127
139
|
if (!isDataReq && routePattern !== "/_error" && routePattern !== "/404" && routePattern !== "/500" && renderStatusCodeOverride === void 0) {
|
|
@@ -131,8 +143,17 @@ function createPagesPageHandler(opts) {
|
|
|
131
143
|
});
|
|
132
144
|
if (methodResponse) return methodResponse;
|
|
133
145
|
}
|
|
134
|
-
const pageModuleUrl = resolveClientModuleUrl(manifest, route.filePath);
|
|
135
|
-
const appModuleUrl = resolveClientModuleUrl(manifest, appAssetPath);
|
|
146
|
+
const pageModuleUrl = resolveClientModuleUrl(manifest, route.filePath, vinextConfig.basePath, vinextConfig.assetPrefix);
|
|
147
|
+
const appModuleUrl = resolveClientModuleUrl(manifest, appAssetPath, vinextConfig.basePath, vinextConfig.assetPrefix);
|
|
148
|
+
const serializedPagesNextData = {
|
|
149
|
+
...pagesNextData,
|
|
150
|
+
__vinext: {
|
|
151
|
+
...pagesNextData.__vinext,
|
|
152
|
+
pageModuleUrl,
|
|
153
|
+
appModuleUrl,
|
|
154
|
+
hasMiddleware
|
|
155
|
+
}
|
|
156
|
+
};
|
|
136
157
|
const scriptNonce = getScriptNonceFromHeaderSources(request.headers, middlewareHeaders);
|
|
137
158
|
let fontLinkHeader = "";
|
|
138
159
|
let allFontPreloads = [];
|
|
@@ -145,6 +166,7 @@ function createPagesPageHandler(opts) {
|
|
|
145
166
|
err,
|
|
146
167
|
applyRequestContexts: applySSRContext,
|
|
147
168
|
buildId,
|
|
169
|
+
deploymentId: process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID,
|
|
148
170
|
createGsspReqRes() {
|
|
149
171
|
return createPagesReqRes({
|
|
150
172
|
body: void 0,
|
|
@@ -164,6 +186,7 @@ function createPagesPageHandler(opts) {
|
|
|
164
186
|
isrSet,
|
|
165
187
|
expireSeconds: vinextConfig.expireTime,
|
|
166
188
|
isBuildTimePrerendering: typeof process !== "undefined" && process.env && process.env.VINEXT_PRERENDER === "1",
|
|
189
|
+
isOnDemandRevalidate: isOnDemandRevalidateRequest(request.headers.get(PRERENDER_REVALIDATE_HEADER)),
|
|
167
190
|
pageModule,
|
|
168
191
|
params,
|
|
169
192
|
query,
|
|
@@ -183,11 +206,11 @@ function createPagesPageHandler(opts) {
|
|
|
183
206
|
scriptNonce,
|
|
184
207
|
statusCode: renderStatusCode,
|
|
185
208
|
triggerBackgroundRegeneration,
|
|
186
|
-
vinext:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
209
|
+
vinext: serializedPagesNextData.__vinext,
|
|
210
|
+
nextData: serializedPagesNextData,
|
|
211
|
+
userAgent: request.headers.get("user-agent") ?? void 0,
|
|
212
|
+
ifNoneMatch: request.headers.get("if-none-match") ?? void 0,
|
|
213
|
+
requestCacheControl: request.headers.get("cache-control") ?? void 0
|
|
191
214
|
});
|
|
192
215
|
if (pageDataResult.kind === "notFound") {
|
|
193
216
|
const notFoundRoute = findNotFoundRoute();
|
|
@@ -212,6 +235,7 @@ function createPagesPageHandler(opts) {
|
|
|
212
235
|
pathname: routePattern,
|
|
213
236
|
query,
|
|
214
237
|
asPath: renderAsPath ?? routeUrl,
|
|
238
|
+
navigationIsReady: false,
|
|
215
239
|
locale,
|
|
216
240
|
locales: i18nConfig ? i18nConfig.locales : void 0,
|
|
217
241
|
defaultLocale: currentDefaultLocale,
|
|
@@ -234,7 +258,11 @@ function createPagesPageHandler(opts) {
|
|
|
234
258
|
hasUserCacheControl = true;
|
|
235
259
|
break;
|
|
236
260
|
}
|
|
237
|
-
if (!hasUserCacheControl) init.headers["Cache-Control"] =
|
|
261
|
+
if (!hasUserCacheControl) init.headers["Cache-Control"] = NEVER_CACHE_CONTROL;
|
|
262
|
+
}
|
|
263
|
+
if (routePattern !== "/_error" && routePattern !== "/500") {
|
|
264
|
+
const deploymentId = process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID;
|
|
265
|
+
if (deploymentId) init.headers[NEXTJS_DEPLOYMENT_ID_HEADER] = deploymentId;
|
|
238
266
|
}
|
|
239
267
|
return buildNextDataJsonResponse(pageProps, safeJsonStringify, init);
|
|
240
268
|
}
|
|
@@ -246,7 +274,9 @@ function createPagesPageHandler(opts) {
|
|
|
246
274
|
manifest,
|
|
247
275
|
moduleIds: pageModuleIds,
|
|
248
276
|
scriptNonce,
|
|
249
|
-
disableOptimizedLoading: vinextConfig.disableOptimizedLoading
|
|
277
|
+
disableOptimizedLoading: vinextConfig.disableOptimizedLoading,
|
|
278
|
+
basePath: vinextConfig.basePath,
|
|
279
|
+
assetPrefix: vinextConfig.assetPrefix
|
|
250
280
|
}),
|
|
251
281
|
buildId,
|
|
252
282
|
clearSsrContext() {
|
|
@@ -288,11 +318,10 @@ function createPagesPageHandler(opts) {
|
|
|
288
318
|
safeJsonStringify,
|
|
289
319
|
scriptNonce,
|
|
290
320
|
statusCode: renderStatusCode,
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
321
|
+
nextData: serializedPagesNextData,
|
|
322
|
+
userAgent: request.headers.get("user-agent") ?? void 0,
|
|
323
|
+
ifNoneMatch: request.headers.get("if-none-match") ?? void 0,
|
|
324
|
+
requestCacheControl: request.headers.get("cache-control") ?? void 0
|
|
296
325
|
});
|
|
297
326
|
} catch (e) {
|
|
298
327
|
console.error("[vinext] SSR error:", e);
|
|
@@ -4,10 +4,40 @@ import { RenderPageEnhancers } from "./pages-document-initial-props.js";
|
|
|
4
4
|
import { ComponentType, ReactNode } from "react";
|
|
5
5
|
|
|
6
6
|
//#region src/server/pages-page-response.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Returns true when the User-Agent belongs to a bot or crawler that cannot
|
|
9
|
+
* reliably consume a streamed HTML response.
|
|
10
|
+
*/
|
|
11
|
+
declare function isPagesStreamingBot(userAgent: string): boolean;
|
|
12
|
+
declare function generatePagesETag(payload: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Mirrors Next.js `sendEtagResponse` semantics (weak/strong comparison).
|
|
15
|
+
*
|
|
16
|
+
* A weak ETag `W/"..."` matches both `W/"..."` and `"..."` in `If-None-Match`.
|
|
17
|
+
* A strong ETag `"..."` only matches the same strong token.
|
|
18
|
+
* `*` always matches.
|
|
19
|
+
*/
|
|
20
|
+
declare function etagMatches(etag: string, ifNoneMatch: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Returns true when a request `Cache-Control` header asks to bypass the 304
|
|
23
|
+
* short-circuit. Mirrors the `fresh` package's check used by Next.js's
|
|
24
|
+
* `sendEtagResponse` (`/(?:^|,)\s*?no-cache\s*?(?:,|$)/`). Shared by the
|
|
25
|
+
* fresh-MISS bot path here and the ISR HIT/STALE paths in
|
|
26
|
+
* `pages-page-data.ts` so the two cannot drift.
|
|
27
|
+
*/
|
|
28
|
+
declare function requestsNoCache(cacheControl: string | undefined): boolean;
|
|
7
29
|
type PagesFontPreload = {
|
|
8
30
|
href: string;
|
|
9
31
|
type: string;
|
|
10
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* The `__NEXT_DATA__` fields beyond the always-present core that the Pages
|
|
35
|
+
* renderer serializes: the `__vinext` block plus the readiness flags
|
|
36
|
+
* (gssp/gsp/gip/appGip/autoExport/isExperimentalCompile) the client uses to
|
|
37
|
+
* recompute the initial `router.isReady`. Shared by every render path
|
|
38
|
+
* (initial, ISR regeneration) so they emit identical readiness state.
|
|
39
|
+
*/
|
|
40
|
+
type PagesNextDataExtras = Pick<VinextNextData, "__vinext" | "appGip" | "autoExport" | "gip" | "gsp" | "gssp" | "isExperimentalCompile">;
|
|
11
41
|
type PagesI18nRenderContext = {
|
|
12
42
|
locale?: string;
|
|
13
43
|
locales?: string[];
|
|
@@ -73,10 +103,33 @@ type RenderPagesPageResponseOptions = {
|
|
|
73
103
|
scriptNonce?: string;
|
|
74
104
|
statusCode?: number;
|
|
75
105
|
vinext?: VinextNextData["__vinext"];
|
|
106
|
+
nextData?: PagesNextDataExtras;
|
|
107
|
+
/**
|
|
108
|
+
* The request's User-Agent string (from `request.headers.get('user-agent')`).
|
|
109
|
+
* When this matches a known crawler / bot pattern, the response is fully
|
|
110
|
+
* buffered before sending so bots receive a single complete HTML chunk with
|
|
111
|
+
* an ETag header. Omitting this field disables bot-detection (streaming as
|
|
112
|
+
* normal), which is the correct behaviour for non-HTML requests and tests.
|
|
113
|
+
*/
|
|
114
|
+
userAgent?: string;
|
|
115
|
+
/**
|
|
116
|
+
* The incoming request's `If-None-Match` header value. When set and the
|
|
117
|
+
* computed ETag matches (weak-ETag semantics, mirroring Next.js's
|
|
118
|
+
* `sendEtagResponse`), a `304 Not Modified` is returned with an empty body.
|
|
119
|
+
* Only evaluated on bot/buffered responses that carry an ETag.
|
|
120
|
+
*/
|
|
121
|
+
ifNoneMatch?: string;
|
|
122
|
+
/**
|
|
123
|
+
* The incoming request's `Cache-Control` header value. When the value
|
|
124
|
+
* contains `no-cache`, the 304 short-circuit is skipped and a full 200
|
|
125
|
+
* response is always returned — mirroring the `fresh` package used by
|
|
126
|
+
* Next.js's `sendEtagResponse`.
|
|
127
|
+
*/
|
|
128
|
+
requestCacheControl?: string;
|
|
76
129
|
};
|
|
77
|
-
declare function buildPagesNextDataScript(options: Pick<RenderPagesPageResponseOptions, "buildId" | "i18n" | "isFallback" | "pageProps" | "params" | "routePattern" | "safeJsonStringify" | "scriptNonce"> & {
|
|
130
|
+
declare function buildPagesNextDataScript(options: Pick<RenderPagesPageResponseOptions, "buildId" | "i18n" | "isFallback" | "pageProps" | "params" | "routePattern" | "safeJsonStringify" | "scriptNonce" | "nextData"> & {
|
|
78
131
|
vinext?: VinextNextData["__vinext"];
|
|
79
132
|
}): string;
|
|
80
133
|
declare function renderPagesPageResponse(options: RenderPagesPageResponseOptions): Promise<Response>;
|
|
81
134
|
//#endregion
|
|
82
|
-
export { PagesGsspResponse, PagesI18nRenderContext, buildPagesNextDataScript, renderPagesPageResponse };
|
|
135
|
+
export { PagesGsspResponse, PagesI18nRenderContext, PagesNextDataExtras, buildPagesNextDataScript, etagMatches, generatePagesETag, isPagesStreamingBot, renderPagesPageResponse, requestsNoCache };
|
|
@@ -1,16 +1,64 @@
|
|
|
1
1
|
import { getRequestExecutionContext } from "../shims/request-context.js";
|
|
2
2
|
import { reportRequestError } from "./instrumentation.js";
|
|
3
3
|
import { setCacheStateHeaders } from "./cache-headers.js";
|
|
4
|
+
import { fnv1a52 } from "../utils/hash.js";
|
|
4
5
|
import { encodeCacheTag } from "../utils/encode-cache-tag.js";
|
|
6
|
+
import { NEVER_CACHE_CONTROL, NO_STORE_CACHE_CONTROL, applyCdnResponseHeaders } from "./cache-control.js";
|
|
7
|
+
import { buildMissIsrCacheControl } from "./isr-decision.js";
|
|
5
8
|
import { withScriptNonce } from "../shims/script-nonce-context.js";
|
|
6
9
|
import { createInlineScriptTag, createNonceAttribute, escapeHtmlAttr } from "./html.js";
|
|
7
10
|
import { getClientTraceMetadataHTML } from "./client-trace-metadata.js";
|
|
8
11
|
import { readStreamAsText } from "../utils/text-stream.js";
|
|
9
12
|
import { loadUserDocumentInitialProps, runDocumentRenderPage } from "./pages-document-initial-props.js";
|
|
10
13
|
import { callDocumentGetInitialProps } from "./document-initial-head.js";
|
|
11
|
-
import { applyCdnResponseHeaders, buildRevalidateCacheControl } from "./cache-control.js";
|
|
12
14
|
import React from "react";
|
|
13
15
|
//#region src/server/pages-page-response.ts
|
|
16
|
+
/**
|
|
17
|
+
* Crawlers that cannot handle streamed HTML: they read metadata only from
|
|
18
|
+
* the first network chunk, so streaming would give them an incomplete <head>.
|
|
19
|
+
* Pattern sourced from Next.js html-bots.ts (updated to match the canary).
|
|
20
|
+
*/
|
|
21
|
+
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;
|
|
22
|
+
/**
|
|
23
|
+
* Googlebot (the main search crawler) executes JavaScript via a headless
|
|
24
|
+
* browser, so it too cannot safely handle mid-stream HTML mutations.
|
|
25
|
+
* Matches "Googlebot" but NOT suffixed variants like "Googlebot-Image".
|
|
26
|
+
*/
|
|
27
|
+
const HEADLESS_BROWSER_BOT_UA_RE = /Googlebot(?!-)|Googlebot$/i;
|
|
28
|
+
/**
|
|
29
|
+
* Returns true when the User-Agent belongs to a bot or crawler that cannot
|
|
30
|
+
* reliably consume a streamed HTML response.
|
|
31
|
+
*/
|
|
32
|
+
function isPagesStreamingBot(userAgent) {
|
|
33
|
+
return HEADLESS_BROWSER_BOT_UA_RE.test(userAgent) || HTML_LIMITED_BOT_UA_RE.test(userAgent);
|
|
34
|
+
}
|
|
35
|
+
function generatePagesETag(payload) {
|
|
36
|
+
return "\"" + fnv1a52(payload).toString(36) + payload.length.toString(36) + "\"";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Mirrors Next.js `sendEtagResponse` semantics (weak/strong comparison).
|
|
40
|
+
*
|
|
41
|
+
* A weak ETag `W/"..."` matches both `W/"..."` and `"..."` in `If-None-Match`.
|
|
42
|
+
* A strong ETag `"..."` only matches the same strong token.
|
|
43
|
+
* `*` always matches.
|
|
44
|
+
*/
|
|
45
|
+
function etagMatches(etag, ifNoneMatch) {
|
|
46
|
+
if (ifNoneMatch === "*") return true;
|
|
47
|
+
const normalize = (t) => t.replace(/^W\//, "");
|
|
48
|
+
const etagNorm = normalize(etag.trim());
|
|
49
|
+
for (const token of ifNoneMatch.split(",")) if (normalize(token.trim()) === etagNorm) return true;
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Returns true when a request `Cache-Control` header asks to bypass the 304
|
|
54
|
+
* short-circuit. Mirrors the `fresh` package's check used by Next.js's
|
|
55
|
+
* `sendEtagResponse` (`/(?:^|,)\s*?no-cache\s*?(?:,|$)/`). Shared by the
|
|
56
|
+
* fresh-MISS bot path here and the ISR HIT/STALE paths in
|
|
57
|
+
* `pages-page-data.ts` so the two cannot drift.
|
|
58
|
+
*/
|
|
59
|
+
function requestsNoCache(cacheControl) {
|
|
60
|
+
return /(?:^|,)\s*no-cache\s*(?:,|$)/.test(cacheControl ?? "");
|
|
61
|
+
}
|
|
14
62
|
function buildPagesFontHeadHtml(fontLinks, fontPreloads, fontStyles, scriptNonce) {
|
|
15
63
|
let html = "";
|
|
16
64
|
const nonceAttr = createNonceAttribute(scriptNonce);
|
|
@@ -27,13 +75,19 @@ function buildPagesNextDataScript(options) {
|
|
|
27
75
|
buildId: options.buildId,
|
|
28
76
|
isFallback: options.isFallback === true
|
|
29
77
|
};
|
|
78
|
+
if (options.nextData) {
|
|
79
|
+
for (const [key, value] of Object.entries(options.nextData)) if (value !== void 0) nextDataPayload[key] = value;
|
|
80
|
+
}
|
|
30
81
|
if (options.i18n.locales) {
|
|
31
82
|
nextDataPayload.locale = options.i18n.locale;
|
|
32
83
|
nextDataPayload.locales = options.i18n.locales;
|
|
33
84
|
nextDataPayload.defaultLocale = options.i18n.defaultLocale;
|
|
34
85
|
nextDataPayload.domainLocales = options.i18n.domainLocales;
|
|
35
86
|
}
|
|
36
|
-
if (options.vinext) nextDataPayload.__vinext =
|
|
87
|
+
if (options.vinext) nextDataPayload.__vinext = {
|
|
88
|
+
...options.nextData?.__vinext,
|
|
89
|
+
...options.vinext
|
|
90
|
+
};
|
|
37
91
|
const localeGlobals = options.i18n.locales ? `;window.__VINEXT_LOCALE__=${options.safeJsonStringify(options.i18n.locale)};window.__VINEXT_LOCALES__=${options.safeJsonStringify(options.i18n.locales)};window.__VINEXT_DEFAULT_LOCALE__=${options.safeJsonStringify(options.i18n.defaultLocale)}` : "";
|
|
38
92
|
return createInlineScriptTag(`window.__NEXT_DATA__ = ${options.safeJsonStringify(nextDataPayload)}${localeGlobals}`, options.scriptNonce);
|
|
39
93
|
}
|
|
@@ -129,6 +183,7 @@ async function renderPagesPageResponse(options) {
|
|
|
129
183
|
routePattern: options.routePattern,
|
|
130
184
|
safeJsonStringify: options.safeJsonStringify,
|
|
131
185
|
scriptNonce: options.scriptNonce,
|
|
186
|
+
nextData: options.nextData,
|
|
132
187
|
vinext: options.vinext
|
|
133
188
|
});
|
|
134
189
|
const bodyMarker = "<!--VINEXT_STREAM_BODY-->";
|
|
@@ -194,21 +249,34 @@ async function renderPagesPageResponse(options) {
|
|
|
194
249
|
}
|
|
195
250
|
const compositeStream = await buildPagesCompositeStream(responseBodyStream, shellPrefix, shellSuffix);
|
|
196
251
|
const userSetCacheControl = responseHeaders.has("Cache-Control");
|
|
197
|
-
if (options.scriptNonce) responseHeaders.set("Cache-Control",
|
|
252
|
+
if (options.scriptNonce) responseHeaders.set("Cache-Control", NO_STORE_CACHE_CONTROL);
|
|
198
253
|
else if (options.isrRevalidateSeconds) {
|
|
199
254
|
const isrPathname = options.routeUrl.split("?")[0];
|
|
200
255
|
const stem = isrPathname.endsWith("/") ? isrPathname.slice(0, -1) : isrPathname;
|
|
201
256
|
applyCdnResponseHeaders(responseHeaders, {
|
|
202
|
-
cacheControl:
|
|
257
|
+
cacheControl: buildMissIsrCacheControl(options.isrRevalidateSeconds, options.expireSeconds),
|
|
203
258
|
tags: [encodeCacheTag(`_N_T_${stem || "/"}`)]
|
|
204
259
|
});
|
|
205
260
|
setCacheStateHeaders(responseHeaders, "MISS");
|
|
206
|
-
} else if (options.gsspRes && !userSetCacheControl) responseHeaders.set("Cache-Control",
|
|
261
|
+
} else if (options.gsspRes && !userSetCacheControl) responseHeaders.set("Cache-Control", NEVER_CACHE_CONTROL);
|
|
207
262
|
if (options.fontLinkHeader) responseHeaders.set("Link", options.fontLinkHeader);
|
|
263
|
+
if (options.userAgent && isPagesStreamingBot(options.userAgent)) {
|
|
264
|
+
const fullHtml = await readStreamAsText(compositeStream);
|
|
265
|
+
const etag = generatePagesETag(fullHtml);
|
|
266
|
+
responseHeaders.set("ETag", etag);
|
|
267
|
+
if (!requestsNoCache(options.requestCacheControl) && options.ifNoneMatch && etagMatches(etag, options.ifNoneMatch)) return new Response(null, {
|
|
268
|
+
status: 304,
|
|
269
|
+
headers: responseHeaders
|
|
270
|
+
});
|
|
271
|
+
return new Response(fullHtml, {
|
|
272
|
+
status: finalStatus,
|
|
273
|
+
headers: responseHeaders
|
|
274
|
+
});
|
|
275
|
+
}
|
|
208
276
|
return Object.assign(new Response(compositeStream, {
|
|
209
277
|
status: finalStatus,
|
|
210
278
|
headers: responseHeaders
|
|
211
279
|
}), { __vinextStreamedHtmlResponse: true });
|
|
212
280
|
}
|
|
213
281
|
//#endregion
|
|
214
|
-
export { buildPagesNextDataScript, renderPagesPageResponse };
|
|
282
|
+
export { buildPagesNextDataScript, etagMatches, generatePagesETag, isPagesStreamingBot, renderPagesPageResponse, requestsNoCache };
|