From 8c5b8e4040da34c8cfc48a43ddc19e3834cb9161 Mon Sep 17 00:00:00 2001
From: Md Yunus
Date: Sat, 21 Mar 2026 14:43:22 +0530
Subject: [PATCH] fix: prevent useActionState state becoming undefined when
redirect() is called
Fixes issue #589 where useActionState receives undefined state when a Server Action
calls redirect() from next/navigation.
Root Cause:
When a server action calls redirect(), the server was sending redirect headers with
empty body. The client tried to use RSC navigation but received empty body, causing
navigation to fail and leaving useActionState with undefined state.
Solution:
Use hard redirect (window.location.assign/replace) for server action redirects.
This ensures navigation completes and the new page loads with fresh initial state.
Server-side (app-rsc-entry.ts):
- Return 303 See Other response with Location header (Next.js behavior)
- Include Set-Cookie headers for cookies set during action
Client-side (app-browser-entry.ts):
- Detect x-action-redirect header from response
- Use window.location.assign/replace for hard redirect
- Respect push/replace type from redirect
Changes:
- packages/vinext/src/entries/app-rsc-entry.ts: Return 303 redirect response
- packages/vinext/src/server/app-browser-entry.ts: Use hard redirect for action redirects
- tests/e2e/app-router/server-actions.spec.ts: Add waitForHydration to test
- tests/fixtures/app-basic/app/actions/actions.ts: Add redirectWithActionState test action
- tests/fixtures/app-basic/app/action-state-redirect/page.tsx: Add test page
- tests/__snapshots__/entry-templates.test.ts.snap: Update snapshots
This matches Next.js behavior where server action redirects use 303 status code
and the browser performs a full page load to the redirect target.
Signed-off-by: Md Yunus
Co-authored-by: Qwen-Coder
---
packages/vinext/src/entries/app-rsc-entry.ts | 19 +-
.../vinext/src/server/app-browser-entry.ts | 14 +-
.../entry-templates.test.ts.snap | 1891 +++--------------
tests/e2e/app-router/server-actions.spec.ts | 19 +
.../app/action-state-redirect/page.tsx | 22 +
.../fixtures/app-basic/app/actions/actions.ts | 15 +
6 files changed, 346 insertions(+), 1634 deletions(-)
create mode 100644 tests/fixtures/app-basic/app/action-state-redirect/page.tsx
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