vinext 0.0.27 → 0.0.28

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 (114) hide show
  1. package/dist/build/static-export.d.ts +1 -1
  2. package/dist/build/static-export.d.ts.map +1 -1
  3. package/dist/build/static-export.js +2 -1
  4. package/dist/build/static-export.js.map +1 -1
  5. package/dist/cloudflare/kv-cache-handler.d.ts +28 -17
  6. package/dist/cloudflare/kv-cache-handler.d.ts.map +1 -1
  7. package/dist/cloudflare/kv-cache-handler.js +95 -30
  8. package/dist/cloudflare/kv-cache-handler.js.map +1 -1
  9. package/dist/config/config-matchers.d.ts +1 -0
  10. package/dist/config/config-matchers.d.ts.map +1 -1
  11. package/dist/config/config-matchers.js +51 -23
  12. package/dist/config/config-matchers.js.map +1 -1
  13. package/dist/deploy.d.ts +1 -1
  14. package/dist/deploy.d.ts.map +1 -1
  15. package/dist/deploy.js +48 -32
  16. package/dist/deploy.js.map +1 -1
  17. package/dist/entries/app-rsc-entry.d.ts +3 -1
  18. package/dist/entries/app-rsc-entry.d.ts.map +1 -1
  19. package/dist/entries/app-rsc-entry.js +495 -75
  20. package/dist/entries/app-rsc-entry.js.map +1 -1
  21. package/dist/entries/pages-server-entry.d.ts.map +1 -1
  22. package/dist/entries/pages-server-entry.js +68 -22
  23. package/dist/entries/pages-server-entry.js.map +1 -1
  24. package/dist/index.d.ts +23 -7
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +128 -41
  27. package/dist/index.js.map +1 -1
  28. package/dist/plugins/client-reference-dedup.d.ts +19 -0
  29. package/dist/plugins/client-reference-dedup.d.ts.map +1 -0
  30. package/dist/plugins/client-reference-dedup.js +96 -0
  31. package/dist/plugins/client-reference-dedup.js.map +1 -0
  32. package/dist/routing/app-router.d.ts.map +1 -1
  33. package/dist/routing/app-router.js +60 -89
  34. package/dist/routing/app-router.js.map +1 -1
  35. package/dist/routing/pages-router.d.ts +1 -1
  36. package/dist/routing/pages-router.d.ts.map +1 -1
  37. package/dist/routing/pages-router.js +25 -13
  38. package/dist/routing/pages-router.js.map +1 -1
  39. package/dist/routing/route-validation.d.ts +8 -0
  40. package/dist/routing/route-validation.d.ts.map +1 -0
  41. package/dist/routing/route-validation.js +124 -0
  42. package/dist/routing/route-validation.js.map +1 -0
  43. package/dist/server/api-handler.d.ts.map +1 -1
  44. package/dist/server/api-handler.js +24 -7
  45. package/dist/server/api-handler.js.map +1 -1
  46. package/dist/server/dev-server.d.ts.map +1 -1
  47. package/dist/server/dev-server.js +9 -3
  48. package/dist/server/dev-server.js.map +1 -1
  49. package/dist/server/isr-cache.d.ts +5 -13
  50. package/dist/server/isr-cache.d.ts.map +1 -1
  51. package/dist/server/isr-cache.js +13 -12
  52. package/dist/server/isr-cache.js.map +1 -1
  53. package/dist/server/metadata-routes.d.ts +8 -2
  54. package/dist/server/metadata-routes.d.ts.map +1 -1
  55. package/dist/server/metadata-routes.js +73 -28
  56. package/dist/server/metadata-routes.js.map +1 -1
  57. package/dist/server/middleware-codegen.d.ts +1 -1
  58. package/dist/server/middleware-codegen.d.ts.map +1 -1
  59. package/dist/server/middleware-codegen.js +165 -12
  60. package/dist/server/middleware-codegen.js.map +1 -1
  61. package/dist/server/middleware.d.ts +9 -8
  62. package/dist/server/middleware.d.ts.map +1 -1
  63. package/dist/server/middleware.js +74 -13
  64. package/dist/server/middleware.js.map +1 -1
  65. package/dist/server/prod-server.d.ts.map +1 -1
  66. package/dist/server/prod-server.js +84 -54
  67. package/dist/server/prod-server.js.map +1 -1
  68. package/dist/shims/cache.d.ts +2 -0
  69. package/dist/shims/cache.d.ts.map +1 -1
  70. package/dist/shims/cache.js +20 -8
  71. package/dist/shims/cache.js.map +1 -1
  72. package/dist/shims/fetch-cache.d.ts.map +1 -1
  73. package/dist/shims/fetch-cache.js +5 -2
  74. package/dist/shims/fetch-cache.js.map +1 -1
  75. package/dist/shims/form.d.ts.map +1 -1
  76. package/dist/shims/form.js +103 -8
  77. package/dist/shims/form.js.map +1 -1
  78. package/dist/shims/headers.d.ts +11 -3
  79. package/dist/shims/headers.d.ts.map +1 -1
  80. package/dist/shims/headers.js +180 -25
  81. package/dist/shims/headers.js.map +1 -1
  82. package/dist/shims/internal/parse-cookie-header.d.ts +12 -0
  83. package/dist/shims/internal/parse-cookie-header.d.ts.map +1 -0
  84. package/dist/shims/internal/parse-cookie-header.js +32 -0
  85. package/dist/shims/internal/parse-cookie-header.js.map +1 -0
  86. package/dist/shims/link.d.ts +2 -1
  87. package/dist/shims/link.d.ts.map +1 -1
  88. package/dist/shims/link.js +8 -2
  89. package/dist/shims/link.js.map +1 -1
  90. package/dist/shims/navigation.d.ts +3 -7
  91. package/dist/shims/navigation.d.ts.map +1 -1
  92. package/dist/shims/navigation.js +20 -10
  93. package/dist/shims/navigation.js.map +1 -1
  94. package/dist/shims/readonly-url-search-params.d.ts +11 -0
  95. package/dist/shims/readonly-url-search-params.d.ts.map +1 -0
  96. package/dist/shims/readonly-url-search-params.js +24 -0
  97. package/dist/shims/readonly-url-search-params.js.map +1 -0
  98. package/dist/shims/router.d.ts +4 -3
  99. package/dist/shims/router.d.ts.map +1 -1
  100. package/dist/shims/router.js +42 -29
  101. package/dist/shims/router.js.map +1 -1
  102. package/dist/shims/server.d.ts +1 -1
  103. package/dist/shims/server.d.ts.map +1 -1
  104. package/dist/shims/server.js +7 -13
  105. package/dist/shims/server.js.map +1 -1
  106. package/dist/utils/manifest-paths.d.ts +4 -0
  107. package/dist/utils/manifest-paths.d.ts.map +1 -0
  108. package/dist/utils/manifest-paths.js +20 -0
  109. package/dist/utils/manifest-paths.js.map +1 -0
  110. package/dist/utils/query.d.ts +9 -0
  111. package/dist/utils/query.d.ts.map +1 -1
  112. package/dist/utils/query.js +59 -9
  113. package/dist/utils/query.js.map +1 -1
  114. package/package.json +1 -1
@@ -17,6 +17,7 @@ import { isProxyFile } from "../server/middleware.js";
17
17
  // resolve these at code-generation time and embed them as absolute paths.
18
18
  const configMatchersPath = fileURLToPath(new URL("../config/config-matchers.js", import.meta.url)).replace(/\\/g, "/");
19
19
  const requestPipelinePath = fileURLToPath(new URL("../server/request-pipeline.js", import.meta.url)).replace(/\\/g, "/");
20
+ const requestContextShimPath = fileURLToPath(new URL("../shims/request-context.js", import.meta.url)).replace(/\\/g, "/");
20
21
  /**
21
22
  * Generate the virtual RSC entry module.
22
23
  *
@@ -32,6 +33,7 @@ export function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes,
32
33
  const headers = config?.headers ?? [];
33
34
  const allowedOrigins = config?.allowedOrigins ?? [];
34
35
  const bodySizeLimit = config?.bodySizeLimit ?? 1 * 1024 * 1024;
36
+ const i18nConfig = config?.i18n ?? null;
35
37
  // Build import map for all page and layout files
36
38
  const imports = [];
37
39
  const importMap = new Map();
@@ -200,7 +202,7 @@ import {
200
202
  import { AsyncLocalStorage } from "node:async_hooks";
201
203
  import { createElement, Suspense, Fragment } from "react";
202
204
  import { setNavigationContext as _setNavigationContextOrig, getNavigationContext as _getNavigationContext } from "next/navigation";
203
- import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader, getAndClearPendingCookies, consumeDynamicUsage, markDynamicUsage, runWithHeadersContext, applyMiddlewareRequestHeaders, getHeadersContext } from "next/headers";
205
+ import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader, getAndClearPendingCookies, consumeDynamicUsage, markDynamicUsage, runWithHeadersContext, applyMiddlewareRequestHeaders, getHeadersContext, setHeadersAccessPhase } from "next/headers";
204
206
  import { NextRequest, NextFetchEvent } from "next/server";
205
207
  import { ErrorBoundary, NotFoundBoundary } from "vinext/error-boundary";
206
208
  import { LayoutSegmentProvider } from "vinext/layout-segment-context";
@@ -208,10 +210,11 @@ import { MetadataHead, mergeMetadata, resolveModuleMetadata, ViewportHead, merge
208
210
  ${middlewarePath ? `import * as middlewareModule from ${JSON.stringify(middlewarePath.replace(/\\/g, "/"))};` : ""}
209
211
  ${instrumentationPath ? `import * as _instrumentation from ${JSON.stringify(instrumentationPath.replace(/\\/g, "/"))};` : ""}
210
212
  ${effectiveMetaRoutes.length > 0 ? `import { sitemapToXml, robotsToText, manifestToJson } from ${JSON.stringify(fileURLToPath(new URL("../server/metadata-routes.js", import.meta.url)).replace(/\\/g, "/"))};` : ""}
211
- import { requestContextFromRequest, matchRedirect, matchRewrite, matchHeaders, isExternalUrl, proxyExternalRequest, sanitizeDestination } from ${JSON.stringify(configMatchersPath)};
213
+ import { requestContextFromRequest, normalizeHost, matchRedirect, matchRewrite, matchHeaders, isExternalUrl, proxyExternalRequest, sanitizeDestination } from ${JSON.stringify(configMatchersPath)};
212
214
  import { validateCsrfOrigin, validateImageUrl, guardProtocolRelativeUrl, hasBasePath, stripBasePath, normalizeTrailingSlash, processMiddlewareHeaders } from ${JSON.stringify(requestPipelinePath)};
213
- import { _consumeRequestScopedCacheLife, _runWithCacheState } from "next/cache";
214
- import { runWithFetchCache } from "vinext/fetch-cache";
215
+ import { _consumeRequestScopedCacheLife, _runWithCacheState, getCacheHandler } from "next/cache";
216
+ import { runWithExecutionContext as _runWithExecutionContext, getRequestExecutionContext as _getRequestExecutionContext } from ${JSON.stringify(requestContextShimPath)};
217
+ import { getCollectedFetchTags, runWithFetchCache } from "vinext/fetch-cache";
215
218
  import { runWithPrivateCache as _runWithPrivateCache } from "vinext/cache-runtime";
216
219
  // Import server-only state module to register ALS-backed accessors.
217
220
  import { runWithNavigationContext as _runWithNavigationContext } from "vinext/navigation-state";
@@ -247,7 +250,94 @@ function setNavigationContext(ctx) {
247
250
  // ISR cache is disabled in dev mode — every request re-renders fresh,
248
251
  // matching Next.js dev behavior. Cache-Control headers are still emitted
249
252
  // based on export const revalidate for testing purposes.
250
- // Production ISR is handled by prod-server.ts and the Cloudflare worker entry.
253
+ // Production ISR uses the MemoryCacheHandler (or configured KV handler).
254
+ //
255
+ // These helpers are inlined instead of imported from isr-cache.js because
256
+ // the virtual RSC entry module runs in the RSC Vite environment which
257
+ // cannot use dynamic imports at the module-evaluation level for server-only
258
+ // modules, and direct imports must use the pre-computed absolute paths.
259
+ async function __isrGet(key) {
260
+ const handler = getCacheHandler();
261
+ const result = await handler.get(key);
262
+ if (!result || !result.value) return null;
263
+ return { value: result, isStale: result.cacheState === "stale" };
264
+ }
265
+ async function __isrSet(key, data, revalidateSeconds, tags) {
266
+ const handler = getCacheHandler();
267
+ await handler.set(key, data, { revalidate: revalidateSeconds, tags: Array.isArray(tags) ? tags : [] });
268
+ }
269
+ function __pageCacheTags(pathname, extraTags) {
270
+ const tags = [pathname, "_N_T_" + pathname];
271
+ if (Array.isArray(extraTags)) {
272
+ for (const tag of extraTags) {
273
+ if (!tags.includes(tag)) tags.push(tag);
274
+ }
275
+ }
276
+ return tags;
277
+ }
278
+ // Note: cache entries are written with \`headers: undefined\`. Next.js stores
279
+ // response headers (e.g. set-cookie from cookies().set() during render) in the
280
+ // cache entry so they can be replayed on HIT. We don't do this because:
281
+ // 1. Pages that call cookies().set() during render trigger dynamicUsedDuringRender,
282
+ // which opts them out of ISR caching before we reach the write path.
283
+ // 2. Custom response headers set via next/headers are not yet captured separately
284
+ // from the live Response object in vinext's server pipeline.
285
+ // In practice this means ISR-cached responses won't replay render-time set-cookie
286
+ // headers — but that case is already prevented by the dynamic-usage opt-out.
287
+ // TODO: capture render-time response headers for full Next.js parity.
288
+ const __pendingRegenerations = new Map();
289
+ function __triggerBackgroundRegeneration(key, renderFn) {
290
+ if (__pendingRegenerations.has(key)) return;
291
+ const promise = renderFn()
292
+ .catch((err) => console.error("[vinext] ISR regen failed for " + key + ":", err))
293
+ .finally(() => __pendingRegenerations.delete(key));
294
+ __pendingRegenerations.set(key, promise);
295
+ const ctx = _getRequestExecutionContext();
296
+ if (ctx && typeof ctx.waitUntil === "function") ctx.waitUntil(promise);
297
+ }
298
+ // HTML and RSC are stored under separate keys — matching Next.js's file-system
299
+ // layout (.html / .rsc) — so each request type reads and writes its own key
300
+ // independently with no races or partial-entry sentinels.
301
+ //
302
+ // Key format: "app:<buildId>:<pathname>:<suffix>"
303
+ // Long-pathname fallback: "app:<buildId>:__hash:<fnv1a64(pathname)>:<suffix>"
304
+ // Without buildId (should not happen in production): "app:<pathname>:<suffix>"
305
+ // The 200-char threshold keeps the full key well under Cloudflare KV's 512-byte limit
306
+ // even after adding the build ID and suffix. FNV-1a 64 is used for the hash (two
307
+ // 32-bit rounds) to give a ~64-bit output with negligible collision probability for
308
+ // realistic pathname lengths.
309
+ // Keep prefix construction and hashing logic in sync with isrCacheKey() in server/isr-cache.ts.
310
+ function __isrFnv1a64(s) {
311
+ // h1 uses the standard FNV-1a 32-bit offset basis (0x811c9dc5).
312
+ let h1 = 0x811c9dc5;
313
+ for (let i = 0; i < s.length; i++) { h1 ^= s.charCodeAt(i); h1 = (h1 * 0x01000193) >>> 0; }
314
+ // h2 uses a different seed (0x050c5d1f — the FNV-1a hash of the string "vinext")
315
+ // so the two rounds are independently seeded and their outputs are decorrelated.
316
+ // Concatenating two independently-seeded 32-bit FNV-1a hashes gives an effective
317
+ // 64-bit hash. A random non-standard seed would also work; we derive it from a
318
+ // fixed string so the choice is auditable and deterministic across rebuilds.
319
+ let h2 = 0x050c5d1f;
320
+ for (let i = 0; i < s.length; i++) { h2 ^= s.charCodeAt(i); h2 = (h2 * 0x01000193) >>> 0; }
321
+ return h1.toString(36) + h2.toString(36);
322
+ }
323
+ function __isrCacheKey(pathname, suffix) {
324
+ const normalized = pathname === "/" ? "/" : pathname.replace(/\\/$/, "");
325
+ // __VINEXT_BUILD_ID is replaced at compile time by Vite's define plugin.
326
+ const buildId = process.env.__VINEXT_BUILD_ID;
327
+ const prefix = buildId ? "app:" + buildId : "app";
328
+ const key = prefix + ":" + normalized + ":" + suffix;
329
+ if (key.length <= 200) return key;
330
+ // Pathname too long — hash it to keep under KV's 512-byte key limit.
331
+ return prefix + ":__hash:" + __isrFnv1a64(normalized) + ":" + suffix;
332
+ }
333
+ function __isrHtmlKey(pathname) { return __isrCacheKey(pathname, "html"); }
334
+ function __isrRscKey(pathname) { return __isrCacheKey(pathname, "rsc"); }
335
+ // Verbose cache logging — opt in with NEXT_PRIVATE_DEBUG_CACHE=1.
336
+ // Matches the env var Next.js uses for its own cache debug output so operators
337
+ // have a single knob for all cache tracing.
338
+ const __isrDebug = process.env.NEXT_PRIVATE_DEBUG_CACHE
339
+ ? console.debug.bind(console, "[vinext] ISR:")
340
+ : undefined;
251
341
 
252
342
  // Normalize null-prototype objects from matchPattern() into thenable objects
253
343
  // that work both as Promises (for Next.js 15+ async params) and as plain
@@ -773,6 +863,7 @@ function matchPattern(urlParts, patternParts) {
773
863
  for (let i = 0; i < patternParts.length; i++) {
774
864
  const pp = patternParts[i];
775
865
  if (pp.endsWith("+")) {
866
+ if (i !== patternParts.length - 1) return null;
776
867
  const paramName = pp.slice(1, -1);
777
868
  const remaining = urlParts.slice(i);
778
869
  if (remaining.length === 0) return null;
@@ -780,6 +871,7 @@ function matchPattern(urlParts, patternParts) {
780
871
  return params;
781
872
  }
782
873
  if (pp.endsWith("*")) {
874
+ if (i !== patternParts.length - 1) return null;
783
875
  const paramName = pp.slice(1, -1);
784
876
  params[paramName] = urlParts.slice(i);
785
877
  return params;
@@ -1128,6 +1220,7 @@ ${middlewarePath ? generateMiddlewareMatcherCode("modern") : ""}
1128
1220
 
1129
1221
  const __basePath = ${JSON.stringify(bp)};
1130
1222
  const __trailingSlash = ${JSON.stringify(ts)};
1223
+ const __i18nConfig = ${JSON.stringify(i18nConfig)};
1131
1224
  const __configRedirects = ${JSON.stringify(redirects)};
1132
1225
  const __configRewrites = ${JSON.stringify(rewrites)};
1133
1226
  const __configHeaders = ${JSON.stringify(headers)};
@@ -1166,7 +1259,7 @@ function __buildPostMwRequestContext(request) {
1166
1259
  headers: ctx.headers,
1167
1260
  cookies: cookiesRecord,
1168
1261
  query: url.searchParams,
1169
- host: ctx.headers.get("host") || url.host,
1262
+ host: normalizeHost(ctx.headers.get("host"), url.hostname),
1170
1263
  };
1171
1264
  }
1172
1265
 
@@ -1236,7 +1329,7 @@ async function __readFormDataWithLimit(request, maxBytes) {
1236
1329
  return new Response(combined, { headers: { "Content-Type": contentType } }).formData();
1237
1330
  }
1238
1331
 
1239
- export default async function handler(request) {
1332
+ export default async function handler(request, ctx) {
1240
1333
  ${instrumentationPath
1241
1334
  ? `// Ensure instrumentation.register() has run before handling the first request.
1242
1335
  // This is a no-op after the first call (guarded by __instrumentationInitialized).
@@ -1248,8 +1341,12 @@ export default async function handler(request) {
1248
1341
  // ALS scope that propagates through all async continuations (including RSC
1249
1342
  // streaming), preventing state leakage between concurrent requests on
1250
1343
  // Cloudflare Workers and other concurrent runtimes.
1344
+ //
1345
+ // runWithExecutionContext stores the Workers ExecutionContext (ctx) in ALS so
1346
+ // that KVCacheHandler._putInBackground can register background KV puts with
1347
+ // ctx.waitUntil() without needing ctx passed at construction time.
1251
1348
  const headersCtx = headersContextFromRequest(request);
1252
- return runWithHeadersContext(headersCtx, () =>
1349
+ const _run = () => runWithHeadersContext(headersCtx, () =>
1253
1350
  _runWithNavigationContext(() =>
1254
1351
  _runWithCacheState(() =>
1255
1352
  _runWithPrivateCache(() =>
@@ -1292,6 +1389,7 @@ export default async function handler(request) {
1292
1389
  )
1293
1390
  )
1294
1391
  );
1392
+ return ctx ? _runWithExecutionContext(ctx, _run) : _run();
1295
1393
  }
1296
1394
 
1297
1395
  async function _handleRequest(request, __reqCtx, _mwCtx) {
@@ -1379,7 +1477,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1379
1477
  throw new Error("The " + _fileType + " file must export a function named \`" + _expectedExport + "\` or a \`default\` function.");
1380
1478
  }
1381
1479
  const middlewareMatcher = middlewareModule.config?.matcher;
1382
- if (matchesMiddleware(cleanPathname, middlewareMatcher)) {
1480
+ if (matchesMiddleware(cleanPathname, middlewareMatcher, request, __i18nConfig)) {
1383
1481
  try {
1384
1482
  // Wrap in NextRequest so middleware gets .nextUrl, .cookies, .geo, .ip, etc.
1385
1483
  // Always construct a new Request with the fully decoded + normalized pathname
@@ -1594,39 +1692,44 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1594
1692
  const action = await loadServerAction(actionId);
1595
1693
  let returnValue;
1596
1694
  let actionRedirect = null;
1695
+ const previousHeadersPhase = setHeadersAccessPhase("action");
1597
1696
  try {
1598
- const data = await action.apply(null, args);
1599
- returnValue = { ok: true, data };
1600
- } catch (e) {
1601
- // Detect redirect() / permanentRedirect() called inside the action.
1602
- // These throw errors with digest "NEXT_REDIRECT;replace;url[;status]".
1603
- // The URL is encodeURIComponent-encoded to prevent semicolons in the URL
1604
- // from corrupting the delimiter-based digest format.
1605
- if (e && typeof e === "object" && "digest" in e) {
1606
- const digest = String(e.digest);
1607
- if (digest.startsWith("NEXT_REDIRECT;")) {
1608
- const parts = digest.split(";");
1609
- actionRedirect = {
1610
- url: decodeURIComponent(parts[2]),
1611
- type: parts[1] || "replace", // "push" or "replace"
1612
- status: parts[3] ? parseInt(parts[3], 10) : 307,
1613
- };
1614
- returnValue = { ok: true, data: undefined };
1615
- } else if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
1616
- // notFound() / forbidden() / unauthorized() in action — package as error
1617
- returnValue = { ok: false, data: e };
1697
+ try {
1698
+ const data = await action.apply(null, args);
1699
+ returnValue = { ok: true, data };
1700
+ } catch (e) {
1701
+ // Detect redirect() / permanentRedirect() called inside the action.
1702
+ // These throw errors with digest "NEXT_REDIRECT;replace;url[;status]".
1703
+ // The URL is encodeURIComponent-encoded to prevent semicolons in the URL
1704
+ // from corrupting the delimiter-based digest format.
1705
+ if (e && typeof e === "object" && "digest" in e) {
1706
+ const digest = String(e.digest);
1707
+ if (digest.startsWith("NEXT_REDIRECT;")) {
1708
+ const parts = digest.split(";");
1709
+ actionRedirect = {
1710
+ url: decodeURIComponent(parts[2]),
1711
+ type: parts[1] || "replace", // "push" or "replace"
1712
+ status: parts[3] ? parseInt(parts[3], 10) : 307,
1713
+ };
1714
+ returnValue = { ok: true, data: undefined };
1715
+ } else if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
1716
+ // notFound() / forbidden() / unauthorized() in action — package as error
1717
+ returnValue = { ok: false, data: e };
1718
+ } else {
1719
+ // Non-navigation digest error — sanitize in production to avoid
1720
+ // leaking internal details (connection strings, paths, etc.)
1721
+ console.error("[vinext] Server action error:", e);
1722
+ returnValue = { ok: false, data: __sanitizeErrorForClient(e) };
1723
+ }
1618
1724
  } else {
1619
- // Non-navigation digest error — sanitize in production to avoid
1620
- // leaking internal details (connection strings, paths, etc.)
1725
+ // Unhandled error — sanitize in production to avoid leaking
1726
+ // internal details (database errors, file paths, stack traces, etc.)
1621
1727
  console.error("[vinext] Server action error:", e);
1622
1728
  returnValue = { ok: false, data: __sanitizeErrorForClient(e) };
1623
1729
  }
1624
- } else {
1625
- // Unhandled error — sanitize in production to avoid leaking
1626
- // internal details (database errors, file paths, stack traces, etc.)
1627
- console.error("[vinext] Server action error:", e);
1628
- returnValue = { ok: false, data: __sanitizeErrorForClient(e) };
1629
1730
  }
1731
+ } finally {
1732
+ setHeadersAccessPhase(previousHeadersPhase);
1630
1733
  }
1631
1734
 
1632
1735
  // If the action called redirect(), signal the client to navigate.
@@ -1779,16 +1882,39 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1779
1882
  }
1780
1883
  const hasDefault = typeof handler["default"] === "function";
1781
1884
 
1885
+ // Route handlers need the same middleware header/status merge behavior as
1886
+ // page responses. This keeps middleware response headers visible on API
1887
+ // routes in Workers/dev, and preserves custom rewrite status overrides.
1888
+ function attachRouteHandlerMiddlewareContext(response) {
1889
+ // _mwCtx.headers is only set (non-null) when middleware actually ran and
1890
+ // produced a continue/rewrite response. An empty Headers object (middleware
1891
+ // ran but produced no response headers) is a harmless edge case: the early
1892
+ // return is skipped, but the copy loop below is a no-op, so no incorrect
1893
+ // headers are added. The allocation cost in that case is acceptable.
1894
+ if (!_mwCtx.headers && _mwCtx.status == null) return response;
1895
+ const responseHeaders = new Headers(response.headers);
1896
+ if (_mwCtx.headers) {
1897
+ for (const [key, value] of _mwCtx.headers) {
1898
+ responseHeaders.append(key, value);
1899
+ }
1900
+ }
1901
+ return new Response(response.body, {
1902
+ status: _mwCtx.status ?? response.status,
1903
+ statusText: response.statusText,
1904
+ headers: responseHeaders,
1905
+ });
1906
+ }
1907
+
1782
1908
  // OPTIONS auto-implementation: respond with Allow header and 204
1783
1909
  if (method === "OPTIONS" && typeof handler["OPTIONS"] !== "function") {
1784
1910
  const allowMethods = hasDefault ? HTTP_METHODS : exportedMethods;
1785
1911
  if (!allowMethods.includes("OPTIONS")) allowMethods.push("OPTIONS");
1786
1912
  setHeadersContext(null);
1787
1913
  setNavigationContext(null);
1788
- return new Response(null, {
1914
+ return attachRouteHandlerMiddlewareContext(new Response(null, {
1789
1915
  status: 204,
1790
1916
  headers: { "Allow": allowMethods.join(", ") },
1791
- });
1917
+ }));
1792
1918
  }
1793
1919
 
1794
1920
  // HEAD auto-implementation: run GET handler and strip body
@@ -1800,6 +1926,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1800
1926
  }
1801
1927
 
1802
1928
  if (typeof handlerFn === "function") {
1929
+ const previousHeadersPhase = setHeadersAccessPhase("route-handler");
1803
1930
  try {
1804
1931
  const response = await handlerFn(request, { params });
1805
1932
  const dynamicUsedInHandler = consumeDynamicUsage();
@@ -1831,28 +1958,28 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1831
1958
  if (draftCookie) newHeaders.append("Set-Cookie", draftCookie);
1832
1959
 
1833
1960
  if (isAutoHead) {
1834
- return new Response(null, {
1961
+ return attachRouteHandlerMiddlewareContext(new Response(null, {
1835
1962
  status: response.status,
1836
1963
  statusText: response.statusText,
1837
1964
  headers: newHeaders,
1838
- });
1965
+ }));
1839
1966
  }
1840
- return new Response(response.body, {
1967
+ return attachRouteHandlerMiddlewareContext(new Response(response.body, {
1841
1968
  status: response.status,
1842
1969
  statusText: response.statusText,
1843
1970
  headers: newHeaders,
1844
- });
1971
+ }));
1845
1972
  }
1846
1973
 
1847
1974
  if (isAutoHead) {
1848
1975
  // Strip body for auto-HEAD, preserve headers and status
1849
- return new Response(null, {
1976
+ return attachRouteHandlerMiddlewareContext(new Response(null, {
1850
1977
  status: response.status,
1851
1978
  statusText: response.statusText,
1852
1979
  headers: response.headers,
1853
- });
1980
+ }));
1854
1981
  }
1855
- return response;
1982
+ return attachRouteHandlerMiddlewareContext(response);
1856
1983
  } catch (err) {
1857
1984
  getAndClearPendingCookies(); // Clear any pending cookies on error
1858
1985
  // Catch redirect() / notFound() thrown from route handlers
@@ -1864,16 +1991,16 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1864
1991
  const statusCode = parts[3] ? parseInt(parts[3], 10) : 307;
1865
1992
  setHeadersContext(null);
1866
1993
  setNavigationContext(null);
1867
- return new Response(null, {
1994
+ return attachRouteHandlerMiddlewareContext(new Response(null, {
1868
1995
  status: statusCode,
1869
1996
  headers: { Location: new URL(redirectUrl, request.url).toString() },
1870
- });
1997
+ }));
1871
1998
  }
1872
1999
  if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
1873
2000
  const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
1874
2001
  setHeadersContext(null);
1875
2002
  setNavigationContext(null);
1876
- return new Response(null, { status: statusCode });
2003
+ return attachRouteHandlerMiddlewareContext(new Response(null, { status: statusCode }));
1877
2004
  }
1878
2005
  }
1879
2006
  setHeadersContext(null);
@@ -1886,15 +2013,17 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1886
2013
  ).catch((reportErr) => {
1887
2014
  console.error("[vinext] Failed to report route handler error:", reportErr);
1888
2015
  });
1889
- return new Response(null, { status: 500 });
2016
+ return attachRouteHandlerMiddlewareContext(new Response(null, { status: 500 }));
2017
+ } finally {
2018
+ setHeadersAccessPhase(previousHeadersPhase);
1890
2019
  }
1891
2020
  }
1892
2021
  setHeadersContext(null);
1893
2022
  setNavigationContext(null);
1894
- return new Response(null, {
2023
+ return attachRouteHandlerMiddlewareContext(new Response(null, {
1895
2024
  status: 405,
1896
2025
  headers: { Allow: exportedMethods.join(", ") },
1897
- });
2026
+ }));
1898
2027
  }
1899
2028
 
1900
2029
  // Build the component tree: layouts wrapping the page
@@ -1923,25 +2052,18 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1923
2052
  });
1924
2053
  }
1925
2054
 
1926
- // dynamic = 'error': set a trap context that throws when headers/cookies are accessed
2055
+ // dynamic = 'error': install an access error so request APIs fail with the
2056
+ // static-generation message even for legacy sync property access.
1927
2057
  if (isDynamicError) {
1928
2058
  const errorMsg = 'Page with \`dynamic = "error"\` used a dynamic API. ' +
1929
2059
  'This page was expected to be fully static, but headers(), cookies(), ' +
1930
2060
  'or searchParams was accessed. Remove the dynamic API usage or change ' +
1931
2061
  'the dynamic config to "auto" or "force-dynamic".';
1932
- const throwingHeaders = new Proxy(new Headers(), {
1933
- get(target, prop) {
1934
- if (typeof prop === "string" && prop !== "then") throw new Error(errorMsg);
1935
- return Reflect.get(target, prop);
1936
- },
1937
- });
1938
- const throwingCookies = new Proxy(new Map(), {
1939
- get(target, prop) {
1940
- if (typeof prop === "string" && prop !== "then") throw new Error(errorMsg);
1941
- return Reflect.get(target, prop);
1942
- },
2062
+ setHeadersContext({
2063
+ headers: new Headers(),
2064
+ cookies: new Map(),
2065
+ accessError: new Error(errorMsg),
1943
2066
  });
1944
- setHeadersContext({ headers: throwingHeaders, cookies: throwingCookies });
1945
2067
  setNavigationContext({
1946
2068
  pathname: cleanPathname,
1947
2069
  searchParams: new URLSearchParams(),
@@ -1949,7 +2071,177 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1949
2071
  });
1950
2072
  }
1951
2073
 
1952
- // dynamicParams = false: only params from generateStaticParams are allowed
2074
+ // force-dynamic: set no-store Cache-Control
2075
+ const isForceDynamic = dynamicConfig === "force-dynamic";
2076
+
2077
+ // ── ISR cache read (production only) ─────────────────────────────────────
2078
+ // Read from cache BEFORE generateStaticParams and all rendering work.
2079
+ // This is the critical performance optimization: on a cache hit we skip
2080
+ // ALL expensive work (generateStaticParams, buildPageElement, layout probe,
2081
+ // page probe, renderToReadableStream, SSR). Both HTML and RSC requests
2082
+ // (client-side navigation / prefetch) are served from cache.
2083
+ //
2084
+ // HTML and RSC are stored under separate keys (matching Next.js's .html/.rsc
2085
+ // file layout) so each request type reads and writes independently — no races,
2086
+ // no partial-entry sentinels, no read-before-write hacks needed.
2087
+ //
2088
+ // force-static and dynamic='error' are compatible with ISR — they control
2089
+ // how dynamic APIs behave during rendering, not whether results are cached.
2090
+ // Only force-dynamic truly bypasses the ISR cache.
2091
+ if (
2092
+ process.env.NODE_ENV === "production" &&
2093
+ !isForceDynamic &&
2094
+ revalidateSeconds !== null && revalidateSeconds > 0 && revalidateSeconds !== Infinity
2095
+ ) {
2096
+ const __isrKey = isRscRequest ? __isrRscKey(cleanPathname) : __isrHtmlKey(cleanPathname);
2097
+ try {
2098
+ const __cached = await __isrGet(__isrKey);
2099
+ if (__cached && !__cached.isStale && __cached.value.value && __cached.value.value.kind === "APP_PAGE") {
2100
+ const __cachedValue = __cached.value.value;
2101
+ const __hasRsc = !!__cachedValue.rscData;
2102
+ const __hasHtml = typeof __cachedValue.html === "string" && __cachedValue.html.length > 0;
2103
+ if (isRscRequest && __hasRsc) {
2104
+ __isrDebug?.("HIT (RSC)", cleanPathname);
2105
+ setHeadersContext(null);
2106
+ setNavigationContext(null);
2107
+ return new Response(__cachedValue.rscData, {
2108
+ status: __cachedValue.status || 200,
2109
+ headers: {
2110
+ "Content-Type": "text/x-component; charset=utf-8",
2111
+ "Cache-Control": "s-maxage=" + revalidateSeconds + ", stale-while-revalidate",
2112
+ "Vary": "RSC, Accept",
2113
+ "X-Vinext-Cache": "HIT",
2114
+ },
2115
+ });
2116
+ }
2117
+ if (!isRscRequest && __hasHtml) {
2118
+ __isrDebug?.("HIT (HTML)", cleanPathname);
2119
+ setHeadersContext(null);
2120
+ setNavigationContext(null);
2121
+ return new Response(__cachedValue.html, {
2122
+ status: __cachedValue.status || 200,
2123
+ headers: {
2124
+ "Content-Type": "text/html; charset=utf-8",
2125
+ "Cache-Control": "s-maxage=" + revalidateSeconds + ", stale-while-revalidate",
2126
+ "Vary": "RSC, Accept",
2127
+ "X-Vinext-Cache": "HIT",
2128
+ },
2129
+ });
2130
+ }
2131
+ __isrDebug?.("MISS (empty cached entry)", cleanPathname);
2132
+ }
2133
+ if (__cached && __cached.isStale && __cached.value.value && __cached.value.value.kind === "APP_PAGE") {
2134
+ // Stale cache hit — serve stale immediately, trigger background regeneration.
2135
+ // Regen writes both keys independently so neither path blocks on the other.
2136
+ const __staleValue = __cached.value.value;
2137
+ const __staleStatus = __staleValue.status || 200;
2138
+ const __revalSecs = revalidateSeconds;
2139
+ __triggerBackgroundRegeneration(cleanPathname, async function() {
2140
+ // Re-render the page to produce fresh HTML + RSC data for the cache
2141
+ // Use an empty headers context for background regeneration — not the original
2142
+ // user request — to prevent user-specific cookies/auth headers from leaking
2143
+ // into content that is cached and served to all subsequent users.
2144
+ const __revalHeadCtx = { headers: new Headers(), cookies: new Map() };
2145
+ const __revalResult = await runWithHeadersContext(__revalHeadCtx, () =>
2146
+ _runWithNavigationContext(() =>
2147
+ _runWithCacheState(() =>
2148
+ _runWithPrivateCache(() =>
2149
+ runWithFetchCache(async () => {
2150
+ setNavigationContext({ pathname: cleanPathname, searchParams: url.searchParams, params });
2151
+ const __revalElement = await buildPageElement(route, params, undefined, url.searchParams);
2152
+ const __revalOnError = createRscOnErrorHandler(request, cleanPathname, route.pattern);
2153
+ const __revalRscStream = renderToReadableStream(__revalElement, { onError: __revalOnError });
2154
+ // Tee RSC stream: one for SSR, one to capture rscData
2155
+ const [__revalRscForSsr, __revalRscForCapture] = __revalRscStream.tee();
2156
+ // Capture rscData bytes in parallel with SSR
2157
+ const __rscDataPromise = (async () => {
2158
+ const __rscReader = __revalRscForCapture.getReader();
2159
+ const __rscChunks = [];
2160
+ let __rscTotal = 0;
2161
+ for (;;) {
2162
+ const { done, value } = await __rscReader.read();
2163
+ if (done) break;
2164
+ __rscChunks.push(value);
2165
+ __rscTotal += value.byteLength;
2166
+ }
2167
+ const __rscBuf = new Uint8Array(__rscTotal);
2168
+ let __rscOff = 0;
2169
+ for (const c of __rscChunks) { __rscBuf.set(c, __rscOff); __rscOff += c.byteLength; }
2170
+ return __rscBuf.buffer;
2171
+ })();
2172
+ const __revalFontData = { links: _getSSRFontLinks(), styles: _getSSRFontStyles(), preloads: _getSSRFontPreloads() };
2173
+ const __revalSsrEntry = await import.meta.viteRsc.loadModule("ssr", "index");
2174
+ const __revalHtmlStream = await __revalSsrEntry.handleSsr(__revalRscForSsr, _getNavigationContext(), __revalFontData);
2175
+ setHeadersContext(null);
2176
+ setNavigationContext(null);
2177
+ // Collect the full HTML string from the stream
2178
+ const __revalReader = __revalHtmlStream.getReader();
2179
+ const __revalDecoder = new TextDecoder();
2180
+ const __revalChunks = [];
2181
+ for (;;) {
2182
+ const { done, value } = await __revalReader.read();
2183
+ if (done) break;
2184
+ __revalChunks.push(__revalDecoder.decode(value, { stream: true }));
2185
+ }
2186
+ __revalChunks.push(__revalDecoder.decode());
2187
+ const __freshHtml = __revalChunks.join("");
2188
+ const __freshRscData = await __rscDataPromise;
2189
+ const __pageTags = __pageCacheTags(cleanPathname, getCollectedFetchTags());
2190
+ return { html: __freshHtml, rscData: __freshRscData, tags: __pageTags };
2191
+ })
2192
+ )
2193
+ )
2194
+ )
2195
+ );
2196
+ // Write HTML and RSC to their own keys independently — no races
2197
+ await Promise.all([
2198
+ __isrSet(__isrHtmlKey(cleanPathname), { kind: "APP_PAGE", html: __revalResult.html, rscData: undefined, headers: undefined, postponed: undefined, status: 200 }, __revalSecs, __revalResult.tags),
2199
+ __isrSet(__isrRscKey(cleanPathname), { kind: "APP_PAGE", html: "", rscData: __revalResult.rscData, headers: undefined, postponed: undefined, status: 200 }, __revalSecs, __revalResult.tags),
2200
+ ]);
2201
+ __isrDebug?.("regen complete", cleanPathname);
2202
+ });
2203
+ if (isRscRequest && __staleValue.rscData) {
2204
+ __isrDebug?.("STALE (RSC)", cleanPathname);
2205
+ setHeadersContext(null);
2206
+ setNavigationContext(null);
2207
+ return new Response(__staleValue.rscData, {
2208
+ status: __staleStatus,
2209
+ headers: {
2210
+ "Content-Type": "text/x-component; charset=utf-8",
2211
+ "Cache-Control": "s-maxage=0, stale-while-revalidate",
2212
+ "Vary": "RSC, Accept",
2213
+ "X-Vinext-Cache": "STALE",
2214
+ },
2215
+ });
2216
+ }
2217
+ if (!isRscRequest && typeof __staleValue.html === "string" && __staleValue.html.length > 0) {
2218
+ __isrDebug?.("STALE (HTML)", cleanPathname);
2219
+ setHeadersContext(null);
2220
+ setNavigationContext(null);
2221
+ return new Response(__staleValue.html, {
2222
+ status: __staleStatus,
2223
+ headers: {
2224
+ "Content-Type": "text/html; charset=utf-8",
2225
+ "Cache-Control": "s-maxage=0, stale-while-revalidate",
2226
+ "Vary": "RSC, Accept",
2227
+ "X-Vinext-Cache": "STALE",
2228
+ },
2229
+ });
2230
+ }
2231
+ // Stale entry exists but is empty for this request type — fall through to render
2232
+ __isrDebug?.("STALE MISS (empty stale entry)", cleanPathname);
2233
+ }
2234
+ if (!__cached) {
2235
+ __isrDebug?.("MISS (no cache entry)", cleanPathname);
2236
+ }
2237
+ } catch (__isrReadErr) {
2238
+ // Cache read failure — fall through to normal rendering
2239
+ console.error("[vinext] ISR cache read error:", __isrReadErr);
2240
+ }
2241
+ }
2242
+
2243
+ // dynamicParams = false: only params from generateStaticParams are allowed.
2244
+ // This runs AFTER the ISR cache read so that a cache hit skips this work entirely.
1953
2245
  if (dynamicParamsConfig === false && route.isDynamic && typeof route.page?.generateStaticParams === "function") {
1954
2246
  try {
1955
2247
  // Pass parent params to generateStaticParams (Next.js top-down params passing).
@@ -1979,9 +2271,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1979
2271
  }
1980
2272
  }
1981
2273
 
1982
- // force-dynamic: set no-store Cache-Control
1983
- const isForceDynamic = dynamicConfig === "force-dynamic";
1984
-
1985
2274
  // Check for intercepting routes on RSC requests (client-side navigation).
1986
2275
  // If the target URL matches an intercepting route in a parallel slot,
1987
2276
  // render the source route with the intercepting page in the slot.
@@ -2214,6 +2503,34 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2214
2503
  };
2215
2504
  const rscStream = renderToReadableStream(element, { onError: onRenderError });
2216
2505
 
2506
+ // For ISR pages in production: tee the RSC stream immediately after creation so we
2507
+ // can capture rscData for BOTH RSC requests (client-side nav/prefetch) and HTML
2508
+ // requests. The tee must happen here — before the isRscRequest branch — so both
2509
+ // paths can use the captured bytes when writing to the ISR cache.
2510
+ // __rscForResponse → sent to the client (RSC response) or to SSR (HTML response)
2511
+ // __isrRscDataPromise → resolves to ArrayBuffer of captured RSC wire bytes
2512
+ let __rscForResponse = rscStream;
2513
+ let __isrRscDataPromise = null;
2514
+ if (process.env.NODE_ENV === "production" && revalidateSeconds !== null && revalidateSeconds > 0 && revalidateSeconds !== Infinity && !isForceDynamic) {
2515
+ const [__rscA, __rscB] = rscStream.tee();
2516
+ __rscForResponse = __rscA;
2517
+ __isrRscDataPromise = (async () => {
2518
+ const __rscReader = __rscB.getReader();
2519
+ const __rscChunks = [];
2520
+ let __rscTotal = 0;
2521
+ for (;;) {
2522
+ const { done, value } = await __rscReader.read();
2523
+ if (done) break;
2524
+ __rscChunks.push(value);
2525
+ __rscTotal += value.byteLength;
2526
+ }
2527
+ const __rscBuf = new Uint8Array(__rscTotal);
2528
+ let __rscOff = 0;
2529
+ for (const c of __rscChunks) { __rscBuf.set(c, __rscOff); __rscOff += c.byteLength; }
2530
+ return __rscBuf.buffer;
2531
+ })();
2532
+ }
2533
+
2217
2534
  if (isRscRequest) {
2218
2535
  // Direct RSC stream response (for client-side navigation)
2219
2536
  // NOTE: Do NOT clear headers/navigation context here!
@@ -2230,6 +2547,9 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2230
2547
  } else if ((isForceStatic || isDynamicError) && !revalidateSeconds) {
2231
2548
  responseHeaders["Cache-Control"] = "s-maxage=31536000, stale-while-revalidate";
2232
2549
  responseHeaders["X-Vinext-Cache"] = "STATIC";
2550
+ } else if (revalidateSeconds === Infinity) {
2551
+ responseHeaders["Cache-Control"] = "s-maxage=31536000, stale-while-revalidate";
2552
+ responseHeaders["X-Vinext-Cache"] = "STATIC";
2233
2553
  } else if (revalidateSeconds) {
2234
2554
  responseHeaders["Cache-Control"] = "s-maxage=" + revalidateSeconds + ", stale-while-revalidate";
2235
2555
  }
@@ -2279,7 +2599,26 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2279
2599
  const compileMs = __compileEnd !== undefined ? Math.round(__compileEnd - __reqStart) : -1;
2280
2600
  responseHeaders["x-vinext-timing"] = handlerStart + "," + compileMs + ",-1";
2281
2601
  }
2282
- return new Response(rscStream, { status: _mwCtx.status || 200, headers: responseHeaders });
2602
+ // For ISR-eligible RSC requests in production: write rscData to its own key.
2603
+ // HTML is stored under a separate key (written by the HTML path below) so
2604
+ // these writes never race or clobber each other.
2605
+ if (process.env.NODE_ENV === "production" && __isrRscDataPromise) {
2606
+ responseHeaders["X-Vinext-Cache"] = "MISS";
2607
+ const __isrKeyRsc = __isrRscKey(cleanPathname);
2608
+ const __revalSecsRsc = revalidateSeconds;
2609
+ const __rscWritePromise = (async () => {
2610
+ try {
2611
+ const __rscDataForCache = await __isrRscDataPromise;
2612
+ const __pageTags = __pageCacheTags(cleanPathname, getCollectedFetchTags());
2613
+ await __isrSet(__isrKeyRsc, { kind: "APP_PAGE", html: "", rscData: __rscDataForCache, headers: undefined, postponed: undefined, status: 200 }, __revalSecsRsc, __pageTags);
2614
+ __isrDebug?.("RSC cache written", __isrKeyRsc);
2615
+ } catch (__rscWriteErr) {
2616
+ console.error("[vinext] ISR RSC cache write error:", __rscWriteErr);
2617
+ }
2618
+ })();
2619
+ _getRequestExecutionContext()?.waitUntil(__rscWritePromise);
2620
+ }
2621
+ return new Response(__rscForResponse, { status: _mwCtx.status || 200, headers: responseHeaders });
2283
2622
  }
2284
2623
 
2285
2624
  // Collect font data from RSC environment before passing to SSR
@@ -2300,11 +2639,16 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2300
2639
  }
2301
2640
  const fontLinkHeader = fontLinkHeaderParts.length > 0 ? fontLinkHeaderParts.join(", ") : "";
2302
2641
 
2642
+ // __rscForResponse was already teed above (before isRscRequest) for ISR pages in
2643
+ // production. For non-ISR or dev, __rscForResponse === rscStream (no tee).
2644
+ // __isrRscDataPromise resolves to rscData bytes used by the RSC write path above;
2645
+ // the HTML write path below uses its own separate key and does not need rscData.
2646
+
2303
2647
  // Delegate to SSR environment for HTML rendering
2304
2648
  let htmlStream;
2305
2649
  try {
2306
2650
  const ssrEntry = await import.meta.viteRsc.loadModule("ssr", "index");
2307
- htmlStream = await ssrEntry.handleSsr(rscStream, _getNavigationContext(), fontData);
2651
+ htmlStream = await ssrEntry.handleSsr(__rscForResponse, _getNavigationContext(), fontData);
2308
2652
  // Shell render complete; Suspense boundaries stream asynchronously
2309
2653
  if (process.env.NODE_ENV !== "production") __renderEnd = performance.now();
2310
2654
  } catch (ssrErr) {
@@ -2408,8 +2752,8 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2408
2752
  // force-static / error: treat as static regardless of dynamic usage.
2409
2753
  // force-static intentionally provides empty headers/cookies context so
2410
2754
  // dynamic APIs return safe defaults; we ignore the dynamic usage signal.
2411
- // dynamic='error' should have already thrown (via throwing Proxy) if user
2412
- // code accessed dynamic APIs, so reaching here means rendering succeeded.
2755
+ // dynamic='error' should have already thrown via the request API accessError
2756
+ // trap if user code touched a dynamic API, so reaching here means rendering succeeded.
2413
2757
  if ((isForceStatic || isDynamicError) && (revalidateSeconds === null || revalidateSeconds === 0)) {
2414
2758
  return attachMiddlewareContext(new Response(htmlStream, {
2415
2759
  headers: {
@@ -2433,9 +2777,72 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2433
2777
  }));
2434
2778
  }
2435
2779
 
2436
- // Emit Cache-Control for ISR pages so tests can verify revalidate values,
2437
- // but skip actual caching in devevery request renders fresh.
2438
- if (revalidateSeconds !== null && revalidateSeconds > 0) {
2780
+ // Emit Cache-Control for ISR pages and write to ISR cache on MISS (production only).
2781
+ // revalidate=Infinity means "cache forever" (no periodic revalidation) treated as
2782
+ // static here so we emit s-maxage=31536000 but skip ISR cache management.
2783
+ if (revalidateSeconds !== null && revalidateSeconds > 0 && revalidateSeconds !== Infinity) {
2784
+ // In production, tee the HTML response body to simultaneously stream to the
2785
+ // client and collect the full HTML string for the ISR cache. rscData was
2786
+ // already captured above by teeing the RSC stream before SSR.
2787
+ // In dev, skip the tee and the X-Vinext-Cache header — every request renders
2788
+ // fresh (no cache reads or writes in dev mode).
2789
+ if (process.env.NODE_ENV === "production") {
2790
+ const __isrResponseProd = attachMiddlewareContext(new Response(htmlStream, {
2791
+ headers: {
2792
+ "Content-Type": "text/html; charset=utf-8",
2793
+ "Cache-Control": "s-maxage=" + revalidateSeconds + ", stale-while-revalidate",
2794
+ "Vary": "RSC, Accept",
2795
+ "X-Vinext-Cache": "MISS",
2796
+ },
2797
+ }));
2798
+ if (__isrResponseProd.body) {
2799
+ const [__streamForClient, __streamForCache] = __isrResponseProd.body.tee();
2800
+ const __isrKey = __isrHtmlKey(cleanPathname);
2801
+ const __isrKeyRscFromHtml = __isrRscKey(cleanPathname);
2802
+ const __revalSecs = revalidateSeconds;
2803
+ const __capturedRscDataPromise = __isrRscDataPromise;
2804
+ const __cachePromise = (async () => {
2805
+ try {
2806
+ const __reader = __streamForCache.getReader();
2807
+ const __decoder = new TextDecoder();
2808
+ const __chunks = [];
2809
+ for (;;) {
2810
+ const { done, value } = await __reader.read();
2811
+ if (done) break;
2812
+ __chunks.push(__decoder.decode(value, { stream: true }));
2813
+ }
2814
+ __chunks.push(__decoder.decode());
2815
+ const __fullHtml = __chunks.join("");
2816
+ const __pageTags = __pageCacheTags(cleanPathname, getCollectedFetchTags());
2817
+ // Write HTML and RSC to their own keys independently.
2818
+ // RSC data was captured by the tee above (before isRscRequest branch)
2819
+ // so an initial browser visit (HTML request) also populates the RSC key,
2820
+ // ensuring the first client-side navigation after a direct visit is a
2821
+ // cache hit rather than a miss.
2822
+ const __writes = [
2823
+ __isrSet(__isrKey, { kind: "APP_PAGE", html: __fullHtml, rscData: undefined, headers: undefined, postponed: undefined, status: 200 }, __revalSecs, __pageTags),
2824
+ ];
2825
+ if (__capturedRscDataPromise) {
2826
+ __writes.push(
2827
+ __capturedRscDataPromise.then((__rscBuf) =>
2828
+ __isrSet(__isrKeyRscFromHtml, { kind: "APP_PAGE", html: "", rscData: __rscBuf, headers: undefined, postponed: undefined, status: 200 }, __revalSecs, __pageTags)
2829
+ )
2830
+ );
2831
+ }
2832
+ await Promise.all(__writes);
2833
+ __isrDebug?.("HTML cache written", __isrKey);
2834
+ } catch (__cacheErr) {
2835
+ console.error("[vinext] ISR cache write error:", __cacheErr);
2836
+ }
2837
+ })();
2838
+ // Register with ExecutionContext (from ALS) so the Workers runtime keeps
2839
+ // the isolate alive until the cache write finishes, even after the response is sent.
2840
+ _getRequestExecutionContext()?.waitUntil(__cachePromise);
2841
+ return new Response(__streamForClient, { status: __isrResponseProd.status, headers: __isrResponseProd.headers });
2842
+ }
2843
+ return __isrResponseProd;
2844
+ }
2845
+ // Dev mode: return Cache-Control header but no X-Vinext-Cache (no cache read/write)
2439
2846
  return attachMiddlewareContext(new Response(htmlStream, {
2440
2847
  headers: {
2441
2848
  "Content-Type": "text/html; charset=utf-8",
@@ -2445,6 +2852,19 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2445
2852
  }));
2446
2853
  }
2447
2854
 
2855
+ // revalidate=Infinity (or false, which Next.js normalises to false/0): treat as
2856
+ // permanent static — emit the longest safe s-maxage but skip ISR cache management.
2857
+ if (revalidateSeconds === Infinity) {
2858
+ return attachMiddlewareContext(new Response(htmlStream, {
2859
+ headers: {
2860
+ "Content-Type": "text/html; charset=utf-8",
2861
+ "Cache-Control": "s-maxage=31536000, stale-while-revalidate",
2862
+ "X-Vinext-Cache": "STATIC",
2863
+ "Vary": "RSC, Accept",
2864
+ },
2865
+ }));
2866
+ }
2867
+
2448
2868
  return attachMiddlewareContext(new Response(htmlStream, {
2449
2869
  headers: { "Content-Type": "text/html; charset=utf-8", "Vary": "RSC, Accept" },
2450
2870
  }));