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.
Files changed (205) hide show
  1. package/README.md +2 -5
  2. package/dist/build/assets-ignore.d.ts +32 -0
  3. package/dist/build/assets-ignore.js +48 -0
  4. package/dist/build/client-build-config.d.ts +33 -1
  5. package/dist/build/client-build-config.js +66 -1
  6. package/dist/check.js +4 -3
  7. package/dist/cli.js +2 -0
  8. package/dist/client/navigation-runtime.d.ts +11 -2
  9. package/dist/client/navigation-runtime.js +1 -1
  10. package/dist/client/vinext-next-data.d.ts +2 -1
  11. package/dist/client/window-next.d.ts +6 -4
  12. package/dist/config/config-matchers.d.ts +31 -5
  13. package/dist/config/config-matchers.js +50 -3
  14. package/dist/config/next-config.d.ts +29 -3
  15. package/dist/config/next-config.js +32 -2
  16. package/dist/deploy.js +47 -304
  17. package/dist/entries/app-rsc-entry.d.ts +8 -2
  18. package/dist/entries/app-rsc-entry.js +61 -5
  19. package/dist/entries/app-rsc-manifest.js +20 -2
  20. package/dist/entries/pages-client-entry.js +1 -1
  21. package/dist/entries/pages-server-entry.js +16 -7
  22. package/dist/index.d.ts +0 -2
  23. package/dist/index.js +233 -280
  24. package/dist/plugins/dynamic-preload-metadata.d.ts +13 -0
  25. package/dist/plugins/dynamic-preload-metadata.js +415 -0
  26. package/dist/plugins/og-assets.js +2 -2
  27. package/dist/plugins/optimize-imports.d.ts +8 -4
  28. package/dist/plugins/optimize-imports.js +16 -12
  29. package/dist/plugins/postcss.js +18 -14
  30. package/dist/plugins/require-context.d.ts +6 -0
  31. package/dist/plugins/require-context.js +184 -0
  32. package/dist/plugins/sass.d.ts +53 -24
  33. package/dist/plugins/sass.js +249 -1
  34. package/dist/plugins/wasm-module-import.d.ts +15 -0
  35. package/dist/plugins/wasm-module-import.js +50 -0
  36. package/dist/routing/app-route-graph.d.ts +35 -2
  37. package/dist/routing/app-route-graph.js +179 -8
  38. package/dist/routing/file-matcher.js +1 -1
  39. package/dist/routing/route-pattern.d.ts +2 -1
  40. package/dist/routing/route-pattern.js +16 -1
  41. package/dist/server/api-handler.js +4 -0
  42. package/dist/server/app-browser-entry.js +155 -215
  43. package/dist/server/app-browser-error.d.ts +4 -1
  44. package/dist/server/app-browser-error.js +7 -1
  45. package/dist/server/app-browser-history-controller.d.ts +104 -0
  46. package/dist/server/app-browser-history-controller.js +210 -0
  47. package/dist/server/app-browser-interception-context.d.ts +2 -1
  48. package/dist/server/app-browser-interception-context.js +15 -2
  49. package/dist/server/app-browser-navigation-controller.d.ts +13 -2
  50. package/dist/server/app-browser-navigation-controller.js +83 -4
  51. package/dist/server/app-browser-popstate.d.ts +12 -3
  52. package/dist/server/app-browser-popstate.js +19 -4
  53. package/dist/server/app-browser-rsc-redirect.d.ts +11 -2
  54. package/dist/server/app-browser-rsc-redirect.js +30 -8
  55. package/dist/server/app-browser-state.d.ts +3 -0
  56. package/dist/server/app-browser-state.js +10 -10
  57. package/dist/server/app-browser-visible-commit.js +10 -8
  58. package/dist/server/app-fallback-renderer.d.ts +2 -1
  59. package/dist/server/app-fallback-renderer.js +3 -1
  60. package/dist/server/app-history-state.d.ts +45 -1
  61. package/dist/server/app-history-state.js +109 -1
  62. package/dist/server/app-middleware.js +1 -0
  63. package/dist/server/app-optimistic-routing.js +22 -1
  64. package/dist/server/app-page-boundary-render.d.ts +2 -1
  65. package/dist/server/app-page-boundary-render.js +45 -21
  66. package/dist/server/app-page-cache.js +9 -7
  67. package/dist/server/app-page-dispatch.d.ts +14 -0
  68. package/dist/server/app-page-dispatch.js +21 -6
  69. package/dist/server/app-page-element-builder.d.ts +23 -2
  70. package/dist/server/app-page-element-builder.js +58 -17
  71. package/dist/server/app-page-execution.d.ts +1 -1
  72. package/dist/server/app-page-execution.js +32 -17
  73. package/dist/server/app-page-render.d.ts +7 -1
  74. package/dist/server/app-page-render.js +11 -16
  75. package/dist/server/app-page-request.d.ts +9 -6
  76. package/dist/server/app-page-request.js +14 -10
  77. package/dist/server/app-page-response.d.ts +2 -2
  78. package/dist/server/app-page-response.js +2 -2
  79. package/dist/server/app-page-route-wiring.d.ts +3 -1
  80. package/dist/server/app-page-route-wiring.js +10 -8
  81. package/dist/server/app-page-stream.d.ts +37 -7
  82. package/dist/server/app-page-stream.js +36 -6
  83. package/dist/server/app-pages-bridge.d.ts +16 -0
  84. package/dist/server/app-pages-bridge.js +23 -3
  85. package/dist/server/app-route-handler-cache.d.ts +1 -0
  86. package/dist/server/app-route-handler-cache.js +1 -0
  87. package/dist/server/app-route-handler-dispatch.d.ts +1 -0
  88. package/dist/server/app-route-handler-dispatch.js +2 -0
  89. package/dist/server/app-route-handler-execution.d.ts +1 -0
  90. package/dist/server/app-route-handler-execution.js +1 -0
  91. package/dist/server/app-route-handler-response.js +11 -10
  92. package/dist/server/app-route-handler-runtime.d.ts +1 -0
  93. package/dist/server/app-route-handler-runtime.js +15 -3
  94. package/dist/server/app-rsc-handler.d.ts +1 -0
  95. package/dist/server/app-rsc-handler.js +5 -4
  96. package/dist/server/app-rsc-response-finalizer.js +1 -1
  97. package/dist/server/app-rsc-route-matching.d.ts +20 -1
  98. package/dist/server/app-rsc-route-matching.js +29 -4
  99. package/dist/server/app-server-action-execution.d.ts +22 -1
  100. package/dist/server/app-server-action-execution.js +73 -12
  101. package/dist/server/app-ssr-entry.d.ts +6 -0
  102. package/dist/server/app-ssr-entry.js +19 -3
  103. package/dist/server/app-ssr-stream.js +9 -1
  104. package/dist/server/dev-lockfile.js +2 -1
  105. package/dist/server/dev-server.d.ts +1 -1
  106. package/dist/server/dev-server.js +97 -43
  107. package/dist/server/headers.d.ts +8 -1
  108. package/dist/server/headers.js +8 -1
  109. package/dist/server/instrumentation-runtime.d.ts +6 -0
  110. package/dist/server/instrumentation-runtime.js +8 -0
  111. package/dist/server/isr-cache.d.ts +37 -1
  112. package/dist/server/isr-cache.js +85 -1
  113. package/dist/server/isr-decision.d.ts +79 -0
  114. package/dist/server/isr-decision.js +70 -0
  115. package/dist/server/metadata-route-response.js +5 -3
  116. package/dist/server/middleware-runtime.d.ts +13 -0
  117. package/dist/server/middleware-runtime.js +11 -7
  118. package/dist/server/middleware.js +1 -0
  119. package/dist/server/navigation-planner.d.ts +62 -1
  120. package/dist/server/navigation-planner.js +193 -3
  121. package/dist/server/navigation-trace.d.ts +12 -2
  122. package/dist/server/navigation-trace.js +11 -1
  123. package/dist/server/normalize-path.d.ts +0 -8
  124. package/dist/server/normalize-path.js +3 -1
  125. package/dist/server/otel-tracer-extension.d.ts +45 -0
  126. package/dist/server/otel-tracer-extension.js +89 -0
  127. package/dist/server/pages-api-route.d.ts +14 -3
  128. package/dist/server/pages-api-route.js +6 -1
  129. package/dist/server/pages-asset-tags.d.ts +15 -4
  130. package/dist/server/pages-asset-tags.js +18 -12
  131. package/dist/server/pages-data-route.js +5 -1
  132. package/dist/server/pages-node-compat.d.ts +5 -11
  133. package/dist/server/pages-node-compat.js +175 -118
  134. package/dist/server/pages-page-data.d.ts +38 -7
  135. package/dist/server/pages-page-data.js +64 -18
  136. package/dist/server/pages-page-handler.d.ts +10 -2
  137. package/dist/server/pages-page-handler.js +49 -20
  138. package/dist/server/pages-page-response.d.ts +55 -2
  139. package/dist/server/pages-page-response.js +74 -6
  140. package/dist/server/pages-readiness.d.ts +36 -0
  141. package/dist/server/pages-readiness.js +21 -0
  142. package/dist/server/pages-request-pipeline.d.ts +113 -0
  143. package/dist/server/pages-request-pipeline.js +230 -0
  144. package/dist/server/pages-revalidate.d.ts +15 -0
  145. package/dist/server/pages-revalidate.js +19 -0
  146. package/dist/server/prod-server.d.ts +45 -3
  147. package/dist/server/prod-server.js +182 -234
  148. package/dist/server/socket-error-backstop.d.ts +19 -1
  149. package/dist/server/socket-error-backstop.js +77 -4
  150. package/dist/shims/app-router-scroll.js +22 -4
  151. package/dist/shims/cache-runtime.js +39 -2
  152. package/dist/shims/dynamic-preload-chunks.d.ts +8 -0
  153. package/dist/shims/dynamic-preload-chunks.js +77 -0
  154. package/dist/shims/dynamic.d.ts +4 -0
  155. package/dist/shims/dynamic.js +4 -2
  156. package/dist/shims/error-boundary.d.ts +17 -7
  157. package/dist/shims/error-boundary.js +8 -1
  158. package/dist/shims/error.js +37 -11
  159. package/dist/shims/fetch-cache.d.ts +22 -1
  160. package/dist/shims/fetch-cache.js +28 -1
  161. package/dist/shims/hash-scroll.d.ts +1 -0
  162. package/dist/shims/hash-scroll.js +3 -1
  163. package/dist/shims/head.js +6 -1
  164. package/dist/shims/headers.d.ts +16 -2
  165. package/dist/shims/headers.js +37 -1
  166. package/dist/shims/image-config.js +7 -1
  167. package/dist/shims/internal/app-route-detection.d.ts +6 -3
  168. package/dist/shims/internal/app-route-detection.js +10 -6
  169. package/dist/shims/internal/app-router-context.d.ts +5 -0
  170. package/dist/shims/internal/link-status-registry.d.ts +43 -0
  171. package/dist/shims/internal/link-status-registry.js +42 -0
  172. package/dist/shims/internal/route-pattern-for-warning.d.ts +27 -0
  173. package/dist/shims/internal/route-pattern-for-warning.js +40 -0
  174. package/dist/shims/internal/utils.d.ts +1 -0
  175. package/dist/shims/link.js +20 -6
  176. package/dist/shims/metadata.d.ts +6 -2
  177. package/dist/shims/metadata.js +32 -14
  178. package/dist/shims/navigation.d.ts +9 -18
  179. package/dist/shims/navigation.js +96 -23
  180. package/dist/shims/router-state.d.ts +1 -0
  181. package/dist/shims/router-state.js +2 -0
  182. package/dist/shims/router.d.ts +6 -3
  183. package/dist/shims/router.js +156 -22
  184. package/dist/shims/script-nonce-context.d.ts +1 -1
  185. package/dist/shims/script-nonce-context.js +11 -3
  186. package/dist/shims/server.d.ts +17 -1
  187. package/dist/shims/server.js +31 -6
  188. package/dist/shims/slot.js +1 -1
  189. package/dist/shims/unified-request-context.js +1 -0
  190. package/dist/typegen.js +1 -0
  191. package/dist/utils/client-build-manifest.d.ts +8 -1
  192. package/dist/utils/client-build-manifest.js +41 -6
  193. package/dist/utils/client-entry-manifest.d.ts +11 -0
  194. package/dist/utils/client-entry-manifest.js +29 -0
  195. package/dist/utils/client-runtime-metadata.d.ts +45 -0
  196. package/dist/utils/client-runtime-metadata.js +63 -0
  197. package/dist/utils/hash.d.ts +17 -1
  198. package/dist/utils/hash.js +36 -1
  199. package/dist/utils/lazy-chunks.d.ts +27 -1
  200. package/dist/utils/lazy-chunks.js +65 -1
  201. package/dist/utils/manifest-paths.d.ts +20 -2
  202. package/dist/utils/manifest-paths.js +38 -3
  203. package/dist/utils/path.d.ts +2 -1
  204. package/dist/utils/path.js +5 -1
  205. 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. Mirrors Next.js's
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
- * Forward-looking plumbing: no caller currently sets this `res.revalidate()`
133
- * is not yet implemented in vinext. The `"on-demand"` branch in the
134
- * `revalidateReason` resolver is intentionally unreachable today; keeping the
135
- * typed contract here means wiring it up will be a one-line change once the
136
- * trigger lands.
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: { "Content-Type": "application/json" }
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) return buildNextDataJsonResponse({
50
- __N_REDIRECT: destination,
51
- __N_REDIRECT_STATUS: resolvePagesRedirectStatus(redirect)
52
- }, options.safeJsonStringify);
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 effectiveRevalidateSeconds = cacheControl?.revalidate ?? revalidateSeconds ?? 60;
85
- const effectiveExpireSeconds = cacheControl === void 0 ? void 0 : cacheControl.expire ?? expireSeconds;
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: buildCachedRevalidateCacheControl(cacheState, effectiveRevalidateSeconds, effectiveExpireSeconds) });
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) return {
178
- kind: "response",
179
- response: buildPagesCacheResponse(cachedValue.html, "HIT", options.fontLinkHeader, void 0, options.expireSeconds, cached.value.cacheControl, cachedValue.status)
180
- };
181
- if (cachedValue?.kind === "PAGES" && cached && cached.isStale && !options.scriptNonce && !options.isDataReq) {
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: buildPagesCacheResponse(cachedValue.html, "STALE", options.fontLinkHeader, void 0, options.expireSeconds, cached.value.cacheControl, cachedValue.status)
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; /** `setSSRContext` from `next/router`. */
51
- setSSRContext: ((ctx: Record<string, unknown> | null) => void) | null; /** `setI18nContext` from `vinext/i18n-context`. */
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 { isrCacheKey, isrGet, isrSet, triggerBackgroundRegeneration } from "./isr-cache.js";
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
- function applySSRContext() {
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
- pageModuleUrl,
188
- appModuleUrl,
189
- hasMiddleware
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"] = "private, no-cache, no-store, max-age=0, must-revalidate";
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
- vinext: {
292
- pageModuleUrl,
293
- appModuleUrl,
294
- hasMiddleware
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 = options.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", "no-store, must-revalidate");
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: buildRevalidateCacheControl(options.isrRevalidateSeconds, options.expireSeconds),
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", "private, no-cache, no-store, max-age=0, must-revalidate");
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 };