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,13 +2,16 @@ import { createRequestContext, runWithRequestContext } from "../shims/unified-re
2
2
  import { createValidFileMatcher, findFileWithExtensions } from "../routing/file-matcher.js";
3
3
  import { patternToNextFormat } from "../routing/route-validation.js";
4
4
  import { matchRoute } from "../routing/pages-router.js";
5
+ import { NEXTJS_DEPLOYMENT_ID_HEADER } from "./headers.js";
5
6
  import { normalizeStaticPathname } from "../routing/route-pattern.js";
6
7
  import { importModule, reportRequestError } from "./instrumentation.js";
7
8
  import { buildCacheStateHeaders } from "./cache-headers.js";
8
9
  import { _runWithCacheState } from "../shims/cache.js";
9
- import { buildPagesCacheValue, getRevalidateDuration, isrCacheKey, isrGet, isrSet, setRevalidateDuration, triggerBackgroundRegeneration } from "./isr-cache.js";
10
- import { runWithPrivateCache } from "../shims/cache-runtime.js";
10
+ import { NEVER_CACHE_CONTROL, NO_STORE_CACHE_CONTROL } from "./cache-control.js";
11
+ import { buildMissIsrCacheControl, decideIsr } from "./isr-decision.js";
12
+ import { PRERENDER_REVALIDATE_HEADER, buildPagesCacheValue, getRevalidateDuration, isOnDemandRevalidateRequest, isrCacheKey, isrGet, isrSet, setRevalidateDuration, triggerBackgroundRegeneration } from "./isr-cache.js";
11
13
  import { ensureFetchPatch, runWithFetchCache } from "../shims/fetch-cache.js";
14
+ import { runWithPrivateCache } from "../shims/cache-runtime.js";
12
15
  import { mergeRouteParamsIntoQuery, parseQueryString } from "../utils/query.js";
13
16
  import "../shims/router-state.js";
14
17
  import { runWithHeadState } from "../shims/head-state.js";
@@ -20,6 +23,7 @@ import { getScriptNonceFromNodeHeaderSources } from "./csp.js";
20
23
  import { logRequest, now } from "./request-log.js";
21
24
  import { detectLocaleFromAcceptLanguage, extractLocaleFromUrl as extractLocaleFromUrl$1, parseCookieLocaleFromHeader, resolvePagesI18nRequest } from "./pages-i18n.js";
22
25
  import { buildDefaultPagesNotFoundResponse } from "./pages-default-404.js";
26
+ import { buildPagesReadinessNextData } from "./pages-readiness.js";
23
27
  import { resolvePagesPageMethodResponse } from "./pages-page-method.js";
24
28
  import { isSerializableProps } from "./pages-serializable-props.js";
25
29
  import { loadUserDocumentInitialProps, runDocumentRenderPage } from "./pages-document-initial-props.js";
@@ -61,7 +65,10 @@ function writeGsspRedirect(res, redirect, isDataReq) {
61
65
  let dest = redirect.destination;
62
66
  if (!dest.startsWith("http://") && !dest.startsWith("https://")) dest = dest.replace(/^[\\/]+/, "/");
63
67
  if (isDataReq) {
64
- res.writeHead(200, { "Content-Type": "application/json" });
68
+ const deploymentId = process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID;
69
+ const dataHeaders = { "Content-Type": "application/json" };
70
+ if (deploymentId) dataHeaders[NEXTJS_DEPLOYMENT_ID_HEADER] = deploymentId;
71
+ res.writeHead(200, dataHeaders);
65
72
  res.end(JSON.stringify({ pageProps: {
66
73
  __N_REDIRECT: dest,
67
74
  __N_REDIRECT_STATUS: status
@@ -183,8 +190,9 @@ function parseCookieLocale(req, i18nConfig) {
183
190
  * 4. Render the component to HTML
184
191
  * 5. Wrap in _document shell and send response
185
192
  */
186
- function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatcher, basePath = "", trailingSlash = false, hasMiddleware = false, clientTraceMetadata) {
193
+ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatcher, basePath = "", trailingSlash = false, hasMiddleware = false, hasRewrites = false, clientTraceMetadata) {
187
194
  const matcher = fileMatcher ?? createValidFileMatcher();
195
+ const pagePatterns = routes.map((r) => patternToNextFormat(r.pattern));
188
196
  const _alsRegistration = Promise.all([runner.import("vinext/head-state"), runner.import("vinext/router-state")]);
189
197
  _alsRegistration.catch(() => {});
190
198
  return async (req, res, url, statusCode, isDataReq = false) => {
@@ -226,7 +234,10 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
226
234
  const match = matchRoute(localeStrippedUrl, routes);
227
235
  if (!match) {
228
236
  if (isDataReq) {
229
- res.writeHead(404, { "Content-Type": "application/json" });
237
+ const deploymentId = process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID;
238
+ const notFoundHeaders = { "Content-Type": "application/json" };
239
+ if (deploymentId) notFoundHeaders[NEXTJS_DEPLOYMENT_ID_HEADER] = deploymentId;
240
+ res.writeHead(404, notFoundHeaders);
230
241
  res.end("{}");
231
242
  return;
232
243
  }
@@ -241,15 +252,6 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
241
252
  try {
242
253
  await _alsRegistration;
243
254
  const routerShim = await importModule(runner, "next/router");
244
- if (typeof routerShim.setSSRContext === "function") routerShim.setSSRContext({
245
- pathname: patternToNextFormat(route.pattern),
246
- query,
247
- asPath: url,
248
- locale: locale ?? currentDefaultLocale,
249
- locales: i18nConfig?.locales,
250
- defaultLocale: currentDefaultLocale,
251
- domainLocales
252
- });
253
255
  if (i18nConfig) {
254
256
  await runner.import("vinext/i18n-state");
255
257
  const i18nCtx = await importModule(runner, "vinext/i18n-context");
@@ -262,6 +264,28 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
262
264
  });
263
265
  }
264
266
  const pageModule = await importModule(runner, route.filePath);
267
+ let AppComponent = null;
268
+ const appPath = path.join(pagesDir, "_app");
269
+ if (findFileWithExtensions(appPath, matcher)) try {
270
+ AppComponent = (await importModule(runner, appPath)).default ?? null;
271
+ } catch {}
272
+ const pagesNextData = buildPagesReadinessNextData({
273
+ pageModule,
274
+ appComponent: AppComponent,
275
+ hasRewrites
276
+ });
277
+ const navigationIsReady = typeof routerShim.getPagesNavigationIsReadyFromSerializedState === "function" ? routerShim.getPagesNavigationIsReadyFromSerializedState(patternToNextFormat(route.pattern), new URL(url, "http://_").search, pagesNextData) : true;
278
+ if (typeof routerShim.setSSRContext === "function") routerShim.setSSRContext({
279
+ pathname: patternToNextFormat(route.pattern),
280
+ query,
281
+ asPath: url,
282
+ navigationIsReady,
283
+ nextData: pagesNextData,
284
+ locale: locale ?? currentDefaultLocale,
285
+ locales: i18nConfig?.locales,
286
+ defaultLocale: currentDefaultLocale,
287
+ domainLocales
288
+ });
265
289
  _compileEnd = now();
266
290
  const PageComponent = pageModule.default;
267
291
  if (!PageComponent) {
@@ -310,7 +334,10 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
310
334
  });
311
335
  if (fallback === false && !isValidPath) {
312
336
  if (isDataReq) {
313
- res.writeHead(404, { "Content-Type": "application/json" });
337
+ const deploymentId = process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID;
338
+ const notFoundHeaders = { "Content-Type": "application/json" };
339
+ if (deploymentId) notFoundHeaders[NEXTJS_DEPLOYMENT_ID_HEADER] = deploymentId;
340
+ res.writeHead(404, notFoundHeaders);
314
341
  res.end("{}");
315
342
  return;
316
343
  }
@@ -323,6 +350,7 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
323
350
  pathname: patternToNextFormat(route.pattern),
324
351
  query,
325
352
  asPath: url,
353
+ navigationIsReady: false,
326
354
  locale: locale ?? currentDefaultLocale,
327
355
  locales: i18nConfig?.locales,
328
356
  defaultLocale: currentDefaultLocale,
@@ -353,7 +381,10 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
353
381
  }
354
382
  if (result && "notFound" in result && result.notFound) {
355
383
  if (isDataReq) {
356
- res.writeHead(404, { "Content-Type": "application/json" });
384
+ const deploymentId = process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID;
385
+ const notFoundHeaders = { "Content-Type": "application/json" };
386
+ if (deploymentId) notFoundHeaders[NEXTJS_DEPLOYMENT_ID_HEADER] = deploymentId;
387
+ res.writeHead(404, notFoundHeaders);
357
388
  res.end("{}");
358
389
  return;
359
390
  }
@@ -369,7 +400,7 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
369
400
  if (Array.isArray(val)) gsspExtraHeaders[key] = val.map(String);
370
401
  else gsspExtraHeaders[key] = String(val);
371
402
  }
372
- if (!Object.keys(gsspExtraHeaders).some((k) => k.toLowerCase() === "cache-control")) gsspExtraHeaders["Cache-Control"] = "private, no-cache, no-store, max-age=0, must-revalidate";
403
+ if (!Object.keys(gsspExtraHeaders).some((k) => k.toLowerCase() === "cache-control")) gsspExtraHeaders["Cache-Control"] = NEVER_CACHE_CONTROL;
373
404
  }
374
405
  const responseHeaders = typeof res.getHeaders === "function" ? res.getHeaders() : void 0;
375
406
  const scriptNonce = getScriptNonceFromNodeHeaderSources(req.headers, responseHeaders);
@@ -385,21 +416,26 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
385
416
  if (typeof pageModule.getStaticProps === "function" && !isFallbackRender) {
386
417
  const cacheKey = pagesIsrCacheKey(url.split("?")[0]);
387
418
  const cached = await isrGet(cacheKey);
388
- if (cached && !cached.isStale && cached.value.value?.kind === "PAGES" && !scriptNonce && !isDataReq) {
419
+ const isOnDemandRevalidate = isOnDemandRevalidateRequest(req.headers[PRERENDER_REVALIDATE_HEADER]);
420
+ if (!isOnDemandRevalidate && cached && !cached.isStale && cached.value.value?.kind === "PAGES" && !scriptNonce && !isDataReq) {
389
421
  const cachedHtml = cached.value.value.html;
390
422
  const transformedHtml = await server.transformIndexHtml(url, cachedHtml);
391
- const revalidateSecs = getRevalidateDuration(cacheKey) ?? 60;
423
+ const { cacheControl: hitCacheControl } = decideIsr({
424
+ cacheState: "HIT",
425
+ kind: "dev",
426
+ revalidateSeconds: getRevalidateDuration(cacheKey) ?? 60
427
+ });
392
428
  const hitHeaders = {
393
429
  "Content-Type": "text/html",
394
430
  ...buildCacheStateHeaders("HIT"),
395
- "Cache-Control": `s-maxage=${revalidateSecs}, stale-while-revalidate`
431
+ "Cache-Control": hitCacheControl
396
432
  };
397
433
  if (earlyFontLinkHeader) hitHeaders["Link"] = earlyFontLinkHeader;
398
434
  res.writeHead(200, hitHeaders);
399
435
  res.end(transformedHtml);
400
436
  return;
401
437
  }
402
- if (cached && cached.isStale && cached.value.value?.kind === "PAGES" && !scriptNonce && !isDataReq) {
438
+ if (!isOnDemandRevalidate && cached && cached.isStale && cached.value.value?.kind === "PAGES" && !scriptNonce && !isDataReq) {
403
439
  const cachedHtml = cached.value.value.html;
404
440
  const transformedHtml = await server.transformIndexHtml(url, cachedHtml);
405
441
  triggerBackgroundRegeneration(cacheKey, async () => {
@@ -420,6 +456,7 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
420
456
  pathname: patternToNextFormat(route.pattern),
421
457
  query,
422
458
  asPath: url,
459
+ navigationIsReady,
423
460
  locale: locale ?? currentDefaultLocale,
424
461
  locales: i18nConfig?.locales,
425
462
  defaultLocale: currentDefaultLocale,
@@ -450,6 +487,15 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
450
487
  const viteRoot = server.config?.root;
451
488
  const regenPageUrl = viteRoot ? "/" + path.relative(viteRoot, route.filePath) : route.filePath;
452
489
  const regenAppUrl = RegenApp ? viteRoot ? "/" + path.relative(viteRoot, path.join(pagesDir, "_app")) : path.join(pagesDir, "_app") : null;
490
+ const freshPagesNextData = {
491
+ ...pagesNextData,
492
+ __vinext: {
493
+ ...pagesNextData.__vinext,
494
+ pageModuleUrl: regenPageUrl,
495
+ appModuleUrl: regenAppUrl,
496
+ hasMiddleware
497
+ }
498
+ };
453
499
  await isrSet(cacheKey, buildPagesCacheValue(`<!DOCTYPE html><html><head></head><body><div id="__next">${freshBody}</div>${`<script>window.__NEXT_DATA__ = ${safeJsonStringify({
454
500
  props: { pageProps: freshProps },
455
501
  page: patternToNextFormat(route.pattern),
@@ -460,11 +506,7 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
460
506
  locales: i18nConfig?.locales,
461
507
  defaultLocale: currentDefaultLocale,
462
508
  domainLocales,
463
- __vinext: {
464
- pageModuleUrl: regenPageUrl,
465
- appModuleUrl: regenAppUrl,
466
- hasMiddleware
467
- }
509
+ ...freshPagesNextData
468
510
  })}${i18nConfig ? `;window.__VINEXT_LOCALE__=${safeJsonStringify(locale ?? currentDefaultLocale)};window.__VINEXT_LOCALES__=${safeJsonStringify(i18nConfig.locales)};window.__VINEXT_DEFAULT_LOCALE__=${safeJsonStringify(currentDefaultLocale)}` : ""}<\/script>`}\n ${cachedHtml.match(/<script type="module">[\s\S]*?<\/script>/)?.[0] ?? ""}</body></html>`, freshProps), revalidate);
469
511
  setRevalidateDuration(cacheKey, revalidate);
470
512
  }
@@ -475,11 +517,15 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
475
517
  routePath: route.pattern,
476
518
  routeType: "render"
477
519
  });
478
- const revalidateSecs = getRevalidateDuration(cacheKey) ?? 60;
520
+ const { cacheControl: staleCacheControl } = decideIsr({
521
+ cacheState: "STALE",
522
+ kind: "dev",
523
+ revalidateSeconds: getRevalidateDuration(cacheKey) ?? 60
524
+ });
479
525
  const staleHeaders = {
480
526
  "Content-Type": "text/html",
481
527
  ...buildCacheStateHeaders("STALE"),
482
- "Cache-Control": `s-maxage=${revalidateSecs}, stale-while-revalidate`
528
+ "Cache-Control": staleCacheControl
483
529
  };
484
530
  if (earlyFontLinkHeader) staleHeaders["Link"] = earlyFontLinkHeader;
485
531
  res.writeHead(200, staleHeaders);
@@ -491,7 +537,7 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
491
537
  locale: locale ?? currentDefaultLocale,
492
538
  locales: i18nConfig?.locales,
493
539
  defaultLocale: currentDefaultLocale,
494
- revalidateReason: "stale"
540
+ revalidateReason: isOnDemandRevalidate ? "on-demand" : "stale"
495
541
  };
496
542
  const result = await pageModule.getStaticProps(context);
497
543
  if (result && "props" in result) pageProps = result.props;
@@ -501,7 +547,10 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
501
547
  }
502
548
  if (result && "notFound" in result && result.notFound) {
503
549
  if (isDataReq) {
504
- res.writeHead(404, { "Content-Type": "application/json" });
550
+ const deploymentId = process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID;
551
+ const notFoundHeaders = { "Content-Type": "application/json" };
552
+ if (deploymentId) notFoundHeaders[NEXTJS_DEPLOYMENT_ID_HEADER] = deploymentId;
553
+ res.writeHead(404, notFoundHeaders);
505
554
  res.end("{}");
506
555
  return;
507
556
  }
@@ -531,16 +580,16 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
531
580
  if (isDataReq) {
532
581
  const dataHeaders = { "Content-Type": "application/json" };
533
582
  if (gsspExtraHeaders) for (const [k, v] of Object.entries(gsspExtraHeaders)) dataHeaders[k] = v;
583
+ const dataRoutePattern = patternToNextFormat(route.pattern);
584
+ if (dataRoutePattern !== "/_error" && dataRoutePattern !== "/500") {
585
+ const deploymentId = process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID;
586
+ if (deploymentId) dataHeaders[NEXTJS_DEPLOYMENT_ID_HEADER] = deploymentId;
587
+ }
534
588
  res.writeHead(statusCode ?? 200, dataHeaders);
535
589
  res.end(JSON.stringify({ pageProps }));
536
590
  _renderEnd = now();
537
591
  return;
538
592
  }
539
- let AppComponent = null;
540
- const appPath = path.join(pagesDir, "_app");
541
- if (findFileWithExtensions(appPath, matcher)) try {
542
- AppComponent = (await importModule(runner, appPath)).default ?? null;
543
- } catch {}
544
593
  const createElement = React.createElement;
545
594
  let element;
546
595
  const wrapWithRouterContext = routerShim.wrapWithRouterContext;
@@ -584,6 +633,15 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
584
633
  const viteRoot = server.config.root;
585
634
  const pageModuleUrl = "/" + path.relative(viteRoot, route.filePath);
586
635
  const appModuleUrl = AppComponent ? "/" + path.relative(viteRoot, path.join(pagesDir, "_app")) : null;
636
+ const serializedPagesNextData = {
637
+ ...pagesNextData,
638
+ __vinext: {
639
+ ...pagesNextData.__vinext,
640
+ pageModuleUrl,
641
+ appModuleUrl,
642
+ hasMiddleware
643
+ }
644
+ };
587
645
  const hydrationScript = `
588
646
  <script type="module"${nonceAttr}>
589
647
  import "vinext/instrumentation-client";
@@ -641,22 +699,18 @@ hydrate();
641
699
  locales: i18nConfig?.locales,
642
700
  defaultLocale: currentDefaultLocale,
643
701
  domainLocales,
644
- __vinext: {
645
- pageModuleUrl,
646
- appModuleUrl,
647
- hasMiddleware
648
- }
702
+ ...serializedPagesNextData
649
703
  })}${i18nConfig ? `;window.__VINEXT_LOCALE__=${safeJsonStringify(locale ?? currentDefaultLocale)};window.__VINEXT_LOCALES__=${safeJsonStringify(i18nConfig.locales)};window.__VINEXT_DEFAULT_LOCALE__=${safeJsonStringify(currentDefaultLocale)}` : ""}`, scriptNonce);
650
704
  const docPath = path.join(pagesDir, "_document");
651
705
  let DocumentComponent = null;
652
706
  if (findFileWithExtensions(docPath, matcher)) try {
653
707
  DocumentComponent = (await runner.import(docPath)).default ?? null;
654
708
  } catch {}
655
- const allScripts = `${nextDataScript}\n ${hydrationScript}`;
709
+ const allScripts = `${nextDataScript}\n ${createInlineScriptTag(`window.__VINEXT_PAGE_PATTERNS__=${safeJsonStringify(pagePatterns)}`, scriptNonce)}\n ${hydrationScript}`;
656
710
  const extraHeaders = { ...gsspExtraHeaders };
657
- if (isrRevalidateSeconds) if (scriptNonce) extraHeaders["Cache-Control"] = "no-store, must-revalidate";
711
+ if (isrRevalidateSeconds) if (scriptNonce) extraHeaders["Cache-Control"] = NO_STORE_CACHE_CONTROL;
658
712
  else {
659
- extraHeaders["Cache-Control"] = `s-maxage=${isrRevalidateSeconds}, stale-while-revalidate`;
713
+ extraHeaders["Cache-Control"] = buildMissIsrCacheControl(isrRevalidateSeconds);
660
714
  Object.assign(extraHeaders, buildCacheStateHeaders("MISS"));
661
715
  }
662
716
  if (allFontPreloads.length > 0) extraHeaders["Link"] = allFontPreloads.map((p) => `<${p.href}>; rel=preload; as=font; type=${p.type}; crossorigin`).join(", ");
@@ -60,6 +60,13 @@ declare const RSC_ACTION_HEADER = "x-rsc-action";
60
60
  declare const NEXT_ACTION_HEADER = "next-action";
61
61
  /** Next.js action-not-found indicator (value "1"). */
62
62
  declare const NEXTJS_ACTION_NOT_FOUND_HEADER = "x-nextjs-action-not-found";
63
+ /**
64
+ * Deployment ID header used by the Pages Router for deployment-skew
65
+ * protection. Set on every `/_next/data/` response so the client can detect
66
+ * when a new deployment has been rolled out and trigger a hard navigation.
67
+ * Mirrors `NEXT_NAV_DEPLOYMENT_ID_HEADER` from Next.js `lib/constants.ts`.
68
+ */
69
+ declare const NEXTJS_DEPLOYMENT_ID_HEADER = "x-nextjs-deployment-id";
63
70
  /** Forwarded action marker — set when a request has already been forwarded between workers. */
64
71
  declare const ACTION_FORWARDED_HEADER = "x-action-forwarded";
65
72
  /** Indicates revalidation occurred — value is JSON kind (1 = path/tag, 2 = dynamic-only). */
@@ -100,4 +107,4 @@ declare const INTERNAL_HEADERS: string[];
100
107
  /** Vinext-only internal headers stripped alongside Next.js protocol internals. */
101
108
  declare const VINEXT_INTERNAL_HEADERS: string[];
102
109
  //#endregion
103
- export { ACTION_FORWARDED_HEADER, ACTION_REDIRECT_HEADER, ACTION_REDIRECT_STATUS_HEADER, ACTION_REDIRECT_TYPE_HEADER, ACTION_REVALIDATED_HEADER, FLIGHT_HEADERS, INTERNAL_HEADERS, MIDDLEWARE_HEADER_PREFIX, MIDDLEWARE_NEXT_HEADER, MIDDLEWARE_OVERRIDE_HEADERS, MIDDLEWARE_REQUEST_HEADER_PREFIX, MIDDLEWARE_REWRITE_HEADER, MIDDLEWARE_SET_COOKIE_HEADER, NEXTJS_ACTION_NOT_FOUND_HEADER, NEXTJS_CACHE_HEADER, NEXT_ACTION_HEADER, NEXT_ROUTER_PREFETCH_HEADER, NEXT_ROUTER_SEGMENT_PREFETCH_HEADER, NEXT_ROUTER_STATE_TREE_HEADER, NEXT_URL_HEADER, RSC_ACTION_HEADER, RSC_HEADER, VINEXT_CACHE_HEADER, VINEXT_CLIENT_REUSE_MANIFEST_HEADER, VINEXT_DYNAMIC_STALE_TIME_HEADER, VINEXT_INTERCEPTION_CONTEXT_HEADER, VINEXT_INTERNAL_HEADERS, VINEXT_MOUNTED_SLOTS_HEADER, VINEXT_MW_CTX_HEADER, VINEXT_PARAMS_HEADER, VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER, VINEXT_REVALIDATE_HEADER, VINEXT_RSC_MARKER_HEADER, VINEXT_RSC_REDIRECT_HEADER, VINEXT_RSC_RENDER_MODE_HEADER, VINEXT_STATIC_FILE_HEADER, VINEXT_TIMING_HEADER };
110
+ export { ACTION_FORWARDED_HEADER, ACTION_REDIRECT_HEADER, ACTION_REDIRECT_STATUS_HEADER, ACTION_REDIRECT_TYPE_HEADER, ACTION_REVALIDATED_HEADER, FLIGHT_HEADERS, INTERNAL_HEADERS, MIDDLEWARE_HEADER_PREFIX, MIDDLEWARE_NEXT_HEADER, MIDDLEWARE_OVERRIDE_HEADERS, MIDDLEWARE_REQUEST_HEADER_PREFIX, MIDDLEWARE_REWRITE_HEADER, MIDDLEWARE_SET_COOKIE_HEADER, NEXTJS_ACTION_NOT_FOUND_HEADER, NEXTJS_CACHE_HEADER, NEXTJS_DEPLOYMENT_ID_HEADER, NEXT_ACTION_HEADER, NEXT_ROUTER_PREFETCH_HEADER, NEXT_ROUTER_SEGMENT_PREFETCH_HEADER, NEXT_ROUTER_STATE_TREE_HEADER, NEXT_URL_HEADER, RSC_ACTION_HEADER, RSC_HEADER, VINEXT_CACHE_HEADER, VINEXT_CLIENT_REUSE_MANIFEST_HEADER, VINEXT_DYNAMIC_STALE_TIME_HEADER, VINEXT_INTERCEPTION_CONTEXT_HEADER, VINEXT_INTERNAL_HEADERS, VINEXT_MOUNTED_SLOTS_HEADER, VINEXT_MW_CTX_HEADER, VINEXT_PARAMS_HEADER, VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER, VINEXT_REVALIDATE_HEADER, VINEXT_RSC_MARKER_HEADER, VINEXT_RSC_REDIRECT_HEADER, VINEXT_RSC_RENDER_MODE_HEADER, VINEXT_STATIC_FILE_HEADER, VINEXT_TIMING_HEADER };
@@ -60,6 +60,13 @@ const RSC_ACTION_HEADER = "x-rsc-action";
60
60
  const NEXT_ACTION_HEADER = "next-action";
61
61
  /** Next.js action-not-found indicator (value "1"). */
62
62
  const NEXTJS_ACTION_NOT_FOUND_HEADER = "x-nextjs-action-not-found";
63
+ /**
64
+ * Deployment ID header used by the Pages Router for deployment-skew
65
+ * protection. Set on every `/_next/data/` response so the client can detect
66
+ * when a new deployment has been rolled out and trigger a hard navigation.
67
+ * Mirrors `NEXT_NAV_DEPLOYMENT_ID_HEADER` from Next.js `lib/constants.ts`.
68
+ */
69
+ const NEXTJS_DEPLOYMENT_ID_HEADER = "x-nextjs-deployment-id";
63
70
  /** Forwarded action marker — set when a request has already been forwarded between workers. */
64
71
  const ACTION_FORWARDED_HEADER = "x-action-forwarded";
65
72
  /** Indicates revalidation occurred — value is JSON kind (1 = path/tag, 2 = dynamic-only). */
@@ -122,4 +129,4 @@ const INTERNAL_HEADERS = [
122
129
  /** Vinext-only internal headers stripped alongside Next.js protocol internals. */
123
130
  const VINEXT_INTERNAL_HEADERS = [VINEXT_PRERENDER_ROUTE_PARAMS_HEADER];
124
131
  //#endregion
125
- export { ACTION_FORWARDED_HEADER, ACTION_REDIRECT_HEADER, ACTION_REDIRECT_STATUS_HEADER, ACTION_REDIRECT_TYPE_HEADER, ACTION_REVALIDATED_HEADER, FLIGHT_HEADERS, INTERNAL_HEADERS, MIDDLEWARE_HEADER_PREFIX, MIDDLEWARE_NEXT_HEADER, MIDDLEWARE_OVERRIDE_HEADERS, MIDDLEWARE_REQUEST_HEADER_PREFIX, MIDDLEWARE_REWRITE_HEADER, MIDDLEWARE_SET_COOKIE_HEADER, NEXTJS_ACTION_NOT_FOUND_HEADER, NEXTJS_CACHE_HEADER, NEXT_ACTION_HEADER, NEXT_ROUTER_PREFETCH_HEADER, NEXT_ROUTER_SEGMENT_PREFETCH_HEADER, NEXT_ROUTER_STATE_TREE_HEADER, NEXT_URL_HEADER, RSC_ACTION_HEADER, RSC_HEADER, VINEXT_CACHE_HEADER, VINEXT_CLIENT_REUSE_MANIFEST_HEADER, VINEXT_DYNAMIC_STALE_TIME_HEADER, VINEXT_INTERCEPTION_CONTEXT_HEADER, VINEXT_INTERNAL_HEADERS, VINEXT_MOUNTED_SLOTS_HEADER, VINEXT_MW_CTX_HEADER, VINEXT_PARAMS_HEADER, VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER, VINEXT_REVALIDATE_HEADER, VINEXT_RSC_MARKER_HEADER, VINEXT_RSC_REDIRECT_HEADER, VINEXT_RSC_RENDER_MODE_HEADER, VINEXT_STATIC_FILE_HEADER, VINEXT_TIMING_HEADER };
132
+ export { ACTION_FORWARDED_HEADER, ACTION_REDIRECT_HEADER, ACTION_REDIRECT_STATUS_HEADER, ACTION_REDIRECT_TYPE_HEADER, ACTION_REVALIDATED_HEADER, FLIGHT_HEADERS, INTERNAL_HEADERS, MIDDLEWARE_HEADER_PREFIX, MIDDLEWARE_NEXT_HEADER, MIDDLEWARE_OVERRIDE_HEADERS, MIDDLEWARE_REQUEST_HEADER_PREFIX, MIDDLEWARE_REWRITE_HEADER, MIDDLEWARE_SET_COOKIE_HEADER, NEXTJS_ACTION_NOT_FOUND_HEADER, NEXTJS_CACHE_HEADER, NEXTJS_DEPLOYMENT_ID_HEADER, NEXT_ACTION_HEADER, NEXT_ROUTER_PREFETCH_HEADER, NEXT_ROUTER_SEGMENT_PREFETCH_HEADER, NEXT_ROUTER_STATE_TREE_HEADER, NEXT_URL_HEADER, RSC_ACTION_HEADER, RSC_HEADER, VINEXT_CACHE_HEADER, VINEXT_CLIENT_REUSE_MANIFEST_HEADER, VINEXT_DYNAMIC_STALE_TIME_HEADER, VINEXT_INTERCEPTION_CONTEXT_HEADER, VINEXT_INTERNAL_HEADERS, VINEXT_MOUNTED_SLOTS_HEADER, VINEXT_MW_CTX_HEADER, VINEXT_PARAMS_HEADER, VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER, VINEXT_REVALIDATE_HEADER, VINEXT_RSC_MARKER_HEADER, VINEXT_RSC_REDIRECT_HEADER, VINEXT_RSC_RENDER_MODE_HEADER, VINEXT_STATIC_FILE_HEADER, VINEXT_TIMING_HEADER };
@@ -34,6 +34,12 @@
34
34
  * Ensure the instrumentation module's `register()` and `onRequestError`
35
35
  * hooks have been applied exactly once.
36
36
  *
37
+ * After `register()` runs, we extend the OTel tracer provider so that spans
38
+ * created inside Cache Component renders (warmup / fallback-resume phases) use
39
+ * a fresh OTel context rather than inheriting the prerender work unit store.
40
+ * This mirrors Next.js's `afterRegistration()` call in
41
+ * `instrumentation-node-extensions.ts`.
42
+ *
37
43
  * @param instrumentationModule - The imported `instrumentation.ts` module.
38
44
  * Passed as an argument so the generated entry can import it normally
39
45
  * without this helper needing to know the module path.
@@ -1,3 +1,4 @@
1
+ import { extendTracerProviderForCacheComponents } from "./otel-tracer-extension.js";
1
2
  //#region src/server/instrumentation-runtime.ts
2
3
  let initialized = false;
3
4
  let initPromise = null;
@@ -8,6 +9,12 @@ function isOnRequestErrorHandler(value) {
8
9
  * Ensure the instrumentation module's `register()` and `onRequestError`
9
10
  * hooks have been applied exactly once.
10
11
  *
12
+ * After `register()` runs, we extend the OTel tracer provider so that spans
13
+ * created inside Cache Component renders (warmup / fallback-resume phases) use
14
+ * a fresh OTel context rather than inheriting the prerender work unit store.
15
+ * This mirrors Next.js's `afterRegistration()` call in
16
+ * `instrumentation-node-extensions.ts`.
17
+ *
11
18
  * @param instrumentationModule - The imported `instrumentation.ts` module.
12
19
  * Passed as an argument so the generated entry can import it normally
13
20
  * without this helper needing to know the module path.
@@ -18,6 +25,7 @@ async function ensureInstrumentationRegistered(instrumentationModule) {
18
25
  if (initPromise) return initPromise;
19
26
  initPromise = (async () => {
20
27
  if (typeof instrumentationModule.register === "function") await instrumentationModule.register();
28
+ extendTracerProviderForCacheComponents();
21
29
  if (isOnRequestErrorHandler(instrumentationModule.onRequestError)) globalThis.__VINEXT_onRequestErrorHandler__ = instrumentationModule.onRequestError;
22
30
  initialized = true;
23
31
  })();
@@ -5,6 +5,42 @@ import { normalizeMountedSlotsHeader } from "./app-mounted-slots-header.js";
5
5
  import { AppRscRenderMode } from "./app-rsc-render-mode.js";
6
6
 
7
7
  //#region src/server/isr-cache.d.ts
8
+ /**
9
+ * Header set on the internal request that `res.revalidate()` issues to
10
+ * trigger on-demand ISR regeneration of a Pages Router route. Mirrors Next.js's
11
+ * `PRERENDER_REVALIDATE_HEADER` (`x-prerender-revalidate`) — see
12
+ * `.nextjs-ref/packages/next/src/lib/constants.ts`.
13
+ *
14
+ * SECURITY: in Next.js this header is NOT a presence flag — it carries the
15
+ * secret `previewModeId`, and `checkIsOnDemandRevalidate`
16
+ * (`.nextjs-ref/packages/next/src/server/api-utils/index.ts`) only treats a
17
+ * request as on-demand revalidation when the value *equals* that secret. If we
18
+ * gated on presence alone, any external client could send
19
+ * `x-prerender-revalidate: <anything>` to force synchronous regeneration of any
20
+ * ISR page, bypassing the fresh/stale cache short-circuits — a
21
+ * cache-stampede/DoS vector. We therefore validate the value against
22
+ * {@link getRevalidateSecret} (a build-time secret shared across all Workers
23
+ * isolates) with a constant-time comparison, and only the matching value (sent
24
+ * by our own `res.revalidate()`) is honored.
25
+ */
26
+ declare const PRERENDER_REVALIDATE_HEADER = "x-prerender-revalidate";
27
+ /**
28
+ * Companion header to {@link PRERENDER_REVALIDATE_HEADER}. When set,
29
+ * `res.revalidate(path, { unstable_onlyGenerated: true })` only revalidates the
30
+ * path if it was already generated, and a 404 response counts as a successful
31
+ * no-op. Mirrors Next.js's `PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER`
32
+ * (`x-prerender-revalidate-if-generated`) — see
33
+ * `.nextjs-ref/packages/next/src/lib/constants.ts`.
34
+ */
35
+ declare const PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER = "x-prerender-revalidate-if-generated";
36
+ declare function getRevalidateSecret(): string;
37
+ /**
38
+ * Authorize an incoming request as an on-demand revalidation trigger. Mirrors
39
+ * Next.js's `checkIsOnDemandRevalidate`: the {@link PRERENDER_REVALIDATE_HEADER}
40
+ * value must *equal* the process revalidate secret. Header presence alone is
41
+ * NOT sufficient — see the security note on {@link PRERENDER_REVALIDATE_HEADER}.
42
+ */
43
+ declare function isOnDemandRevalidateRequest(headerValue: string | string[] | null | undefined): boolean;
8
44
  type ISRCacheEntry = {
9
45
  value: CacheHandlerValue;
10
46
  isStale: boolean;
@@ -88,4 +124,4 @@ declare function setRevalidateDuration(key: string, seconds: number): void;
88
124
  */
89
125
  declare function getRevalidateDuration(key: string): number | undefined;
90
126
  //#endregion
91
- export { ISRCacheEntry, appIsrHtmlKey, appIsrRouteKey, appIsrRscKey, buildAppPageCacheValue, buildPagesCacheValue, getRevalidateDuration, isrCacheKey, isrGet, isrSet, isrSetPrerenderedAppPage, normalizeMountedSlotsHeader, setRevalidateDuration, triggerBackgroundRegeneration };
127
+ export { ISRCacheEntry, PRERENDER_REVALIDATE_HEADER, PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER, appIsrHtmlKey, appIsrRouteKey, appIsrRscKey, buildAppPageCacheValue, buildPagesCacheValue, getRevalidateDuration, getRevalidateSecret, isOnDemandRevalidateRequest, isrCacheKey, isrGet, isrSet, isrSetPrerenderedAppPage, normalizeMountedSlotsHeader, setRevalidateDuration, triggerBackgroundRegeneration };
@@ -7,6 +7,90 @@ import { APP_RSC_RENDER_MODE_NAVIGATION, getRscRenderModeCacheVariant } from "./
7
7
  import { normalizeAppPageInterceptionProofPathname } from "./app-page-render-identity.js";
8
8
  //#region src/server/isr-cache.ts
9
9
  /**
10
+ * Header set on the internal request that `res.revalidate()` issues to
11
+ * trigger on-demand ISR regeneration of a Pages Router route. Mirrors Next.js's
12
+ * `PRERENDER_REVALIDATE_HEADER` (`x-prerender-revalidate`) — see
13
+ * `.nextjs-ref/packages/next/src/lib/constants.ts`.
14
+ *
15
+ * SECURITY: in Next.js this header is NOT a presence flag — it carries the
16
+ * secret `previewModeId`, and `checkIsOnDemandRevalidate`
17
+ * (`.nextjs-ref/packages/next/src/server/api-utils/index.ts`) only treats a
18
+ * request as on-demand revalidation when the value *equals* that secret. If we
19
+ * gated on presence alone, any external client could send
20
+ * `x-prerender-revalidate: <anything>` to force synchronous regeneration of any
21
+ * ISR page, bypassing the fresh/stale cache short-circuits — a
22
+ * cache-stampede/DoS vector. We therefore validate the value against
23
+ * {@link getRevalidateSecret} (a build-time secret shared across all Workers
24
+ * isolates) with a constant-time comparison, and only the matching value (sent
25
+ * by our own `res.revalidate()`) is honored.
26
+ */
27
+ const PRERENDER_REVALIDATE_HEADER = "x-prerender-revalidate";
28
+ /**
29
+ * Companion header to {@link PRERENDER_REVALIDATE_HEADER}. When set,
30
+ * `res.revalidate(path, { unstable_onlyGenerated: true })` only revalidates the
31
+ * path if it was already generated, and a 404 response counts as a successful
32
+ * no-op. Mirrors Next.js's `PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER`
33
+ * (`x-prerender-revalidate-if-generated`) — see
34
+ * `.nextjs-ref/packages/next/src/lib/constants.ts`.
35
+ */
36
+ const PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER = "x-prerender-revalidate-if-generated";
37
+ /**
38
+ * Build-time secret that authenticates on-demand revalidation requests, the
39
+ * vinext analog of Next.js's prerender-manifest `previewModeId`.
40
+ *
41
+ * `res.revalidate()` loops back into the server via an internal `fetch()`. On
42
+ * Cloudflare Workers that loopback can land on a *different* isolate than the
43
+ * sender, so a per-process random secret would mismatch across isolates and
44
+ * false-reject legitimate revalidations (and, symmetrically, two isolates with
45
+ * independently-rolled secrets could never agree). The fix mirrors Next.js's
46
+ * `previewModeId`: the secret is generated once at BUILD time and baked
47
+ * (server-only — never into the client bundle) into every server bundle via the
48
+ * `__VINEXT_REVALIDATE_SECRET` Vite `define`, so it is byte-for-byte identical in
49
+ * every isolate. See `vinext build` CLI (`__VINEXT_SHARED_REVALIDATE_SECRET`) and
50
+ * the `vinext:compiler-define-server` plugin. The sender attaches it as the
51
+ * {@link PRERENDER_REVALIDATE_HEADER} value; the receiver authorizes a request
52
+ * only when the incoming value equals this secret (see
53
+ * {@link isOnDemandRevalidateRequest}).
54
+ *
55
+ * When the build-time define is absent — dev mode, and any path that doesn't
56
+ * run through `vinext build` — we fall back to a lazily-generated random secret.
57
+ * Those paths are single-process, so a module-scoped value is shared by sender
58
+ * and receiver there — no regression.
59
+ */
60
+ let devRevalidateSecret;
61
+ function getRevalidateSecret() {
62
+ const baked = process.env.__VINEXT_REVALIDATE_SECRET;
63
+ if (baked) return baked;
64
+ if (devRevalidateSecret === void 0) {
65
+ const bytes = new Uint8Array(32);
66
+ crypto.getRandomValues(bytes);
67
+ devRevalidateSecret = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
68
+ }
69
+ return devRevalidateSecret;
70
+ }
71
+ /**
72
+ * Constant-time string equality. Avoids leaking secret length / prefix via
73
+ * early-exit timing on the on-demand revalidation auth check. Returns false
74
+ * for length mismatch (the only safe option without revealing the secret
75
+ * length, and equality is impossible anyway).
76
+ */
77
+ function safeEqual(a, b) {
78
+ if (a.length !== b.length) return false;
79
+ let mismatch = 0;
80
+ for (let i = 0; i < a.length; i++) mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
81
+ return mismatch === 0;
82
+ }
83
+ /**
84
+ * Authorize an incoming request as an on-demand revalidation trigger. Mirrors
85
+ * Next.js's `checkIsOnDemandRevalidate`: the {@link PRERENDER_REVALIDATE_HEADER}
86
+ * value must *equal* the process revalidate secret. Header presence alone is
87
+ * NOT sufficient — see the security note on {@link PRERENDER_REVALIDATE_HEADER}.
88
+ */
89
+ function isOnDemandRevalidateRequest(headerValue) {
90
+ if (typeof headerValue !== "string" || headerValue.length === 0) return false;
91
+ return safeEqual(headerValue, getRevalidateSecret());
92
+ }
93
+ /**
10
94
  * Get a cache entry with staleness information.
11
95
  *
12
96
  * Returns { value, isStale: false } for fresh entries,
@@ -195,4 +279,4 @@ function getRevalidateDuration(key) {
195
279
  return revalidateDurations.get(key);
196
280
  }
197
281
  //#endregion
198
- export { appIsrHtmlKey, appIsrRouteKey, appIsrRscKey, buildAppPageCacheValue, buildPagesCacheValue, getRevalidateDuration, isrCacheKey, isrGet, isrSet, isrSetPrerenderedAppPage, normalizeMountedSlotsHeader, setRevalidateDuration, triggerBackgroundRegeneration };
282
+ export { PRERENDER_REVALIDATE_HEADER, PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER, appIsrHtmlKey, appIsrRouteKey, appIsrRscKey, buildAppPageCacheValue, buildPagesCacheValue, getRevalidateDuration, getRevalidateSecret, isOnDemandRevalidateRequest, isrCacheKey, isrGet, isrSet, isrSetPrerenderedAppPage, normalizeMountedSlotsHeader, setRevalidateDuration, triggerBackgroundRegeneration };
@@ -0,0 +1,79 @@
1
+ import { CacheControlMetadata } from "../shims/cache.js";
2
+ import { NEVER_CACHE_CONTROL, NO_STORE_CACHE_CONTROL } from "./cache-control.js";
3
+
4
+ //#region src/server/isr-decision.d.ts
5
+ type IsrDisposition = "HIT" | "STALE" | "MISS";
6
+ type IsrDecision = {
7
+ disposition: IsrDisposition; /** True when the caller must schedule a background regeneration. */
8
+ scheduleRegeneration: boolean; /** The `Cache-Control` string to stamp on the response. */
9
+ cacheControl: string;
10
+ };
11
+ /**
12
+ * Per-router special-case policies for `Cache-Control`.
13
+ *
14
+ * - `"app-page"` / `"pages"`: `buildCachedRevalidateCacheControl` for HIT/STALE.
15
+ * - `"app-route"`: same, but `revalidateSeconds=0` forces `NEVER_CACHE_CONTROL`
16
+ * and `revalidateSeconds=Infinity` forces `STATIC_CACHE_CONTROL`.
17
+ * - `"dev"`: like `"pages"`, but `revalidate=0`/`Infinity` guards are absent
18
+ * (dev never caches when revalidate=0 and never has Infinity entries in practice).
19
+ */
20
+ type IsrPolicyKind = "app-page" | "app-route" | "pages" | "dev";
21
+ type DecideIsrOptions = {
22
+ /**
23
+ * The cache state. Content guards (kind-mismatch, empty body,
24
+ * query-variant-unproven) must have already passed before passing
25
+ * `"HIT"` or `"STALE"` here.
26
+ */
27
+ cacheState: "HIT" | "STALE" | "MISS"; /** Which router is making the decision. */
28
+ kind: IsrPolicyKind;
29
+ /**
30
+ * The route's configured revalidate window in seconds. Used as the fallback
31
+ * when `cacheControlMeta` is absent.
32
+ *
33
+ * For `"dev"` call sites this is the only source of the revalidate value —
34
+ * dev never has metadata attached to a cache entry.
35
+ */
36
+ revalidateSeconds: number;
37
+ /**
38
+ * The expire ceiling (seconds from epoch) read from the route config.
39
+ * Absent when the route pre-dates expire metadata support.
40
+ */
41
+ expireSeconds?: number;
42
+ /**
43
+ * Optional per-entry metadata written alongside the cache value.
44
+ * When present its `revalidate`/`expire` fields override the route defaults,
45
+ * exactly as the call sites do today with `cacheControl?.revalidate ?? revalidateSeconds`.
46
+ */
47
+ cacheControlMeta?: CacheControlMetadata;
48
+ };
49
+ /**
50
+ * Derive the `Cache-Control` string for an ISR response.
51
+ *
52
+ * Content guards (kind mismatch, query-variant-unproven, empty body) are the
53
+ * caller's responsibility and must happen *before* this call. `cacheState`
54
+ * must only be `"HIT"` or `"STALE"` when those guards have already passed.
55
+ */
56
+ declare function decideIsr(options: DecideIsrOptions): IsrDecision;
57
+ /**
58
+ * Build the `Cache-Control` string for a fresh MISS response whose ISR policy
59
+ * is known (i.e. revalidate is set and > 0). Uses the unbounded SWR form when
60
+ * no expire ceiling is available, exactly as `buildRevalidateCacheControl` does.
61
+ *
62
+ * Separate from `decideIsr` because a MISS doesn't read a cache entry and
63
+ * therefore never has `cacheControlMeta`. `expireSeconds` here is the route
64
+ * config ceiling passed directly from the caller (not a per-entry fallback).
65
+ */
66
+ declare function buildMissIsrCacheControl(revalidateSeconds: number, expireSeconds?: number): string;
67
+ /**
68
+ * Build the `Cache-Control` string for a fresh (MISS) app-route response.
69
+ *
70
+ * Applies the same `revalidateSeconds=0`→NEVER and `Infinity`→STATIC gates
71
+ * that `decideIsr` uses for app-route cached responses. `expireSeconds` is
72
+ * the route config ceiling passed directly (not per-entry metadata fallback).
73
+ *
74
+ * Used by `applyRouteHandlerRevalidateHeader` which operates on a fresh
75
+ * response that has no per-entry cache metadata.
76
+ */
77
+ declare function buildAppRouteMissIsrCacheControl(revalidateSeconds: number, expireSeconds?: number): string;
78
+ //#endregion
79
+ export { NEVER_CACHE_CONTROL as ISR_NEVER_CACHE_CONTROL, NO_STORE_CACHE_CONTROL as ISR_NO_STORE_CACHE_CONTROL, buildAppRouteMissIsrCacheControl, buildMissIsrCacheControl, decideIsr };