vinext 0.0.27 → 0.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build/report.d.ts +117 -0
- package/dist/build/report.d.ts.map +1 -0
- package/dist/build/report.js +303 -0
- package/dist/build/report.js.map +1 -0
- package/dist/build/static-export.d.ts +1 -1
- package/dist/build/static-export.d.ts.map +1 -1
- package/dist/build/static-export.js +2 -1
- package/dist/build/static-export.js.map +1 -1
- package/dist/cli.js +106 -9
- package/dist/cli.js.map +1 -1
- package/dist/cloudflare/kv-cache-handler.d.ts +28 -17
- package/dist/cloudflare/kv-cache-handler.d.ts.map +1 -1
- package/dist/cloudflare/kv-cache-handler.js +109 -42
- package/dist/cloudflare/kv-cache-handler.js.map +1 -1
- package/dist/cloudflare/tpr.d.ts +10 -0
- package/dist/cloudflare/tpr.d.ts.map +1 -1
- package/dist/cloudflare/tpr.js +36 -41
- package/dist/cloudflare/tpr.js.map +1 -1
- package/dist/config/config-matchers.d.ts +1 -0
- package/dist/config/config-matchers.d.ts.map +1 -1
- package/dist/config/config-matchers.js +51 -23
- package/dist/config/config-matchers.js.map +1 -1
- package/dist/config/next-config.d.ts.map +1 -1
- package/dist/config/next-config.js +16 -0
- package/dist/config/next-config.js.map +1 -1
- package/dist/deploy.d.ts +1 -1
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +48 -32
- package/dist/deploy.js.map +1 -1
- package/dist/entries/app-rsc-entry.d.ts +3 -1
- package/dist/entries/app-rsc-entry.d.ts.map +1 -1
- package/dist/entries/app-rsc-entry.js +514 -99
- package/dist/entries/app-rsc-entry.js.map +1 -1
- package/dist/entries/pages-server-entry.d.ts.map +1 -1
- package/dist/entries/pages-server-entry.js +154 -58
- package/dist/entries/pages-server-entry.js.map +1 -1
- package/dist/index.d.ts +40 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +239 -79
- package/dist/index.js.map +1 -1
- package/dist/plugins/client-reference-dedup.d.ts +19 -0
- package/dist/plugins/client-reference-dedup.d.ts.map +1 -0
- package/dist/plugins/client-reference-dedup.js +96 -0
- package/dist/plugins/client-reference-dedup.js.map +1 -0
- package/dist/routing/app-router.d.ts +2 -0
- package/dist/routing/app-router.d.ts.map +1 -1
- package/dist/routing/app-router.js +145 -161
- package/dist/routing/app-router.js.map +1 -1
- package/dist/routing/pages-router.d.ts +1 -1
- package/dist/routing/pages-router.d.ts.map +1 -1
- package/dist/routing/pages-router.js +37 -65
- package/dist/routing/pages-router.js.map +1 -1
- package/dist/routing/route-trie.d.ts +57 -0
- package/dist/routing/route-trie.d.ts.map +1 -0
- package/dist/routing/route-trie.js +160 -0
- package/dist/routing/route-trie.js.map +1 -0
- package/dist/routing/route-validation.d.ts +8 -0
- package/dist/routing/route-validation.d.ts.map +1 -0
- package/dist/routing/route-validation.js +136 -0
- package/dist/routing/route-validation.js.map +1 -0
- package/dist/routing/utils.d.ts +19 -0
- package/dist/routing/utils.d.ts.map +1 -1
- package/dist/routing/utils.js +47 -0
- package/dist/routing/utils.js.map +1 -1
- package/dist/server/api-handler.d.ts.map +1 -1
- package/dist/server/api-handler.js +52 -20
- package/dist/server/api-handler.js.map +1 -1
- package/dist/server/dev-server.d.ts.map +1 -1
- package/dist/server/dev-server.js +67 -9
- package/dist/server/dev-server.js.map +1 -1
- package/dist/server/image-optimization.d.ts.map +1 -1
- package/dist/server/image-optimization.js +1 -1
- package/dist/server/image-optimization.js.map +1 -1
- package/dist/server/instrumentation.d.ts.map +1 -1
- package/dist/server/instrumentation.js +17 -8
- package/dist/server/instrumentation.js.map +1 -1
- package/dist/server/isr-cache.d.ts +5 -13
- package/dist/server/isr-cache.d.ts.map +1 -1
- package/dist/server/isr-cache.js +13 -12
- package/dist/server/isr-cache.js.map +1 -1
- package/dist/server/metadata-routes.d.ts +8 -2
- package/dist/server/metadata-routes.d.ts.map +1 -1
- package/dist/server/metadata-routes.js +73 -28
- package/dist/server/metadata-routes.js.map +1 -1
- package/dist/server/middleware-codegen.d.ts +11 -1
- package/dist/server/middleware-codegen.d.ts.map +1 -1
- package/dist/server/middleware-codegen.js +204 -12
- package/dist/server/middleware-codegen.js.map +1 -1
- package/dist/server/middleware.d.ts +9 -8
- package/dist/server/middleware.d.ts.map +1 -1
- package/dist/server/middleware.js +76 -14
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/prod-server.d.ts +8 -2
- package/dist/server/prod-server.d.ts.map +1 -1
- package/dist/server/prod-server.js +144 -74
- package/dist/server/prod-server.js.map +1 -1
- package/dist/shims/cache.d.ts +2 -0
- package/dist/shims/cache.d.ts.map +1 -1
- package/dist/shims/cache.js +20 -8
- package/dist/shims/cache.js.map +1 -1
- package/dist/shims/fetch-cache.d.ts.map +1 -1
- package/dist/shims/fetch-cache.js +5 -2
- package/dist/shims/fetch-cache.js.map +1 -1
- package/dist/shims/form.d.ts.map +1 -1
- package/dist/shims/form.js +103 -8
- package/dist/shims/form.js.map +1 -1
- package/dist/shims/headers.d.ts +11 -3
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/headers.js +182 -30
- package/dist/shims/headers.js.map +1 -1
- package/dist/shims/internal/parse-cookie-header.d.ts +12 -0
- package/dist/shims/internal/parse-cookie-header.d.ts.map +1 -0
- package/dist/shims/internal/parse-cookie-header.js +32 -0
- package/dist/shims/internal/parse-cookie-header.js.map +1 -0
- package/dist/shims/link.d.ts +2 -1
- package/dist/shims/link.d.ts.map +1 -1
- package/dist/shims/link.js +19 -45
- package/dist/shims/link.js.map +1 -1
- package/dist/shims/metadata.d.ts +56 -0
- package/dist/shims/metadata.d.ts.map +1 -1
- package/dist/shims/metadata.js +66 -0
- package/dist/shims/metadata.js.map +1 -1
- package/dist/shims/navigation.d.ts +5 -7
- package/dist/shims/navigation.d.ts.map +1 -1
- package/dist/shims/navigation.js +61 -39
- package/dist/shims/navigation.js.map +1 -1
- package/dist/shims/readonly-url-search-params.d.ts +11 -0
- package/dist/shims/readonly-url-search-params.d.ts.map +1 -0
- package/dist/shims/readonly-url-search-params.js +24 -0
- package/dist/shims/readonly-url-search-params.js.map +1 -0
- package/dist/shims/router.d.ts +4 -3
- package/dist/shims/router.d.ts.map +1 -1
- package/dist/shims/router.js +55 -48
- package/dist/shims/router.js.map +1 -1
- package/dist/shims/server.d.ts +1 -1
- package/dist/shims/server.d.ts.map +1 -1
- package/dist/shims/server.js +7 -13
- package/dist/shims/server.js.map +1 -1
- package/dist/shims/url-utils.d.ts +20 -6
- package/dist/shims/url-utils.d.ts.map +1 -1
- package/dist/shims/url-utils.js +79 -0
- package/dist/shims/url-utils.js.map +1 -1
- package/dist/utils/manifest-paths.d.ts +4 -0
- package/dist/utils/manifest-paths.d.ts.map +1 -0
- package/dist/utils/manifest-paths.js +20 -0
- package/dist/utils/manifest-paths.js.map +1 -0
- package/dist/utils/query.d.ts +9 -0
- package/dist/utils/query.d.ts.map +1 -1
- package/dist/utils/query.js +59 -9
- package/dist/utils/query.js.map +1 -1
- package/package.json +2 -2
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
import fs from "node:fs";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { generateDevOriginCheckCode } from "../server/dev-origin-check.js";
|
|
13
|
-
import { generateSafeRegExpCode, generateMiddlewareMatcherCode, generateNormalizePathCode, } from "../server/middleware-codegen.js";
|
|
13
|
+
import { generateSafeRegExpCode, generateMiddlewareMatcherCode, generateNormalizePathCode, generateRouteMatchNormalizationCode, } from "../server/middleware-codegen.js";
|
|
14
14
|
import { isProxyFile } from "../server/middleware.js";
|
|
15
15
|
// Pre-computed absolute paths for generated-code imports. The virtual RSC
|
|
16
16
|
// entry can't use relative imports (it has no real file location), so we
|
|
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, "/");
|
|
21
|
+
const routeTriePath = fileURLToPath(new URL("../routing/route-trie.js", import.meta.url)).replace(/\\/g, "/");
|
|
20
22
|
/**
|
|
21
23
|
* Generate the virtual RSC entry module.
|
|
22
24
|
*
|
|
@@ -32,6 +34,7 @@ export function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes,
|
|
|
32
34
|
const headers = config?.headers ?? [];
|
|
33
35
|
const allowedOrigins = config?.allowedOrigins ?? [];
|
|
34
36
|
const bodySizeLimit = config?.bodySizeLimit ?? 1 * 1024 * 1024;
|
|
37
|
+
const i18nConfig = config?.i18n ?? null;
|
|
35
38
|
// Build import map for all page and layout files
|
|
36
39
|
const imports = [];
|
|
37
40
|
const importMap = new Map();
|
|
@@ -200,7 +203,7 @@ import {
|
|
|
200
203
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
201
204
|
import { createElement, Suspense, Fragment } from "react";
|
|
202
205
|
import { setNavigationContext as _setNavigationContextOrig, getNavigationContext as _getNavigationContext } from "next/navigation";
|
|
203
|
-
import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader, getAndClearPendingCookies, consumeDynamicUsage, markDynamicUsage, runWithHeadersContext, applyMiddlewareRequestHeaders, getHeadersContext } from "next/headers";
|
|
206
|
+
import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader, getAndClearPendingCookies, consumeDynamicUsage, markDynamicUsage, runWithHeadersContext, applyMiddlewareRequestHeaders, getHeadersContext, setHeadersAccessPhase } from "next/headers";
|
|
204
207
|
import { NextRequest, NextFetchEvent } from "next/server";
|
|
205
208
|
import { ErrorBoundary, NotFoundBoundary } from "vinext/error-boundary";
|
|
206
209
|
import { LayoutSegmentProvider } from "vinext/layout-segment-context";
|
|
@@ -208,10 +211,12 @@ import { MetadataHead, mergeMetadata, resolveModuleMetadata, ViewportHead, merge
|
|
|
208
211
|
${middlewarePath ? `import * as middlewareModule from ${JSON.stringify(middlewarePath.replace(/\\/g, "/"))};` : ""}
|
|
209
212
|
${instrumentationPath ? `import * as _instrumentation from ${JSON.stringify(instrumentationPath.replace(/\\/g, "/"))};` : ""}
|
|
210
213
|
${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)};
|
|
214
|
+
import { requestContextFromRequest, normalizeHost, matchRedirect, matchRewrite, matchHeaders, isExternalUrl, proxyExternalRequest, sanitizeDestination } from ${JSON.stringify(configMatchersPath)};
|
|
212
215
|
import { validateCsrfOrigin, validateImageUrl, guardProtocolRelativeUrl, hasBasePath, stripBasePath, normalizeTrailingSlash, processMiddlewareHeaders } from ${JSON.stringify(requestPipelinePath)};
|
|
213
|
-
import { _consumeRequestScopedCacheLife, _runWithCacheState } from "next/cache";
|
|
214
|
-
import {
|
|
216
|
+
import { _consumeRequestScopedCacheLife, _runWithCacheState, getCacheHandler } from "next/cache";
|
|
217
|
+
import { runWithExecutionContext as _runWithExecutionContext, getRequestExecutionContext as _getRequestExecutionContext } from ${JSON.stringify(requestContextShimPath)};
|
|
218
|
+
import { getCollectedFetchTags, runWithFetchCache } from "vinext/fetch-cache";
|
|
219
|
+
import { buildRouteTrie as _buildRouteTrie, trieMatch as _trieMatch } from ${JSON.stringify(routeTriePath)};
|
|
215
220
|
import { runWithPrivateCache as _runWithPrivateCache } from "vinext/cache-runtime";
|
|
216
221
|
// Import server-only state module to register ALS-backed accessors.
|
|
217
222
|
import { runWithNavigationContext as _runWithNavigationContext } from "vinext/navigation-state";
|
|
@@ -247,7 +252,94 @@ function setNavigationContext(ctx) {
|
|
|
247
252
|
// ISR cache is disabled in dev mode — every request re-renders fresh,
|
|
248
253
|
// matching Next.js dev behavior. Cache-Control headers are still emitted
|
|
249
254
|
// based on export const revalidate for testing purposes.
|
|
250
|
-
// Production ISR
|
|
255
|
+
// Production ISR uses the MemoryCacheHandler (or configured KV handler).
|
|
256
|
+
//
|
|
257
|
+
// These helpers are inlined instead of imported from isr-cache.js because
|
|
258
|
+
// the virtual RSC entry module runs in the RSC Vite environment which
|
|
259
|
+
// cannot use dynamic imports at the module-evaluation level for server-only
|
|
260
|
+
// modules, and direct imports must use the pre-computed absolute paths.
|
|
261
|
+
async function __isrGet(key) {
|
|
262
|
+
const handler = getCacheHandler();
|
|
263
|
+
const result = await handler.get(key);
|
|
264
|
+
if (!result || !result.value) return null;
|
|
265
|
+
return { value: result, isStale: result.cacheState === "stale" };
|
|
266
|
+
}
|
|
267
|
+
async function __isrSet(key, data, revalidateSeconds, tags) {
|
|
268
|
+
const handler = getCacheHandler();
|
|
269
|
+
await handler.set(key, data, { revalidate: revalidateSeconds, tags: Array.isArray(tags) ? tags : [] });
|
|
270
|
+
}
|
|
271
|
+
function __pageCacheTags(pathname, extraTags) {
|
|
272
|
+
const tags = [pathname, "_N_T_" + pathname];
|
|
273
|
+
if (Array.isArray(extraTags)) {
|
|
274
|
+
for (const tag of extraTags) {
|
|
275
|
+
if (!tags.includes(tag)) tags.push(tag);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return tags;
|
|
279
|
+
}
|
|
280
|
+
// Note: cache entries are written with \`headers: undefined\`. Next.js stores
|
|
281
|
+
// response headers (e.g. set-cookie from cookies().set() during render) in the
|
|
282
|
+
// cache entry so they can be replayed on HIT. We don't do this because:
|
|
283
|
+
// 1. Pages that call cookies().set() during render trigger dynamicUsedDuringRender,
|
|
284
|
+
// which opts them out of ISR caching before we reach the write path.
|
|
285
|
+
// 2. Custom response headers set via next/headers are not yet captured separately
|
|
286
|
+
// from the live Response object in vinext's server pipeline.
|
|
287
|
+
// In practice this means ISR-cached responses won't replay render-time set-cookie
|
|
288
|
+
// headers — but that case is already prevented by the dynamic-usage opt-out.
|
|
289
|
+
// TODO: capture render-time response headers for full Next.js parity.
|
|
290
|
+
const __pendingRegenerations = new Map();
|
|
291
|
+
function __triggerBackgroundRegeneration(key, renderFn) {
|
|
292
|
+
if (__pendingRegenerations.has(key)) return;
|
|
293
|
+
const promise = renderFn()
|
|
294
|
+
.catch((err) => console.error("[vinext] ISR regen failed for " + key + ":", err))
|
|
295
|
+
.finally(() => __pendingRegenerations.delete(key));
|
|
296
|
+
__pendingRegenerations.set(key, promise);
|
|
297
|
+
const ctx = _getRequestExecutionContext();
|
|
298
|
+
if (ctx && typeof ctx.waitUntil === "function") ctx.waitUntil(promise);
|
|
299
|
+
}
|
|
300
|
+
// HTML and RSC are stored under separate keys — matching Next.js's file-system
|
|
301
|
+
// layout (.html / .rsc) — so each request type reads and writes its own key
|
|
302
|
+
// independently with no races or partial-entry sentinels.
|
|
303
|
+
//
|
|
304
|
+
// Key format: "app:<buildId>:<pathname>:<suffix>"
|
|
305
|
+
// Long-pathname fallback: "app:<buildId>:__hash:<fnv1a64(pathname)>:<suffix>"
|
|
306
|
+
// Without buildId (should not happen in production): "app:<pathname>:<suffix>"
|
|
307
|
+
// The 200-char threshold keeps the full key well under Cloudflare KV's 512-byte limit
|
|
308
|
+
// even after adding the build ID and suffix. FNV-1a 64 is used for the hash (two
|
|
309
|
+
// 32-bit rounds) to give a ~64-bit output with negligible collision probability for
|
|
310
|
+
// realistic pathname lengths.
|
|
311
|
+
// Keep prefix construction and hashing logic in sync with isrCacheKey() in server/isr-cache.ts.
|
|
312
|
+
function __isrFnv1a64(s) {
|
|
313
|
+
// h1 uses the standard FNV-1a 32-bit offset basis (0x811c9dc5).
|
|
314
|
+
let h1 = 0x811c9dc5;
|
|
315
|
+
for (let i = 0; i < s.length; i++) { h1 ^= s.charCodeAt(i); h1 = (h1 * 0x01000193) >>> 0; }
|
|
316
|
+
// h2 uses a different seed (0x050c5d1f — the FNV-1a hash of the string "vinext")
|
|
317
|
+
// so the two rounds are independently seeded and their outputs are decorrelated.
|
|
318
|
+
// Concatenating two independently-seeded 32-bit FNV-1a hashes gives an effective
|
|
319
|
+
// 64-bit hash. A random non-standard seed would also work; we derive it from a
|
|
320
|
+
// fixed string so the choice is auditable and deterministic across rebuilds.
|
|
321
|
+
let h2 = 0x050c5d1f;
|
|
322
|
+
for (let i = 0; i < s.length; i++) { h2 ^= s.charCodeAt(i); h2 = (h2 * 0x01000193) >>> 0; }
|
|
323
|
+
return h1.toString(36) + h2.toString(36);
|
|
324
|
+
}
|
|
325
|
+
function __isrCacheKey(pathname, suffix) {
|
|
326
|
+
const normalized = pathname === "/" ? "/" : pathname.replace(/\\/$/, "");
|
|
327
|
+
// __VINEXT_BUILD_ID is replaced at compile time by Vite's define plugin.
|
|
328
|
+
const buildId = process.env.__VINEXT_BUILD_ID;
|
|
329
|
+
const prefix = buildId ? "app:" + buildId : "app";
|
|
330
|
+
const key = prefix + ":" + normalized + ":" + suffix;
|
|
331
|
+
if (key.length <= 200) return key;
|
|
332
|
+
// Pathname too long — hash it to keep under KV's 512-byte key limit.
|
|
333
|
+
return prefix + ":__hash:" + __isrFnv1a64(normalized) + ":" + suffix;
|
|
334
|
+
}
|
|
335
|
+
function __isrHtmlKey(pathname) { return __isrCacheKey(pathname, "html"); }
|
|
336
|
+
function __isrRscKey(pathname) { return __isrCacheKey(pathname, "rsc"); }
|
|
337
|
+
// Verbose cache logging — opt in with NEXT_PRIVATE_DEBUG_CACHE=1.
|
|
338
|
+
// Matches the env var Next.js uses for its own cache debug output so operators
|
|
339
|
+
// have a single knob for all cache tracing.
|
|
340
|
+
const __isrDebug = process.env.NEXT_PRIVATE_DEBUG_CACHE
|
|
341
|
+
? console.debug.bind(console, "[vinext] ISR:")
|
|
342
|
+
: undefined;
|
|
251
343
|
|
|
252
344
|
// Normalize null-prototype objects from matchPattern() into thenable objects
|
|
253
345
|
// that work both as Promises (for Next.js 15+ async params) and as plain
|
|
@@ -394,9 +486,7 @@ function rscOnError(error, requestInfo, errorContext) {
|
|
|
394
486
|
error instanceof Error ? error : new Error(String(error)),
|
|
395
487
|
requestInfo,
|
|
396
488
|
errorContext,
|
|
397
|
-
)
|
|
398
|
-
console.error("[vinext] Failed to report render error:", reportErr);
|
|
399
|
-
});
|
|
489
|
+
);
|
|
400
490
|
}
|
|
401
491
|
|
|
402
492
|
// In production, generate a digest hash for non-navigation errors
|
|
@@ -462,6 +552,7 @@ async function __ensureInstrumentation() {
|
|
|
462
552
|
const routes = [
|
|
463
553
|
${routeEntries.join(",\n")}
|
|
464
554
|
];
|
|
555
|
+
const _routeTrie = _buildRouteTrie(routes);
|
|
465
556
|
|
|
466
557
|
const metadataRoutes = [
|
|
467
558
|
${metaRouteEntries.join(",\n")}
|
|
@@ -754,25 +845,23 @@ async function renderErrorBoundaryPage(route, error, isRscRequest, request, matc
|
|
|
754
845
|
});
|
|
755
846
|
}
|
|
756
847
|
|
|
757
|
-
function matchRoute(url
|
|
848
|
+
function matchRoute(url) {
|
|
758
849
|
const pathname = url.split("?")[0];
|
|
759
850
|
let normalizedUrl = pathname === "/" ? "/" : pathname.replace(/\\/$/, "");
|
|
760
851
|
// NOTE: Do NOT decodeURIComponent here. The caller is responsible for decoding
|
|
761
852
|
// the pathname exactly once at the request entry point. Decoding again here
|
|
762
853
|
// would cause inconsistent path matching between middleware and routing.
|
|
763
854
|
const urlParts = normalizedUrl.split("/").filter(Boolean);
|
|
764
|
-
|
|
765
|
-
const params = matchPattern(urlParts, route.patternParts);
|
|
766
|
-
if (params !== null) return { route, params };
|
|
767
|
-
}
|
|
768
|
-
return null;
|
|
855
|
+
return _trieMatch(_routeTrie, urlParts);
|
|
769
856
|
}
|
|
770
857
|
|
|
858
|
+
// matchPattern is kept for findIntercept (linear scan over small interceptLookup array).
|
|
771
859
|
function matchPattern(urlParts, patternParts) {
|
|
772
860
|
const params = Object.create(null);
|
|
773
861
|
for (let i = 0; i < patternParts.length; i++) {
|
|
774
862
|
const pp = patternParts[i];
|
|
775
863
|
if (pp.endsWith("+")) {
|
|
864
|
+
if (i !== patternParts.length - 1) return null;
|
|
776
865
|
const paramName = pp.slice(1, -1);
|
|
777
866
|
const remaining = urlParts.slice(i);
|
|
778
867
|
if (remaining.length === 0) return null;
|
|
@@ -780,6 +869,7 @@ function matchPattern(urlParts, patternParts) {
|
|
|
780
869
|
return params;
|
|
781
870
|
}
|
|
782
871
|
if (pp.endsWith("*")) {
|
|
872
|
+
if (i !== patternParts.length - 1) return null;
|
|
783
873
|
const paramName = pp.slice(1, -1);
|
|
784
874
|
params[paramName] = urlParts.slice(i);
|
|
785
875
|
return params;
|
|
@@ -1128,6 +1218,7 @@ ${middlewarePath ? generateMiddlewareMatcherCode("modern") : ""}
|
|
|
1128
1218
|
|
|
1129
1219
|
const __basePath = ${JSON.stringify(bp)};
|
|
1130
1220
|
const __trailingSlash = ${JSON.stringify(ts)};
|
|
1221
|
+
const __i18nConfig = ${JSON.stringify(i18nConfig)};
|
|
1131
1222
|
const __configRedirects = ${JSON.stringify(redirects)};
|
|
1132
1223
|
const __configRewrites = ${JSON.stringify(rewrites)};
|
|
1133
1224
|
const __configHeaders = ${JSON.stringify(headers)};
|
|
@@ -1140,6 +1231,7 @@ ${generateSafeRegExpCode("modern")}
|
|
|
1140
1231
|
|
|
1141
1232
|
// ── Path normalization ──────────────────────────────────────────────────
|
|
1142
1233
|
${generateNormalizePathCode("modern")}
|
|
1234
|
+
${generateRouteMatchNormalizationCode("modern")}
|
|
1143
1235
|
|
|
1144
1236
|
// ── Config pattern matching, redirects, rewrites, headers, CSRF validation,
|
|
1145
1237
|
// external URL proxy, cookie parsing, and request context are imported from
|
|
@@ -1166,7 +1258,7 @@ function __buildPostMwRequestContext(request) {
|
|
|
1166
1258
|
headers: ctx.headers,
|
|
1167
1259
|
cookies: cookiesRecord,
|
|
1168
1260
|
query: url.searchParams,
|
|
1169
|
-
host: ctx.headers.get("host")
|
|
1261
|
+
host: normalizeHost(ctx.headers.get("host"), url.hostname),
|
|
1170
1262
|
};
|
|
1171
1263
|
}
|
|
1172
1264
|
|
|
@@ -1236,7 +1328,7 @@ async function __readFormDataWithLimit(request, maxBytes) {
|
|
|
1236
1328
|
return new Response(combined, { headers: { "Content-Type": contentType } }).formData();
|
|
1237
1329
|
}
|
|
1238
1330
|
|
|
1239
|
-
export default async function handler(request) {
|
|
1331
|
+
export default async function handler(request, ctx) {
|
|
1240
1332
|
${instrumentationPath
|
|
1241
1333
|
? `// Ensure instrumentation.register() has run before handling the first request.
|
|
1242
1334
|
// This is a no-op after the first call (guarded by __instrumentationInitialized).
|
|
@@ -1248,8 +1340,12 @@ export default async function handler(request) {
|
|
|
1248
1340
|
// ALS scope that propagates through all async continuations (including RSC
|
|
1249
1341
|
// streaming), preventing state leakage between concurrent requests on
|
|
1250
1342
|
// Cloudflare Workers and other concurrent runtimes.
|
|
1343
|
+
//
|
|
1344
|
+
// runWithExecutionContext stores the Workers ExecutionContext (ctx) in ALS so
|
|
1345
|
+
// that KVCacheHandler._putInBackground can register background KV puts with
|
|
1346
|
+
// ctx.waitUntil() without needing ctx passed at construction time.
|
|
1251
1347
|
const headersCtx = headersContextFromRequest(request);
|
|
1252
|
-
|
|
1348
|
+
const _run = () => runWithHeadersContext(headersCtx, () =>
|
|
1253
1349
|
_runWithNavigationContext(() =>
|
|
1254
1350
|
_runWithCacheState(() =>
|
|
1255
1351
|
_runWithPrivateCache(() =>
|
|
@@ -1267,7 +1363,7 @@ export default async function handler(request) {
|
|
|
1267
1363
|
if (__configHeaders.length) {
|
|
1268
1364
|
const url = new URL(request.url);
|
|
1269
1365
|
let pathname;
|
|
1270
|
-
try { pathname = __normalizePath(
|
|
1366
|
+
try { pathname = __normalizePath(__normalizePathnameForRouteMatch(url.pathname)); } catch { pathname = url.pathname; }
|
|
1271
1367
|
${bp ? `if (pathname.startsWith(${JSON.stringify(bp)})) pathname = pathname.slice(${JSON.stringify(bp)}.length) || "/";` : ""}
|
|
1272
1368
|
const extraHeaders = matchHeaders(pathname, __configHeaders, __reqCtx);
|
|
1273
1369
|
for (const h of extraHeaders) {
|
|
@@ -1292,6 +1388,7 @@ export default async function handler(request) {
|
|
|
1292
1388
|
)
|
|
1293
1389
|
)
|
|
1294
1390
|
);
|
|
1391
|
+
return ctx ? _runWithExecutionContext(ctx, _run) : _run();
|
|
1295
1392
|
}
|
|
1296
1393
|
|
|
1297
1394
|
async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
@@ -1315,11 +1412,11 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1315
1412
|
const __protoGuard = guardProtocolRelativeUrl(url.pathname);
|
|
1316
1413
|
if (__protoGuard) return __protoGuard;
|
|
1317
1414
|
|
|
1318
|
-
// Decode percent-encoding and normalize pathname to canonical form.
|
|
1319
|
-
//
|
|
1415
|
+
// Decode percent-encoding segment-wise and normalize pathname to canonical form.
|
|
1416
|
+
// This preserves encoded path delimiters like %2F within a single segment.
|
|
1320
1417
|
// __normalizePath collapses //foo///bar → /foo/bar, resolves . and .. segments.
|
|
1321
1418
|
let decodedUrlPathname;
|
|
1322
|
-
try { decodedUrlPathname =
|
|
1419
|
+
try { decodedUrlPathname = __normalizePathnameForRouteMatchStrict(url.pathname); } catch (e) {
|
|
1323
1420
|
return new Response("Bad Request", { status: 400 });
|
|
1324
1421
|
}
|
|
1325
1422
|
let pathname = __normalizePath(decodedUrlPathname);
|
|
@@ -1379,7 +1476,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1379
1476
|
throw new Error("The " + _fileType + " file must export a function named \`" + _expectedExport + "\` or a \`default\` function.");
|
|
1380
1477
|
}
|
|
1381
1478
|
const middlewareMatcher = middlewareModule.config?.matcher;
|
|
1382
|
-
if (matchesMiddleware(cleanPathname, middlewareMatcher)) {
|
|
1479
|
+
if (matchesMiddleware(cleanPathname, middlewareMatcher, request, __i18nConfig)) {
|
|
1383
1480
|
try {
|
|
1384
1481
|
// Wrap in NextRequest so middleware gets .nextUrl, .cookies, .geo, .ip, etc.
|
|
1385
1482
|
// Always construct a new Request with the fully decoded + normalized pathname
|
|
@@ -1594,39 +1691,44 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1594
1691
|
const action = await loadServerAction(actionId);
|
|
1595
1692
|
let returnValue;
|
|
1596
1693
|
let actionRedirect = null;
|
|
1694
|
+
const previousHeadersPhase = setHeadersAccessPhase("action");
|
|
1597
1695
|
try {
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1696
|
+
try {
|
|
1697
|
+
const data = await action.apply(null, args);
|
|
1698
|
+
returnValue = { ok: true, data };
|
|
1699
|
+
} catch (e) {
|
|
1700
|
+
// Detect redirect() / permanentRedirect() called inside the action.
|
|
1701
|
+
// These throw errors with digest "NEXT_REDIRECT;replace;url[;status]".
|
|
1702
|
+
// The URL is encodeURIComponent-encoded to prevent semicolons in the URL
|
|
1703
|
+
// from corrupting the delimiter-based digest format.
|
|
1704
|
+
if (e && typeof e === "object" && "digest" in e) {
|
|
1705
|
+
const digest = String(e.digest);
|
|
1706
|
+
if (digest.startsWith("NEXT_REDIRECT;")) {
|
|
1707
|
+
const parts = digest.split(";");
|
|
1708
|
+
actionRedirect = {
|
|
1709
|
+
url: decodeURIComponent(parts[2]),
|
|
1710
|
+
type: parts[1] || "replace", // "push" or "replace"
|
|
1711
|
+
status: parts[3] ? parseInt(parts[3], 10) : 307,
|
|
1712
|
+
};
|
|
1713
|
+
returnValue = { ok: true, data: undefined };
|
|
1714
|
+
} else if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
|
|
1715
|
+
// notFound() / forbidden() / unauthorized() in action — package as error
|
|
1716
|
+
returnValue = { ok: false, data: e };
|
|
1717
|
+
} else {
|
|
1718
|
+
// Non-navigation digest error — sanitize in production to avoid
|
|
1719
|
+
// leaking internal details (connection strings, paths, etc.)
|
|
1720
|
+
console.error("[vinext] Server action error:", e);
|
|
1721
|
+
returnValue = { ok: false, data: __sanitizeErrorForClient(e) };
|
|
1722
|
+
}
|
|
1618
1723
|
} else {
|
|
1619
|
-
//
|
|
1620
|
-
//
|
|
1724
|
+
// Unhandled error — sanitize in production to avoid leaking
|
|
1725
|
+
// internal details (database errors, file paths, stack traces, etc.)
|
|
1621
1726
|
console.error("[vinext] Server action error:", e);
|
|
1622
1727
|
returnValue = { ok: false, data: __sanitizeErrorForClient(e) };
|
|
1623
1728
|
}
|
|
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
1729
|
}
|
|
1730
|
+
} finally {
|
|
1731
|
+
setHeadersAccessPhase(previousHeadersPhase);
|
|
1630
1732
|
}
|
|
1631
1733
|
|
|
1632
1734
|
// If the action called redirect(), signal the client to navigate.
|
|
@@ -1655,7 +1757,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1655
1757
|
|
|
1656
1758
|
// After the action, re-render the current page so the client
|
|
1657
1759
|
// gets an updated React tree reflecting any mutations.
|
|
1658
|
-
const match = matchRoute(cleanPathname
|
|
1760
|
+
const match = matchRoute(cleanPathname);
|
|
1659
1761
|
let element;
|
|
1660
1762
|
if (match) {
|
|
1661
1763
|
const { route: actionRoute, params: actionParams } = match;
|
|
@@ -1703,9 +1805,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1703
1805
|
err instanceof Error ? err : new Error(String(err)),
|
|
1704
1806
|
{ path: cleanPathname, method: request.method, headers: Object.fromEntries(request.headers.entries()) },
|
|
1705
1807
|
{ routerKind: "App Router", routePath: cleanPathname, routeType: "action" },
|
|
1706
|
-
)
|
|
1707
|
-
console.error("[vinext] Failed to report server action error:", reportErr);
|
|
1708
|
-
});
|
|
1808
|
+
);
|
|
1709
1809
|
setHeadersContext(null);
|
|
1710
1810
|
setNavigationContext(null);
|
|
1711
1811
|
return new Response(
|
|
@@ -1730,7 +1830,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1730
1830
|
}
|
|
1731
1831
|
}
|
|
1732
1832
|
|
|
1733
|
-
let match = matchRoute(cleanPathname
|
|
1833
|
+
let match = matchRoute(cleanPathname);
|
|
1734
1834
|
|
|
1735
1835
|
// ── Fallback rewrites from next.config.js (if no route matched) ───────
|
|
1736
1836
|
if (!match && __configRewrites.fallback && __configRewrites.fallback.length) {
|
|
@@ -1742,7 +1842,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1742
1842
|
return proxyExternalRequest(request, __fallbackRewritten);
|
|
1743
1843
|
}
|
|
1744
1844
|
cleanPathname = __fallbackRewritten;
|
|
1745
|
-
match = matchRoute(cleanPathname
|
|
1845
|
+
match = matchRoute(cleanPathname);
|
|
1746
1846
|
}
|
|
1747
1847
|
}
|
|
1748
1848
|
|
|
@@ -1779,16 +1879,39 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1779
1879
|
}
|
|
1780
1880
|
const hasDefault = typeof handler["default"] === "function";
|
|
1781
1881
|
|
|
1882
|
+
// Route handlers need the same middleware header/status merge behavior as
|
|
1883
|
+
// page responses. This keeps middleware response headers visible on API
|
|
1884
|
+
// routes in Workers/dev, and preserves custom rewrite status overrides.
|
|
1885
|
+
function attachRouteHandlerMiddlewareContext(response) {
|
|
1886
|
+
// _mwCtx.headers is only set (non-null) when middleware actually ran and
|
|
1887
|
+
// produced a continue/rewrite response. An empty Headers object (middleware
|
|
1888
|
+
// ran but produced no response headers) is a harmless edge case: the early
|
|
1889
|
+
// return is skipped, but the copy loop below is a no-op, so no incorrect
|
|
1890
|
+
// headers are added. The allocation cost in that case is acceptable.
|
|
1891
|
+
if (!_mwCtx.headers && _mwCtx.status == null) return response;
|
|
1892
|
+
const responseHeaders = new Headers(response.headers);
|
|
1893
|
+
if (_mwCtx.headers) {
|
|
1894
|
+
for (const [key, value] of _mwCtx.headers) {
|
|
1895
|
+
responseHeaders.append(key, value);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
return new Response(response.body, {
|
|
1899
|
+
status: _mwCtx.status ?? response.status,
|
|
1900
|
+
statusText: response.statusText,
|
|
1901
|
+
headers: responseHeaders,
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1782
1905
|
// OPTIONS auto-implementation: respond with Allow header and 204
|
|
1783
1906
|
if (method === "OPTIONS" && typeof handler["OPTIONS"] !== "function") {
|
|
1784
1907
|
const allowMethods = hasDefault ? HTTP_METHODS : exportedMethods;
|
|
1785
1908
|
if (!allowMethods.includes("OPTIONS")) allowMethods.push("OPTIONS");
|
|
1786
1909
|
setHeadersContext(null);
|
|
1787
1910
|
setNavigationContext(null);
|
|
1788
|
-
return new Response(null, {
|
|
1911
|
+
return attachRouteHandlerMiddlewareContext(new Response(null, {
|
|
1789
1912
|
status: 204,
|
|
1790
1913
|
headers: { "Allow": allowMethods.join(", ") },
|
|
1791
|
-
});
|
|
1914
|
+
}));
|
|
1792
1915
|
}
|
|
1793
1916
|
|
|
1794
1917
|
// HEAD auto-implementation: run GET handler and strip body
|
|
@@ -1800,6 +1923,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1800
1923
|
}
|
|
1801
1924
|
|
|
1802
1925
|
if (typeof handlerFn === "function") {
|
|
1926
|
+
const previousHeadersPhase = setHeadersAccessPhase("route-handler");
|
|
1803
1927
|
try {
|
|
1804
1928
|
const response = await handlerFn(request, { params });
|
|
1805
1929
|
const dynamicUsedInHandler = consumeDynamicUsage();
|
|
@@ -1831,28 +1955,28 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1831
1955
|
if (draftCookie) newHeaders.append("Set-Cookie", draftCookie);
|
|
1832
1956
|
|
|
1833
1957
|
if (isAutoHead) {
|
|
1834
|
-
return new Response(null, {
|
|
1958
|
+
return attachRouteHandlerMiddlewareContext(new Response(null, {
|
|
1835
1959
|
status: response.status,
|
|
1836
1960
|
statusText: response.statusText,
|
|
1837
1961
|
headers: newHeaders,
|
|
1838
|
-
});
|
|
1962
|
+
}));
|
|
1839
1963
|
}
|
|
1840
|
-
return new Response(response.body, {
|
|
1964
|
+
return attachRouteHandlerMiddlewareContext(new Response(response.body, {
|
|
1841
1965
|
status: response.status,
|
|
1842
1966
|
statusText: response.statusText,
|
|
1843
1967
|
headers: newHeaders,
|
|
1844
|
-
});
|
|
1968
|
+
}));
|
|
1845
1969
|
}
|
|
1846
1970
|
|
|
1847
1971
|
if (isAutoHead) {
|
|
1848
1972
|
// Strip body for auto-HEAD, preserve headers and status
|
|
1849
|
-
return new Response(null, {
|
|
1973
|
+
return attachRouteHandlerMiddlewareContext(new Response(null, {
|
|
1850
1974
|
status: response.status,
|
|
1851
1975
|
statusText: response.statusText,
|
|
1852
1976
|
headers: response.headers,
|
|
1853
|
-
});
|
|
1977
|
+
}));
|
|
1854
1978
|
}
|
|
1855
|
-
return response;
|
|
1979
|
+
return attachRouteHandlerMiddlewareContext(response);
|
|
1856
1980
|
} catch (err) {
|
|
1857
1981
|
getAndClearPendingCookies(); // Clear any pending cookies on error
|
|
1858
1982
|
// Catch redirect() / notFound() thrown from route handlers
|
|
@@ -1864,16 +1988,16 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1864
1988
|
const statusCode = parts[3] ? parseInt(parts[3], 10) : 307;
|
|
1865
1989
|
setHeadersContext(null);
|
|
1866
1990
|
setNavigationContext(null);
|
|
1867
|
-
return new Response(null, {
|
|
1991
|
+
return attachRouteHandlerMiddlewareContext(new Response(null, {
|
|
1868
1992
|
status: statusCode,
|
|
1869
1993
|
headers: { Location: new URL(redirectUrl, request.url).toString() },
|
|
1870
|
-
});
|
|
1994
|
+
}));
|
|
1871
1995
|
}
|
|
1872
1996
|
if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
|
|
1873
1997
|
const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
|
|
1874
1998
|
setHeadersContext(null);
|
|
1875
1999
|
setNavigationContext(null);
|
|
1876
|
-
return new Response(null, { status: statusCode });
|
|
2000
|
+
return attachRouteHandlerMiddlewareContext(new Response(null, { status: statusCode }));
|
|
1877
2001
|
}
|
|
1878
2002
|
}
|
|
1879
2003
|
setHeadersContext(null);
|
|
@@ -1883,18 +2007,18 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1883
2007
|
err instanceof Error ? err : new Error(String(err)),
|
|
1884
2008
|
{ path: cleanPathname, method: request.method, headers: Object.fromEntries(request.headers.entries()) },
|
|
1885
2009
|
{ routerKind: "App Router", routePath: route.pattern, routeType: "route" },
|
|
1886
|
-
)
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2010
|
+
);
|
|
2011
|
+
return attachRouteHandlerMiddlewareContext(new Response(null, { status: 500 }));
|
|
2012
|
+
} finally {
|
|
2013
|
+
setHeadersAccessPhase(previousHeadersPhase);
|
|
1890
2014
|
}
|
|
1891
2015
|
}
|
|
1892
2016
|
setHeadersContext(null);
|
|
1893
2017
|
setNavigationContext(null);
|
|
1894
|
-
return new Response(null, {
|
|
2018
|
+
return attachRouteHandlerMiddlewareContext(new Response(null, {
|
|
1895
2019
|
status: 405,
|
|
1896
2020
|
headers: { Allow: exportedMethods.join(", ") },
|
|
1897
|
-
});
|
|
2021
|
+
}));
|
|
1898
2022
|
}
|
|
1899
2023
|
|
|
1900
2024
|
// Build the component tree: layouts wrapping the page
|
|
@@ -1923,25 +2047,18 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1923
2047
|
});
|
|
1924
2048
|
}
|
|
1925
2049
|
|
|
1926
|
-
// dynamic = 'error':
|
|
2050
|
+
// dynamic = 'error': install an access error so request APIs fail with the
|
|
2051
|
+
// static-generation message even for legacy sync property access.
|
|
1927
2052
|
if (isDynamicError) {
|
|
1928
2053
|
const errorMsg = 'Page with \`dynamic = "error"\` used a dynamic API. ' +
|
|
1929
2054
|
'This page was expected to be fully static, but headers(), cookies(), ' +
|
|
1930
2055
|
'or searchParams was accessed. Remove the dynamic API usage or change ' +
|
|
1931
2056
|
'the dynamic config to "auto" or "force-dynamic".';
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
},
|
|
2057
|
+
setHeadersContext({
|
|
2058
|
+
headers: new Headers(),
|
|
2059
|
+
cookies: new Map(),
|
|
2060
|
+
accessError: new Error(errorMsg),
|
|
1937
2061
|
});
|
|
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
|
-
},
|
|
1943
|
-
});
|
|
1944
|
-
setHeadersContext({ headers: throwingHeaders, cookies: throwingCookies });
|
|
1945
2062
|
setNavigationContext({
|
|
1946
2063
|
pathname: cleanPathname,
|
|
1947
2064
|
searchParams: new URLSearchParams(),
|
|
@@ -1949,7 +2066,177 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1949
2066
|
});
|
|
1950
2067
|
}
|
|
1951
2068
|
|
|
1952
|
-
//
|
|
2069
|
+
// force-dynamic: set no-store Cache-Control
|
|
2070
|
+
const isForceDynamic = dynamicConfig === "force-dynamic";
|
|
2071
|
+
|
|
2072
|
+
// ── ISR cache read (production only) ─────────────────────────────────────
|
|
2073
|
+
// Read from cache BEFORE generateStaticParams and all rendering work.
|
|
2074
|
+
// This is the critical performance optimization: on a cache hit we skip
|
|
2075
|
+
// ALL expensive work (generateStaticParams, buildPageElement, layout probe,
|
|
2076
|
+
// page probe, renderToReadableStream, SSR). Both HTML and RSC requests
|
|
2077
|
+
// (client-side navigation / prefetch) are served from cache.
|
|
2078
|
+
//
|
|
2079
|
+
// HTML and RSC are stored under separate keys (matching Next.js's .html/.rsc
|
|
2080
|
+
// file layout) so each request type reads and writes independently — no races,
|
|
2081
|
+
// no partial-entry sentinels, no read-before-write hacks needed.
|
|
2082
|
+
//
|
|
2083
|
+
// force-static and dynamic='error' are compatible with ISR — they control
|
|
2084
|
+
// how dynamic APIs behave during rendering, not whether results are cached.
|
|
2085
|
+
// Only force-dynamic truly bypasses the ISR cache.
|
|
2086
|
+
if (
|
|
2087
|
+
process.env.NODE_ENV === "production" &&
|
|
2088
|
+
!isForceDynamic &&
|
|
2089
|
+
revalidateSeconds !== null && revalidateSeconds > 0 && revalidateSeconds !== Infinity
|
|
2090
|
+
) {
|
|
2091
|
+
const __isrKey = isRscRequest ? __isrRscKey(cleanPathname) : __isrHtmlKey(cleanPathname);
|
|
2092
|
+
try {
|
|
2093
|
+
const __cached = await __isrGet(__isrKey);
|
|
2094
|
+
if (__cached && !__cached.isStale && __cached.value.value && __cached.value.value.kind === "APP_PAGE") {
|
|
2095
|
+
const __cachedValue = __cached.value.value;
|
|
2096
|
+
const __hasRsc = !!__cachedValue.rscData;
|
|
2097
|
+
const __hasHtml = typeof __cachedValue.html === "string" && __cachedValue.html.length > 0;
|
|
2098
|
+
if (isRscRequest && __hasRsc) {
|
|
2099
|
+
__isrDebug?.("HIT (RSC)", cleanPathname);
|
|
2100
|
+
setHeadersContext(null);
|
|
2101
|
+
setNavigationContext(null);
|
|
2102
|
+
return new Response(__cachedValue.rscData, {
|
|
2103
|
+
status: __cachedValue.status || 200,
|
|
2104
|
+
headers: {
|
|
2105
|
+
"Content-Type": "text/x-component; charset=utf-8",
|
|
2106
|
+
"Cache-Control": "s-maxage=" + revalidateSeconds + ", stale-while-revalidate",
|
|
2107
|
+
"Vary": "RSC, Accept",
|
|
2108
|
+
"X-Vinext-Cache": "HIT",
|
|
2109
|
+
},
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
if (!isRscRequest && __hasHtml) {
|
|
2113
|
+
__isrDebug?.("HIT (HTML)", cleanPathname);
|
|
2114
|
+
setHeadersContext(null);
|
|
2115
|
+
setNavigationContext(null);
|
|
2116
|
+
return new Response(__cachedValue.html, {
|
|
2117
|
+
status: __cachedValue.status || 200,
|
|
2118
|
+
headers: {
|
|
2119
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
2120
|
+
"Cache-Control": "s-maxage=" + revalidateSeconds + ", stale-while-revalidate",
|
|
2121
|
+
"Vary": "RSC, Accept",
|
|
2122
|
+
"X-Vinext-Cache": "HIT",
|
|
2123
|
+
},
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
__isrDebug?.("MISS (empty cached entry)", cleanPathname);
|
|
2127
|
+
}
|
|
2128
|
+
if (__cached && __cached.isStale && __cached.value.value && __cached.value.value.kind === "APP_PAGE") {
|
|
2129
|
+
// Stale cache hit — serve stale immediately, trigger background regeneration.
|
|
2130
|
+
// Regen writes both keys independently so neither path blocks on the other.
|
|
2131
|
+
const __staleValue = __cached.value.value;
|
|
2132
|
+
const __staleStatus = __staleValue.status || 200;
|
|
2133
|
+
const __revalSecs = revalidateSeconds;
|
|
2134
|
+
__triggerBackgroundRegeneration(cleanPathname, async function() {
|
|
2135
|
+
// Re-render the page to produce fresh HTML + RSC data for the cache
|
|
2136
|
+
// Use an empty headers context for background regeneration — not the original
|
|
2137
|
+
// user request — to prevent user-specific cookies/auth headers from leaking
|
|
2138
|
+
// into content that is cached and served to all subsequent users.
|
|
2139
|
+
const __revalHeadCtx = { headers: new Headers(), cookies: new Map() };
|
|
2140
|
+
const __revalResult = await runWithHeadersContext(__revalHeadCtx, () =>
|
|
2141
|
+
_runWithNavigationContext(() =>
|
|
2142
|
+
_runWithCacheState(() =>
|
|
2143
|
+
_runWithPrivateCache(() =>
|
|
2144
|
+
runWithFetchCache(async () => {
|
|
2145
|
+
setNavigationContext({ pathname: cleanPathname, searchParams: url.searchParams, params });
|
|
2146
|
+
const __revalElement = await buildPageElement(route, params, undefined, url.searchParams);
|
|
2147
|
+
const __revalOnError = createRscOnErrorHandler(request, cleanPathname, route.pattern);
|
|
2148
|
+
const __revalRscStream = renderToReadableStream(__revalElement, { onError: __revalOnError });
|
|
2149
|
+
// Tee RSC stream: one for SSR, one to capture rscData
|
|
2150
|
+
const [__revalRscForSsr, __revalRscForCapture] = __revalRscStream.tee();
|
|
2151
|
+
// Capture rscData bytes in parallel with SSR
|
|
2152
|
+
const __rscDataPromise = (async () => {
|
|
2153
|
+
const __rscReader = __revalRscForCapture.getReader();
|
|
2154
|
+
const __rscChunks = [];
|
|
2155
|
+
let __rscTotal = 0;
|
|
2156
|
+
for (;;) {
|
|
2157
|
+
const { done, value } = await __rscReader.read();
|
|
2158
|
+
if (done) break;
|
|
2159
|
+
__rscChunks.push(value);
|
|
2160
|
+
__rscTotal += value.byteLength;
|
|
2161
|
+
}
|
|
2162
|
+
const __rscBuf = new Uint8Array(__rscTotal);
|
|
2163
|
+
let __rscOff = 0;
|
|
2164
|
+
for (const c of __rscChunks) { __rscBuf.set(c, __rscOff); __rscOff += c.byteLength; }
|
|
2165
|
+
return __rscBuf.buffer;
|
|
2166
|
+
})();
|
|
2167
|
+
const __revalFontData = { links: _getSSRFontLinks(), styles: _getSSRFontStyles(), preloads: _getSSRFontPreloads() };
|
|
2168
|
+
const __revalSsrEntry = await import.meta.viteRsc.loadModule("ssr", "index");
|
|
2169
|
+
const __revalHtmlStream = await __revalSsrEntry.handleSsr(__revalRscForSsr, _getNavigationContext(), __revalFontData);
|
|
2170
|
+
setHeadersContext(null);
|
|
2171
|
+
setNavigationContext(null);
|
|
2172
|
+
// Collect the full HTML string from the stream
|
|
2173
|
+
const __revalReader = __revalHtmlStream.getReader();
|
|
2174
|
+
const __revalDecoder = new TextDecoder();
|
|
2175
|
+
const __revalChunks = [];
|
|
2176
|
+
for (;;) {
|
|
2177
|
+
const { done, value } = await __revalReader.read();
|
|
2178
|
+
if (done) break;
|
|
2179
|
+
__revalChunks.push(__revalDecoder.decode(value, { stream: true }));
|
|
2180
|
+
}
|
|
2181
|
+
__revalChunks.push(__revalDecoder.decode());
|
|
2182
|
+
const __freshHtml = __revalChunks.join("");
|
|
2183
|
+
const __freshRscData = await __rscDataPromise;
|
|
2184
|
+
const __pageTags = __pageCacheTags(cleanPathname, getCollectedFetchTags());
|
|
2185
|
+
return { html: __freshHtml, rscData: __freshRscData, tags: __pageTags };
|
|
2186
|
+
})
|
|
2187
|
+
)
|
|
2188
|
+
)
|
|
2189
|
+
)
|
|
2190
|
+
);
|
|
2191
|
+
// Write HTML and RSC to their own keys independently — no races
|
|
2192
|
+
await Promise.all([
|
|
2193
|
+
__isrSet(__isrHtmlKey(cleanPathname), { kind: "APP_PAGE", html: __revalResult.html, rscData: undefined, headers: undefined, postponed: undefined, status: 200 }, __revalSecs, __revalResult.tags),
|
|
2194
|
+
__isrSet(__isrRscKey(cleanPathname), { kind: "APP_PAGE", html: "", rscData: __revalResult.rscData, headers: undefined, postponed: undefined, status: 200 }, __revalSecs, __revalResult.tags),
|
|
2195
|
+
]);
|
|
2196
|
+
__isrDebug?.("regen complete", cleanPathname);
|
|
2197
|
+
});
|
|
2198
|
+
if (isRscRequest && __staleValue.rscData) {
|
|
2199
|
+
__isrDebug?.("STALE (RSC)", cleanPathname);
|
|
2200
|
+
setHeadersContext(null);
|
|
2201
|
+
setNavigationContext(null);
|
|
2202
|
+
return new Response(__staleValue.rscData, {
|
|
2203
|
+
status: __staleStatus,
|
|
2204
|
+
headers: {
|
|
2205
|
+
"Content-Type": "text/x-component; charset=utf-8",
|
|
2206
|
+
"Cache-Control": "s-maxage=0, stale-while-revalidate",
|
|
2207
|
+
"Vary": "RSC, Accept",
|
|
2208
|
+
"X-Vinext-Cache": "STALE",
|
|
2209
|
+
},
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
if (!isRscRequest && typeof __staleValue.html === "string" && __staleValue.html.length > 0) {
|
|
2213
|
+
__isrDebug?.("STALE (HTML)", cleanPathname);
|
|
2214
|
+
setHeadersContext(null);
|
|
2215
|
+
setNavigationContext(null);
|
|
2216
|
+
return new Response(__staleValue.html, {
|
|
2217
|
+
status: __staleStatus,
|
|
2218
|
+
headers: {
|
|
2219
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
2220
|
+
"Cache-Control": "s-maxage=0, stale-while-revalidate",
|
|
2221
|
+
"Vary": "RSC, Accept",
|
|
2222
|
+
"X-Vinext-Cache": "STALE",
|
|
2223
|
+
},
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
// Stale entry exists but is empty for this request type — fall through to render
|
|
2227
|
+
__isrDebug?.("STALE MISS (empty stale entry)", cleanPathname);
|
|
2228
|
+
}
|
|
2229
|
+
if (!__cached) {
|
|
2230
|
+
__isrDebug?.("MISS (no cache entry)", cleanPathname);
|
|
2231
|
+
}
|
|
2232
|
+
} catch (__isrReadErr) {
|
|
2233
|
+
// Cache read failure — fall through to normal rendering
|
|
2234
|
+
console.error("[vinext] ISR cache read error:", __isrReadErr);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
// dynamicParams = false: only params from generateStaticParams are allowed.
|
|
2239
|
+
// This runs AFTER the ISR cache read so that a cache hit skips this work entirely.
|
|
1953
2240
|
if (dynamicParamsConfig === false && route.isDynamic && typeof route.page?.generateStaticParams === "function") {
|
|
1954
2241
|
try {
|
|
1955
2242
|
// Pass parent params to generateStaticParams (Next.js top-down params passing).
|
|
@@ -1979,9 +2266,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1979
2266
|
}
|
|
1980
2267
|
}
|
|
1981
2268
|
|
|
1982
|
-
// force-dynamic: set no-store Cache-Control
|
|
1983
|
-
const isForceDynamic = dynamicConfig === "force-dynamic";
|
|
1984
|
-
|
|
1985
2269
|
// Check for intercepting routes on RSC requests (client-side navigation).
|
|
1986
2270
|
// If the target URL matches an intercepting route in a parallel slot,
|
|
1987
2271
|
// render the source route with the intercepting page in the slot.
|
|
@@ -1992,7 +2276,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1992
2276
|
const sourceRoute = routes[intercept.sourceRouteIndex];
|
|
1993
2277
|
if (sourceRoute && sourceRoute !== route) {
|
|
1994
2278
|
// Render the source route (e.g. /feed) with the intercepting page in the slot
|
|
1995
|
-
const sourceMatch = matchRoute(sourceRoute.pattern
|
|
2279
|
+
const sourceMatch = matchRoute(sourceRoute.pattern);
|
|
1996
2280
|
const sourceParams = sourceMatch ? sourceMatch.params : {};
|
|
1997
2281
|
setNavigationContext({
|
|
1998
2282
|
pathname: cleanPathname,
|
|
@@ -2214,6 +2498,34 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2214
2498
|
};
|
|
2215
2499
|
const rscStream = renderToReadableStream(element, { onError: onRenderError });
|
|
2216
2500
|
|
|
2501
|
+
// For ISR pages in production: tee the RSC stream immediately after creation so we
|
|
2502
|
+
// can capture rscData for BOTH RSC requests (client-side nav/prefetch) and HTML
|
|
2503
|
+
// requests. The tee must happen here — before the isRscRequest branch — so both
|
|
2504
|
+
// paths can use the captured bytes when writing to the ISR cache.
|
|
2505
|
+
// __rscForResponse → sent to the client (RSC response) or to SSR (HTML response)
|
|
2506
|
+
// __isrRscDataPromise → resolves to ArrayBuffer of captured RSC wire bytes
|
|
2507
|
+
let __rscForResponse = rscStream;
|
|
2508
|
+
let __isrRscDataPromise = null;
|
|
2509
|
+
if (process.env.NODE_ENV === "production" && revalidateSeconds !== null && revalidateSeconds > 0 && revalidateSeconds !== Infinity && !isForceDynamic) {
|
|
2510
|
+
const [__rscA, __rscB] = rscStream.tee();
|
|
2511
|
+
__rscForResponse = __rscA;
|
|
2512
|
+
__isrRscDataPromise = (async () => {
|
|
2513
|
+
const __rscReader = __rscB.getReader();
|
|
2514
|
+
const __rscChunks = [];
|
|
2515
|
+
let __rscTotal = 0;
|
|
2516
|
+
for (;;) {
|
|
2517
|
+
const { done, value } = await __rscReader.read();
|
|
2518
|
+
if (done) break;
|
|
2519
|
+
__rscChunks.push(value);
|
|
2520
|
+
__rscTotal += value.byteLength;
|
|
2521
|
+
}
|
|
2522
|
+
const __rscBuf = new Uint8Array(__rscTotal);
|
|
2523
|
+
let __rscOff = 0;
|
|
2524
|
+
for (const c of __rscChunks) { __rscBuf.set(c, __rscOff); __rscOff += c.byteLength; }
|
|
2525
|
+
return __rscBuf.buffer;
|
|
2526
|
+
})();
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2217
2529
|
if (isRscRequest) {
|
|
2218
2530
|
// Direct RSC stream response (for client-side navigation)
|
|
2219
2531
|
// NOTE: Do NOT clear headers/navigation context here!
|
|
@@ -2230,6 +2542,9 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2230
2542
|
} else if ((isForceStatic || isDynamicError) && !revalidateSeconds) {
|
|
2231
2543
|
responseHeaders["Cache-Control"] = "s-maxage=31536000, stale-while-revalidate";
|
|
2232
2544
|
responseHeaders["X-Vinext-Cache"] = "STATIC";
|
|
2545
|
+
} else if (revalidateSeconds === Infinity) {
|
|
2546
|
+
responseHeaders["Cache-Control"] = "s-maxage=31536000, stale-while-revalidate";
|
|
2547
|
+
responseHeaders["X-Vinext-Cache"] = "STATIC";
|
|
2233
2548
|
} else if (revalidateSeconds) {
|
|
2234
2549
|
responseHeaders["Cache-Control"] = "s-maxage=" + revalidateSeconds + ", stale-while-revalidate";
|
|
2235
2550
|
}
|
|
@@ -2279,7 +2594,26 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2279
2594
|
const compileMs = __compileEnd !== undefined ? Math.round(__compileEnd - __reqStart) : -1;
|
|
2280
2595
|
responseHeaders["x-vinext-timing"] = handlerStart + "," + compileMs + ",-1";
|
|
2281
2596
|
}
|
|
2282
|
-
|
|
2597
|
+
// For ISR-eligible RSC requests in production: write rscData to its own key.
|
|
2598
|
+
// HTML is stored under a separate key (written by the HTML path below) so
|
|
2599
|
+
// these writes never race or clobber each other.
|
|
2600
|
+
if (process.env.NODE_ENV === "production" && __isrRscDataPromise) {
|
|
2601
|
+
responseHeaders["X-Vinext-Cache"] = "MISS";
|
|
2602
|
+
const __isrKeyRsc = __isrRscKey(cleanPathname);
|
|
2603
|
+
const __revalSecsRsc = revalidateSeconds;
|
|
2604
|
+
const __rscWritePromise = (async () => {
|
|
2605
|
+
try {
|
|
2606
|
+
const __rscDataForCache = await __isrRscDataPromise;
|
|
2607
|
+
const __pageTags = __pageCacheTags(cleanPathname, getCollectedFetchTags());
|
|
2608
|
+
await __isrSet(__isrKeyRsc, { kind: "APP_PAGE", html: "", rscData: __rscDataForCache, headers: undefined, postponed: undefined, status: 200 }, __revalSecsRsc, __pageTags);
|
|
2609
|
+
__isrDebug?.("RSC cache written", __isrKeyRsc);
|
|
2610
|
+
} catch (__rscWriteErr) {
|
|
2611
|
+
console.error("[vinext] ISR RSC cache write error:", __rscWriteErr);
|
|
2612
|
+
}
|
|
2613
|
+
})();
|
|
2614
|
+
_getRequestExecutionContext()?.waitUntil(__rscWritePromise);
|
|
2615
|
+
}
|
|
2616
|
+
return new Response(__rscForResponse, { status: _mwCtx.status || 200, headers: responseHeaders });
|
|
2283
2617
|
}
|
|
2284
2618
|
|
|
2285
2619
|
// Collect font data from RSC environment before passing to SSR
|
|
@@ -2300,11 +2634,16 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2300
2634
|
}
|
|
2301
2635
|
const fontLinkHeader = fontLinkHeaderParts.length > 0 ? fontLinkHeaderParts.join(", ") : "";
|
|
2302
2636
|
|
|
2637
|
+
// __rscForResponse was already teed above (before isRscRequest) for ISR pages in
|
|
2638
|
+
// production. For non-ISR or dev, __rscForResponse === rscStream (no tee).
|
|
2639
|
+
// __isrRscDataPromise resolves to rscData bytes used by the RSC write path above;
|
|
2640
|
+
// the HTML write path below uses its own separate key and does not need rscData.
|
|
2641
|
+
|
|
2303
2642
|
// Delegate to SSR environment for HTML rendering
|
|
2304
2643
|
let htmlStream;
|
|
2305
2644
|
try {
|
|
2306
2645
|
const ssrEntry = await import.meta.viteRsc.loadModule("ssr", "index");
|
|
2307
|
-
htmlStream = await ssrEntry.handleSsr(
|
|
2646
|
+
htmlStream = await ssrEntry.handleSsr(__rscForResponse, _getNavigationContext(), fontData);
|
|
2308
2647
|
// Shell render complete; Suspense boundaries stream asynchronously
|
|
2309
2648
|
if (process.env.NODE_ENV !== "production") __renderEnd = performance.now();
|
|
2310
2649
|
} catch (ssrErr) {
|
|
@@ -2408,8 +2747,8 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2408
2747
|
// force-static / error: treat as static regardless of dynamic usage.
|
|
2409
2748
|
// force-static intentionally provides empty headers/cookies context so
|
|
2410
2749
|
// dynamic APIs return safe defaults; we ignore the dynamic usage signal.
|
|
2411
|
-
// dynamic='error' should have already thrown
|
|
2412
|
-
// code
|
|
2750
|
+
// dynamic='error' should have already thrown via the request API accessError
|
|
2751
|
+
// trap if user code touched a dynamic API, so reaching here means rendering succeeded.
|
|
2413
2752
|
if ((isForceStatic || isDynamicError) && (revalidateSeconds === null || revalidateSeconds === 0)) {
|
|
2414
2753
|
return attachMiddlewareContext(new Response(htmlStream, {
|
|
2415
2754
|
headers: {
|
|
@@ -2433,9 +2772,72 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2433
2772
|
}));
|
|
2434
2773
|
}
|
|
2435
2774
|
|
|
2436
|
-
// Emit Cache-Control for ISR pages
|
|
2437
|
-
//
|
|
2438
|
-
|
|
2775
|
+
// Emit Cache-Control for ISR pages and write to ISR cache on MISS (production only).
|
|
2776
|
+
// revalidate=Infinity means "cache forever" (no periodic revalidation) — treated as
|
|
2777
|
+
// static here so we emit s-maxage=31536000 but skip ISR cache management.
|
|
2778
|
+
if (revalidateSeconds !== null && revalidateSeconds > 0 && revalidateSeconds !== Infinity) {
|
|
2779
|
+
// In production, tee the HTML response body to simultaneously stream to the
|
|
2780
|
+
// client and collect the full HTML string for the ISR cache. rscData was
|
|
2781
|
+
// already captured above by teeing the RSC stream before SSR.
|
|
2782
|
+
// In dev, skip the tee and the X-Vinext-Cache header — every request renders
|
|
2783
|
+
// fresh (no cache reads or writes in dev mode).
|
|
2784
|
+
if (process.env.NODE_ENV === "production") {
|
|
2785
|
+
const __isrResponseProd = attachMiddlewareContext(new Response(htmlStream, {
|
|
2786
|
+
headers: {
|
|
2787
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
2788
|
+
"Cache-Control": "s-maxage=" + revalidateSeconds + ", stale-while-revalidate",
|
|
2789
|
+
"Vary": "RSC, Accept",
|
|
2790
|
+
"X-Vinext-Cache": "MISS",
|
|
2791
|
+
},
|
|
2792
|
+
}));
|
|
2793
|
+
if (__isrResponseProd.body) {
|
|
2794
|
+
const [__streamForClient, __streamForCache] = __isrResponseProd.body.tee();
|
|
2795
|
+
const __isrKey = __isrHtmlKey(cleanPathname);
|
|
2796
|
+
const __isrKeyRscFromHtml = __isrRscKey(cleanPathname);
|
|
2797
|
+
const __revalSecs = revalidateSeconds;
|
|
2798
|
+
const __capturedRscDataPromise = __isrRscDataPromise;
|
|
2799
|
+
const __cachePromise = (async () => {
|
|
2800
|
+
try {
|
|
2801
|
+
const __reader = __streamForCache.getReader();
|
|
2802
|
+
const __decoder = new TextDecoder();
|
|
2803
|
+
const __chunks = [];
|
|
2804
|
+
for (;;) {
|
|
2805
|
+
const { done, value } = await __reader.read();
|
|
2806
|
+
if (done) break;
|
|
2807
|
+
__chunks.push(__decoder.decode(value, { stream: true }));
|
|
2808
|
+
}
|
|
2809
|
+
__chunks.push(__decoder.decode());
|
|
2810
|
+
const __fullHtml = __chunks.join("");
|
|
2811
|
+
const __pageTags = __pageCacheTags(cleanPathname, getCollectedFetchTags());
|
|
2812
|
+
// Write HTML and RSC to their own keys independently.
|
|
2813
|
+
// RSC data was captured by the tee above (before isRscRequest branch)
|
|
2814
|
+
// so an initial browser visit (HTML request) also populates the RSC key,
|
|
2815
|
+
// ensuring the first client-side navigation after a direct visit is a
|
|
2816
|
+
// cache hit rather than a miss.
|
|
2817
|
+
const __writes = [
|
|
2818
|
+
__isrSet(__isrKey, { kind: "APP_PAGE", html: __fullHtml, rscData: undefined, headers: undefined, postponed: undefined, status: 200 }, __revalSecs, __pageTags),
|
|
2819
|
+
];
|
|
2820
|
+
if (__capturedRscDataPromise) {
|
|
2821
|
+
__writes.push(
|
|
2822
|
+
__capturedRscDataPromise.then((__rscBuf) =>
|
|
2823
|
+
__isrSet(__isrKeyRscFromHtml, { kind: "APP_PAGE", html: "", rscData: __rscBuf, headers: undefined, postponed: undefined, status: 200 }, __revalSecs, __pageTags)
|
|
2824
|
+
)
|
|
2825
|
+
);
|
|
2826
|
+
}
|
|
2827
|
+
await Promise.all(__writes);
|
|
2828
|
+
__isrDebug?.("HTML cache written", __isrKey);
|
|
2829
|
+
} catch (__cacheErr) {
|
|
2830
|
+
console.error("[vinext] ISR cache write error:", __cacheErr);
|
|
2831
|
+
}
|
|
2832
|
+
})();
|
|
2833
|
+
// Register with ExecutionContext (from ALS) so the Workers runtime keeps
|
|
2834
|
+
// the isolate alive until the cache write finishes, even after the response is sent.
|
|
2835
|
+
_getRequestExecutionContext()?.waitUntil(__cachePromise);
|
|
2836
|
+
return new Response(__streamForClient, { status: __isrResponseProd.status, headers: __isrResponseProd.headers });
|
|
2837
|
+
}
|
|
2838
|
+
return __isrResponseProd;
|
|
2839
|
+
}
|
|
2840
|
+
// Dev mode: return Cache-Control header but no X-Vinext-Cache (no cache read/write)
|
|
2439
2841
|
return attachMiddlewareContext(new Response(htmlStream, {
|
|
2440
2842
|
headers: {
|
|
2441
2843
|
"Content-Type": "text/html; charset=utf-8",
|
|
@@ -2445,6 +2847,19 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2445
2847
|
}));
|
|
2446
2848
|
}
|
|
2447
2849
|
|
|
2850
|
+
// revalidate=Infinity (or false, which Next.js normalises to false/0): treat as
|
|
2851
|
+
// permanent static — emit the longest safe s-maxage but skip ISR cache management.
|
|
2852
|
+
if (revalidateSeconds === Infinity) {
|
|
2853
|
+
return attachMiddlewareContext(new Response(htmlStream, {
|
|
2854
|
+
headers: {
|
|
2855
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
2856
|
+
"Cache-Control": "s-maxage=31536000, stale-while-revalidate",
|
|
2857
|
+
"X-Vinext-Cache": "STATIC",
|
|
2858
|
+
"Vary": "RSC, Accept",
|
|
2859
|
+
},
|
|
2860
|
+
}));
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2448
2863
|
return attachMiddlewareContext(new Response(htmlStream, {
|
|
2449
2864
|
headers: { "Content-Type": "text/html; charset=utf-8", "Vary": "RSC, Accept" },
|
|
2450
2865
|
}));
|