vinext 0.0.24 → 0.0.25
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 +24 -0
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +3 -2
- package/dist/check.js.map +1 -1
- package/dist/client/entry.js +1 -1
- package/dist/client/entry.js.map +1 -1
- package/dist/config/config-matchers.d.ts +21 -0
- package/dist/config/config-matchers.d.ts.map +1 -1
- package/dist/config/config-matchers.js +46 -6
- package/dist/config/config-matchers.js.map +1 -1
- package/dist/config/next-config.d.ts +8 -2
- package/dist/config/next-config.d.ts.map +1 -1
- package/dist/config/next-config.js +90 -35
- package/dist/config/next-config.js.map +1 -1
- package/dist/deploy.d.ts +10 -0
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +70 -35
- package/dist/deploy.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +172 -18
- package/dist/index.js.map +1 -1
- package/dist/plugins/async-hooks-stub.d.ts +16 -0
- package/dist/plugins/async-hooks-stub.d.ts.map +1 -0
- package/dist/plugins/async-hooks-stub.js +45 -0
- package/dist/plugins/async-hooks-stub.js.map +1 -0
- package/dist/routing/app-router.d.ts +12 -6
- package/dist/routing/app-router.d.ts.map +1 -1
- package/dist/routing/app-router.js +19 -40
- package/dist/routing/app-router.js.map +1 -1
- package/dist/routing/pages-router.d.ts.map +1 -1
- package/dist/routing/pages-router.js +3 -9
- package/dist/routing/pages-router.js.map +1 -1
- package/dist/routing/utils.d.ts +9 -0
- package/dist/routing/utils.d.ts.map +1 -1
- package/dist/routing/utils.js +10 -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 +6 -0
- package/dist/server/api-handler.js.map +1 -1
- package/dist/server/app-dev-server.d.ts +2 -2
- package/dist/server/app-dev-server.d.ts.map +1 -1
- package/dist/server/app-dev-server.js +238 -114
- package/dist/server/app-dev-server.js.map +1 -1
- package/dist/server/dev-module-runner.d.ts +84 -0
- package/dist/server/dev-module-runner.d.ts.map +1 -0
- package/dist/server/dev-module-runner.js +105 -0
- package/dist/server/dev-module-runner.js.map +1 -0
- package/dist/server/dev-server.js.map +1 -1
- package/dist/server/instrumentation.d.ts +52 -9
- package/dist/server/instrumentation.d.ts.map +1 -1
- package/dist/server/instrumentation.js +52 -15
- package/dist/server/instrumentation.js.map +1 -1
- package/dist/server/middleware.d.ts +7 -3
- package/dist/server/middleware.d.ts.map +1 -1
- package/dist/server/middleware.js +16 -6
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/prod-server.d.ts.map +1 -1
- package/dist/server/prod-server.js +15 -25
- package/dist/server/prod-server.js.map +1 -1
- package/dist/shims/cache.d.ts.map +1 -1
- package/dist/shims/cache.js +14 -2
- package/dist/shims/cache.js.map +1 -1
- package/dist/shims/fetch-cache.d.ts.map +1 -1
- package/dist/shims/fetch-cache.js +139 -29
- package/dist/shims/fetch-cache.js.map +1 -1
- package/dist/shims/form.d.ts.map +1 -1
- package/dist/shims/form.js +2 -3
- package/dist/shims/form.js.map +1 -1
- package/dist/shims/layout-segment-context.d.ts +5 -4
- package/dist/shims/layout-segment-context.d.ts.map +1 -1
- package/dist/shims/layout-segment-context.js +6 -5
- package/dist/shims/layout-segment-context.js.map +1 -1
- package/dist/shims/link.d.ts.map +1 -1
- package/dist/shims/link.js +32 -17
- package/dist/shims/link.js.map +1 -1
- package/dist/shims/navigation.d.ts +14 -11
- package/dist/shims/navigation.d.ts.map +1 -1
- package/dist/shims/navigation.js +122 -102
- package/dist/shims/navigation.js.map +1 -1
- package/dist/shims/router.d.ts.map +1 -1
- package/dist/shims/router.js +37 -21
- package/dist/shims/router.js.map +1 -1
- package/dist/shims/server.d.ts +2 -0
- package/dist/shims/server.d.ts.map +1 -1
- package/dist/shims/server.js +4 -0
- package/dist/shims/server.js.map +1 -1
- package/dist/shims/url-utils.d.ts +13 -0
- package/dist/shims/url-utils.d.ts.map +1 -0
- package/dist/shims/url-utils.js +28 -0
- package/dist/shims/url-utils.js.map +1 -0
- package/package.json +1 -1
|
@@ -18,7 +18,7 @@ import { isProxyFile } from "./middleware.js";
|
|
|
18
18
|
* It matches the incoming request URL to an app route, builds the
|
|
19
19
|
* nested layout + page tree, and renders it to an RSC stream.
|
|
20
20
|
*/
|
|
21
|
-
export function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes, globalErrorPath, basePath, trailingSlash, config) {
|
|
21
|
+
export function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes, globalErrorPath, basePath, trailingSlash, config, instrumentationPath) {
|
|
22
22
|
const bp = basePath ?? "";
|
|
23
23
|
const ts = trailingSlash ?? false;
|
|
24
24
|
const redirects = config?.redirects ?? [];
|
|
@@ -119,7 +119,8 @@ ${interceptEntries.join(",\n")}
|
|
|
119
119
|
page: ${route.pagePath ? getImportVar(route.pagePath) : "null"},
|
|
120
120
|
routeHandler: ${route.routePath ? getImportVar(route.routePath) : "null"},
|
|
121
121
|
layouts: [${layoutVars.join(", ")}],
|
|
122
|
-
|
|
122
|
+
routeSegments: ${JSON.stringify(route.routeSegments)},
|
|
123
|
+
layoutTreePositions: ${JSON.stringify(route.layoutTreePositions)},
|
|
123
124
|
templates: [${templateVars.join(", ")}],
|
|
124
125
|
errors: [${layoutErrorVars.join(", ")}],
|
|
125
126
|
slots: {
|
|
@@ -194,14 +195,16 @@ import {
|
|
|
194
195
|
loadServerAction,
|
|
195
196
|
createTemporaryReferenceSet,
|
|
196
197
|
} from "@vitejs/plugin-rsc/rsc";
|
|
198
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
197
199
|
import { createElement, Suspense, Fragment } from "react";
|
|
198
200
|
import { setNavigationContext as _setNavigationContextOrig, getNavigationContext as _getNavigationContext } from "next/navigation";
|
|
199
201
|
import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader, getAndClearPendingCookies, consumeDynamicUsage, markDynamicUsage, runWithHeadersContext, applyMiddlewareRequestHeaders, getHeadersContext } from "next/headers";
|
|
200
|
-
import { NextRequest } from "next/server";
|
|
202
|
+
import { NextRequest, NextFetchEvent } from "next/server";
|
|
201
203
|
import { ErrorBoundary, NotFoundBoundary } from "vinext/error-boundary";
|
|
202
204
|
import { LayoutSegmentProvider } from "vinext/layout-segment-context";
|
|
203
205
|
import { MetadataHead, mergeMetadata, resolveModuleMetadata, ViewportHead, mergeViewport, resolveModuleViewport } from "vinext/metadata";
|
|
204
206
|
${middlewarePath ? `import * as middlewareModule from ${JSON.stringify(middlewarePath.replace(/\\/g, "/"))};` : ""}
|
|
207
|
+
${instrumentationPath ? `import * as _instrumentation from ${JSON.stringify(instrumentationPath.replace(/\\/g, "/"))};` : ""}
|
|
205
208
|
${effectiveMetaRoutes.length > 0 ? `import { sitemapToXml, robotsToText, manifestToJson } from ${JSON.stringify(fileURLToPath(new URL("./metadata-routes.js", import.meta.url)).replace(/\\/g, "/"))};` : ""}
|
|
206
209
|
import { _consumeRequestScopedCacheLife, _runWithCacheState } from "next/cache";
|
|
207
210
|
import { runWithFetchCache } from "vinext/fetch-cache";
|
|
@@ -214,6 +217,20 @@ import { getSSRFontStyles as _getSSRFontStylesLocal, getSSRFontPreloads as _getS
|
|
|
214
217
|
function _getSSRFontStyles() { return [..._getSSRFontStylesGoogle(), ..._getSSRFontStylesLocal()]; }
|
|
215
218
|
function _getSSRFontPreloads() { return [..._getSSRFontPreloadsGoogle(), ..._getSSRFontPreloadsLocal()]; }
|
|
216
219
|
|
|
220
|
+
// ALS used to suppress the expected "Invalid hook call" dev warning when
|
|
221
|
+
// layout/page components are probed outside React's render cycle. Patching
|
|
222
|
+
// console.error once at module load (instead of per-request) avoids the
|
|
223
|
+
// concurrent-request issue where request A's suppression filter could
|
|
224
|
+
// swallow real errors from request B.
|
|
225
|
+
const _suppressHookWarningAls = new AsyncLocalStorage();
|
|
226
|
+
const _origConsoleError = console.error;
|
|
227
|
+
console.error = (...args) => {
|
|
228
|
+
if (_suppressHookWarningAls.getStore() === true &&
|
|
229
|
+
typeof args[0] === "string" &&
|
|
230
|
+
args[0].includes("Invalid hook call")) return;
|
|
231
|
+
_origConsoleError.apply(console, args);
|
|
232
|
+
};
|
|
233
|
+
|
|
217
234
|
// Set navigation context in the ALS-backed store. "use client" components
|
|
218
235
|
// rendered during SSR need the pathname/searchParams/params but the SSR
|
|
219
236
|
// environment has a separate module instance of next/navigation.
|
|
@@ -242,6 +259,38 @@ function makeThenableParams(obj) {
|
|
|
242
259
|
return Object.assign(Promise.resolve(plain), plain);
|
|
243
260
|
}
|
|
244
261
|
|
|
262
|
+
// Resolve route tree segments to actual values using matched params.
|
|
263
|
+
// Dynamic segments like [id] are replaced with param values, catch-all
|
|
264
|
+
// segments like [...slug] are joined with "/", and route groups are kept as-is.
|
|
265
|
+
function __resolveChildSegments(routeSegments, treePosition, params) {
|
|
266
|
+
var raw = routeSegments.slice(treePosition);
|
|
267
|
+
var result = [];
|
|
268
|
+
for (var j = 0; j < raw.length; j++) {
|
|
269
|
+
var seg = raw[j];
|
|
270
|
+
// Optional catch-all: [[...param]]
|
|
271
|
+
if (seg.indexOf("[[...") === 0 && seg.charAt(seg.length - 1) === "]" && seg.charAt(seg.length - 2) === "]") {
|
|
272
|
+
var pn = seg.slice(5, -2);
|
|
273
|
+
var v = params[pn];
|
|
274
|
+
// Skip empty optional catch-all (e.g., visiting /blog on [[...slug]] route)
|
|
275
|
+
if (Array.isArray(v) && v.length === 0) continue;
|
|
276
|
+
if (v == null) continue;
|
|
277
|
+
result.push(Array.isArray(v) ? v.join("/") : v);
|
|
278
|
+
// Catch-all: [...param]
|
|
279
|
+
} else if (seg.indexOf("[...") === 0 && seg.charAt(seg.length - 1) === "]") {
|
|
280
|
+
var pn2 = seg.slice(4, -1);
|
|
281
|
+
var v2 = params[pn2];
|
|
282
|
+
result.push(Array.isArray(v2) ? v2.join("/") : (v2 || seg));
|
|
283
|
+
// Dynamic: [param]
|
|
284
|
+
} else if (seg.charAt(0) === "[" && seg.charAt(seg.length - 1) === "]" && seg.indexOf(".") === -1) {
|
|
285
|
+
var pn3 = seg.slice(1, -1);
|
|
286
|
+
result.push(params[pn3] || seg);
|
|
287
|
+
} else {
|
|
288
|
+
result.push(seg);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
|
|
245
294
|
// djb2 hash — matches Next.js's stringHash for digest generation.
|
|
246
295
|
// Produces a stable numeric string from error message + stack.
|
|
247
296
|
function __errorDigest(str) {
|
|
@@ -347,6 +396,23 @@ function rscOnError(error) {
|
|
|
347
396
|
|
|
348
397
|
${imports.join("\n")}
|
|
349
398
|
|
|
399
|
+
${instrumentationPath ? `// Run instrumentation register() once at module evaluation time — before any
|
|
400
|
+
// requests are handled. This runs inside the Worker process (or RSC environment),
|
|
401
|
+
// which is exactly where request handling happens. Matches Next.js semantics:
|
|
402
|
+
// register() is called once on startup in the process that handles requests.
|
|
403
|
+
if (typeof _instrumentation.register === "function") {
|
|
404
|
+
await _instrumentation.register();
|
|
405
|
+
}
|
|
406
|
+
// Store the onRequestError handler on globalThis so it is visible to
|
|
407
|
+
// reportRequestError() (imported as _reportRequestError above) regardless
|
|
408
|
+
// of which Vite environment module graph it is called from. With
|
|
409
|
+
// @vitejs/plugin-rsc the RSC and SSR environments run in the same Node.js
|
|
410
|
+
// process and share globalThis. With @cloudflare/vite-plugin everything
|
|
411
|
+
// runs inside the Worker so globalThis is the Worker's global — also correct.
|
|
412
|
+
if (typeof _instrumentation.onRequestError === "function") {
|
|
413
|
+
globalThis.__VINEXT_onRequestErrorHandler__ = _instrumentation.onRequestError;
|
|
414
|
+
}` : ""}
|
|
415
|
+
|
|
350
416
|
const routes = [
|
|
351
417
|
${routeEntries.join(",\n")}
|
|
352
418
|
];
|
|
@@ -418,13 +484,17 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
|
|
|
418
484
|
// We wrap each layout with LayoutSegmentProvider and add GlobalErrorBoundary
|
|
419
485
|
// to match the wrapping order in buildPageElement(), ensuring smooth
|
|
420
486
|
// client-side tree reconciliation.
|
|
421
|
-
const
|
|
487
|
+
const _treePositions = route?.layoutTreePositions;
|
|
488
|
+
const _routeSegs = route?.routeSegments || [];
|
|
489
|
+
const _fallbackParams = opts?.matchedParams ?? route?.params ?? {};
|
|
490
|
+
const _asyncFallbackParams = makeThenableParams(_fallbackParams);
|
|
422
491
|
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
423
492
|
const LayoutComponent = layouts[i]?.default;
|
|
424
493
|
if (LayoutComponent) {
|
|
425
|
-
element = createElement(LayoutComponent, { children: element });
|
|
426
|
-
const
|
|
427
|
-
|
|
494
|
+
element = createElement(LayoutComponent, { children: element, params: _asyncFallbackParams });
|
|
495
|
+
const _tp = _treePositions ? _treePositions[i] : 0;
|
|
496
|
+
const _cs = __resolveChildSegments(_routeSegs, _tp, _fallbackParams);
|
|
497
|
+
element = createElement(LayoutSegmentProvider, { childSegments: _cs }, element);
|
|
428
498
|
}
|
|
429
499
|
}
|
|
430
500
|
${globalErrorVar ? `
|
|
@@ -446,10 +516,12 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
|
|
|
446
516
|
}
|
|
447
517
|
// For HTML (full page load) responses, wrap with layouts only (no client-side
|
|
448
518
|
// wrappers needed since SSR generates the complete HTML document).
|
|
519
|
+
const _fallbackParamsHtml = opts?.matchedParams ?? route?.params ?? {};
|
|
520
|
+
const _asyncFallbackParamsHtml = makeThenableParams(_fallbackParamsHtml);
|
|
449
521
|
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
450
522
|
const LayoutComponent = layouts[i]?.default;
|
|
451
523
|
if (LayoutComponent) {
|
|
452
|
-
element = createElement(LayoutComponent, { children: element });
|
|
524
|
+
element = createElement(LayoutComponent, { children: element, params: _asyncFallbackParamsHtml });
|
|
453
525
|
}
|
|
454
526
|
}
|
|
455
527
|
const rscStream = renderToReadableStream(element, { onError: rscOnError });
|
|
@@ -473,8 +545,8 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
|
|
|
473
545
|
}
|
|
474
546
|
|
|
475
547
|
/** Convenience: render a not-found page (404) */
|
|
476
|
-
async function renderNotFoundPage(route, isRscRequest, request) {
|
|
477
|
-
return renderHTTPAccessFallbackPage(route, 404, isRscRequest, request);
|
|
548
|
+
async function renderNotFoundPage(route, isRscRequest, request, matchedParams) {
|
|
549
|
+
return renderHTTPAccessFallbackPage(route, 404, isRscRequest, request, { matchedParams });
|
|
478
550
|
}
|
|
479
551
|
|
|
480
552
|
/**
|
|
@@ -484,7 +556,7 @@ async function renderNotFoundPage(route, isRscRequest, request) {
|
|
|
484
556
|
* Next.js returns HTTP 200 when error.tsx catches an error (the error is "handled"
|
|
485
557
|
* by the boundary). This matches that behavior intentionally.
|
|
486
558
|
*/
|
|
487
|
-
async function renderErrorBoundaryPage(route, error, isRscRequest, request) {
|
|
559
|
+
async function renderErrorBoundaryPage(route, error, isRscRequest, request, matchedParams) {
|
|
488
560
|
// Resolve the error boundary component: leaf error.tsx first, then walk per-layout
|
|
489
561
|
// errors from innermost to outermost (matching ancestor inheritance), then global-error.tsx.
|
|
490
562
|
let ErrorComponent = route?.error?.default ?? null;
|
|
@@ -517,13 +589,17 @@ async function renderErrorBoundaryPage(route, error, isRscRequest, request) {
|
|
|
517
589
|
// wrappers that buildPageElement() uses (LayoutSegmentProvider, GlobalErrorBoundary).
|
|
518
590
|
// This ensures React can reconcile the tree without destroying the DOM.
|
|
519
591
|
// Same rationale as renderHTTPAccessFallbackPage — see comment there.
|
|
520
|
-
const
|
|
592
|
+
const _errTreePositions = route?.layoutTreePositions;
|
|
593
|
+
const _errRouteSegs = route?.routeSegments || [];
|
|
594
|
+
const _errParams = matchedParams ?? route?.params ?? {};
|
|
595
|
+
const _asyncErrParams = makeThenableParams(_errParams);
|
|
521
596
|
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
522
597
|
const LayoutComponent = layouts[i]?.default;
|
|
523
598
|
if (LayoutComponent) {
|
|
524
|
-
element = createElement(LayoutComponent, { children: element });
|
|
525
|
-
const
|
|
526
|
-
|
|
599
|
+
element = createElement(LayoutComponent, { children: element, params: _asyncErrParams });
|
|
600
|
+
const _etp = _errTreePositions ? _errTreePositions[i] : 0;
|
|
601
|
+
const _ecs = __resolveChildSegments(_errRouteSegs, _etp, _errParams);
|
|
602
|
+
element = createElement(LayoutSegmentProvider, { childSegments: _ecs }, element);
|
|
527
603
|
}
|
|
528
604
|
}
|
|
529
605
|
${globalErrorVar ? `
|
|
@@ -544,10 +620,12 @@ async function renderErrorBoundaryPage(route, error, isRscRequest, request) {
|
|
|
544
620
|
});
|
|
545
621
|
}
|
|
546
622
|
// For HTML (full page load) responses, wrap with layouts only.
|
|
623
|
+
const _errParamsHtml = matchedParams ?? route?.params ?? {};
|
|
624
|
+
const _asyncErrParamsHtml = makeThenableParams(_errParamsHtml);
|
|
547
625
|
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
548
626
|
const LayoutComponent = layouts[i]?.default;
|
|
549
627
|
if (LayoutComponent) {
|
|
550
|
-
element = createElement(LayoutComponent, { children: element });
|
|
628
|
+
element = createElement(LayoutComponent, { children: element, params: _asyncErrParamsHtml });
|
|
551
629
|
}
|
|
552
630
|
}
|
|
553
631
|
const rscStream = renderToReadableStream(element, { onError: rscOnError });
|
|
@@ -703,6 +781,10 @@ async function buildPageElement(route, params, opts, searchParams) {
|
|
|
703
781
|
}
|
|
704
782
|
let element = createElement(PageComponent, pageProps);
|
|
705
783
|
|
|
784
|
+
// Wrap page with empty segment provider so useSelectedLayoutSegments()
|
|
785
|
+
// returns [] when called from inside a page component (leaf node).
|
|
786
|
+
element = createElement(LayoutSegmentProvider, { childSegments: [] }, element);
|
|
787
|
+
|
|
706
788
|
// Add metadata + viewport head tags (React 19 hoists title/meta/link to <head>)
|
|
707
789
|
// Next.js always injects charset and default viewport even when no metadata/viewport
|
|
708
790
|
// is exported. We replicate that by always emitting these essential head elements.
|
|
@@ -853,12 +935,13 @@ async function buildPageElement(route, params, opts, searchParams) {
|
|
|
853
935
|
element = createElement(LayoutComponent, layoutProps);
|
|
854
936
|
|
|
855
937
|
// Wrap the layout with LayoutSegmentProvider so useSelectedLayoutSegments()
|
|
856
|
-
// called INSIDE this layout
|
|
857
|
-
//
|
|
858
|
-
//
|
|
859
|
-
//
|
|
860
|
-
const
|
|
861
|
-
|
|
938
|
+
// called INSIDE this layout gets the correct child segments. We resolve the
|
|
939
|
+
// route tree segments using actual param values and pass them through context.
|
|
940
|
+
// We wrap the layout (not just children) because hooks are called from
|
|
941
|
+
// components rendered inside the layout's own JSX.
|
|
942
|
+
const treePos = route.layoutTreePositions ? route.layoutTreePositions[i] : 0;
|
|
943
|
+
const childSegs = __resolveChildSegments(route.routeSegments || [], treePos, params);
|
|
944
|
+
element = createElement(LayoutSegmentProvider, { childSegments: childSegs }, element);
|
|
862
945
|
}
|
|
863
946
|
}
|
|
864
947
|
|
|
@@ -1187,12 +1270,6 @@ async function __proxyExternalRequest(request, externalUrl) {
|
|
|
1187
1270
|
const headers = new Headers(request.headers);
|
|
1188
1271
|
headers.set("host", targetUrl.host);
|
|
1189
1272
|
headers.delete("connection");
|
|
1190
|
-
// Strip credentials and internal headers to prevent leaking auth tokens,
|
|
1191
|
-
// session cookies, and middleware internals to third-party origins.
|
|
1192
|
-
headers.delete("cookie");
|
|
1193
|
-
headers.delete("authorization");
|
|
1194
|
-
headers.delete("x-api-key");
|
|
1195
|
-
headers.delete("proxy-authorization");
|
|
1196
1273
|
for (const key of [...headers.keys()]) {
|
|
1197
1274
|
if (key.startsWith("x-middleware-")) headers.delete(key);
|
|
1198
1275
|
}
|
|
@@ -1282,19 +1359,13 @@ export default async function handler(request) {
|
|
|
1282
1359
|
const lk = h.key.toLowerCase();
|
|
1283
1360
|
if (lk === "vary" || lk === "set-cookie") {
|
|
1284
1361
|
response.headers.append(h.key, h.value);
|
|
1285
|
-
} else {
|
|
1362
|
+
} else if (!response.headers.has(lk)) {
|
|
1363
|
+
// Middleware headers take precedence: skip config keys already
|
|
1364
|
+
// set by middleware so middleware headers always win.
|
|
1286
1365
|
response.headers.set(h.key, h.value);
|
|
1287
1366
|
}
|
|
1288
1367
|
}
|
|
1289
1368
|
}
|
|
1290
|
-
// Merge middleware response headers into the final response.
|
|
1291
|
-
// This runs at the top level so every response path (route
|
|
1292
|
-
// handlers, server actions, metadata, errors, etc.) gets them.
|
|
1293
|
-
if (_mwCtx.headers) {
|
|
1294
|
-
for (const [key, value] of _mwCtx.headers) {
|
|
1295
|
-
response.headers.append(key, value);
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
1369
|
}
|
|
1299
1370
|
return response;
|
|
1300
1371
|
})
|
|
@@ -1405,7 +1476,9 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1405
1476
|
mwUrl.pathname = cleanPathname;
|
|
1406
1477
|
const mwRequest = new Request(mwUrl, request);
|
|
1407
1478
|
const nextRequest = mwRequest instanceof NextRequest ? mwRequest : new NextRequest(mwRequest);
|
|
1408
|
-
const
|
|
1479
|
+
const mwFetchEvent = new NextFetchEvent({ page: cleanPathname });
|
|
1480
|
+
const mwResponse = await middlewareFn(nextRequest, mwFetchEvent);
|
|
1481
|
+
mwFetchEvent.drainWaitUntil();
|
|
1409
1482
|
if (mwResponse) {
|
|
1410
1483
|
// Check for x-middleware-next (continue)
|
|
1411
1484
|
if (mwResponse.headers.get("x-middleware-next") === "1") {
|
|
@@ -1763,6 +1836,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1763
1836
|
if (route.routeHandler) {
|
|
1764
1837
|
const handler = route.routeHandler;
|
|
1765
1838
|
const method = request.method.toUpperCase();
|
|
1839
|
+
const revalidateSeconds = typeof handler.revalidate === "number" && handler.revalidate > 0 ? handler.revalidate : null;
|
|
1766
1840
|
|
|
1767
1841
|
// Collect exported HTTP methods for OPTIONS auto-response and Allow header
|
|
1768
1842
|
const HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"];
|
|
@@ -1797,6 +1871,12 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1797
1871
|
try {
|
|
1798
1872
|
const response = await handlerFn(request, { params });
|
|
1799
1873
|
|
|
1874
|
+
// Apply Cache-Control from route segment config (export const revalidate = N).
|
|
1875
|
+
// Next.js sets s-maxage on GET route handlers with a numeric revalidate value.
|
|
1876
|
+
if (revalidateSeconds !== null && (method === "GET" || isAutoHead) && !response.headers.has("cache-control")) {
|
|
1877
|
+
response.headers.set("cache-control", "s-maxage=" + revalidateSeconds + ", stale-while-revalidate");
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1800
1880
|
// Collect any Set-Cookie headers from cookies().set()/delete() calls
|
|
1801
1881
|
const pendingCookies = getAndClearPendingCookies();
|
|
1802
1882
|
const draftCookie = getDraftModeCookieHeader();
|
|
@@ -2018,7 +2098,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2018
2098
|
}
|
|
2019
2099
|
if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
|
|
2020
2100
|
const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
|
|
2021
|
-
const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request);
|
|
2101
|
+
const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request, { matchedParams: params });
|
|
2022
2102
|
if (fallbackResp) return fallbackResp;
|
|
2023
2103
|
setHeadersContext(null);
|
|
2024
2104
|
setNavigationContext(null);
|
|
@@ -2027,7 +2107,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2027
2107
|
}
|
|
2028
2108
|
}
|
|
2029
2109
|
// Non-special error (e.g. generateMetadata() threw) — render error.tsx if available
|
|
2030
|
-
const errorBoundaryResp = await renderErrorBoundaryPage(route, buildErr, isRscRequest, request);
|
|
2110
|
+
const errorBoundaryResp = await renderErrorBoundaryPage(route, buildErr, isRscRequest, request, params);
|
|
2031
2111
|
if (errorBoundaryResp) return errorBoundaryResp;
|
|
2032
2112
|
throw buildErr;
|
|
2033
2113
|
}
|
|
@@ -2049,7 +2129,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2049
2129
|
}
|
|
2050
2130
|
if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
|
|
2051
2131
|
const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
|
|
2052
|
-
const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request);
|
|
2132
|
+
const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request, { matchedParams: params });
|
|
2053
2133
|
if (fallbackResp) return fallbackResp;
|
|
2054
2134
|
setHeadersContext(null);
|
|
2055
2135
|
setNavigationContext(null);
|
|
@@ -2074,54 +2154,61 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2074
2154
|
// layouts itself throws notFound() during the fallback rendering (causing a 500).
|
|
2075
2155
|
if (route.layouts && route.layouts.length > 0) {
|
|
2076
2156
|
const asyncParams = makeThenableParams(params);
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
const
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2157
|
+
// Run inside ALS context so the module-level console.error patch suppresses
|
|
2158
|
+
// "Invalid hook call" only for this request's probe — concurrent requests
|
|
2159
|
+
// each have their own ALS store and are unaffected.
|
|
2160
|
+
const _layoutProbeResult = await _suppressHookWarningAls.run(true, async () => {
|
|
2161
|
+
for (let li = route.layouts.length - 1; li >= 0; li--) {
|
|
2162
|
+
const LayoutComp = route.layouts[li]?.default;
|
|
2163
|
+
if (!LayoutComp) continue;
|
|
2164
|
+
try {
|
|
2165
|
+
const lr = LayoutComp({ params: asyncParams, children: null });
|
|
2166
|
+
if (lr && typeof lr === "object" && typeof lr.then === "function") await lr;
|
|
2167
|
+
} catch (layoutErr) {
|
|
2168
|
+
if (layoutErr && typeof layoutErr === "object" && "digest" in layoutErr) {
|
|
2169
|
+
const digest = String(layoutErr.digest);
|
|
2170
|
+
if (digest.startsWith("NEXT_REDIRECT;")) {
|
|
2171
|
+
const parts = digest.split(";");
|
|
2172
|
+
const redirectUrl = decodeURIComponent(parts[2]);
|
|
2173
|
+
const statusCode = parts[3] ? parseInt(parts[3], 10) : 307;
|
|
2174
|
+
setHeadersContext(null);
|
|
2175
|
+
setNavigationContext(null);
|
|
2176
|
+
return Response.redirect(new URL(redirectUrl, request.url), statusCode);
|
|
2177
|
+
}
|
|
2178
|
+
if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
|
|
2179
|
+
const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
|
|
2180
|
+
// Find the not-found component from the parent level (the boundary that
|
|
2181
|
+
// would catch this in Next.js). Walk up from the throwing layout to find
|
|
2182
|
+
// the nearest not-found at a parent layout's directory.
|
|
2183
|
+
let parentNotFound = null;
|
|
2184
|
+
if (route.notFounds) {
|
|
2185
|
+
for (let pi = li - 1; pi >= 0; pi--) {
|
|
2186
|
+
if (route.notFounds[pi]?.default) {
|
|
2187
|
+
parentNotFound = route.notFounds[pi].default;
|
|
2188
|
+
break;
|
|
2189
|
+
}
|
|
2105
2190
|
}
|
|
2106
2191
|
}
|
|
2192
|
+
if (!parentNotFound) parentNotFound = ${rootNotFoundVar ? `${rootNotFoundVar}?.default` : "null"};
|
|
2193
|
+
// Wrap in only the layouts above the throwing one
|
|
2194
|
+
const parentLayouts = route.layouts.slice(0, li);
|
|
2195
|
+
const fallbackResp = await renderHTTPAccessFallbackPage(
|
|
2196
|
+
route, statusCode, isRscRequest, request,
|
|
2197
|
+
{ boundaryComponent: parentNotFound, layouts: parentLayouts, matchedParams: params }
|
|
2198
|
+
);
|
|
2199
|
+
if (fallbackResp) return fallbackResp;
|
|
2200
|
+
setHeadersContext(null);
|
|
2201
|
+
setNavigationContext(null);
|
|
2202
|
+
const statusText = statusCode === 403 ? "Forbidden" : statusCode === 401 ? "Unauthorized" : "Not Found";
|
|
2203
|
+
return new Response(statusText, { status: statusCode });
|
|
2107
2204
|
}
|
|
2108
|
-
if (!parentNotFound) parentNotFound = ${rootNotFoundVar ? `${rootNotFoundVar}?.default` : "null"};
|
|
2109
|
-
// Wrap in only the layouts above the throwing one
|
|
2110
|
-
const parentLayouts = route.layouts.slice(0, li);
|
|
2111
|
-
const fallbackResp = await renderHTTPAccessFallbackPage(
|
|
2112
|
-
route, statusCode, isRscRequest, request,
|
|
2113
|
-
{ boundaryComponent: parentNotFound, layouts: parentLayouts }
|
|
2114
|
-
);
|
|
2115
|
-
if (fallbackResp) return fallbackResp;
|
|
2116
|
-
setHeadersContext(null);
|
|
2117
|
-
setNavigationContext(null);
|
|
2118
|
-
const statusText = statusCode === 403 ? "Forbidden" : statusCode === 401 ? "Unauthorized" : "Not Found";
|
|
2119
|
-
return new Response(statusText, { status: statusCode });
|
|
2120
2205
|
}
|
|
2206
|
+
// Not a special error — let it propagate through normal RSC rendering
|
|
2121
2207
|
}
|
|
2122
|
-
// Not a special error — let it propagate through normal RSC rendering
|
|
2123
2208
|
}
|
|
2124
|
-
|
|
2209
|
+
return null;
|
|
2210
|
+
});
|
|
2211
|
+
if (_layoutProbeResult instanceof Response) return _layoutProbeResult;
|
|
2125
2212
|
}
|
|
2126
2213
|
|
|
2127
2214
|
// Pre-render the page component to catch redirect()/notFound() thrown synchronously.
|
|
@@ -2134,37 +2221,35 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2134
2221
|
// would be hit before the RSC stream even starts).
|
|
2135
2222
|
//
|
|
2136
2223
|
// Because this calls the component outside React's render cycle, hooks like use()
|
|
2137
|
-
// trigger "Invalid hook call" console.error in dev.
|
|
2224
|
+
// trigger "Invalid hook call" console.error in dev. The module-level ALS patch
|
|
2225
|
+
// suppresses the warning only within this request's execution context.
|
|
2138
2226
|
const _hasLoadingBoundary = !!(route.loading && route.loading.default);
|
|
2139
|
-
const
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
// Suppress unhandled promise rejection — with a loading boundary,
|
|
2154
|
-
// redirect/notFound errors are handled by rscOnError during streaming.
|
|
2155
|
-
testResult.catch(() => {});
|
|
2227
|
+
const _pageProbeResult = await _suppressHookWarningAls.run(true, async () => {
|
|
2228
|
+
try {
|
|
2229
|
+
const testResult = PageComponent({ params });
|
|
2230
|
+
// If it's a promise (async component), only await if there's no loading boundary.
|
|
2231
|
+
// With a loading boundary, the Suspense streaming pipeline handles async resolution
|
|
2232
|
+
// and any redirect/notFound errors via rscOnError.
|
|
2233
|
+
if (testResult && typeof testResult === "object" && typeof testResult.then === "function") {
|
|
2234
|
+
if (!_hasLoadingBoundary) {
|
|
2235
|
+
await testResult;
|
|
2236
|
+
} else {
|
|
2237
|
+
// Suppress unhandled promise rejection — with a loading boundary,
|
|
2238
|
+
// redirect/notFound errors are handled by rscOnError during streaming.
|
|
2239
|
+
testResult.catch(() => {});
|
|
2240
|
+
}
|
|
2156
2241
|
}
|
|
2242
|
+
} catch (preRenderErr) {
|
|
2243
|
+
const specialResponse = await handleRenderError(preRenderErr);
|
|
2244
|
+
if (specialResponse) return specialResponse;
|
|
2245
|
+
// Non-special errors from the pre-render test are expected (e.g. use() hook
|
|
2246
|
+
// fails outside React's render cycle, client references can't execute on server).
|
|
2247
|
+
// Only redirect/notFound/forbidden/unauthorized are actionable here — other
|
|
2248
|
+
// errors will be properly caught during actual RSC/SSR rendering below.
|
|
2157
2249
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
// Non-special errors from the pre-render test are expected (e.g. use() hook
|
|
2162
|
-
// fails outside React's render cycle, client references can't execute on server).
|
|
2163
|
-
// Only redirect/notFound/forbidden/unauthorized are actionable here — other
|
|
2164
|
-
// errors will be properly caught during actual RSC/SSR rendering below.
|
|
2165
|
-
} finally {
|
|
2166
|
-
console.error = _origConsoleError;
|
|
2167
|
-
}
|
|
2250
|
+
return null;
|
|
2251
|
+
});
|
|
2252
|
+
if (_pageProbeResult instanceof Response) return _pageProbeResult;
|
|
2168
2253
|
|
|
2169
2254
|
// Mark end of compile phase: route matching, middleware, tree building are done.
|
|
2170
2255
|
if (process.env.NODE_ENV !== "production") __compileEnd = performance.now();
|
|
@@ -2191,7 +2276,37 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2191
2276
|
} else if (revalidateSeconds) {
|
|
2192
2277
|
responseHeaders["Cache-Control"] = "s-maxage=" + revalidateSeconds + ", stale-while-revalidate";
|
|
2193
2278
|
}
|
|
2194
|
-
//
|
|
2279
|
+
// Merge middleware response headers into the RSC response.
|
|
2280
|
+
// set-cookie and vary are accumulated to preserve existing values
|
|
2281
|
+
// (e.g. "Vary: RSC, Accept" set above); all other keys use plain
|
|
2282
|
+
// assignment so middleware headers win over config headers, which
|
|
2283
|
+
// the outer handler applies afterward and skips keys already present.
|
|
2284
|
+
if (_mwCtx.headers) {
|
|
2285
|
+
for (const [key, value] of _mwCtx.headers) {
|
|
2286
|
+
const lk = key.toLowerCase();
|
|
2287
|
+
if (lk === "set-cookie") {
|
|
2288
|
+
const existing = responseHeaders[lk];
|
|
2289
|
+
if (Array.isArray(existing)) {
|
|
2290
|
+
existing.push(value);
|
|
2291
|
+
} else if (existing) {
|
|
2292
|
+
responseHeaders[lk] = [existing, value];
|
|
2293
|
+
} else {
|
|
2294
|
+
responseHeaders[lk] = [value];
|
|
2295
|
+
}
|
|
2296
|
+
} else if (lk === "vary") {
|
|
2297
|
+
// Accumulate Vary values to preserve the existing "RSC, Accept" entry.
|
|
2298
|
+
const existing = responseHeaders["Vary"] ?? responseHeaders["vary"];
|
|
2299
|
+
if (existing) {
|
|
2300
|
+
responseHeaders["Vary"] = existing + ", " + value;
|
|
2301
|
+
if (responseHeaders["vary"] !== undefined) delete responseHeaders["vary"];
|
|
2302
|
+
} else {
|
|
2303
|
+
responseHeaders[key] = value;
|
|
2304
|
+
}
|
|
2305
|
+
} else {
|
|
2306
|
+
responseHeaders[key] = value;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2195
2310
|
// Attach internal timing header so the dev server middleware can log it.
|
|
2196
2311
|
// Format: "handlerStart,compileMs,renderMs"
|
|
2197
2312
|
// handlerStart - absolute performance.now() when _handleRequest began,
|
|
@@ -2239,7 +2354,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2239
2354
|
const specialResponse = await handleRenderError(ssrErr);
|
|
2240
2355
|
if (specialResponse) return specialResponse;
|
|
2241
2356
|
// Non-special error during SSR — render error.tsx if available
|
|
2242
|
-
const errorBoundaryResp = await renderErrorBoundaryPage(route, ssrErr, isRscRequest, request);
|
|
2357
|
+
const errorBoundaryResp = await renderErrorBoundaryPage(route, ssrErr, isRscRequest, request, params);
|
|
2243
2358
|
if (errorBoundaryResp) return errorBoundaryResp;
|
|
2244
2359
|
throw ssrErr;
|
|
2245
2360
|
}
|
|
@@ -2259,7 +2374,16 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2259
2374
|
if (fontLinkHeader) {
|
|
2260
2375
|
response.headers.set("Link", fontLinkHeader);
|
|
2261
2376
|
}
|
|
2262
|
-
//
|
|
2377
|
+
// Merge middleware response headers into the final response.
|
|
2378
|
+
// The response is freshly constructed above (new Response(htmlStream, {...})),
|
|
2379
|
+
// so set() and append() are equivalent — there are no same-key conflicts yet.
|
|
2380
|
+
// Precedence over config headers is handled by the outer handler, which
|
|
2381
|
+
// skips config keys that middleware already placed on the response.
|
|
2382
|
+
if (_mwCtx.headers) {
|
|
2383
|
+
for (const [key, value] of _mwCtx.headers) {
|
|
2384
|
+
response.headers.append(key, value);
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2263
2387
|
// Attach internal timing header so the dev server middleware can log it.
|
|
2264
2388
|
// Format: "handlerStart,compileMs,renderMs"
|
|
2265
2389
|
// handlerStart - absolute performance.now() when _handleRequest began,
|