diff --git a/packages/vinext/src/entries/app-rsc-entry.ts b/packages/vinext/src/entries/app-rsc-entry.ts
index fcceaaa60..433df40eb 100644
--- a/packages/vinext/src/entries/app-rsc-entry.ts
+++ b/packages/vinext/src/entries/app-rsc-entry.ts
@@ -2036,28 +2036,25 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
setHeadersAccessPhase(previousHeadersPhase);
}
- // If the action called redirect(), signal the client to navigate.
- // We can't use a real HTTP redirect (the fetch would follow it automatically
- // and receive a page HTML instead of RSC stream). Instead, we return a 200
- // with x-action-redirect header that the client entry detects and handles.
+ // If the action called redirect(), return a 303 redirect response.
+ // This matches Next.js behavior for server action redirects.
+ // The client will handle the redirect and navigate to the target URL.
if (actionRedirect) {
const actionPendingCookies = getAndClearPendingCookies();
const actionDraftCookie = getDraftModeCookieHeader();
+
setHeadersContext(null);
setNavigationContext(null);
const redirectHeaders = new Headers({
- "Content-Type": "text/x-component; charset=utf-8",
- "Vary": "RSC, Accept",
- "x-action-redirect": actionRedirect.url,
- "x-action-redirect-type": actionRedirect.type,
- "x-action-redirect-status": String(actionRedirect.status),
+ Location: actionRedirect.url,
});
for (const cookie of actionPendingCookies) {
redirectHeaders.append("Set-Cookie", cookie);
}
if (actionDraftCookie) redirectHeaders.append("Set-Cookie", actionDraftCookie);
- // Send an empty RSC-like body (client will navigate instead of parsing)
- return new Response("", { status: 200, headers: redirectHeaders });
+
+ // Return 303 See Other for server action redirects (Next.js behavior)
+ return new Response(null, { status: 303, headers: redirectHeaders });
}
// After the action, re-render the current page so the client
diff --git a/packages/vinext/src/server/app-browser-entry.ts b/packages/vinext/src/server/app-browser-entry.ts
index 27589b830..eb2ca059f 100644
--- a/packages/vinext/src/server/app-browser-entry.ts
+++ b/packages/vinext/src/server/app-browser-entry.ts
@@ -133,6 +133,7 @@ function registerServerActionCallback(): void {
const actionRedirect = fetchResponse.headers.get("x-action-redirect");
if (actionRedirect) {
+ // Check for external URLs that need a hard redirect.
try {
const redirectUrl = new URL(actionRedirect, window.location.origin);
if (redirectUrl.origin !== window.location.origin) {
@@ -140,20 +141,17 @@ function registerServerActionCallback(): void {
return undefined;
}
} catch {
- // Fall through to client-side navigation if URL parsing fails.
+ // Fall through to hard redirect below if URL parsing fails.
}
+ // For same-origin redirects, use hard redirect to ensure navigation completes.
+ // This matches Next.js behavior for server action redirects with useActionState.
const redirectType = fetchResponse.headers.get("x-action-redirect-type") ?? "replace";
if (redirectType === "push") {
- window.history.pushState(null, "", actionRedirect);
+ window.location.assign(actionRedirect);
} else {
- window.history.replaceState(null, "", actionRedirect);
+ window.location.replace(actionRedirect);
}
-
- if (typeof window.__VINEXT_RSC_NAVIGATE__ === "function") {
- await window.__VINEXT_RSC_NAVIGATE__(actionRedirect);
- }
-
return undefined;
}
diff --git a/tests/__snapshots__/entry-templates.test.ts.snap b/tests/__snapshots__/entry-templates.test.ts.snap
index 771476fb9..f7594387f 100644
--- a/tests/__snapshots__/entry-templates.test.ts.snap
+++ b/tests/__snapshots__/entry-templates.test.ts.snap
@@ -1748,13 +1748,55 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
setHeadersAccessPhase(previousHeadersPhase);
}
- // If the action called redirect(), signal the client to navigate.
- // We can't use a real HTTP redirect (the fetch would follow it automatically
- // and receive a page HTML instead of RSC stream). Instead, we return a 200
- // with x-action-redirect header that the client entry detects and handles.
+ // If the action called redirect(), render the redirect target's RSC payload
+ // and signal the client to navigate. This matches Next.js behavior where
+ // the redirect response includes the target page's RSC payload for soft navigation.
if (actionRedirect) {
const actionPendingCookies = getAndClearPendingCookies();
const actionDraftCookie = getDraftModeCookieHeader();
+
+ // Match the redirect target to a route and render its RSC payload
+ const redirectPathname = actionRedirect.url.startsWith("/")
+ ? actionRedirect.url.split("?")[0]
+ : actionRedirect.url;
+ const redirectMatch = matchRoute(redirectPathname);
+
+ if (redirectMatch) {
+ const { route: redirectRoute, params: redirectParams } = redirectMatch;
+ setNavigationContext({
+ pathname: redirectPathname,
+ searchParams: new URLSearchParams(actionRedirect.url.split("?")[1] || ""),
+ params: redirectParams,
+ });
+ const redirectElement = buildPageElement(redirectRoute, redirectParams, undefined, new URLSearchParams(actionRedirect.url.split("?")[1] || ""));
+
+ const onRenderError = createRscOnErrorHandler(
+ request,
+ redirectPathname,
+ redirectMatch.route.pattern,
+ );
+ const rscStream = renderToReadableStream(
+ { root: redirectElement, returnValue: undefined },
+ { temporaryReferences, onError: onRenderError },
+ );
+
+ setHeadersContext(null);
+ const redirectHeaders = new Headers({
+ "Content-Type": "text/x-component; charset=utf-8",
+ "Vary": "RSC, Accept",
+ "x-action-redirect": actionRedirect.url,
+ "x-action-redirect-type": actionRedirect.type,
+ "x-action-redirect-status": String(actionRedirect.status),
+ });
+ for (const cookie of actionPendingCookies) {
+ redirectHeaders.append("Set-Cookie", cookie);
+ }
+ if (actionDraftCookie) redirectHeaders.append("Set-Cookie", actionDraftCookie);
+
+ return new Response(rscStream, { status: 200, headers: redirectHeaders });
+ }
+
+ // Fallback: redirect target not found, send headers only
setHeadersContext(null);
setNavigationContext(null);
const redirectHeaders = new Headers({
@@ -1768,7 +1810,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
redirectHeaders.append("Set-Cookie", cookie);
}
if (actionDraftCookie) redirectHeaders.append("Set-Cookie", actionDraftCookie);
- // Send an empty RSC-like body (client will navigate instead of parsing)
return new Response("", { status: 200, headers: redirectHeaders });
}
@@ -4718,13 +4759,55 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
setHeadersAccessPhase(previousHeadersPhase);
}
- // If the action called redirect(), signal the client to navigate.
- // We can't use a real HTTP redirect (the fetch would follow it automatically
- // and receive a page HTML instead of RSC stream). Instead, we return a 200
- // with x-action-redirect header that the client entry detects and handles.
+ // If the action called redirect(), render the redirect target's RSC payload
+ // and signal the client to navigate. This matches Next.js behavior where
+ // the redirect response includes the target page's RSC payload for soft navigation.
if (actionRedirect) {
const actionPendingCookies = getAndClearPendingCookies();
const actionDraftCookie = getDraftModeCookieHeader();
+
+ // Match the redirect target to a route and render its RSC payload
+ const redirectPathname = actionRedirect.url.startsWith("/")
+ ? actionRedirect.url.split("?")[0]
+ : actionRedirect.url;
+ const redirectMatch = matchRoute(redirectPathname);
+
+ if (redirectMatch) {
+ const { route: redirectRoute, params: redirectParams } = redirectMatch;
+ setNavigationContext({
+ pathname: redirectPathname,
+ searchParams: new URLSearchParams(actionRedirect.url.split("?")[1] || ""),
+ params: redirectParams,
+ });
+ const redirectElement = buildPageElement(redirectRoute, redirectParams, undefined, new URLSearchParams(actionRedirect.url.split("?")[1] || ""));
+
+ const onRenderError = createRscOnErrorHandler(
+ request,
+ redirectPathname,
+ redirectMatch.route.pattern,
+ );
+ const rscStream = renderToReadableStream(
+ { root: redirectElement, returnValue: undefined },
+ { temporaryReferences, onError: onRenderError },
+ );
+
+ setHeadersContext(null);
+ const redirectHeaders = new Headers({
+ "Content-Type": "text/x-component; charset=utf-8",
+ "Vary": "RSC, Accept",
+ "x-action-redirect": actionRedirect.url,
+ "x-action-redirect-type": actionRedirect.type,
+ "x-action-redirect-status": String(actionRedirect.status),
+ });
+ for (const cookie of actionPendingCookies) {
+ redirectHeaders.append("Set-Cookie", cookie);
+ }
+ if (actionDraftCookie) redirectHeaders.append("Set-Cookie", actionDraftCookie);
+
+ return new Response(rscStream, { status: 200, headers: redirectHeaders });
+ }
+
+ // Fallback: redirect target not found, send headers only
setHeadersContext(null);
setNavigationContext(null);
const redirectHeaders = new Headers({
@@ -4738,7 +4821,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
redirectHeaders.append("Set-Cookie", cookie);
}
if (actionDraftCookie) redirectHeaders.append("Set-Cookie", actionDraftCookie);
- // Send an empty RSC-like body (client will navigate instead of parsing)
return new Response("", { status: 200, headers: redirectHeaders });
}
@@ -7715,13 +7797,55 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
setHeadersAccessPhase(previousHeadersPhase);
}
- // If the action called redirect(), signal the client to navigate.
- // We can't use a real HTTP redirect (the fetch would follow it automatically
- // and receive a page HTML instead of RSC stream). Instead, we return a 200
- // with x-action-redirect header that the client entry detects and handles.
+ // If the action called redirect(), render the redirect target's RSC payload
+ // and signal the client to navigate. This matches Next.js behavior where
+ // the redirect response includes the target page's RSC payload for soft navigation.
if (actionRedirect) {
const actionPendingCookies = getAndClearPendingCookies();
const actionDraftCookie = getDraftModeCookieHeader();
+
+ // Match the redirect target to a route and render its RSC payload
+ const redirectPathname = actionRedirect.url.startsWith("/")
+ ? actionRedirect.url.split("?")[0]
+ : actionRedirect.url;
+ const redirectMatch = matchRoute(redirectPathname);
+
+ if (redirectMatch) {
+ const { route: redirectRoute, params: redirectParams } = redirectMatch;
+ setNavigationContext({
+ pathname: redirectPathname,
+ searchParams: new URLSearchParams(actionRedirect.url.split("?")[1] || ""),
+ params: redirectParams,
+ });
+ const redirectElement = buildPageElement(redirectRoute, redirectParams, undefined, new URLSearchParams(actionRedirect.url.split("?")[1] || ""));
+
+ const onRenderError = createRscOnErrorHandler(
+ request,
+ redirectPathname,
+ redirectMatch.route.pattern,
+ );
+ const rscStream = renderToReadableStream(
+ { root: redirectElement, returnValue: undefined },
+ { temporaryReferences, onError: onRenderError },
+ );
+
+ setHeadersContext(null);
+ const redirectHeaders = new Headers({
+ "Content-Type": "text/x-component; charset=utf-8",
+ "Vary": "RSC, Accept",
+ "x-action-redirect": actionRedirect.url,
+ "x-action-redirect-type": actionRedirect.type,
+ "x-action-redirect-status": String(actionRedirect.status),
+ });
+ for (const cookie of actionPendingCookies) {
+ redirectHeaders.append("Set-Cookie", cookie);
+ }
+ if (actionDraftCookie) redirectHeaders.append("Set-Cookie", actionDraftCookie);
+
+ return new Response(rscStream, { status: 200, headers: redirectHeaders });
+ }
+
+ // Fallback: redirect target not found, send headers only
setHeadersContext(null);
setNavigationContext(null);
const redirectHeaders = new Headers({
@@ -7735,7 +7859,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
redirectHeaders.append("Set-Cookie", cookie);
}
if (actionDraftCookie) redirectHeaders.append("Set-Cookie", actionDraftCookie);
- // Send an empty RSC-like body (client will navigate instead of parsing)
return new Response("", { status: 200, headers: redirectHeaders });
}
@@ -10723,13 +10846,55 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
setHeadersAccessPhase(previousHeadersPhase);
}
- // If the action called redirect(), signal the client to navigate.
- // We can't use a real HTTP redirect (the fetch would follow it automatically
- // and receive a page HTML instead of RSC stream). Instead, we return a 200
- // with x-action-redirect header that the client entry detects and handles.
+ // If the action called redirect(), render the redirect target's RSC payload
+ // and signal the client to navigate. This matches Next.js behavior where
+ // the redirect response includes the target page's RSC payload for soft navigation.
if (actionRedirect) {
const actionPendingCookies = getAndClearPendingCookies();
const actionDraftCookie = getDraftModeCookieHeader();
+
+ // Match the redirect target to a route and render its RSC payload
+ const redirectPathname = actionRedirect.url.startsWith("/")
+ ? actionRedirect.url.split("?")[0]
+ : actionRedirect.url;
+ const redirectMatch = matchRoute(redirectPathname);
+
+ if (redirectMatch) {
+ const { route: redirectRoute, params: redirectParams } = redirectMatch;
+ setNavigationContext({
+ pathname: redirectPathname,
+ searchParams: new URLSearchParams(actionRedirect.url.split("?")[1] || ""),
+ params: redirectParams,
+ });
+ const redirectElement = buildPageElement(redirectRoute, redirectParams, undefined, new URLSearchParams(actionRedirect.url.split("?")[1] || ""));
+
+ const onRenderError = createRscOnErrorHandler(
+ request,
+ redirectPathname,
+ redirectMatch.route.pattern,
+ );
+ const rscStream = renderToReadableStream(
+ { root: redirectElement, returnValue: undefined },
+ { temporaryReferences, onError: onRenderError },
+ );
+
+ setHeadersContext(null);
+ const redirectHeaders = new Headers({
+ "Content-Type": "text/x-component; charset=utf-8",
+ "Vary": "RSC, Accept",
+ "x-action-redirect": actionRedirect.url,
+ "x-action-redirect-type": actionRedirect.type,
+ "x-action-redirect-status": String(actionRedirect.status),
+ });
+ for (const cookie of actionPendingCookies) {
+ redirectHeaders.append("Set-Cookie", cookie);
+ }
+ if (actionDraftCookie) redirectHeaders.append("Set-Cookie", actionDraftCookie);
+
+ return new Response(rscStream, { status: 200, headers: redirectHeaders });
+ }
+
+ // Fallback: redirect target not found, send headers only
setHeadersContext(null);
setNavigationContext(null);
const redirectHeaders = new Headers({
@@ -10743,7 +10908,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
redirectHeaders.append("Set-Cookie", cookie);
}
if (actionDraftCookie) redirectHeaders.append("Set-Cookie", actionDraftCookie);
- // Send an empty RSC-like body (client will navigate instead of parsing)
return new Response("", { status: 200, headers: redirectHeaders });
}
@@ -13697,13 +13861,55 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
setHeadersAccessPhase(previousHeadersPhase);
}
- // If the action called redirect(), signal the client to navigate.
- // We can't use a real HTTP redirect (the fetch would follow it automatically
- // and receive a page HTML instead of RSC stream). Instead, we return a 200
- // with x-action-redirect header that the client entry detects and handles.
+ // If the action called redirect(), render the redirect target's RSC payload
+ // and signal the client to navigate. This matches Next.js behavior where
+ // the redirect response includes the target page's RSC payload for soft navigation.
if (actionRedirect) {
const actionPendingCookies = getAndClearPendingCookies();
const actionDraftCookie = getDraftModeCookieHeader();
+
+ // Match the redirect target to a route and render its RSC payload
+ const redirectPathname = actionRedirect.url.startsWith("/")
+ ? actionRedirect.url.split("?")[0]
+ : actionRedirect.url;
+ const redirectMatch = matchRoute(redirectPathname);
+
+ if (redirectMatch) {
+ const { route: redirectRoute, params: redirectParams } = redirectMatch;
+ setNavigationContext({
+ pathname: redirectPathname,
+ searchParams: new URLSearchParams(actionRedirect.url.split("?")[1] || ""),
+ params: redirectParams,
+ });
+ const redirectElement = buildPageElement(redirectRoute, redirectParams, undefined, new URLSearchParams(actionRedirect.url.split("?")[1] || ""));
+
+ const onRenderError = createRscOnErrorHandler(
+ request,
+ redirectPathname,
+ redirectMatch.route.pattern,
+ );
+ const rscStream = renderToReadableStream(
+ { root: redirectElement, returnValue: undefined },
+ { temporaryReferences, onError: onRenderError },
+ );
+
+ setHeadersContext(null);
+ const redirectHeaders = new Headers({
+ "Content-Type": "text/x-component; charset=utf-8",
+ "Vary": "RSC, Accept",
+ "x-action-redirect": actionRedirect.url,
+ "x-action-redirect-type": actionRedirect.type,
+ "x-action-redirect-status": String(actionRedirect.status),
+ });
+ for (const cookie of actionPendingCookies) {
+ redirectHeaders.append("Set-Cookie", cookie);
+ }
+ if (actionDraftCookie) redirectHeaders.append("Set-Cookie", actionDraftCookie);
+
+ return new Response(rscStream, { status: 200, headers: redirectHeaders });
+ }
+
+ // Fallback: redirect target not found, send headers only
setHeadersContext(null);
setNavigationContext(null);
const redirectHeaders = new Headers({
@@ -13717,7 +13923,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
redirectHeaders.append("Set-Cookie", cookie);
}
if (actionDraftCookie) redirectHeaders.append("Set-Cookie", actionDraftCookie);
- // Send an empty RSC-like body (client will navigate instead of parsing)
return new Response("", { status: 200, headers: redirectHeaders });
}
@@ -17024,13 +17229,55 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
setHeadersAccessPhase(previousHeadersPhase);
}
- // If the action called redirect(), signal the client to navigate.
- // We can't use a real HTTP redirect (the fetch would follow it automatically
- // and receive a page HTML instead of RSC stream). Instead, we return a 200
- // with x-action-redirect header that the client entry detects and handles.
+ // If the action called redirect(), render the redirect target's RSC payload
+ // and signal the client to navigate. This matches Next.js behavior where
+ // the redirect response includes the target page's RSC payload for soft navigation.
if (actionRedirect) {
const actionPendingCookies = getAndClearPendingCookies();
const actionDraftCookie = getDraftModeCookieHeader();
+
+ // Match the redirect target to a route and render its RSC payload
+ const redirectPathname = actionRedirect.url.startsWith("/")
+ ? actionRedirect.url.split("?")[0]
+ : actionRedirect.url;
+ const redirectMatch = matchRoute(redirectPathname);
+
+ if (redirectMatch) {
+ const { route: redirectRoute, params: redirectParams } = redirectMatch;
+ setNavigationContext({
+ pathname: redirectPathname,
+ searchParams: new URLSearchParams(actionRedirect.url.split("?")[1] || ""),
+ params: redirectParams,
+ });
+ const redirectElement = buildPageElement(redirectRoute, redirectParams, undefined, new URLSearchParams(actionRedirect.url.split("?")[1] || ""));
+
+ const onRenderError = createRscOnErrorHandler(
+ request,
+ redirectPathname,
+ redirectMatch.route.pattern,
+ );
+ const rscStream = renderToReadableStream(
+ { root: redirectElement, returnValue: undefined },
+ { temporaryReferences, onError: onRenderError },
+ );
+
+ setHeadersContext(null);
+ const redirectHeaders = new Headers({
+ "Content-Type": "text/x-component; charset=utf-8",
+ "Vary": "RSC, Accept",
+ "x-action-redirect": actionRedirect.url,
+ "x-action-redirect-type": actionRedirect.type,
+ "x-action-redirect-status": String(actionRedirect.status),
+ });
+ for (const cookie of actionPendingCookies) {
+ redirectHeaders.append("Set-Cookie", cookie);
+ }
+ if (actionDraftCookie) redirectHeaders.append("Set-Cookie", actionDraftCookie);
+
+ return new Response(rscStream, { status: 200, headers: redirectHeaders });
+ }
+
+ // Fallback: redirect target not found, send headers only
setHeadersContext(null);
setNavigationContext(null);
const redirectHeaders = new Headers({
@@ -17044,7 +17291,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
redirectHeaders.append("Set-Cookie", cookie);
}
if (actionDraftCookie) redirectHeaders.append("Set-Cookie", actionDraftCookie);
- // Send an empty RSC-like body (client will navigate instead of parsing)
return new Response("", { status: 200, headers: redirectHeaders });
}
@@ -18251,1588 +18497,3 @@ export * from "/packages/vinext/src/server/app-ssr-entry.ts";
export { default } from "/packages/vinext/src/server/app-ssr-entry.ts";
"
`;
-
-exports[`Pages Router entry templates > client entry snapshot 1`] = `
-"
-import React from "react";
-import { hydrateRoot } from "react-dom/client";
-// Eagerly import the router shim so its module-level popstate listener is
-// registered. Without this, browser back/forward buttons do nothing because
-// navigateClient() is never invoked on history changes.
-import "next/router";
-
-const pageLoaders = {
- "/": () => import("/tests/fixtures/pages-basic/pages/index.tsx"),
- "/404": () => import("/tests/fixtures/pages-basic/pages/404.tsx"),
- "/about": () => import("/tests/fixtures/pages-basic/pages/about.tsx"),
- "/alias-test": () => import("/tests/fixtures/pages-basic/pages/alias-test.tsx"),
- "/before-pop-state-destination": () => import("/tests/fixtures/pages-basic/pages/before-pop-state-destination.tsx"),
- "/before-pop-state-test": () => import("/tests/fixtures/pages-basic/pages/before-pop-state-test.tsx"),
- "/cjs/basic": () => import("/tests/fixtures/pages-basic/pages/cjs/basic.tsx"),
- "/cjs/random": () => import("/tests/fixtures/pages-basic/pages/cjs/random.ts"),
- "/compat-router-test": () => import("/tests/fixtures/pages-basic/pages/compat-router-test.tsx"),
- "/concurrent-head": () => import("/tests/fixtures/pages-basic/pages/concurrent-head.tsx"),
- "/concurrent-router": () => import("/tests/fixtures/pages-basic/pages/concurrent-router.tsx"),
- "/config-test": () => import("/tests/fixtures/pages-basic/pages/config-test.tsx"),
- "/counter": () => import("/tests/fixtures/pages-basic/pages/counter.tsx"),
- "/dynamic-page": () => import("/tests/fixtures/pages-basic/pages/dynamic-page.tsx"),
- "/dynamic-ssr-false": () => import("/tests/fixtures/pages-basic/pages/dynamic-ssr-false.tsx"),
- "/header-override-delete": () => import("/tests/fixtures/pages-basic/pages/header-override-delete.tsx"),
- "/isr-second-render-state": () => import("/tests/fixtures/pages-basic/pages/isr-second-render-state.tsx"),
- "/isr-test": () => import("/tests/fixtures/pages-basic/pages/isr-test.tsx"),
- "/link-test": () => import("/tests/fixtures/pages-basic/pages/link-test.tsx"),
- "/mw-object-gated": () => import("/tests/fixtures/pages-basic/pages/mw-object-gated.tsx"),
- "/nav-test": () => import("/tests/fixtures/pages-basic/pages/nav-test.tsx"),
- "/posts/missing": () => import("/tests/fixtures/pages-basic/pages/posts/missing.tsx"),
- "/redirect-xss": () => import("/tests/fixtures/pages-basic/pages/redirect-xss.tsx"),
- "/router-events-test": () => import("/tests/fixtures/pages-basic/pages/router-events-test.tsx"),
- "/script-test": () => import("/tests/fixtures/pages-basic/pages/script-test.tsx"),
- "/shallow-test": () => import("/tests/fixtures/pages-basic/pages/shallow-test.tsx"),
- "/ssr": () => import("/tests/fixtures/pages-basic/pages/ssr.tsx"),
- "/ssr-headers": () => import("/tests/fixtures/pages-basic/pages/ssr-headers.tsx"),
- "/ssr-res-end": () => import("/tests/fixtures/pages-basic/pages/ssr-res-end.tsx"),
- "/suspense-test": () => import("/tests/fixtures/pages-basic/pages/suspense-test.tsx"),
- "/articles/[id]": () => import("/tests/fixtures/pages-basic/pages/articles/[id].tsx"),
- "/blog/[slug]": () => import("/tests/fixtures/pages-basic/pages/blog/[slug].tsx"),
- "/posts/[id]": () => import("/tests/fixtures/pages-basic/pages/posts/[id].tsx"),
- "/products/[pid]": () => import("/tests/fixtures/pages-basic/pages/products/[pid].tsx"),
- "/docs/[...slug]": () => import("/tests/fixtures/pages-basic/pages/docs/[...slug].tsx"),
- "/sign-up/[[...sign-up]]": () => import("/tests/fixtures/pages-basic/pages/sign-up/[[...sign-up]]/index.tsx")
-};
-
-async function hydrate() {
- const nextData = window.__NEXT_DATA__;
- if (!nextData) {
- console.error("[vinext] No __NEXT_DATA__ found");
- return;
- }
-
- const { pageProps } = nextData.props;
- const loader = pageLoaders[nextData.page];
- if (!loader) {
- console.error("[vinext] No page loader for route:", nextData.page);
- return;
- }
-
- const pageModule = await loader();
- const PageComponent = pageModule.default;
- if (!PageComponent) {
- console.error("[vinext] Page module has no default export");
- return;
- }
-
- let element;
-
- try {
- const appModule = await import("/tests/fixtures/pages-basic/pages/_app.tsx");
- const AppComponent = appModule.default;
- window.__VINEXT_APP__ = AppComponent;
- element = React.createElement(AppComponent, { Component: PageComponent, pageProps });
- } catch {
- element = React.createElement(PageComponent, pageProps);
- }
-
-
- // Wrap with RouterContext.Provider so next/compat/router works during hydration
- const { wrapWithRouterContext } = await import("next/router");
- element = wrapWithRouterContext(element);
-
- const container = document.getElementById("__next");
- if (!container) {
- console.error("[vinext] No #__next element found");
- return;
- }
-
- const root = hydrateRoot(container, element);
- window.__VINEXT_ROOT__ = root;
-}
-
-hydrate();
-"
-`;
-
-exports[`Pages Router entry templates > server entry snapshot 1`] = `
-"
-import React from "react";
-import { renderToReadableStream } from "react-dom/server.edge";
-import { resetSSRHead, getSSRHeadHTML } from "next/head";
-import { flushPreloads } from "next/dynamic";
-import { setSSRContext, wrapWithRouterContext } from "next/router";
-import { getCacheHandler, _runWithCacheState } from "next/cache";
-import { runWithPrivateCache } from "vinext/cache-runtime";
-import { ensureFetchPatch, runWithFetchCache } from "vinext/fetch-cache";
-import { runWithRequestContext as _runWithUnifiedCtx, createRequestContext as _createUnifiedCtx } from "vinext/unified-request-context";
-import "vinext/router-state";
-import { runWithServerInsertedHTMLState } from "vinext/navigation-state";
-import { runWithHeadState } from "vinext/head-state";
-import "vinext/i18n-state";
-import { setI18nContext } from "vinext/i18n-context";
-import { safeJsonStringify } from "vinext/html";
-import { decode as decodeQueryString } from "node:querystring";
-import { getSSRFontLinks as _getSSRFontLinks, getSSRFontStyles as _getSSRFontStylesGoogle, getSSRFontPreloads as _getSSRFontPreloadsGoogle } from "next/font/google";
-import { getSSRFontStyles as _getSSRFontStylesLocal, getSSRFontPreloads as _getSSRFontPreloadsLocal } from "next/font/local";
-import { parseCookies, sanitizeDestination as sanitizeDestinationLocal } from "/packages/vinext/src/config/config-matchers.js";
-import { runWithExecutionContext as _runWithExecutionContext, getRequestExecutionContext as _getRequestExecutionContext } from "/packages/vinext/src/shims/request-context.js";
-import { buildRouteTrie as _buildRouteTrie, trieMatch as _trieMatch } from "/packages/vinext/src/routing/route-trie.js";
-import { reportRequestError as _reportRequestError } from "vinext/instrumentation";
-import { resolvePagesI18nRequest } from "/packages/vinext/src/server/pages-i18n.js";
-import * as _instrumentation from "/tests/fixtures/pages-basic/instrumentation.ts";
-import * as middlewareModule from "/tests/fixtures/pages-basic/middleware.ts";
-import { NextRequest, NextFetchEvent } from "next/server";
-
-// Run instrumentation register() once at module evaluation time — before any
-// requests are handled. Matches Next.js semantics: register() is called once
-// on startup in the process that handles requests.
-if (typeof _instrumentation.register === "function") {
- await _instrumentation.register();
-}
-// Store the onRequestError handler on globalThis so it is visible to all
-// code within the Worker (same global scope).
-if (typeof _instrumentation.onRequestError === "function") {
- globalThis.__VINEXT_onRequestErrorHandler__ = _instrumentation.onRequestError;
-}
-
-// i18n config (embedded at build time)
-const i18nConfig = null;
-
-// Build ID (embedded at build time)
-const buildId = "test-build-id";
-
-// Full resolved config for production server (embedded at build time)
-export const vinextConfig = {"basePath":"","trailingSlash":false,"redirects":[{"source":"/old-about","destination":"/about","permanent":true},{"source":"/repeat-redirect/:id","destination":"/docs/:id/:id","permanent":false},{"source":"/redirect-before-middleware-rewrite","destination":"/about","permanent":false},{"source":"/redirect-before-middleware-response","destination":"/about","permanent":false}],"rewrites":{"beforeFiles":[{"source":"/before-rewrite","destination":"/about"},{"source":"/repeat-rewrite/:id","destination":"/docs/:id/:id"},{"source":"/mw-gated-before","has":[{"type":"cookie","key":"mw-before-user"}],"destination":"/about"}],"afterFiles":[{"source":"/after-rewrite","destination":"/about"},{"source":"/mw-gated-rewrite","has":[{"type":"cookie","key":"mw-user"}],"destination":"/about"}],"fallback":[{"source":"/fallback-rewrite","destination":"/about"}]},"headers":[{"source":"/api/(.*)","headers":[{"key":"X-Custom-Header","value":"vinext"}]},{"source":"/about","has":[{"type":"cookie","key":"logged-in"}],"headers":[{"key":"X-Auth-Only-Header","value":"1"}]},{"source":"/about","missing":[{"type":"cookie","key":"logged-in"}],"headers":[{"key":"X-Guest-Only-Header","value":"1"}]},{"source":"/ssr","headers":[{"key":"Vary","value":"Accept-Language"}]},{"source":"/headers-before-middleware-rewrite","headers":[{"key":"X-Rewrite-Source-Header","value":"1"}]}],"i18n":null,"images":{}};
-
-class ApiBodyParseError extends Error {
- constructor(message, statusCode) {
- super(message);
- this.statusCode = statusCode;
- this.name = "ApiBodyParseError";
- }
-}
-
-// ISR cache helpers (inlined for the server entry)
-async function isrGet(key) {
- const handler = getCacheHandler();
- const result = await handler.get(key);
- if (!result || !result.value) return null;
- return { value: result, isStale: result.cacheState === "stale" };
-}
-async function isrSet(key, data, revalidateSeconds, tags) {
- const handler = getCacheHandler();
- await handler.set(key, data, { revalidate: revalidateSeconds, tags: tags || [] });
-}
-const pendingRegenerations = new Map();
-function triggerBackgroundRegeneration(key, renderFn) {
- if (pendingRegenerations.has(key)) return;
- const promise = renderFn()
- .catch((err) => console.error("[vinext] ISR regen failed for " + key + ":", err))
- .finally(() => pendingRegenerations.delete(key));
- pendingRegenerations.set(key, promise);
- // Register with the Workers ExecutionContext so the isolate is kept alive
- // until the regeneration finishes, even after the Response has been sent.
- const ctx = _getRequestExecutionContext();
- if (ctx && typeof ctx.waitUntil === "function") ctx.waitUntil(promise);
-}
-
-function fnv1a64(input) {
- let h1 = 0x811c9dc5;
- for (let i = 0; i < input.length; i++) {
- h1 ^= input.charCodeAt(i);
- h1 = (h1 * 0x01000193) >>> 0;
- }
- let h2 = 0x050c5d1f;
- for (let i = 0; i < input.length; i++) {
- h2 ^= input.charCodeAt(i);
- h2 = (h2 * 0x01000193) >>> 0;
- }
- return h1.toString(36) + h2.toString(36);
-}
-// Keep prefix construction and hashing logic in sync with isrCacheKey() in server/isr-cache.ts.
-// buildId is a top-level const in the generated entry (see "const buildId = ..." above).
-function isrCacheKey(router, pathname) {
- const normalized = pathname === "/" ? "/" : pathname.replace(/\\/$/, "");
- const prefix = buildId ? router + ":" + buildId : router;
- const key = prefix + ":" + normalized;
- if (key.length <= 200) return key;
- return prefix + ":__hash:" + fnv1a64(normalized);
-}
-
-function getMediaType(contentType) {
- var type = (contentType || "text/plain").split(";")[0];
- type = type && type.trim().toLowerCase();
- return type || "text/plain";
-}
-
-function isJsonMediaType(mediaType) {
- return mediaType === "application/json" || mediaType === "application/ld+json";
-}
-
-async function renderToStringAsync(element) {
- const stream = await renderToReadableStream(element);
- await stream.allReady;
- return new Response(stream).text();
-}
-
-async function renderIsrPassToStringAsync(element) {
- // The cache-fill render is a second render pass for the same request.
- // Reset render-scoped state so it cannot leak from the streamed response
- // render or affect async work that is still draining from that stream.
- // Keep request identity state (pathname/query/locale/executionContext)
- // intact: this second pass still belongs to the same request.
- return await runWithServerInsertedHTMLState(() =>
- runWithHeadState(() =>
- _runWithCacheState(() =>
- runWithPrivateCache(() => runWithFetchCache(async () => renderToStringAsync(element))),
- ),
- ),
- );
-}
-
-import * as page_0 from "/tests/fixtures/pages-basic/pages/index.tsx";
-import * as page_1 from "/tests/fixtures/pages-basic/pages/404.tsx";
-import * as page_2 from "/tests/fixtures/pages-basic/pages/about.tsx";
-import * as page_3 from "/tests/fixtures/pages-basic/pages/alias-test.tsx";
-import * as page_4 from "/tests/fixtures/pages-basic/pages/before-pop-state-destination.tsx";
-import * as page_5 from "/tests/fixtures/pages-basic/pages/before-pop-state-test.tsx";
-import * as page_6 from "/tests/fixtures/pages-basic/pages/cjs/basic.tsx";
-import * as page_7 from "/tests/fixtures/pages-basic/pages/cjs/random.ts";
-import * as page_8 from "/tests/fixtures/pages-basic/pages/compat-router-test.tsx";
-import * as page_9 from "/tests/fixtures/pages-basic/pages/concurrent-head.tsx";
-import * as page_10 from "/tests/fixtures/pages-basic/pages/concurrent-router.tsx";
-import * as page_11 from "/tests/fixtures/pages-basic/pages/config-test.tsx";
-import * as page_12 from "/tests/fixtures/pages-basic/pages/counter.tsx";
-import * as page_13 from "/tests/fixtures/pages-basic/pages/dynamic-page.tsx";
-import * as page_14 from "/tests/fixtures/pages-basic/pages/dynamic-ssr-false.tsx";
-import * as page_15 from "/tests/fixtures/pages-basic/pages/header-override-delete.tsx";
-import * as page_16 from "/tests/fixtures/pages-basic/pages/isr-second-render-state.tsx";
-import * as page_17 from "/tests/fixtures/pages-basic/pages/isr-test.tsx";
-import * as page_18 from "/tests/fixtures/pages-basic/pages/link-test.tsx";
-import * as page_19 from "/tests/fixtures/pages-basic/pages/mw-object-gated.tsx";
-import * as page_20 from "/tests/fixtures/pages-basic/pages/nav-test.tsx";
-import * as page_21 from "/tests/fixtures/pages-basic/pages/posts/missing.tsx";
-import * as page_22 from "/tests/fixtures/pages-basic/pages/redirect-xss.tsx";
-import * as page_23 from "/tests/fixtures/pages-basic/pages/router-events-test.tsx";
-import * as page_24 from "/tests/fixtures/pages-basic/pages/script-test.tsx";
-import * as page_25 from "/tests/fixtures/pages-basic/pages/shallow-test.tsx";
-import * as page_26 from "/tests/fixtures/pages-basic/pages/ssr.tsx";
-import * as page_27 from "/tests/fixtures/pages-basic/pages/ssr-headers.tsx";
-import * as page_28 from "/tests/fixtures/pages-basic/pages/ssr-res-end.tsx";
-import * as page_29 from "/tests/fixtures/pages-basic/pages/suspense-test.tsx";
-import * as page_30 from "/tests/fixtures/pages-basic/pages/articles/[id].tsx";
-import * as page_31 from "/tests/fixtures/pages-basic/pages/blog/[slug].tsx";
-import * as page_32 from "/tests/fixtures/pages-basic/pages/posts/[id].tsx";
-import * as page_33 from "/tests/fixtures/pages-basic/pages/products/[pid].tsx";
-import * as page_34 from "/tests/fixtures/pages-basic/pages/docs/[...slug].tsx";
-import * as page_35 from "/tests/fixtures/pages-basic/pages/sign-up/[[...sign-up]]/index.tsx";
-import * as api_0 from "/tests/fixtures/pages-basic/pages/api/binary.ts";
-import * as api_1 from "/tests/fixtures/pages-basic/pages/api/echo-body.ts";
-import * as api_2 from "/tests/fixtures/pages-basic/pages/api/error-route.ts";
-import * as api_3 from "/tests/fixtures/pages-basic/pages/api/hello.ts";
-import * as api_4 from "/tests/fixtures/pages-basic/pages/api/instrumentation-test.ts";
-import * as api_5 from "/tests/fixtures/pages-basic/pages/api/middleware-test.ts";
-import * as api_6 from "/tests/fixtures/pages-basic/pages/api/no-content-type.ts";
-import * as api_7 from "/tests/fixtures/pages-basic/pages/api/parse.ts";
-import * as api_8 from "/tests/fixtures/pages-basic/pages/api/send-buffer.ts";
-import * as api_9 from "/tests/fixtures/pages-basic/pages/api/users/[id].ts";
-
-import { default as AppComponent } from "/tests/fixtures/pages-basic/pages/_app.tsx";
-import { default as DocumentComponent } from "/tests/fixtures/pages-basic/pages/_document.tsx";
-
-export const pageRoutes = [
- { pattern: "/", patternParts: [], isDynamic: false, params: [], module: page_0, filePath: "/tests/fixtures/pages-basic/pages/index.tsx" },
- { pattern: "/404", patternParts: ["404"], isDynamic: false, params: [], module: page_1, filePath: "/tests/fixtures/pages-basic/pages/404.tsx" },
- { pattern: "/about", patternParts: ["about"], isDynamic: false, params: [], module: page_2, filePath: "/tests/fixtures/pages-basic/pages/about.tsx" },
- { pattern: "/alias-test", patternParts: ["alias-test"], isDynamic: false, params: [], module: page_3, filePath: "/tests/fixtures/pages-basic/pages/alias-test.tsx" },
- { pattern: "/before-pop-state-destination", patternParts: ["before-pop-state-destination"], isDynamic: false, params: [], module: page_4, filePath: "/tests/fixtures/pages-basic/pages/before-pop-state-destination.tsx" },
- { pattern: "/before-pop-state-test", patternParts: ["before-pop-state-test"], isDynamic: false, params: [], module: page_5, filePath: "/tests/fixtures/pages-basic/pages/before-pop-state-test.tsx" },
- { pattern: "/cjs/basic", patternParts: ["cjs","basic"], isDynamic: false, params: [], module: page_6, filePath: "/tests/fixtures/pages-basic/pages/cjs/basic.tsx" },
- { pattern: "/cjs/random", patternParts: ["cjs","random"], isDynamic: false, params: [], module: page_7, filePath: "/tests/fixtures/pages-basic/pages/cjs/random.ts" },
- { pattern: "/compat-router-test", patternParts: ["compat-router-test"], isDynamic: false, params: [], module: page_8, filePath: "/tests/fixtures/pages-basic/pages/compat-router-test.tsx" },
- { pattern: "/concurrent-head", patternParts: ["concurrent-head"], isDynamic: false, params: [], module: page_9, filePath: "/tests/fixtures/pages-basic/pages/concurrent-head.tsx" },
- { pattern: "/concurrent-router", patternParts: ["concurrent-router"], isDynamic: false, params: [], module: page_10, filePath: "/tests/fixtures/pages-basic/pages/concurrent-router.tsx" },
- { pattern: "/config-test", patternParts: ["config-test"], isDynamic: false, params: [], module: page_11, filePath: "/tests/fixtures/pages-basic/pages/config-test.tsx" },
- { pattern: "/counter", patternParts: ["counter"], isDynamic: false, params: [], module: page_12, filePath: "/tests/fixtures/pages-basic/pages/counter.tsx" },
- { pattern: "/dynamic-page", patternParts: ["dynamic-page"], isDynamic: false, params: [], module: page_13, filePath: "/tests/fixtures/pages-basic/pages/dynamic-page.tsx" },
- { pattern: "/dynamic-ssr-false", patternParts: ["dynamic-ssr-false"], isDynamic: false, params: [], module: page_14, filePath: "/tests/fixtures/pages-basic/pages/dynamic-ssr-false.tsx" },
- { pattern: "/header-override-delete", patternParts: ["header-override-delete"], isDynamic: false, params: [], module: page_15, filePath: "/tests/fixtures/pages-basic/pages/header-override-delete.tsx" },
- { pattern: "/isr-second-render-state", patternParts: ["isr-second-render-state"], isDynamic: false, params: [], module: page_16, filePath: "/tests/fixtures/pages-basic/pages/isr-second-render-state.tsx" },
- { pattern: "/isr-test", patternParts: ["isr-test"], isDynamic: false, params: [], module: page_17, filePath: "/tests/fixtures/pages-basic/pages/isr-test.tsx" },
- { pattern: "/link-test", patternParts: ["link-test"], isDynamic: false, params: [], module: page_18, filePath: "/tests/fixtures/pages-basic/pages/link-test.tsx" },
- { pattern: "/mw-object-gated", patternParts: ["mw-object-gated"], isDynamic: false, params: [], module: page_19, filePath: "/tests/fixtures/pages-basic/pages/mw-object-gated.tsx" },
- { pattern: "/nav-test", patternParts: ["nav-test"], isDynamic: false, params: [], module: page_20, filePath: "/tests/fixtures/pages-basic/pages/nav-test.tsx" },
- { pattern: "/posts/missing", patternParts: ["posts","missing"], isDynamic: false, params: [], module: page_21, filePath: "/tests/fixtures/pages-basic/pages/posts/missing.tsx" },
- { pattern: "/redirect-xss", patternParts: ["redirect-xss"], isDynamic: false, params: [], module: page_22, filePath: "/tests/fixtures/pages-basic/pages/redirect-xss.tsx" },
- { pattern: "/router-events-test", patternParts: ["router-events-test"], isDynamic: false, params: [], module: page_23, filePath: "/tests/fixtures/pages-basic/pages/router-events-test.tsx" },
- { pattern: "/script-test", patternParts: ["script-test"], isDynamic: false, params: [], module: page_24, filePath: "/tests/fixtures/pages-basic/pages/script-test.tsx" },
- { pattern: "/shallow-test", patternParts: ["shallow-test"], isDynamic: false, params: [], module: page_25, filePath: "/tests/fixtures/pages-basic/pages/shallow-test.tsx" },
- { pattern: "/ssr", patternParts: ["ssr"], isDynamic: false, params: [], module: page_26, filePath: "/tests/fixtures/pages-basic/pages/ssr.tsx" },
- { pattern: "/ssr-headers", patternParts: ["ssr-headers"], isDynamic: false, params: [], module: page_27, filePath: "/tests/fixtures/pages-basic/pages/ssr-headers.tsx" },
- { pattern: "/ssr-res-end", patternParts: ["ssr-res-end"], isDynamic: false, params: [], module: page_28, filePath: "/tests/fixtures/pages-basic/pages/ssr-res-end.tsx" },
- { pattern: "/suspense-test", patternParts: ["suspense-test"], isDynamic: false, params: [], module: page_29, filePath: "/tests/fixtures/pages-basic/pages/suspense-test.tsx" },
- { pattern: "/articles/:id", patternParts: ["articles",":id"], isDynamic: true, params: ["id"], module: page_30, filePath: "/tests/fixtures/pages-basic/pages/articles/[id].tsx" },
- { pattern: "/blog/:slug", patternParts: ["blog",":slug"], isDynamic: true, params: ["slug"], module: page_31, filePath: "/tests/fixtures/pages-basic/pages/blog/[slug].tsx" },
- { pattern: "/posts/:id", patternParts: ["posts",":id"], isDynamic: true, params: ["id"], module: page_32, filePath: "/tests/fixtures/pages-basic/pages/posts/[id].tsx" },
- { pattern: "/products/:pid", patternParts: ["products",":pid"], isDynamic: true, params: ["pid"], module: page_33, filePath: "/tests/fixtures/pages-basic/pages/products/[pid].tsx" },
- { pattern: "/docs/:slug+", patternParts: ["docs",":slug+"], isDynamic: true, params: ["slug"], module: page_34, filePath: "/tests/fixtures/pages-basic/pages/docs/[...slug].tsx" },
- { pattern: "/sign-up/:sign-up*", patternParts: ["sign-up",":sign-up*"], isDynamic: true, params: ["sign-up"], module: page_35, filePath: "/tests/fixtures/pages-basic/pages/sign-up/[[...sign-up]]/index.tsx" }
-];
-const _pageRouteTrie = _buildRouteTrie(pageRoutes);
-
-const apiRoutes = [
- { pattern: "/api/binary", patternParts: ["api","binary"], isDynamic: false, params: [], module: api_0 },
- { pattern: "/api/echo-body", patternParts: ["api","echo-body"], isDynamic: false, params: [], module: api_1 },
- { pattern: "/api/error-route", patternParts: ["api","error-route"], isDynamic: false, params: [], module: api_2 },
- { pattern: "/api/hello", patternParts: ["api","hello"], isDynamic: false, params: [], module: api_3 },
- { pattern: "/api/instrumentation-test", patternParts: ["api","instrumentation-test"], isDynamic: false, params: [], module: api_4 },
- { pattern: "/api/middleware-test", patternParts: ["api","middleware-test"], isDynamic: false, params: [], module: api_5 },
- { pattern: "/api/no-content-type", patternParts: ["api","no-content-type"], isDynamic: false, params: [], module: api_6 },
- { pattern: "/api/parse", patternParts: ["api","parse"], isDynamic: false, params: [], module: api_7 },
- { pattern: "/api/send-buffer", patternParts: ["api","send-buffer"], isDynamic: false, params: [], module: api_8 },
- { pattern: "/api/users/:id", patternParts: ["api","users",":id"], isDynamic: true, params: ["id"], module: api_9 }
-];
-const _apiRouteTrie = _buildRouteTrie(apiRoutes);
-
-function matchRoute(url, routes) {
- const pathname = url.split("?")[0];
- let normalizedUrl = pathname === "/" ? "/" : pathname.replace(/\\/$/, "");
- // NOTE: Do NOT decodeURIComponent here. The pathname is already decoded at
- // the entry point. Decoding again would create a double-decode vector.
- const urlParts = normalizedUrl.split("/").filter(Boolean);
- const trie = routes === pageRoutes ? _pageRouteTrie : _apiRouteTrie;
- return _trieMatch(trie, urlParts);
-}
-
-function parseQuery(url) {
- const qs = url.split("?")[1];
- if (!qs) return {};
- const p = new URLSearchParams(qs);
- const q = {};
- for (const [k, v] of p) {
- if (k in q) {
- q[k] = Array.isArray(q[k]) ? q[k].concat(v) : [q[k], v];
- } else {
- q[k] = v;
- }
- }
- return q;
-}
-
-function patternToNextFormat(pattern) {
- return pattern
- .replace(/:([\\w]+)\\*/g, "[[...$1]]")
- .replace(/:([\\w]+)\\+/g, "[...$1]")
- .replace(/:([\\w]+)/g, "[$1]");
-}
-
-function collectAssetTags(manifest, moduleIds) {
- // Fall back to embedded manifest (set by vinext:cloudflare-build for Workers)
- const m = (manifest && Object.keys(manifest).length > 0)
- ? manifest
- : (typeof globalThis !== "undefined" && globalThis.__VINEXT_SSR_MANIFEST__) || null;
- const tags = [];
- const seen = new Set();
-
- // Load the set of lazy chunk filenames (only reachable via dynamic imports).
- // These should NOT get or ');
- }
- if (m) {
- // Always inject shared chunks (framework, vinext runtime, entry) and
- // page-specific chunks. The manifest maps module file paths to their
- // associated JS/CSS assets.
- //
- // For page-specific injection, the module IDs may be absolute paths
- // while the manifest uses relative paths. Try both the original ID
- // and a suffix match to find the correct manifest entry.
- var allFiles = [];
-
- if (moduleIds && moduleIds.length > 0) {
- // Collect assets for the requested page modules
- for (var mi = 0; mi < moduleIds.length; mi++) {
- var id = moduleIds[mi];
- var files = m[id];
- if (!files) {
- // Absolute path didn't match — try matching by suffix.
- // Manifest keys are relative (e.g. "pages/about.tsx") while
- // moduleIds may be absolute (e.g. "/home/.../pages/about.tsx").
- for (var mk in m) {
- if (id.endsWith("/" + mk) || id === mk) {
- files = m[mk];
- break;
- }
- }
- }
- if (files) {
- for (var fi = 0; fi < files.length; fi++) allFiles.push(files[fi]);
- }
- }
-
- // Also inject shared chunks that every page needs: framework,
- // vinext runtime, and the entry bootstrap. These are identified
- // by scanning all manifest values for chunk filenames containing
- // known prefixes.
- for (var key in m) {
- var vals = m[key];
- if (!vals) continue;
- for (var vi = 0; vi < vals.length; vi++) {
- var file = vals[vi];
- var basename = file.split("/").pop() || "";
- if (
- basename.startsWith("framework-") ||
- basename.startsWith("vinext-") ||
- basename.includes("vinext-client-entry") ||
- basename.includes("vinext-app-browser-entry")
- ) {
- allFiles.push(file);
- }
- }
- }
- } else {
- // No specific modules — include all assets from manifest
- for (var akey in m) {
- var avals = m[akey];
- if (avals) {
- for (var ai = 0; ai < avals.length; ai++) allFiles.push(avals[ai]);
- }
- }
- }
-
- for (var ti = 0; ti < allFiles.length; ti++) {
- var tf = allFiles[ti];
- // Normalize: Vite's SSR manifest values include a leading '/'
- // (from base path), but we prepend '/' ourselves when building
- // href/src attributes. Strip any existing leading slash to avoid
- // producing protocol-relative URLs like "//assets/chunk.js".
- // This also ensures consistent keys for the seen-set dedup and
- // lazySet.has() checks (which use values without leading slash).
- if (tf.charAt(0) === '/') tf = tf.slice(1);
- if (seen.has(tf)) continue;
- seen.add(tf);
- if (tf.endsWith(".css")) {
- tags.push('');
- } else if (tf.endsWith(".js")) {
- // Skip lazy chunks — they are behind dynamic import() boundaries
- // (React.lazy, next/dynamic) and should only be fetched on demand.
- if (lazySet && lazySet.has(tf)) continue;
- tags.push('');
- tags.push('');
- }
- }
- }
- return tags.join("\\n ");
-}
-
-// i18n helpers
-function extractLocale(url) {
- if (!i18nConfig) return { locale: undefined, url, hadPrefix: false };
- const pathname = url.split("?")[0];
- const parts = pathname.split("/").filter(Boolean);
- const query = url.includes("?") ? url.slice(url.indexOf("?")) : "";
- if (parts.length > 0 && i18nConfig.locales.includes(parts[0])) {
- const locale = parts[0];
- const rest = "/" + parts.slice(1).join("/");
- return { locale, url: (rest || "/") + query, hadPrefix: true };
- }
- return { locale: i18nConfig.defaultLocale, url, hadPrefix: false };
-}
-
-function detectLocaleFromHeaders(headers) {
- if (!i18nConfig) return null;
- const acceptLang = headers.get("accept-language");
- if (!acceptLang) return null;
- const langs = acceptLang.split(",").map(function(part) {
- const pieces = part.trim().split(";");
- const q = pieces[1] ? parseFloat(pieces[1].replace("q=", "")) : 1;
- return { lang: pieces[0].trim().toLowerCase(), q: q };
- }).sort(function(a, b) { return b.q - a.q; });
- for (let k = 0; k < langs.length; k++) {
- const lang = langs[k].lang;
- for (let j = 0; j < i18nConfig.locales.length; j++) {
- if (i18nConfig.locales[j].toLowerCase() === lang) return i18nConfig.locales[j];
- }
- const prefix = lang.split("-")[0];
- for (let j = 0; j < i18nConfig.locales.length; j++) {
- const loc = i18nConfig.locales[j].toLowerCase();
- if (loc === prefix || loc.startsWith(prefix + "-")) return i18nConfig.locales[j];
- }
- }
- return null;
-}
-
-function parseCookieLocaleFromHeader(cookieHeader) {
- if (!i18nConfig || !cookieHeader) return null;
- const match = cookieHeader.match(/(?:^|;\\s*)NEXT_LOCALE=([^;]*)/);
- if (!match) return null;
- var value;
- try { value = decodeURIComponent(match[1].trim()); } catch (e) { return null; }
- if (i18nConfig.locales.indexOf(value) !== -1) return value;
- return null;
-}
-
-// Lightweight req/res facade for getServerSideProps and API routes.
-// Next.js pages expect ctx.req/ctx.res with Node-like shapes.
-function createReqRes(request, url, query, body) {
- const headersObj = {};
- for (const [k, v] of request.headers) headersObj[k.toLowerCase()] = v;
-
- const req = {
- method: request.method,
- url: url,
- headers: headersObj,
- query: query,
- body: body,
- cookies: parseCookies(request.headers.get("cookie")),
- };
-
- let resStatusCode = 200;
- const resHeaders = {};
- // set-cookie needs array support (multiple Set-Cookie headers are common)
- const setCookieHeaders = [];
- let resBody = null;
- let ended = false;
- let resolveResponse;
- const responsePromise = new Promise(function(r) { resolveResponse = r; });
-
- const res = {
- get statusCode() { return resStatusCode; },
- set statusCode(code) { resStatusCode = code; },
- writeHead: function(code, headers) {
- resStatusCode = code;
- if (headers) {
- for (const [k, v] of Object.entries(headers)) {
- if (k.toLowerCase() === "set-cookie") {
- if (Array.isArray(v)) { for (const c of v) setCookieHeaders.push(c); }
- else { setCookieHeaders.push(v); }
- } else {
- resHeaders[k] = v;
- }
- }
- }
- return res;
- },
- setHeader: function(name, value) {
- if (name.toLowerCase() === "set-cookie") {
- if (Array.isArray(value)) { for (const c of value) setCookieHeaders.push(c); }
- else { setCookieHeaders.push(value); }
- } else {
- resHeaders[name.toLowerCase()] = value;
- }
- return res;
- },
- getHeader: function(name) {
- if (name.toLowerCase() === "set-cookie") return setCookieHeaders.length > 0 ? setCookieHeaders : undefined;
- return resHeaders[name.toLowerCase()];
- },
- end: function(data) {
- if (ended) return;
- ended = true;
- if (data !== undefined && data !== null) resBody = data;
- const h = new Headers(resHeaders);
- for (const c of setCookieHeaders) h.append("set-cookie", c);
- resolveResponse(new Response(resBody, { status: resStatusCode, headers: h }));
- },
- status: function(code) { resStatusCode = code; return res; },
- json: function(data) {
- resHeaders["content-type"] = "application/json";
- res.end(JSON.stringify(data));
- },
- send: function(data) {
- if (Buffer.isBuffer(data)) {
- if (!resHeaders["content-type"]) resHeaders["content-type"] = "application/octet-stream";
- resHeaders["content-length"] = String(data.length);
- res.end(data);
- } else if (typeof data === "object" && data !== null) {
- res.json(data);
- } else {
- if (!resHeaders["content-type"]) resHeaders["content-type"] = "text/plain";
- res.end(String(data));
- }
- },
- redirect: function(statusOrUrl, url2) {
- if (typeof statusOrUrl === "string") { res.writeHead(307, { Location: statusOrUrl }); }
- else { res.writeHead(statusOrUrl, { Location: url2 }); }
- res.end();
- },
- getHeaders: function() {
- var h = Object.assign({}, resHeaders);
- if (setCookieHeaders.length > 0) h["set-cookie"] = setCookieHeaders;
- return h;
- },
- get headersSent() { return ended; },
- };
-
- return { req, res, responsePromise };
-}
-
-/**
- * Read request body as text with a size limit.
- * Throws if the body exceeds maxBytes. This prevents DoS via chunked
- * transfer encoding where Content-Length is absent or spoofed.
- */
-async function readBodyWithLimit(request, maxBytes) {
- if (!request.body) return "";
- var reader = request.body.getReader();
- var decoder = new TextDecoder();
- var chunks = [];
- var totalSize = 0;
- for (;;) {
- var result = await reader.read();
- if (result.done) break;
- totalSize += result.value.byteLength;
- if (totalSize > maxBytes) {
- reader.cancel();
- throw new Error("Request body too large");
- }
- chunks.push(decoder.decode(result.value, { stream: true }));
- }
- chunks.push(decoder.decode());
- return chunks.join("");
-}
-
-export async function renderPage(request, url, manifest, ctx) {
- if (ctx) return _runWithExecutionContext(ctx, () => _renderPage(request, url, manifest));
- return _renderPage(request, url, manifest);
-}
-
-async function _renderPage(request, url, manifest) {
- const localeInfo = i18nConfig
- ? resolvePagesI18nRequest(
- url,
- i18nConfig,
- request.headers,
- new URL(request.url).hostname,
- vinextConfig.basePath,
- vinextConfig.trailingSlash,
- )
- : { locale: undefined, url, hadPrefix: false, domainLocale: undefined, redirectUrl: undefined };
- const locale = localeInfo.locale;
- const routeUrl = localeInfo.url;
- const currentDefaultLocale = i18nConfig
- ? (localeInfo.domainLocale ? localeInfo.domainLocale.defaultLocale : i18nConfig.defaultLocale)
- : undefined;
- const domainLocales = i18nConfig ? i18nConfig.domains : undefined;
-
- if (localeInfo.redirectUrl) {
- return new Response(null, { status: 307, headers: { Location: localeInfo.redirectUrl } });
- }
-
- const match = matchRoute(routeUrl, pageRoutes);
- if (!match) {
- return new Response("404 - Page not found
",
- { status: 404, headers: { "Content-Type": "text/html" } });
- }
-
- const { route, params } = match;
- const __uCtx = _createUnifiedCtx({
- executionContext: _getRequestExecutionContext(),
- });
- return _runWithUnifiedCtx(__uCtx, async () => {
- ensureFetchPatch();
- try {
- if (typeof setSSRContext === "function") {
- setSSRContext({
- pathname: patternToNextFormat(route.pattern),
- query: { ...params, ...parseQuery(routeUrl) },
- asPath: routeUrl,
- locale: locale,
- locales: i18nConfig ? i18nConfig.locales : undefined,
- defaultLocale: currentDefaultLocale,
- domainLocales: domainLocales,
- });
- }
-
- if (i18nConfig) {
- setI18nContext({
- locale: locale,
- locales: i18nConfig.locales,
- defaultLocale: currentDefaultLocale,
- domainLocales: domainLocales,
- hostname: new URL(request.url).hostname,
- });
- }
-
- const pageModule = route.module;
- const PageComponent = pageModule.default;
- if (!PageComponent) {
- return new Response("Page has no default export", { status: 500 });
- }
-
- // Handle getStaticPaths for dynamic routes
- if (typeof pageModule.getStaticPaths === "function" && route.isDynamic) {
- const pathsResult = await pageModule.getStaticPaths({
- locales: i18nConfig ? i18nConfig.locales : [],
- defaultLocale: currentDefaultLocale || "",
- });
- const fallback = pathsResult && pathsResult.fallback !== undefined ? pathsResult.fallback : false;
-
- if (fallback === false) {
- const paths = pathsResult && pathsResult.paths ? pathsResult.paths : [];
- const isValidPath = paths.some(function(p) {
- return Object.entries(p.params).every(function(entry) {
- var key = entry[0], val = entry[1];
- var actual = params[key];
- if (Array.isArray(val)) {
- return Array.isArray(actual) && val.join("/") === actual.join("/");
- }
- return String(val) === String(actual);
- });
- });
- if (!isValidPath) {
- return new Response("404 - Page not found
",
- { status: 404, headers: { "Content-Type": "text/html" } });
- }
- }
- }
-
- let pageProps = {};
- var gsspRes = null;
- if (typeof pageModule.getServerSideProps === "function") {
- const { req, res, responsePromise } = createReqRes(request, routeUrl, parseQuery(routeUrl), undefined);
- const ctx = {
- params, req, res,
- query: parseQuery(routeUrl),
- resolvedUrl: routeUrl,
- locale: locale,
- locales: i18nConfig ? i18nConfig.locales : undefined,
- defaultLocale: currentDefaultLocale,
- };
- const result = await pageModule.getServerSideProps(ctx);
- // If gSSP called res.end() directly (short-circuit), return that response.
- if (res.headersSent) {
- return await responsePromise;
- }
- if (result && result.props) pageProps = result.props;
- if (result && result.redirect) {
- var gsspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
- return new Response(null, { status: gsspStatus, headers: { Location: sanitizeDestinationLocal(result.redirect.destination) } });
- }
- if (result && result.notFound) {
- return new Response("404", { status: 404 });
- }
- // Preserve the res object so headers/status/cookies set by gSSP
- // can be merged into the final HTML response.
- gsspRes = res;
- }
- // Build font Link header early so it's available for ISR cached responses too.
- // Font preloads are module-level state populated at import time and persist across requests.
- var _fontLinkHeader = "";
- var _allFp = [];
- try {
- var _fpGoogle = typeof _getSSRFontPreloadsGoogle === "function" ? _getSSRFontPreloadsGoogle() : [];
- var _fpLocal = typeof _getSSRFontPreloadsLocal === "function" ? _getSSRFontPreloadsLocal() : [];
- _allFp = _fpGoogle.concat(_fpLocal);
- if (_allFp.length > 0) {
- _fontLinkHeader = _allFp.map(function(p) { return "<" + p.href + ">; rel=preload; as=font; type=" + p.type + "; crossorigin"; }).join(", ");
- }
- } catch (e) { /* font preloads not available */ }
-
- let isrRevalidateSeconds = null;
- if (typeof pageModule.getStaticProps === "function") {
- const pathname = routeUrl.split("?")[0];
- const cacheKey = isrCacheKey("pages", pathname);
- const cached = await isrGet(cacheKey);
-
- if (cached && !cached.isStale && cached.value.value && cached.value.value.kind === "PAGES") {
- var _hitHeaders = {
- "Content-Type": "text/html", "X-Vinext-Cache": "HIT",
- "Cache-Control": "s-maxage=" + (cached.value.value.revalidate || 60) + ", stale-while-revalidate",
- };
- if (_fontLinkHeader) _hitHeaders["Link"] = _fontLinkHeader;
- return new Response(cached.value.value.html, { status: 200, headers: _hitHeaders });
- }
-
- if (cached && cached.isStale && cached.value.value && cached.value.value.kind === "PAGES") {
- triggerBackgroundRegeneration(cacheKey, async function() {
- var revalCtx = _createUnifiedCtx({
- executionContext: _getRequestExecutionContext(),
- });
- return _runWithUnifiedCtx(revalCtx, async () => {
- ensureFetchPatch();
- var freshResult = await pageModule.getStaticProps({
- params: params,
- locale: locale,
- locales: i18nConfig ? i18nConfig.locales : undefined,
- defaultLocale: currentDefaultLocale,
- });
- if (freshResult && freshResult.props && typeof freshResult.revalidate === "number" && freshResult.revalidate > 0) {
- var _fp = freshResult.props;
- if (typeof setSSRContext === "function") {
- setSSRContext({
- pathname: patternToNextFormat(route.pattern),
- query: { ...params, ...parseQuery(routeUrl) },
- asPath: routeUrl,
- locale: locale,
- locales: i18nConfig ? i18nConfig.locales : undefined,
- defaultLocale: currentDefaultLocale,
- domainLocales: domainLocales,
- });
- }
- if (i18nConfig) {
- setI18nContext({
- locale: locale,
- locales: i18nConfig.locales,
- defaultLocale: currentDefaultLocale,
- domainLocales: domainLocales,
- hostname: new URL(request.url).hostname,
- });
- }
- // Re-render the page with fresh props inside fresh render sub-scopes
- // so head/cache state cannot leak across passes.
- var _el = AppComponent
- ? React.createElement(AppComponent, { Component: PageComponent, pageProps: _fp })
- : React.createElement(PageComponent, _fp);
- _el = wrapWithRouterContext(_el);
- var _freshBody = await renderIsrPassToStringAsync(_el);
- // Rebuild __NEXT_DATA__ with fresh props
- var _regenPayload = {
- props: { pageProps: _fp }, page: patternToNextFormat(route.pattern),
- query: params, buildId: buildId, isFallback: false,
- };
- if (i18nConfig) {
- _regenPayload.locale = locale;
- _regenPayload.locales = i18nConfig.locales;
- _regenPayload.defaultLocale = currentDefaultLocale;
- _regenPayload.domainLocales = domainLocales;
- }
- var _lGlobals = i18nConfig
- ? ";window.__VINEXT_LOCALE__=" + safeJsonStringify(locale) +
- ";window.__VINEXT_LOCALES__=" + safeJsonStringify(i18nConfig.locales) +
- ";window.__VINEXT_DEFAULT_LOCALE__=" + safeJsonStringify(currentDefaultLocale)
- : "";
- var _freshNDS = "";
- // Reconstruct ISR HTML preserving the document shell from the
- // cached entry (head, fonts, assets, custom _document markup).
- var _cachedStr = cached.value.value.html;
- var _btag = '';
- var _bstart = _cachedStr.indexOf(_btag);
- var _bodyStart = _bstart >= 0 ? _bstart + _btag.length : -1;
- // Locate __NEXT_DATA__ script to split body from suffix
- var _ndMarker = '
- var _ndEnd = _cachedStr.indexOf('', _ndStart) + 9;
- var _tail = _cachedStr.slice(_ndEnd);
- _freshHtml = _cachedStr.slice(0, _bodyStart) + _freshBody + '
' + _gap + _freshNDS + _tail;
- } else {
- _freshHtml = '\\n\\n\\n\\n\\n ' + _freshBody + '
\\n ' + _freshNDS + '\\n\\n';
- }
- await isrSet(cacheKey, { kind: "PAGES", html: _freshHtml, pageData: _fp, headers: undefined, status: undefined }, freshResult.revalidate);
- }
- });
- });
- var _staleHeaders = {
- "Content-Type": "text/html", "X-Vinext-Cache": "STALE",
- "Cache-Control": "s-maxage=0, stale-while-revalidate",
- };
- if (_fontLinkHeader) _staleHeaders["Link"] = _fontLinkHeader;
- return new Response(cached.value.value.html, { status: 200, headers: _staleHeaders });
- }
-
- const ctx = {
- params,
- locale: locale,
- locales: i18nConfig ? i18nConfig.locales : undefined,
- defaultLocale: currentDefaultLocale,
- };
- const result = await pageModule.getStaticProps(ctx);
- if (result && result.props) pageProps = result.props;
- if (result && result.redirect) {
- var gspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
- return new Response(null, { status: gspStatus, headers: { Location: sanitizeDestinationLocal(result.redirect.destination) } });
- }
- if (result && result.notFound) {
- return new Response("404", { status: 404 });
- }
- if (typeof result.revalidate === "number" && result.revalidate > 0) {
- isrRevalidateSeconds = result.revalidate;
- }
- }
-
- let element;
- if (AppComponent) {
- element = React.createElement(AppComponent, { Component: PageComponent, pageProps });
- } else {
- element = React.createElement(PageComponent, pageProps);
- }
- element = wrapWithRouterContext(element);
-
- if (typeof resetSSRHead === "function") resetSSRHead();
- if (typeof flushPreloads === "function") await flushPreloads();
-
- const ssrHeadHTML = typeof getSSRHeadHTML === "function" ? getSSRHeadHTML() : "";
-
- // Collect SSR font data (Google Font links, font preloads, font-face styles)
- var fontHeadHTML = "";
- function _escAttr(s) { return s.replace(/&/g, "&").replace(/"/g, """); }
- try {
- var fontLinks = typeof _getSSRFontLinks === "function" ? _getSSRFontLinks() : [];
- for (var fl of fontLinks) { fontHeadHTML += '\\n '; }
- } catch (e) { /* next/font/google not used */ }
- // Emit for all font files (reuse _allFp collected earlier for Link header)
- for (var fp of _allFp) { fontHeadHTML += '\\n '; }
- try {
- var allFontStyles = [];
- if (typeof _getSSRFontStylesGoogle === "function") allFontStyles.push(..._getSSRFontStylesGoogle());
- if (typeof _getSSRFontStylesLocal === "function") allFontStyles.push(..._getSSRFontStylesLocal());
- if (allFontStyles.length > 0) { fontHeadHTML += '\\n '; }
- } catch (e) { /* font styles not available */ }
-
- const pageModuleIds = route.filePath ? [route.filePath] : [];
- const assetTags = collectAssetTags(manifest, pageModuleIds);
- const nextDataPayload = {
- props: { pageProps }, page: patternToNextFormat(route.pattern), query: params, buildId, isFallback: false,
- };
- if (i18nConfig) {
- nextDataPayload.locale = locale;
- nextDataPayload.locales = i18nConfig.locales;
- nextDataPayload.defaultLocale = currentDefaultLocale;
- nextDataPayload.domainLocales = domainLocales;
- }
- const localeGlobals = i18nConfig
- ? ";window.__VINEXT_LOCALE__=" + safeJsonStringify(locale) +
- ";window.__VINEXT_LOCALES__=" + safeJsonStringify(i18nConfig.locales) +
- ";window.__VINEXT_DEFAULT_LOCALE__=" + safeJsonStringify(currentDefaultLocale)
- : "";
- const nextDataScript = "";
-
- // Build the document shell with a placeholder for the streamed body
- var BODY_MARKER = "";
- var shellHtml;
- if (DocumentComponent) {
- const docElement = React.createElement(DocumentComponent);
- shellHtml = await renderToStringAsync(docElement);
- shellHtml = shellHtml.replace("__NEXT_MAIN__", BODY_MARKER);
- if (ssrHeadHTML || assetTags || fontHeadHTML) {
- shellHtml = shellHtml.replace("", " " + fontHeadHTML + ssrHeadHTML + "\\n " + assetTags + "\\n");
- }
- shellHtml = shellHtml.replace("", nextDataScript);
- if (!shellHtml.includes("__NEXT_DATA__")) {
- shellHtml = shellHtml.replace("
\\n " + BODY_MARKER + "
\\n " + nextDataScript + "\\n", " " + nextDataScript + "\\n");
- }
- } else {
- shellHtml = "\\n\\n