vinext 0.0.26 → 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.
- package/README.md +89 -85
- 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 +5 -9
- package/dist/build/static-export.js.map +1 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +152 -48
- package/dist/check.js.map +1 -1
- package/dist/cli.js +10 -11
- package/dist/cli.js.map +1 -1
- package/dist/cloudflare/kv-cache-handler.d.ts +43 -1
- package/dist/cloudflare/kv-cache-handler.d.ts.map +1 -1
- package/dist/cloudflare/kv-cache-handler.js +135 -44
- package/dist/cloudflare/kv-cache-handler.js.map +1 -1
- package/dist/cloudflare/tpr.d.ts.map +1 -1
- package/dist/cloudflare/tpr.js +15 -4
- package/dist/cloudflare/tpr.js.map +1 -1
- package/dist/config/config-matchers.d.ts +28 -0
- package/dist/config/config-matchers.d.ts.map +1 -1
- package/dist/config/config-matchers.js +353 -79
- package/dist/config/config-matchers.js.map +1 -1
- package/dist/config/dotenv.d.ts.map +1 -1
- package/dist/config/dotenv.js +1 -6
- package/dist/config/dotenv.js.map +1 -1
- package/dist/config/next-config.d.ts +7 -0
- package/dist/config/next-config.d.ts.map +1 -1
- package/dist/config/next-config.js +44 -19
- 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 +81 -48
- 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 +584 -113
- package/dist/entries/app-rsc-entry.js.map +1 -1
- package/dist/entries/pages-client-entry.d.ts.map +1 -1
- package/dist/entries/pages-client-entry.js +5 -3
- package/dist/entries/pages-client-entry.js.map +1 -1
- package/dist/entries/pages-server-entry.d.ts.map +1 -1
- package/dist/entries/pages-server-entry.js +100 -32
- package/dist/entries/pages-server-entry.js.map +1 -1
- package/dist/index.d.ts +24 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +327 -154
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +6 -5
- package/dist/init.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 +70 -107
- package/dist/routing/app-router.js.map +1 -1
- package/dist/routing/file-matcher.d.ts.map +1 -1
- package/dist/routing/file-matcher.js.map +1 -1
- package/dist/routing/pages-router.d.ts +3 -1
- package/dist/routing/pages-router.d.ts.map +1 -1
- package/dist/routing/pages-router.js +33 -18
- package/dist/routing/pages-router.js.map +1 -1
- 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 +124 -0
- package/dist/routing/route-validation.js.map +1 -0
- package/dist/routing/utils.d.ts.map +1 -1
- package/dist/routing/utils.js.map +1 -1
- package/dist/server/api-handler.d.ts.map +1 -1
- package/dist/server/api-handler.js +31 -9
- package/dist/server/api-handler.js.map +1 -1
- package/dist/server/app-router-entry.d.ts +3 -2
- package/dist/server/app-router-entry.d.ts.map +1 -1
- package/dist/server/app-router-entry.js +8 -4
- package/dist/server/app-router-entry.js.map +1 -1
- package/dist/server/dev-module-runner.d.ts.map +1 -1
- package/dist/server/dev-module-runner.js +1 -1
- package/dist/server/dev-module-runner.js.map +1 -1
- package/dist/server/dev-origin-check.d.ts.map +1 -1
- package/dist/server/dev-origin-check.js.map +1 -1
- package/dist/server/dev-server.d.ts.map +1 -1
- package/dist/server/dev-server.js +39 -21
- 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.map +1 -1
- package/dist/server/instrumentation.js +1 -1
- package/dist/server/instrumentation.js.map +1 -1
- package/dist/server/isr-cache.d.ts +5 -1
- package/dist/server/isr-cache.d.ts.map +1 -1
- package/dist/server/isr-cache.js +13 -3
- 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 +78 -45
- package/dist/server/metadata-routes.js.map +1 -1
- package/dist/server/middleware-codegen.d.ts +1 -1
- package/dist/server/middleware-codegen.d.ts.map +1 -1
- package/dist/server/middleware-codegen.js +177 -22
- package/dist/server/middleware-codegen.js.map +1 -1
- package/dist/server/middleware-request-headers.d.ts +9 -0
- package/dist/server/middleware-request-headers.d.ts.map +1 -0
- package/dist/server/middleware-request-headers.js +77 -0
- package/dist/server/middleware-request-headers.js.map +1 -0
- package/dist/server/middleware.d.ts +9 -8
- package/dist/server/middleware.d.ts.map +1 -1
- package/dist/server/middleware.js +112 -32
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/normalize-path.js.map +1 -1
- package/dist/server/prod-server.d.ts +1 -1
- package/dist/server/prod-server.d.ts.map +1 -1
- package/dist/server/prod-server.js +127 -82
- package/dist/server/prod-server.js.map +1 -1
- package/dist/server/request-pipeline.d.ts +2 -1
- package/dist/server/request-pipeline.d.ts.map +1 -1
- package/dist/server/request-pipeline.js +5 -7
- package/dist/server/request-pipeline.js.map +1 -1
- package/dist/shims/cache-runtime.d.ts.map +1 -1
- package/dist/shims/cache-runtime.js +21 -16
- package/dist/shims/cache-runtime.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 +38 -25
- package/dist/shims/cache.js.map +1 -1
- package/dist/shims/constants.d.ts.map +1 -1
- package/dist/shims/constants.js +1 -6
- package/dist/shims/constants.js.map +1 -1
- package/dist/shims/dynamic.d.ts.map +1 -1
- package/dist/shims/dynamic.js +1 -1
- package/dist/shims/dynamic.js.map +1 -1
- package/dist/shims/error-boundary.d.ts.map +1 -1
- package/dist/shims/error-boundary.js +2 -3
- package/dist/shims/error-boundary.js.map +1 -1
- package/dist/shims/error.d.ts.map +1 -1
- package/dist/shims/error.js +1 -3
- package/dist/shims/error.js.map +1 -1
- package/dist/shims/fetch-cache.d.ts.map +1 -1
- package/dist/shims/fetch-cache.js +57 -30
- package/dist/shims/fetch-cache.js.map +1 -1
- package/dist/shims/font-google-base.d.ts.map +1 -1
- package/dist/shims/font-google-base.js +16 -4
- package/dist/shims/font-google-base.js.map +1 -1
- package/dist/shims/font-google.d.ts +1 -1
- package/dist/shims/font-google.d.ts.map +1 -1
- package/dist/shims/font-google.generated.d.ts.map +1 -1
- package/dist/shims/font-google.generated.js +412 -206
- package/dist/shims/font-google.generated.js.map +1 -1
- package/dist/shims/font-google.js +1 -1
- package/dist/shims/font-google.js.map +1 -1
- package/dist/shims/font-local.d.ts.map +1 -1
- package/dist/shims/font-local.js +13 -3
- package/dist/shims/font-local.js.map +1 -1
- package/dist/shims/form.d.ts.map +1 -1
- package/dist/shims/form.js +105 -10
- package/dist/shims/form.js.map +1 -1
- package/dist/shims/head.d.ts.map +1 -1
- package/dist/shims/head.js +10 -8
- package/dist/shims/head.js.map +1 -1
- package/dist/shims/headers.d.ts +34 -8
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/headers.js +268 -53
- package/dist/shims/headers.js.map +1 -1
- package/dist/shims/image.d.ts.map +1 -1
- package/dist/shims/image.js +35 -8
- package/dist/shims/image.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/legacy-image.d.ts.map +1 -1
- package/dist/shims/legacy-image.js +1 -1
- package/dist/shims/legacy-image.js.map +1 -1
- package/dist/shims/link.d.ts +2 -1
- package/dist/shims/link.d.ts.map +1 -1
- package/dist/shims/link.js +37 -17
- package/dist/shims/link.js.map +1 -1
- package/dist/shims/metadata.d.ts +12 -2
- package/dist/shims/metadata.d.ts.map +1 -1
- package/dist/shims/metadata.js +10 -8
- package/dist/shims/metadata.js.map +1 -1
- package/dist/shims/navigation-state.d.ts.map +1 -1
- package/dist/shims/navigation-state.js +3 -2
- package/dist/shims/navigation-state.js.map +1 -1
- package/dist/shims/navigation.d.ts +3 -7
- package/dist/shims/navigation.d.ts.map +1 -1
- package/dist/shims/navigation.js +46 -29
- 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/request-context.d.ts +50 -0
- package/dist/shims/request-context.d.ts.map +1 -0
- package/dist/shims/request-context.js +59 -0
- package/dist/shims/request-context.js.map +1 -0
- package/dist/shims/router-state.d.ts.map +1 -1
- package/dist/shims/router-state.js +2 -1
- package/dist/shims/router-state.js.map +1 -1
- package/dist/shims/router.d.ts +4 -3
- package/dist/shims/router.d.ts.map +1 -1
- package/dist/shims/router.js +59 -53
- package/dist/shims/router.js.map +1 -1
- package/dist/shims/script.d.ts.map +1 -1
- package/dist/shims/script.js.map +1 -1
- package/dist/shims/server.d.ts +14 -1
- package/dist/shims/server.d.ts.map +1 -1
- package/dist/shims/server.js +107 -47
- package/dist/shims/server.js.map +1 -1
- package/dist/shims/url-utils.d.ts.map +1 -1
- package/dist/shims/url-utils.js +1 -3
- package/dist/shims/url-utils.js.map +1 -1
- package/dist/utils/base-path.d.ts +17 -0
- package/dist/utils/base-path.d.ts.map +1 -0
- package/dist/utils/base-path.js +25 -0
- package/dist/utils/base-path.js.map +1 -0
- 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/project.d.ts.map +1 -1
- package/dist/utils/project.js +2 -4
- package/dist/utils/project.js.map +1 -1
- package/dist/utils/query.d.ts +9 -0
- package/dist/utils/query.d.ts.map +1 -1
- package/dist/utils/query.js +59 -7
- package/dist/utils/query.js.map +1 -1
- package/package.json +47 -33
|
@@ -10,13 +10,14 @@
|
|
|
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, } 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, "/");
|
|
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();
|
|
@@ -96,7 +98,7 @@ export function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes,
|
|
|
96
98
|
const routeEntries = routes.map((route) => {
|
|
97
99
|
const layoutVars = route.layouts.map((l) => getImportVar(l));
|
|
98
100
|
const templateVars = route.templates.map((t) => getImportVar(t));
|
|
99
|
-
const notFoundVars = (route.notFoundPaths || []).map((nf) => nf ? getImportVar(nf) : "null");
|
|
101
|
+
const notFoundVars = (route.notFoundPaths || []).map((nf) => (nf ? getImportVar(nf) : "null"));
|
|
100
102
|
const slotEntries = route.parallelSlots.map((slot) => {
|
|
101
103
|
const interceptEntries = slot.interceptingRoutes.map((ir) => {
|
|
102
104
|
return ` {
|
|
@@ -121,6 +123,7 @@ ${interceptEntries.join(",\n")}
|
|
|
121
123
|
const layoutErrorVars = (route.layoutErrorPaths || []).map((ep) => ep ? getImportVar(ep) : "null");
|
|
122
124
|
return ` {
|
|
123
125
|
pattern: ${JSON.stringify(route.pattern)},
|
|
126
|
+
patternParts: ${JSON.stringify(route.patternParts)},
|
|
124
127
|
isDynamic: ${route.isDynamic},
|
|
125
128
|
params: ${JSON.stringify(route.params)},
|
|
126
129
|
page: ${route.pagePath ? getImportVar(route.pagePath) : "null"},
|
|
@@ -143,18 +146,12 @@ ${slotEntries.join(",\n")}
|
|
|
143
146
|
});
|
|
144
147
|
// Find root not-found/forbidden/unauthorized pages and root layouts for global error handling
|
|
145
148
|
const rootRoute = routes.find((r) => r.pattern === "/");
|
|
146
|
-
const rootNotFoundVar = rootRoute?.notFoundPath
|
|
147
|
-
|
|
148
|
-
: null;
|
|
149
|
-
const rootForbiddenVar = rootRoute?.forbiddenPath
|
|
150
|
-
? getImportVar(rootRoute.forbiddenPath)
|
|
151
|
-
: null;
|
|
149
|
+
const rootNotFoundVar = rootRoute?.notFoundPath ? getImportVar(rootRoute.notFoundPath) : null;
|
|
150
|
+
const rootForbiddenVar = rootRoute?.forbiddenPath ? getImportVar(rootRoute.forbiddenPath) : null;
|
|
152
151
|
const rootUnauthorizedVar = rootRoute?.unauthorizedPath
|
|
153
152
|
? getImportVar(rootRoute.unauthorizedPath)
|
|
154
153
|
: null;
|
|
155
|
-
const rootLayoutVars = rootRoute
|
|
156
|
-
? rootRoute.layouts.map((l) => getImportVar(l))
|
|
157
|
-
: [];
|
|
154
|
+
const rootLayoutVars = rootRoute ? rootRoute.layouts.map((l) => getImportVar(l)) : [];
|
|
158
155
|
// Global error boundary (app/global-error.tsx)
|
|
159
156
|
const globalErrorVar = globalErrorPath ? getImportVar(globalErrorPath) : null;
|
|
160
157
|
// Build metadata route handling
|
|
@@ -205,7 +202,7 @@ import {
|
|
|
205
202
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
206
203
|
import { createElement, Suspense, Fragment } from "react";
|
|
207
204
|
import { setNavigationContext as _setNavigationContextOrig, getNavigationContext as _getNavigationContext } from "next/navigation";
|
|
208
|
-
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";
|
|
209
206
|
import { NextRequest, NextFetchEvent } from "next/server";
|
|
210
207
|
import { ErrorBoundary, NotFoundBoundary } from "vinext/error-boundary";
|
|
211
208
|
import { LayoutSegmentProvider } from "vinext/layout-segment-context";
|
|
@@ -213,10 +210,11 @@ import { MetadataHead, mergeMetadata, resolveModuleMetadata, ViewportHead, merge
|
|
|
213
210
|
${middlewarePath ? `import * as middlewareModule from ${JSON.stringify(middlewarePath.replace(/\\/g, "/"))};` : ""}
|
|
214
211
|
${instrumentationPath ? `import * as _instrumentation from ${JSON.stringify(instrumentationPath.replace(/\\/g, "/"))};` : ""}
|
|
215
212
|
${effectiveMetaRoutes.length > 0 ? `import { sitemapToXml, robotsToText, manifestToJson } from ${JSON.stringify(fileURLToPath(new URL("../server/metadata-routes.js", import.meta.url)).replace(/\\/g, "/"))};` : ""}
|
|
216
|
-
import { requestContextFromRequest, matchRedirect, matchRewrite, matchHeaders, isExternalUrl, proxyExternalRequest, sanitizeDestination } from ${JSON.stringify(configMatchersPath)};
|
|
217
|
-
import { validateCsrfOrigin, validateImageUrl, guardProtocolRelativeUrl, stripBasePath, normalizeTrailingSlash, processMiddlewareHeaders } from ${JSON.stringify(requestPipelinePath)};
|
|
218
|
-
import { _consumeRequestScopedCacheLife, _runWithCacheState } from "next/cache";
|
|
219
|
-
import {
|
|
213
|
+
import { requestContextFromRequest, normalizeHost, matchRedirect, matchRewrite, matchHeaders, isExternalUrl, proxyExternalRequest, sanitizeDestination } from ${JSON.stringify(configMatchersPath)};
|
|
214
|
+
import { validateCsrfOrigin, validateImageUrl, guardProtocolRelativeUrl, hasBasePath, stripBasePath, normalizeTrailingSlash, processMiddlewareHeaders } from ${JSON.stringify(requestPipelinePath)};
|
|
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";
|
|
220
218
|
import { runWithPrivateCache as _runWithPrivateCache } from "vinext/cache-runtime";
|
|
221
219
|
// Import server-only state module to register ALS-backed accessors.
|
|
222
220
|
import { runWithNavigationContext as _runWithNavigationContext } from "vinext/navigation-state";
|
|
@@ -252,7 +250,94 @@ function setNavigationContext(ctx) {
|
|
|
252
250
|
// ISR cache is disabled in dev mode — every request re-renders fresh,
|
|
253
251
|
// matching Next.js dev behavior. Cache-Control headers are still emitted
|
|
254
252
|
// based on export const revalidate for testing purposes.
|
|
255
|
-
// Production ISR
|
|
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;
|
|
256
341
|
|
|
257
342
|
// Normalize null-prototype objects from matchPattern() into thenable objects
|
|
258
343
|
// that work both as Promises (for Next.js 15+ async params) and as plain
|
|
@@ -431,7 +516,8 @@ function createRscOnErrorHandler(request, pathname, routePath) {
|
|
|
431
516
|
|
|
432
517
|
${imports.join("\n")}
|
|
433
518
|
|
|
434
|
-
${instrumentationPath
|
|
519
|
+
${instrumentationPath
|
|
520
|
+
? `// Run instrumentation register() exactly once, lazily on the first request.
|
|
435
521
|
// Previously this was a top-level await, which blocked the entire module graph
|
|
436
522
|
// from finishing initialization until register() resolved — adding that latency
|
|
437
523
|
// to every cold start. Moving it here preserves the "runs before any request is
|
|
@@ -460,7 +546,8 @@ async function __ensureInstrumentation() {
|
|
|
460
546
|
__instrumentationInitialized = true;
|
|
461
547
|
})();
|
|
462
548
|
return __instrumentationInitPromise;
|
|
463
|
-
}`
|
|
549
|
+
}`
|
|
550
|
+
: ""}
|
|
464
551
|
|
|
465
552
|
const routes = [
|
|
466
553
|
${routeEntries.join(",\n")}
|
|
@@ -556,7 +643,8 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
|
|
|
556
643
|
element = createElement(LayoutSegmentProvider, { childSegments: _cs }, element);
|
|
557
644
|
}
|
|
558
645
|
}
|
|
559
|
-
${globalErrorVar
|
|
646
|
+
${globalErrorVar
|
|
647
|
+
? `
|
|
560
648
|
const _GlobalErrorComponent = ${globalErrorVar}.default;
|
|
561
649
|
if (_GlobalErrorComponent) {
|
|
562
650
|
element = createElement(ErrorBoundary, {
|
|
@@ -564,7 +652,8 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
|
|
|
564
652
|
children: element,
|
|
565
653
|
});
|
|
566
654
|
}
|
|
567
|
-
`
|
|
655
|
+
`
|
|
656
|
+
: ""}
|
|
568
657
|
const _pathname = new URL(request.url).pathname;
|
|
569
658
|
const onRenderError = createRscOnErrorHandler(
|
|
570
659
|
request,
|
|
@@ -644,12 +733,14 @@ async function renderErrorBoundaryPage(route, error, isRscRequest, request, matc
|
|
|
644
733
|
}
|
|
645
734
|
}
|
|
646
735
|
}
|
|
647
|
-
${globalErrorVar
|
|
736
|
+
${globalErrorVar
|
|
737
|
+
? `
|
|
648
738
|
if (!ErrorComponent) {
|
|
649
739
|
ErrorComponent = ${globalErrorVar}?.default ?? null;
|
|
650
740
|
_isGlobalError = !!ErrorComponent;
|
|
651
741
|
}
|
|
652
|
-
`
|
|
742
|
+
`
|
|
743
|
+
: ""}
|
|
653
744
|
if (!ErrorComponent) return null;
|
|
654
745
|
|
|
655
746
|
const rawError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -687,7 +778,8 @@ async function renderErrorBoundaryPage(route, error, isRscRequest, request, matc
|
|
|
687
778
|
element = createElement(LayoutSegmentProvider, { childSegments: _ecs }, element);
|
|
688
779
|
}
|
|
689
780
|
}
|
|
690
|
-
${globalErrorVar
|
|
781
|
+
${globalErrorVar
|
|
782
|
+
? `
|
|
691
783
|
const _ErrGlobalComponent = ${globalErrorVar}.default;
|
|
692
784
|
if (_ErrGlobalComponent) {
|
|
693
785
|
element = createElement(ErrorBoundary, {
|
|
@@ -695,7 +787,8 @@ async function renderErrorBoundaryPage(route, error, isRscRequest, request, matc
|
|
|
695
787
|
children: element,
|
|
696
788
|
});
|
|
697
789
|
}
|
|
698
|
-
`
|
|
790
|
+
`
|
|
791
|
+
: ""}
|
|
699
792
|
} else {
|
|
700
793
|
// For HTML (full page load) responses, wrap with layouts only.
|
|
701
794
|
const _errParamsHtml = matchedParams ?? route?.params ?? {};
|
|
@@ -757,20 +850,20 @@ function matchRoute(url, routes) {
|
|
|
757
850
|
// NOTE: Do NOT decodeURIComponent here. The caller is responsible for decoding
|
|
758
851
|
// the pathname exactly once at the request entry point. Decoding again here
|
|
759
852
|
// would cause inconsistent path matching between middleware and routing.
|
|
853
|
+
const urlParts = normalizedUrl.split("/").filter(Boolean);
|
|
760
854
|
for (const route of routes) {
|
|
761
|
-
const params = matchPattern(
|
|
855
|
+
const params = matchPattern(urlParts, route.patternParts);
|
|
762
856
|
if (params !== null) return { route, params };
|
|
763
857
|
}
|
|
764
858
|
return null;
|
|
765
859
|
}
|
|
766
860
|
|
|
767
|
-
function matchPattern(
|
|
768
|
-
const urlParts = url.split("/").filter(Boolean);
|
|
769
|
-
const patternParts = pattern.split("/").filter(Boolean);
|
|
861
|
+
function matchPattern(urlParts, patternParts) {
|
|
770
862
|
const params = Object.create(null);
|
|
771
863
|
for (let i = 0; i < patternParts.length; i++) {
|
|
772
864
|
const pp = patternParts[i];
|
|
773
865
|
if (pp.endsWith("+")) {
|
|
866
|
+
if (i !== patternParts.length - 1) return null;
|
|
774
867
|
const paramName = pp.slice(1, -1);
|
|
775
868
|
const remaining = urlParts.slice(i);
|
|
776
869
|
if (remaining.length === 0) return null;
|
|
@@ -778,6 +871,7 @@ function matchPattern(url, pattern) {
|
|
|
778
871
|
return params;
|
|
779
872
|
}
|
|
780
873
|
if (pp.endsWith("*")) {
|
|
874
|
+
if (i !== patternParts.length - 1) return null;
|
|
781
875
|
const paramName = pp.slice(1, -1);
|
|
782
876
|
params[paramName] = urlParts.slice(i);
|
|
783
877
|
return params;
|
|
@@ -806,6 +900,7 @@ for (let ri = 0; ri < routes.length; ri++) {
|
|
|
806
900
|
sourceRouteIndex: ri,
|
|
807
901
|
slotName,
|
|
808
902
|
targetPattern: intercept.targetPattern,
|
|
903
|
+
targetPatternParts: intercept.targetPattern.split("/").filter(Boolean),
|
|
809
904
|
page: intercept.page,
|
|
810
905
|
params: intercept.params,
|
|
811
906
|
});
|
|
@@ -818,8 +913,9 @@ for (let ri = 0; ri < routes.length; ri++) {
|
|
|
818
913
|
* Returns the match info or null.
|
|
819
914
|
*/
|
|
820
915
|
function findIntercept(pathname) {
|
|
916
|
+
const urlParts = pathname.split("/").filter(Boolean);
|
|
821
917
|
for (const entry of interceptLookup) {
|
|
822
|
-
const params = matchPattern(
|
|
918
|
+
const params = matchPattern(urlParts, entry.targetPatternParts);
|
|
823
919
|
if (params !== null) {
|
|
824
920
|
return { ...entry, matchedParams: params };
|
|
825
921
|
}
|
|
@@ -1105,7 +1201,8 @@ async function buildPageElement(route, params, opts, searchParams) {
|
|
|
1105
1201
|
// For HTML requests (initial page load), the ErrorBoundary catches during SSR
|
|
1106
1202
|
// but produces double <html>/<body> (root layout + global-error). The request
|
|
1107
1203
|
// handler detects this via the rscOnError flag and re-renders without layouts.
|
|
1108
|
-
${globalErrorVar
|
|
1204
|
+
${globalErrorVar
|
|
1205
|
+
? `
|
|
1109
1206
|
const GlobalErrorComponent = ${globalErrorVar}.default;
|
|
1110
1207
|
if (GlobalErrorComponent) {
|
|
1111
1208
|
element = createElement(ErrorBoundary, {
|
|
@@ -1113,7 +1210,8 @@ async function buildPageElement(route, params, opts, searchParams) {
|
|
|
1113
1210
|
children: element,
|
|
1114
1211
|
});
|
|
1115
1212
|
}
|
|
1116
|
-
`
|
|
1213
|
+
`
|
|
1214
|
+
: ""}
|
|
1117
1215
|
|
|
1118
1216
|
return element;
|
|
1119
1217
|
}
|
|
@@ -1122,6 +1220,7 @@ ${middlewarePath ? generateMiddlewareMatcherCode("modern") : ""}
|
|
|
1122
1220
|
|
|
1123
1221
|
const __basePath = ${JSON.stringify(bp)};
|
|
1124
1222
|
const __trailingSlash = ${JSON.stringify(ts)};
|
|
1223
|
+
const __i18nConfig = ${JSON.stringify(i18nConfig)};
|
|
1125
1224
|
const __configRedirects = ${JSON.stringify(redirects)};
|
|
1126
1225
|
const __configRewrites = ${JSON.stringify(rewrites)};
|
|
1127
1226
|
const __configHeaders = ${JSON.stringify(headers)};
|
|
@@ -1160,7 +1259,7 @@ function __buildPostMwRequestContext(request) {
|
|
|
1160
1259
|
headers: ctx.headers,
|
|
1161
1260
|
cookies: cookiesRecord,
|
|
1162
1261
|
query: url.searchParams,
|
|
1163
|
-
host: ctx.headers.get("host")
|
|
1262
|
+
host: normalizeHost(ctx.headers.get("host"), url.hostname),
|
|
1164
1263
|
};
|
|
1165
1264
|
}
|
|
1166
1265
|
|
|
@@ -1230,18 +1329,24 @@ async function __readFormDataWithLimit(request, maxBytes) {
|
|
|
1230
1329
|
return new Response(combined, { headers: { "Content-Type": contentType } }).formData();
|
|
1231
1330
|
}
|
|
1232
1331
|
|
|
1233
|
-
export default async function handler(request) {
|
|
1234
|
-
${instrumentationPath
|
|
1332
|
+
export default async function handler(request, ctx) {
|
|
1333
|
+
${instrumentationPath
|
|
1334
|
+
? `// Ensure instrumentation.register() has run before handling the first request.
|
|
1235
1335
|
// This is a no-op after the first call (guarded by __instrumentationInitialized).
|
|
1236
1336
|
await __ensureInstrumentation();
|
|
1237
|
-
`
|
|
1337
|
+
`
|
|
1338
|
+
: ""}
|
|
1238
1339
|
// Wrap the entire request in nested AsyncLocalStorage.run() scopes to ensure
|
|
1239
1340
|
// per-request isolation for all state modules. Each runWith*() creates an
|
|
1240
1341
|
// ALS scope that propagates through all async continuations (including RSC
|
|
1241
1342
|
// streaming), preventing state leakage between concurrent requests on
|
|
1242
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.
|
|
1243
1348
|
const headersCtx = headersContextFromRequest(request);
|
|
1244
|
-
|
|
1349
|
+
const _run = () => runWithHeadersContext(headersCtx, () =>
|
|
1245
1350
|
_runWithNavigationContext(() =>
|
|
1246
1351
|
_runWithCacheState(() =>
|
|
1247
1352
|
_runWithPrivateCache(() =>
|
|
@@ -1284,6 +1389,7 @@ export default async function handler(request) {
|
|
|
1284
1389
|
)
|
|
1285
1390
|
)
|
|
1286
1391
|
);
|
|
1392
|
+
return ctx ? _runWithExecutionContext(ctx, _run) : _run();
|
|
1287
1393
|
}
|
|
1288
1394
|
|
|
1289
1395
|
async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
@@ -1316,10 +1422,12 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1316
1422
|
}
|
|
1317
1423
|
let pathname = __normalizePath(decodedUrlPathname);
|
|
1318
1424
|
|
|
1319
|
-
${bp
|
|
1425
|
+
${bp
|
|
1426
|
+
? `
|
|
1320
1427
|
// Strip basePath prefix
|
|
1321
1428
|
pathname = stripBasePath(pathname, __basePath);
|
|
1322
|
-
`
|
|
1429
|
+
`
|
|
1430
|
+
: ""}
|
|
1323
1431
|
|
|
1324
1432
|
// Trailing slash normalization (redirect to canonical form)
|
|
1325
1433
|
const __tsRedirect = normalizeTrailingSlash(pathname, __basePath, __trailingSlash, url.search);
|
|
@@ -1334,7 +1442,9 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1334
1442
|
const __redir = matchRedirect(__redirPathname, __configRedirects, __reqCtx);
|
|
1335
1443
|
if (__redir) {
|
|
1336
1444
|
const __redirDest = sanitizeDestination(
|
|
1337
|
-
__basePath &&
|
|
1445
|
+
__basePath &&
|
|
1446
|
+
!isExternalUrl(__redir.destination) &&
|
|
1447
|
+
!hasBasePath(__redir.destination, __basePath)
|
|
1338
1448
|
? __basePath + __redir.destination
|
|
1339
1449
|
: __redir.destination
|
|
1340
1450
|
);
|
|
@@ -1352,7 +1462,8 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1352
1462
|
// _mwCtx (per-request container) so handler() can merge them into
|
|
1353
1463
|
// every response path without module-level state that races on Workers.
|
|
1354
1464
|
|
|
1355
|
-
${middlewarePath
|
|
1465
|
+
${middlewarePath
|
|
1466
|
+
? `
|
|
1356
1467
|
// Run proxy/middleware if present and path matches.
|
|
1357
1468
|
// Validate exports match the file type (proxy.ts vs middleware.ts), matching Next.js behavior.
|
|
1358
1469
|
// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/proxy-missing-export/proxy-missing-export.test.ts
|
|
@@ -1366,7 +1477,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1366
1477
|
throw new Error("The " + _fileType + " file must export a function named \`" + _expectedExport + "\` or a \`default\` function.");
|
|
1367
1478
|
}
|
|
1368
1479
|
const middlewareMatcher = middlewareModule.config?.matcher;
|
|
1369
|
-
if (matchesMiddleware(cleanPathname, middlewareMatcher)) {
|
|
1480
|
+
if (matchesMiddleware(cleanPathname, middlewareMatcher, request, __i18nConfig)) {
|
|
1370
1481
|
try {
|
|
1371
1482
|
// Wrap in NextRequest so middleware gets .nextUrl, .cookies, .geo, .ip, etc.
|
|
1372
1483
|
// Always construct a new Request with the fully decoded + normalized pathname
|
|
@@ -1434,7 +1545,8 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1434
1545
|
applyMiddlewareRequestHeaders(_mwCtx.headers);
|
|
1435
1546
|
processMiddlewareHeaders(_mwCtx.headers);
|
|
1436
1547
|
}
|
|
1437
|
-
`
|
|
1548
|
+
`
|
|
1549
|
+
: ""}
|
|
1438
1550
|
|
|
1439
1551
|
// Build post-middleware request context for afterFiles/fallback rewrites.
|
|
1440
1552
|
// These run after middleware in the App Router execution order and should
|
|
@@ -1467,6 +1579,34 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1467
1579
|
|
|
1468
1580
|
// Handle metadata routes (sitemap.xml, robots.txt, manifest.webmanifest, etc.)
|
|
1469
1581
|
for (const metaRoute of metadataRoutes) {
|
|
1582
|
+
// generateSitemaps() support — paginated sitemaps at /{prefix}/sitemap/{id}.xml
|
|
1583
|
+
// When a sitemap module exports generateSitemaps, the base URL (e.g. /products/sitemap.xml)
|
|
1584
|
+
// is no longer served. Instead, individual sitemaps are served at /products/sitemap/{id}.xml.
|
|
1585
|
+
if (
|
|
1586
|
+
metaRoute.type === "sitemap" &&
|
|
1587
|
+
metaRoute.isDynamic &&
|
|
1588
|
+
typeof metaRoute.module.generateSitemaps === "function"
|
|
1589
|
+
) {
|
|
1590
|
+
const sitemapPrefix = metaRoute.servedUrl.slice(0, -4); // strip ".xml"
|
|
1591
|
+
// Match exactly /{prefix}/{id}.xml — one segment only (no slashes in id)
|
|
1592
|
+
if (cleanPathname.startsWith(sitemapPrefix + "/") && cleanPathname.endsWith(".xml")) {
|
|
1593
|
+
const rawId = cleanPathname.slice(sitemapPrefix.length + 1, -4);
|
|
1594
|
+
if (rawId.includes("/")) continue; // multi-segment — not a paginated sitemap
|
|
1595
|
+
const sitemaps = await metaRoute.module.generateSitemaps();
|
|
1596
|
+
const matched = sitemaps.find(function(s) { return String(s.id) === rawId; });
|
|
1597
|
+
if (!matched) return new Response("Not Found", { status: 404 });
|
|
1598
|
+
// Pass the original typed id from generateSitemaps() so numeric IDs stay numeric.
|
|
1599
|
+
// TODO: wrap with makeThenableParams-style Promise when upgrading to Next.js 16
|
|
1600
|
+
// full-Promise param semantics (id becomes Promise<string> in v16).
|
|
1601
|
+
const result = await metaRoute.module.default({ id: matched.id });
|
|
1602
|
+
if (result instanceof Response) return result;
|
|
1603
|
+
return new Response(sitemapToXml(result), {
|
|
1604
|
+
headers: { "Content-Type": metaRoute.contentType },
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
// Skip — the base servedUrl is not served when generateSitemaps exists
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1470
1610
|
if (cleanPathname === metaRoute.servedUrl) {
|
|
1471
1611
|
if (metaRoute.isDynamic) {
|
|
1472
1612
|
// Dynamic metadata route — call the default export and serialize
|
|
@@ -1552,39 +1692,44 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1552
1692
|
const action = await loadServerAction(actionId);
|
|
1553
1693
|
let returnValue;
|
|
1554
1694
|
let actionRedirect = null;
|
|
1695
|
+
const previousHeadersPhase = setHeadersAccessPhase("action");
|
|
1555
1696
|
try {
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
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
|
+
}
|
|
1576
1724
|
} else {
|
|
1577
|
-
//
|
|
1578
|
-
//
|
|
1725
|
+
// Unhandled error — sanitize in production to avoid leaking
|
|
1726
|
+
// internal details (database errors, file paths, stack traces, etc.)
|
|
1579
1727
|
console.error("[vinext] Server action error:", e);
|
|
1580
1728
|
returnValue = { ok: false, data: __sanitizeErrorForClient(e) };
|
|
1581
1729
|
}
|
|
1582
|
-
} else {
|
|
1583
|
-
// Unhandled error — sanitize in production to avoid leaking
|
|
1584
|
-
// internal details (database errors, file paths, stack traces, etc.)
|
|
1585
|
-
console.error("[vinext] Server action error:", e);
|
|
1586
|
-
returnValue = { ok: false, data: __sanitizeErrorForClient(e) };
|
|
1587
1730
|
}
|
|
1731
|
+
} finally {
|
|
1732
|
+
setHeadersAccessPhase(previousHeadersPhase);
|
|
1588
1733
|
}
|
|
1589
1734
|
|
|
1590
1735
|
// If the action called redirect(), signal the client to navigate.
|
|
@@ -1737,16 +1882,39 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1737
1882
|
}
|
|
1738
1883
|
const hasDefault = typeof handler["default"] === "function";
|
|
1739
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
|
+
|
|
1740
1908
|
// OPTIONS auto-implementation: respond with Allow header and 204
|
|
1741
1909
|
if (method === "OPTIONS" && typeof handler["OPTIONS"] !== "function") {
|
|
1742
1910
|
const allowMethods = hasDefault ? HTTP_METHODS : exportedMethods;
|
|
1743
1911
|
if (!allowMethods.includes("OPTIONS")) allowMethods.push("OPTIONS");
|
|
1744
1912
|
setHeadersContext(null);
|
|
1745
1913
|
setNavigationContext(null);
|
|
1746
|
-
return new Response(null, {
|
|
1914
|
+
return attachRouteHandlerMiddlewareContext(new Response(null, {
|
|
1747
1915
|
status: 204,
|
|
1748
1916
|
headers: { "Allow": allowMethods.join(", ") },
|
|
1749
|
-
});
|
|
1917
|
+
}));
|
|
1750
1918
|
}
|
|
1751
1919
|
|
|
1752
1920
|
// HEAD auto-implementation: run GET handler and strip body
|
|
@@ -1758,12 +1926,20 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1758
1926
|
}
|
|
1759
1927
|
|
|
1760
1928
|
if (typeof handlerFn === "function") {
|
|
1929
|
+
const previousHeadersPhase = setHeadersAccessPhase("route-handler");
|
|
1761
1930
|
try {
|
|
1762
1931
|
const response = await handlerFn(request, { params });
|
|
1932
|
+
const dynamicUsedInHandler = consumeDynamicUsage();
|
|
1763
1933
|
|
|
1764
1934
|
// Apply Cache-Control from route segment config (export const revalidate = N).
|
|
1765
|
-
//
|
|
1766
|
-
|
|
1935
|
+
// Runtime request APIs like headers() / cookies() make GET handlers dynamic,
|
|
1936
|
+
// so only attach ISR headers when the handler stayed static.
|
|
1937
|
+
if (
|
|
1938
|
+
revalidateSeconds !== null &&
|
|
1939
|
+
!dynamicUsedInHandler &&
|
|
1940
|
+
(method === "GET" || isAutoHead) &&
|
|
1941
|
+
!response.headers.has("cache-control")
|
|
1942
|
+
) {
|
|
1767
1943
|
response.headers.set("cache-control", "s-maxage=" + revalidateSeconds + ", stale-while-revalidate");
|
|
1768
1944
|
}
|
|
1769
1945
|
|
|
@@ -1782,28 +1958,28 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1782
1958
|
if (draftCookie) newHeaders.append("Set-Cookie", draftCookie);
|
|
1783
1959
|
|
|
1784
1960
|
if (isAutoHead) {
|
|
1785
|
-
return new Response(null, {
|
|
1961
|
+
return attachRouteHandlerMiddlewareContext(new Response(null, {
|
|
1786
1962
|
status: response.status,
|
|
1787
1963
|
statusText: response.statusText,
|
|
1788
1964
|
headers: newHeaders,
|
|
1789
|
-
});
|
|
1965
|
+
}));
|
|
1790
1966
|
}
|
|
1791
|
-
return new Response(response.body, {
|
|
1967
|
+
return attachRouteHandlerMiddlewareContext(new Response(response.body, {
|
|
1792
1968
|
status: response.status,
|
|
1793
1969
|
statusText: response.statusText,
|
|
1794
1970
|
headers: newHeaders,
|
|
1795
|
-
});
|
|
1971
|
+
}));
|
|
1796
1972
|
}
|
|
1797
1973
|
|
|
1798
1974
|
if (isAutoHead) {
|
|
1799
1975
|
// Strip body for auto-HEAD, preserve headers and status
|
|
1800
|
-
return new Response(null, {
|
|
1976
|
+
return attachRouteHandlerMiddlewareContext(new Response(null, {
|
|
1801
1977
|
status: response.status,
|
|
1802
1978
|
statusText: response.statusText,
|
|
1803
1979
|
headers: response.headers,
|
|
1804
|
-
});
|
|
1980
|
+
}));
|
|
1805
1981
|
}
|
|
1806
|
-
return response;
|
|
1982
|
+
return attachRouteHandlerMiddlewareContext(response);
|
|
1807
1983
|
} catch (err) {
|
|
1808
1984
|
getAndClearPendingCookies(); // Clear any pending cookies on error
|
|
1809
1985
|
// Catch redirect() / notFound() thrown from route handlers
|
|
@@ -1815,16 +1991,16 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1815
1991
|
const statusCode = parts[3] ? parseInt(parts[3], 10) : 307;
|
|
1816
1992
|
setHeadersContext(null);
|
|
1817
1993
|
setNavigationContext(null);
|
|
1818
|
-
return new Response(null, {
|
|
1994
|
+
return attachRouteHandlerMiddlewareContext(new Response(null, {
|
|
1819
1995
|
status: statusCode,
|
|
1820
1996
|
headers: { Location: new URL(redirectUrl, request.url).toString() },
|
|
1821
|
-
});
|
|
1997
|
+
}));
|
|
1822
1998
|
}
|
|
1823
1999
|
if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
|
|
1824
2000
|
const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
|
|
1825
2001
|
setHeadersContext(null);
|
|
1826
2002
|
setNavigationContext(null);
|
|
1827
|
-
return new Response(null, { status: statusCode });
|
|
2003
|
+
return attachRouteHandlerMiddlewareContext(new Response(null, { status: statusCode }));
|
|
1828
2004
|
}
|
|
1829
2005
|
}
|
|
1830
2006
|
setHeadersContext(null);
|
|
@@ -1837,15 +2013,17 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1837
2013
|
).catch((reportErr) => {
|
|
1838
2014
|
console.error("[vinext] Failed to report route handler error:", reportErr);
|
|
1839
2015
|
});
|
|
1840
|
-
return new Response(null, { status: 500 });
|
|
2016
|
+
return attachRouteHandlerMiddlewareContext(new Response(null, { status: 500 }));
|
|
2017
|
+
} finally {
|
|
2018
|
+
setHeadersAccessPhase(previousHeadersPhase);
|
|
1841
2019
|
}
|
|
1842
2020
|
}
|
|
1843
2021
|
setHeadersContext(null);
|
|
1844
2022
|
setNavigationContext(null);
|
|
1845
|
-
return new Response(null, {
|
|
2023
|
+
return attachRouteHandlerMiddlewareContext(new Response(null, {
|
|
1846
2024
|
status: 405,
|
|
1847
2025
|
headers: { Allow: exportedMethods.join(", ") },
|
|
1848
|
-
});
|
|
2026
|
+
}));
|
|
1849
2027
|
}
|
|
1850
2028
|
|
|
1851
2029
|
// Build the component tree: layouts wrapping the page
|
|
@@ -1874,25 +2052,18 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1874
2052
|
});
|
|
1875
2053
|
}
|
|
1876
2054
|
|
|
1877
|
-
// dynamic = 'error':
|
|
2055
|
+
// dynamic = 'error': install an access error so request APIs fail with the
|
|
2056
|
+
// static-generation message even for legacy sync property access.
|
|
1878
2057
|
if (isDynamicError) {
|
|
1879
2058
|
const errorMsg = 'Page with \`dynamic = "error"\` used a dynamic API. ' +
|
|
1880
2059
|
'This page was expected to be fully static, but headers(), cookies(), ' +
|
|
1881
2060
|
'or searchParams was accessed. Remove the dynamic API usage or change ' +
|
|
1882
2061
|
'the dynamic config to "auto" or "force-dynamic".';
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
},
|
|
2062
|
+
setHeadersContext({
|
|
2063
|
+
headers: new Headers(),
|
|
2064
|
+
cookies: new Map(),
|
|
2065
|
+
accessError: new Error(errorMsg),
|
|
1888
2066
|
});
|
|
1889
|
-
const throwingCookies = new Proxy(new Map(), {
|
|
1890
|
-
get(target, prop) {
|
|
1891
|
-
if (typeof prop === "string" && prop !== "then") throw new Error(errorMsg);
|
|
1892
|
-
return Reflect.get(target, prop);
|
|
1893
|
-
},
|
|
1894
|
-
});
|
|
1895
|
-
setHeadersContext({ headers: throwingHeaders, cookies: throwingCookies });
|
|
1896
2067
|
setNavigationContext({
|
|
1897
2068
|
pathname: cleanPathname,
|
|
1898
2069
|
searchParams: new URLSearchParams(),
|
|
@@ -1900,7 +2071,177 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1900
2071
|
});
|
|
1901
2072
|
}
|
|
1902
2073
|
|
|
1903
|
-
//
|
|
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.
|
|
1904
2245
|
if (dynamicParamsConfig === false && route.isDynamic && typeof route.page?.generateStaticParams === "function") {
|
|
1905
2246
|
try {
|
|
1906
2247
|
// Pass parent params to generateStaticParams (Next.js top-down params passing).
|
|
@@ -1930,9 +2271,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1930
2271
|
}
|
|
1931
2272
|
}
|
|
1932
2273
|
|
|
1933
|
-
// force-dynamic: set no-store Cache-Control
|
|
1934
|
-
const isForceDynamic = dynamicConfig === "force-dynamic";
|
|
1935
|
-
|
|
1936
2274
|
// Check for intercepting routes on RSC requests (client-side navigation).
|
|
1937
2275
|
// If the target URL matches an intercepting route in a parallel slot,
|
|
1938
2276
|
// render the source route with the intercepting page in the slot.
|
|
@@ -2165,6 +2503,34 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2165
2503
|
};
|
|
2166
2504
|
const rscStream = renderToReadableStream(element, { onError: onRenderError });
|
|
2167
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
|
+
|
|
2168
2534
|
if (isRscRequest) {
|
|
2169
2535
|
// Direct RSC stream response (for client-side navigation)
|
|
2170
2536
|
// NOTE: Do NOT clear headers/navigation context here!
|
|
@@ -2181,6 +2547,9 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2181
2547
|
} else if ((isForceStatic || isDynamicError) && !revalidateSeconds) {
|
|
2182
2548
|
responseHeaders["Cache-Control"] = "s-maxage=31536000, stale-while-revalidate";
|
|
2183
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";
|
|
2184
2553
|
} else if (revalidateSeconds) {
|
|
2185
2554
|
responseHeaders["Cache-Control"] = "s-maxage=" + revalidateSeconds + ", stale-while-revalidate";
|
|
2186
2555
|
}
|
|
@@ -2230,7 +2599,26 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2230
2599
|
const compileMs = __compileEnd !== undefined ? Math.round(__compileEnd - __reqStart) : -1;
|
|
2231
2600
|
responseHeaders["x-vinext-timing"] = handlerStart + "," + compileMs + ",-1";
|
|
2232
2601
|
}
|
|
2233
|
-
|
|
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 });
|
|
2234
2622
|
}
|
|
2235
2623
|
|
|
2236
2624
|
// Collect font data from RSC environment before passing to SSR
|
|
@@ -2251,11 +2639,16 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2251
2639
|
}
|
|
2252
2640
|
const fontLinkHeader = fontLinkHeaderParts.length > 0 ? fontLinkHeaderParts.join(", ") : "";
|
|
2253
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
|
+
|
|
2254
2647
|
// Delegate to SSR environment for HTML rendering
|
|
2255
2648
|
let htmlStream;
|
|
2256
2649
|
try {
|
|
2257
2650
|
const ssrEntry = await import.meta.viteRsc.loadModule("ssr", "index");
|
|
2258
|
-
htmlStream = await ssrEntry.handleSsr(
|
|
2651
|
+
htmlStream = await ssrEntry.handleSsr(__rscForResponse, _getNavigationContext(), fontData);
|
|
2259
2652
|
// Shell render complete; Suspense boundaries stream asynchronously
|
|
2260
2653
|
if (process.env.NODE_ENV !== "production") __renderEnd = performance.now();
|
|
2261
2654
|
} catch (ssrErr) {
|
|
@@ -2271,7 +2664,8 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2271
2664
|
// the HTML output has double <html>/<body> (root layout + global-error.tsx).
|
|
2272
2665
|
// Discard it and re-render using renderErrorBoundaryPage which skips layouts
|
|
2273
2666
|
// when the error falls through to global-error.tsx.
|
|
2274
|
-
${globalErrorVar
|
|
2667
|
+
${globalErrorVar
|
|
2668
|
+
? `
|
|
2275
2669
|
if (_rscErrorForRerender && !isRscRequest) {
|
|
2276
2670
|
const _hasLocalBoundary = !!(route?.error?.default) || !!(route?.errors && route.errors.some(function(e) { return e?.default; }));
|
|
2277
2671
|
if (!_hasLocalBoundary) {
|
|
@@ -2279,7 +2673,8 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2279
2673
|
if (cleanResp) return cleanResp;
|
|
2280
2674
|
}
|
|
2281
2675
|
}
|
|
2282
|
-
`
|
|
2676
|
+
`
|
|
2677
|
+
: ""}
|
|
2283
2678
|
|
|
2284
2679
|
// Check for draftMode Set-Cookie header (from draftMode().enable()/disable())
|
|
2285
2680
|
const draftCookie = getDraftModeCookieHeader();
|
|
@@ -2357,8 +2752,8 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2357
2752
|
// force-static / error: treat as static regardless of dynamic usage.
|
|
2358
2753
|
// force-static intentionally provides empty headers/cookies context so
|
|
2359
2754
|
// dynamic APIs return safe defaults; we ignore the dynamic usage signal.
|
|
2360
|
-
// dynamic='error' should have already thrown
|
|
2361
|
-
// code
|
|
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.
|
|
2362
2757
|
if ((isForceStatic || isDynamicError) && (revalidateSeconds === null || revalidateSeconds === 0)) {
|
|
2363
2758
|
return attachMiddlewareContext(new Response(htmlStream, {
|
|
2364
2759
|
headers: {
|
|
@@ -2382,9 +2777,72 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2382
2777
|
}));
|
|
2383
2778
|
}
|
|
2384
2779
|
|
|
2385
|
-
// Emit Cache-Control for ISR pages
|
|
2386
|
-
//
|
|
2387
|
-
|
|
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)
|
|
2388
2846
|
return attachMiddlewareContext(new Response(htmlStream, {
|
|
2389
2847
|
headers: {
|
|
2390
2848
|
"Content-Type": "text/html; charset=utf-8",
|
|
@@ -2394,6 +2852,19 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2394
2852
|
}));
|
|
2395
2853
|
}
|
|
2396
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
|
+
|
|
2397
2868
|
return attachMiddlewareContext(new Response(htmlStream, {
|
|
2398
2869
|
headers: { "Content-Type": "text/html; charset=utf-8", "Vary": "RSC, Accept" },
|
|
2399
2870
|
}));
|