vinext 0.0.23 → 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 +59 -9
- 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 +140 -56
- package/dist/deploy.js.map +1 -1
- package/dist/index.d.ts +14 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +195 -25
- 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 +294 -133
- 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 +8 -2
- package/dist/server/prod-server.d.ts.map +1 -1
- package/dist/server/prod-server.js +56 -35
- package/dist/server/prod-server.js.map +1 -1
- package/dist/server/worker-utils.d.ts +15 -0
- package/dist/server/worker-utils.d.ts.map +1 -0
- package/dist/server/worker-utils.js +41 -0
- package/dist/server/worker-utils.js.map +1 -0
- 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/headers.d.ts +6 -0
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/headers.js +8 -0
- package/dist/shims/headers.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/dist/utils/project.d.ts +13 -1
- package/dist/utils/project.d.ts.map +1 -1
- package/dist/utils/project.js +63 -13
- package/dist/utils/project.js.map +1 -1
- package/package.json +6 -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
|
-
import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader, getAndClearPendingCookies, consumeDynamicUsage, markDynamicUsage, runWithHeadersContext, applyMiddlewareRequestHeaders } from "next/headers";
|
|
200
|
-
import { NextRequest } from "next/server";
|
|
201
|
+
import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader, getAndClearPendingCookies, consumeDynamicUsage, markDynamicUsage, runWithHeadersContext, applyMiddlewareRequestHeaders, getHeadersContext } from "next/headers";
|
|
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
|
|
|
@@ -1051,6 +1134,28 @@ function __buildRequestContext(request) {
|
|
|
1051
1134
|
};
|
|
1052
1135
|
}
|
|
1053
1136
|
|
|
1137
|
+
/**
|
|
1138
|
+
* Build a request context from the live ALS HeadersContext, which reflects
|
|
1139
|
+
* any x-middleware-request-* header mutations applied by middleware.
|
|
1140
|
+
* Used for afterFiles and fallback rewrite has/missing evaluation — these
|
|
1141
|
+
* run after middleware in the App Router execution order.
|
|
1142
|
+
*/
|
|
1143
|
+
function __buildPostMwRequestContext(request) {
|
|
1144
|
+
const url = new URL(request.url);
|
|
1145
|
+
const ctx = getHeadersContext();
|
|
1146
|
+
if (!ctx) return __buildRequestContext(request);
|
|
1147
|
+
// ctx.cookies is a Map<string, string> (HeadersContext), but RequestContext
|
|
1148
|
+
// requires a plain Record<string, string> for has/missing cookie evaluation
|
|
1149
|
+
// (config-matchers.ts uses obj[key] not Map.get()). Convert here.
|
|
1150
|
+
const cookiesRecord = Object.fromEntries(ctx.cookies);
|
|
1151
|
+
return {
|
|
1152
|
+
headers: ctx.headers,
|
|
1153
|
+
cookies: cookiesRecord,
|
|
1154
|
+
query: url.searchParams,
|
|
1155
|
+
host: ctx.headers.get("host") || url.host,
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1054
1159
|
function __sanitizeDestination(dest) {
|
|
1055
1160
|
if (dest.startsWith("http://") || dest.startsWith("https://")) return dest;
|
|
1056
1161
|
dest = dest.replace(/^[\\\\/]+/, "/");
|
|
@@ -1165,12 +1270,6 @@ async function __proxyExternalRequest(request, externalUrl) {
|
|
|
1165
1270
|
const headers = new Headers(request.headers);
|
|
1166
1271
|
headers.set("host", targetUrl.host);
|
|
1167
1272
|
headers.delete("connection");
|
|
1168
|
-
// Strip credentials and internal headers to prevent leaking auth tokens,
|
|
1169
|
-
// session cookies, and middleware internals to third-party origins.
|
|
1170
|
-
headers.delete("cookie");
|
|
1171
|
-
headers.delete("authorization");
|
|
1172
|
-
headers.delete("x-api-key");
|
|
1173
|
-
headers.delete("proxy-authorization");
|
|
1174
1273
|
for (const key of [...headers.keys()]) {
|
|
1175
1274
|
if (key.startsWith("x-middleware-")) headers.delete(key);
|
|
1176
1275
|
}
|
|
@@ -1185,7 +1284,16 @@ async function __proxyExternalRequest(request, externalUrl) {
|
|
|
1185
1284
|
console.error("[vinext] External rewrite proxy error:", e); return new Response("Bad Gateway", { status: 502 });
|
|
1186
1285
|
}
|
|
1187
1286
|
const respHeaders = new Headers();
|
|
1188
|
-
|
|
1287
|
+
// Node.js fetch() auto-decompresses response bodies, while Workers fetch()
|
|
1288
|
+
// preserves wire encoding. Only strip encoding/length on Node.js to avoid
|
|
1289
|
+
// double-decompression errors without breaking Workers parity.
|
|
1290
|
+
const __isNodeRuntime = typeof process !== "undefined" && !!(process.versions && process.versions.node);
|
|
1291
|
+
upstream.headers.forEach(function(value, key) {
|
|
1292
|
+
var lower = key.toLowerCase();
|
|
1293
|
+
if (__hopByHopHeaders.has(lower)) return;
|
|
1294
|
+
if (__isNodeRuntime && (lower === "content-encoding" || lower === "content-length")) return;
|
|
1295
|
+
respHeaders.append(key, value);
|
|
1296
|
+
});
|
|
1189
1297
|
return new Response(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers: respHeaders });
|
|
1190
1298
|
}
|
|
1191
1299
|
|
|
@@ -1251,19 +1359,13 @@ export default async function handler(request) {
|
|
|
1251
1359
|
const lk = h.key.toLowerCase();
|
|
1252
1360
|
if (lk === "vary" || lk === "set-cookie") {
|
|
1253
1361
|
response.headers.append(h.key, h.value);
|
|
1254
|
-
} 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.
|
|
1255
1365
|
response.headers.set(h.key, h.value);
|
|
1256
1366
|
}
|
|
1257
1367
|
}
|
|
1258
1368
|
}
|
|
1259
|
-
// Merge middleware response headers into the final response.
|
|
1260
|
-
// This runs at the top level so every response path (route
|
|
1261
|
-
// handlers, server actions, metadata, errors, etc.) gets them.
|
|
1262
|
-
if (_mwCtx.headers) {
|
|
1263
|
-
for (const [key, value] of _mwCtx.headers) {
|
|
1264
|
-
response.headers.append(key, value);
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
1369
|
}
|
|
1268
1370
|
return response;
|
|
1269
1371
|
})
|
|
@@ -1344,21 +1446,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1344
1446
|
}
|
|
1345
1447
|
}
|
|
1346
1448
|
|
|
1347
|
-
// ── Apply beforeFiles rewrites from next.config.js ────────────────────
|
|
1348
|
-
if (__configRewrites.beforeFiles && __configRewrites.beforeFiles.length) {
|
|
1349
|
-
// Strip .rsc suffix before matching rewrite rules — same reason as redirects above.
|
|
1350
|
-
const __rewritePathname = pathname.endsWith(".rsc") ? pathname.slice(0, -4) : pathname;
|
|
1351
|
-
const __rewritten = __applyConfigRewrites(__rewritePathname, __configRewrites.beforeFiles, __reqCtx);
|
|
1352
|
-
if (__rewritten) {
|
|
1353
|
-
if (__isExternalUrl(__rewritten)) {
|
|
1354
|
-
setHeadersContext(null);
|
|
1355
|
-
setNavigationContext(null);
|
|
1356
|
-
return __proxyExternalRequest(request, __rewritten);
|
|
1357
|
-
}
|
|
1358
|
-
pathname = __rewritten;
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
1449
|
const isRscRequest = pathname.endsWith(".rsc") || request.headers.get("accept")?.includes("text/x-component");
|
|
1363
1450
|
let cleanPathname = pathname.replace(/\\.rsc$/, "");
|
|
1364
1451
|
|
|
@@ -1389,7 +1476,9 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1389
1476
|
mwUrl.pathname = cleanPathname;
|
|
1390
1477
|
const mwRequest = new Request(mwUrl, request);
|
|
1391
1478
|
const nextRequest = mwRequest instanceof NextRequest ? mwRequest : new NextRequest(mwRequest);
|
|
1392
|
-
const
|
|
1479
|
+
const mwFetchEvent = new NextFetchEvent({ page: cleanPathname });
|
|
1480
|
+
const mwResponse = await middlewareFn(nextRequest, mwFetchEvent);
|
|
1481
|
+
mwFetchEvent.drainWaitUntil();
|
|
1393
1482
|
if (mwResponse) {
|
|
1394
1483
|
// Check for x-middleware-next (continue)
|
|
1395
1484
|
if (mwResponse.headers.get("x-middleware-next") === "1") {
|
|
@@ -1452,6 +1541,27 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1452
1541
|
}
|
|
1453
1542
|
` : ""}
|
|
1454
1543
|
|
|
1544
|
+
// Build post-middleware request context for afterFiles/fallback rewrites.
|
|
1545
|
+
// These run after middleware in the App Router execution order and should
|
|
1546
|
+
// evaluate has/missing conditions against middleware-modified headers.
|
|
1547
|
+
// When no middleware is present, this falls back to __buildRequestContext.
|
|
1548
|
+
const __postMwReqCtx = __buildPostMwRequestContext(request);
|
|
1549
|
+
|
|
1550
|
+
// ── Apply beforeFiles rewrites from next.config.js ────────────────────
|
|
1551
|
+
// In App Router execution order, beforeFiles runs after middleware so that
|
|
1552
|
+
// has/missing conditions can evaluate against middleware-modified headers.
|
|
1553
|
+
if (__configRewrites.beforeFiles && __configRewrites.beforeFiles.length) {
|
|
1554
|
+
const __rewritten = __applyConfigRewrites(cleanPathname, __configRewrites.beforeFiles, __postMwReqCtx);
|
|
1555
|
+
if (__rewritten) {
|
|
1556
|
+
if (__isExternalUrl(__rewritten)) {
|
|
1557
|
+
setHeadersContext(null);
|
|
1558
|
+
setNavigationContext(null);
|
|
1559
|
+
return __proxyExternalRequest(request, __rewritten);
|
|
1560
|
+
}
|
|
1561
|
+
cleanPathname = __rewritten;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1455
1565
|
// ── Image optimization passthrough (dev mode — no transformation) ───────
|
|
1456
1566
|
if (cleanPathname === "/_vinext/image") {
|
|
1457
1567
|
const __rawImgUrl = url.searchParams.get("url");
|
|
@@ -1677,7 +1787,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1677
1787
|
|
|
1678
1788
|
// ── Apply afterFiles rewrites from next.config.js ──────────────────────
|
|
1679
1789
|
if (__configRewrites.afterFiles && __configRewrites.afterFiles.length) {
|
|
1680
|
-
const __afterRewritten = __applyConfigRewrites(cleanPathname, __configRewrites.afterFiles,
|
|
1790
|
+
const __afterRewritten = __applyConfigRewrites(cleanPathname, __configRewrites.afterFiles, __postMwReqCtx);
|
|
1681
1791
|
if (__afterRewritten) {
|
|
1682
1792
|
if (__isExternalUrl(__afterRewritten)) {
|
|
1683
1793
|
setHeadersContext(null);
|
|
@@ -1692,7 +1802,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1692
1802
|
|
|
1693
1803
|
// ── Fallback rewrites from next.config.js (if no route matched) ───────
|
|
1694
1804
|
if (!match && __configRewrites.fallback && __configRewrites.fallback.length) {
|
|
1695
|
-
const __fallbackRewritten = __applyConfigRewrites(cleanPathname, __configRewrites.fallback,
|
|
1805
|
+
const __fallbackRewritten = __applyConfigRewrites(cleanPathname, __configRewrites.fallback, __postMwReqCtx);
|
|
1696
1806
|
if (__fallbackRewritten) {
|
|
1697
1807
|
if (__isExternalUrl(__fallbackRewritten)) {
|
|
1698
1808
|
setHeadersContext(null);
|
|
@@ -1726,6 +1836,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1726
1836
|
if (route.routeHandler) {
|
|
1727
1837
|
const handler = route.routeHandler;
|
|
1728
1838
|
const method = request.method.toUpperCase();
|
|
1839
|
+
const revalidateSeconds = typeof handler.revalidate === "number" && handler.revalidate > 0 ? handler.revalidate : null;
|
|
1729
1840
|
|
|
1730
1841
|
// Collect exported HTTP methods for OPTIONS auto-response and Allow header
|
|
1731
1842
|
const HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"];
|
|
@@ -1760,6 +1871,12 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1760
1871
|
try {
|
|
1761
1872
|
const response = await handlerFn(request, { params });
|
|
1762
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
|
+
|
|
1763
1880
|
// Collect any Set-Cookie headers from cookies().set()/delete() calls
|
|
1764
1881
|
const pendingCookies = getAndClearPendingCookies();
|
|
1765
1882
|
const draftCookie = getDraftModeCookieHeader();
|
|
@@ -1981,7 +2098,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1981
2098
|
}
|
|
1982
2099
|
if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
|
|
1983
2100
|
const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
|
|
1984
|
-
const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request);
|
|
2101
|
+
const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request, { matchedParams: params });
|
|
1985
2102
|
if (fallbackResp) return fallbackResp;
|
|
1986
2103
|
setHeadersContext(null);
|
|
1987
2104
|
setNavigationContext(null);
|
|
@@ -1990,7 +2107,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
1990
2107
|
}
|
|
1991
2108
|
}
|
|
1992
2109
|
// Non-special error (e.g. generateMetadata() threw) — render error.tsx if available
|
|
1993
|
-
const errorBoundaryResp = await renderErrorBoundaryPage(route, buildErr, isRscRequest, request);
|
|
2110
|
+
const errorBoundaryResp = await renderErrorBoundaryPage(route, buildErr, isRscRequest, request, params);
|
|
1994
2111
|
if (errorBoundaryResp) return errorBoundaryResp;
|
|
1995
2112
|
throw buildErr;
|
|
1996
2113
|
}
|
|
@@ -2012,7 +2129,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2012
2129
|
}
|
|
2013
2130
|
if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
|
|
2014
2131
|
const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
|
|
2015
|
-
const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request);
|
|
2132
|
+
const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request, { matchedParams: params });
|
|
2016
2133
|
if (fallbackResp) return fallbackResp;
|
|
2017
2134
|
setHeadersContext(null);
|
|
2018
2135
|
setNavigationContext(null);
|
|
@@ -2037,54 +2154,61 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2037
2154
|
// layouts itself throws notFound() during the fallback rendering (causing a 500).
|
|
2038
2155
|
if (route.layouts && route.layouts.length > 0) {
|
|
2039
2156
|
const asyncParams = makeThenableParams(params);
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
const
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
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
|
+
}
|
|
2068
2190
|
}
|
|
2069
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 });
|
|
2070
2204
|
}
|
|
2071
|
-
if (!parentNotFound) parentNotFound = ${rootNotFoundVar ? `${rootNotFoundVar}?.default` : "null"};
|
|
2072
|
-
// Wrap in only the layouts above the throwing one
|
|
2073
|
-
const parentLayouts = route.layouts.slice(0, li);
|
|
2074
|
-
const fallbackResp = await renderHTTPAccessFallbackPage(
|
|
2075
|
-
route, statusCode, isRscRequest, request,
|
|
2076
|
-
{ boundaryComponent: parentNotFound, layouts: parentLayouts }
|
|
2077
|
-
);
|
|
2078
|
-
if (fallbackResp) return fallbackResp;
|
|
2079
|
-
setHeadersContext(null);
|
|
2080
|
-
setNavigationContext(null);
|
|
2081
|
-
const statusText = statusCode === 403 ? "Forbidden" : statusCode === 401 ? "Unauthorized" : "Not Found";
|
|
2082
|
-
return new Response(statusText, { status: statusCode });
|
|
2083
2205
|
}
|
|
2206
|
+
// Not a special error — let it propagate through normal RSC rendering
|
|
2084
2207
|
}
|
|
2085
|
-
// Not a special error — let it propagate through normal RSC rendering
|
|
2086
2208
|
}
|
|
2087
|
-
|
|
2209
|
+
return null;
|
|
2210
|
+
});
|
|
2211
|
+
if (_layoutProbeResult instanceof Response) return _layoutProbeResult;
|
|
2088
2212
|
}
|
|
2089
2213
|
|
|
2090
2214
|
// Pre-render the page component to catch redirect()/notFound() thrown synchronously.
|
|
@@ -2097,37 +2221,35 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2097
2221
|
// would be hit before the RSC stream even starts).
|
|
2098
2222
|
//
|
|
2099
2223
|
// Because this calls the component outside React's render cycle, hooks like use()
|
|
2100
|
-
// 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.
|
|
2101
2226
|
const _hasLoadingBoundary = !!(route.loading && route.loading.default);
|
|
2102
|
-
const
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
// Suppress unhandled promise rejection — with a loading boundary,
|
|
2117
|
-
// redirect/notFound errors are handled by rscOnError during streaming.
|
|
2118
|
-
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
|
+
}
|
|
2119
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.
|
|
2120
2249
|
}
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
// Non-special errors from the pre-render test are expected (e.g. use() hook
|
|
2125
|
-
// fails outside React's render cycle, client references can't execute on server).
|
|
2126
|
-
// Only redirect/notFound/forbidden/unauthorized are actionable here — other
|
|
2127
|
-
// errors will be properly caught during actual RSC/SSR rendering below.
|
|
2128
|
-
} finally {
|
|
2129
|
-
console.error = _origConsoleError;
|
|
2130
|
-
}
|
|
2250
|
+
return null;
|
|
2251
|
+
});
|
|
2252
|
+
if (_pageProbeResult instanceof Response) return _pageProbeResult;
|
|
2131
2253
|
|
|
2132
2254
|
// Mark end of compile phase: route matching, middleware, tree building are done.
|
|
2133
2255
|
if (process.env.NODE_ENV !== "production") __compileEnd = performance.now();
|
|
@@ -2154,7 +2276,37 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2154
2276
|
} else if (revalidateSeconds) {
|
|
2155
2277
|
responseHeaders["Cache-Control"] = "s-maxage=" + revalidateSeconds + ", stale-while-revalidate";
|
|
2156
2278
|
}
|
|
2157
|
-
//
|
|
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
|
+
}
|
|
2158
2310
|
// Attach internal timing header so the dev server middleware can log it.
|
|
2159
2311
|
// Format: "handlerStart,compileMs,renderMs"
|
|
2160
2312
|
// handlerStart - absolute performance.now() when _handleRequest began,
|
|
@@ -2202,7 +2354,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2202
2354
|
const specialResponse = await handleRenderError(ssrErr);
|
|
2203
2355
|
if (specialResponse) return specialResponse;
|
|
2204
2356
|
// Non-special error during SSR — render error.tsx if available
|
|
2205
|
-
const errorBoundaryResp = await renderErrorBoundaryPage(route, ssrErr, isRscRequest, request);
|
|
2357
|
+
const errorBoundaryResp = await renderErrorBoundaryPage(route, ssrErr, isRscRequest, request, params);
|
|
2206
2358
|
if (errorBoundaryResp) return errorBoundaryResp;
|
|
2207
2359
|
throw ssrErr;
|
|
2208
2360
|
}
|
|
@@ -2222,7 +2374,16 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
|
|
|
2222
2374
|
if (fontLinkHeader) {
|
|
2223
2375
|
response.headers.set("Link", fontLinkHeader);
|
|
2224
2376
|
}
|
|
2225
|
-
//
|
|
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
|
+
}
|
|
2226
2387
|
// Attach internal timing header so the dev server middleware can log it.
|
|
2227
2388
|
// Format: "handlerStart,compileMs,renderMs"
|
|
2228
2389
|
// handlerStart - absolute performance.now() when _handleRequest began,
|