From a9223b6ed9d6a51464f7ee9cd42924ed600f8b42 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Thu, 9 Apr 2026 21:26:27 +0800 Subject: [PATCH 1/3] add test --- .../layout-search-params.spec.ts | 60 +++++++++++++++++++ .../[id]/layout-shell.tsx | 42 +++++++++++++ .../layout-search-params/[id]/layout.tsx | 17 ++++++ .../layout-search-params/[id]/page.tsx | 14 +++++ .../layout-search-params/layout.tsx | 5 ++ 5 files changed, 138 insertions(+) create mode 100644 tests/e2e/app-router/nextjs-compat/layout-search-params.spec.ts create mode 100644 tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/layout-shell.tsx create mode 100644 tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/layout.tsx create mode 100644 tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/page.tsx create mode 100644 tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/layout.tsx diff --git a/tests/e2e/app-router/nextjs-compat/layout-search-params.spec.ts b/tests/e2e/app-router/nextjs-compat/layout-search-params.spec.ts new file mode 100644 index 000000000..c9de35996 --- /dev/null +++ b/tests/e2e/app-router/nextjs-compat/layout-search-params.spec.ts @@ -0,0 +1,60 @@ +/** + * Next.js compat: layout state across search param changes. + * + * Based on Next.js: test/e2e/app-dir/search-params-react-key/layout-params.test.ts + * https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/search-params-react-key/layout-params.test.ts + * + * Extends the same expectation to a parent client layout rendered by a server layout: + * query-only push/replace should not remount that layout. + */ + +import { expect, test } from "@playwright/test"; +import { waitForAppRouterHydration } from "../../helpers"; + +const BASE = "http://localhost:4174"; + +test.describe("Next.js compat: layout state across search param changes", () => { + test("router.push() keeps parent client layout mounted on query-only navigation", async ({ + page, + }) => { + await page.goto(`${BASE}/nextjs-compat/layout-search-params/demo`); + await waitForAppRouterHydration(page); + + await page.click("#layout-increment"); + await page.click("#layout-increment"); + await expect(page.locator("#layout-count")).toHaveText("2"); + await expect(page.locator("#layout-mount-count")).toHaveText("1"); + + await page.click("#layout-push"); + + await expect(async () => { + expect(page.url()).toContain("foo=bar"); + }).toPass({ timeout: 10_000 }); + + await expect(page.locator("#search-params")).toContainText('"foo":"bar"'); + await expect(page.locator("#layout-count")).toHaveText("2"); + await expect(page.locator("#layout-mount-count")).toHaveText("1"); + }); + + test("router.replace() keeps parent client layout mounted on query-only navigation", async ({ + page, + }) => { + await page.goto(`${BASE}/nextjs-compat/layout-search-params/demo`); + await waitForAppRouterHydration(page); + + await page.click("#layout-increment"); + await page.click("#layout-increment"); + await expect(page.locator("#layout-count")).toHaveText("2"); + await expect(page.locator("#layout-mount-count")).toHaveText("1"); + + await page.click("#layout-replace"); + + await expect(async () => { + expect(page.url()).toContain("foo=baz"); + }).toPass({ timeout: 10_000 }); + + await expect(page.locator("#search-params")).toContainText('"foo":"baz"'); + await expect(page.locator("#layout-count")).toHaveText("2"); + await expect(page.locator("#layout-mount-count")).toHaveText("1"); + }); +}); diff --git a/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/layout-shell.tsx b/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/layout-shell.tsx new file mode 100644 index 000000000..97388c4a6 --- /dev/null +++ b/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/layout-shell.tsx @@ -0,0 +1,42 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; + +declare global { + interface Window { + __vinextLayoutSearchParamsMountCount__?: number; + } +} + +function LayoutShellInner({ children }: { children: React.ReactNode }) { + const router = useRouter(); + const [count, setCount] = useState(0); + const [mountCount, setMountCount] = useState(0); + + useEffect(() => { + window.__vinextLayoutSearchParamsMountCount__ = + (window.__vinextLayoutSearchParamsMountCount__ ?? 0) + 1; + setMountCount(window.__vinextLayoutSearchParamsMountCount__); + }, []); + + return ( +
+

Layout Search Params

+
{count}
+
{mountCount}
+ + + + {children} +
+ ); +} + +export const LayoutShell = React.memo(LayoutShellInner); diff --git a/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/layout.tsx b/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/layout.tsx new file mode 100644 index 000000000..1f0864871 --- /dev/null +++ b/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/layout.tsx @@ -0,0 +1,17 @@ +import { LayoutShell } from "./layout-shell"; + +export default async function LayoutSearchParamsLayout({ + children, + params, +}: { + children: React.ReactNode; + params: Promise<{ id: string }>; +}) { + const { id } = await params; + return ( + + {children} +
{id}
+
+ ); +} diff --git a/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/page.tsx b/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/page.tsx new file mode 100644 index 000000000..be885279b --- /dev/null +++ b/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/[id]/page.tsx @@ -0,0 +1,14 @@ +export default async function LayoutSearchParamsPage({ + searchParams, +}: { + searchParams: Promise>; +}) { + const params = await searchParams; + + return ( +
+
{JSON.stringify(params)}
+

Query-only navigation should preserve parent layout state.

+
+ ); +} diff --git a/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/layout.tsx b/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/layout.tsx new file mode 100644 index 000000000..f48091142 --- /dev/null +++ b/tests/fixtures/app-basic/app/nextjs-compat/layout-search-params/layout.tsx @@ -0,0 +1,5 @@ +"use client"; + +export default function LayoutSearchParamsOuterLayout({ children }: { children: React.ReactNode }) { + return <>{children}; +} From 7693c176efc002236d8af9c94543c633e99efc62 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Thu, 9 Apr 2026 21:26:32 +0800 Subject: [PATCH 2/3] code --- .../src/server/app-browser-client-loader.ts | 202 ++++++++++++++++++ .../vinext/src/server/app-browser-entry.ts | 30 ++- 2 files changed, 227 insertions(+), 5 deletions(-) create mode 100644 packages/vinext/src/server/app-browser-client-loader.ts diff --git a/packages/vinext/src/server/app-browser-client-loader.ts b/packages/vinext/src/server/app-browser-client-loader.ts new file mode 100644 index 000000000..eb795874c --- /dev/null +++ b/packages/vinext/src/server/app-browser-client-loader.ts @@ -0,0 +1,202 @@ +import { createElement, forwardRef } from "react"; +import { setRequireModule } from "@vitejs/plugin-rsc/core/browser"; +import * as clientReferences from "virtual:vite-rsc/client-references"; +import type { CachedRscResponse } from "../shims/navigation.js"; + +declare const __vite_rsc_raw_import__: (id: string) => Promise; + +type TrackedPromise = Promise & { + reason?: unknown; + status?: "fulfilled" | "pending" | "rejected"; + value?: T; +}; + +type MemoLikeValue = { + $$typeof: symbol; + displayName?: string; + type?: unknown; +}; + +const CLIENT_REFERENCE_ROW_PATTERN = /(?:^|\n)[0-9a-fA-F]+:I\[("(?:\\.|[^"\\])*")/g; +const REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"); +const REACT_MEMO_TYPE = Symbol.for("react.memo"); +const textDecoder = new TextDecoder(); +const clientModuleCache = new Map>(); +const stableMemoExportCache = new Map(); +const stableModuleExportCache = new Map(); +let installed = false; + +function withTrailingSlash(path: string): string { + return path[path.length - 1] === "/" ? path : `${path}/`; +} + +function normalizeClientReferenceId(id: string): string { + return id.split("$$cache=")[0]; +} + +function trackPromise(promise: Promise): TrackedPromise { + const tracked = promise as TrackedPromise; + if (tracked.status) { + return tracked; + } + + tracked.status = "pending"; + promise.then( + (value) => { + tracked.status = "fulfilled"; + tracked.value = value; + }, + (reason) => { + tracked.status = "rejected"; + tracked.reason = reason; + }, + ); + return tracked; +} + +function isObjectRecord(value: unknown): value is Record { + return !!value && typeof value === "object"; +} + +function isForwardRefLikeValue(value: unknown): value is { $$typeof: symbol } { + return isObjectRecord(value) && "$$typeof" in value && value.$$typeof === REACT_FORWARD_REF_TYPE; +} + +function isMemoLikeValue(value: unknown): value is MemoLikeValue { + return isObjectRecord(value) && "$$typeof" in value && value.$$typeof === REACT_MEMO_TYPE; +} + +function createStableMemoProxy( + normalizedId: string, + exportName: string, + value: MemoLikeValue, +): unknown { + const cacheKey = `${normalizedId}#${exportName}`; + const cached = stableMemoExportCache.get(cacheKey); + if (cached) { + return cached; + } + + const target = value as unknown as Parameters[0]; + const supportsRef = isForwardRefLikeValue(value.type); + + const proxy = supportsRef + ? forwardRef>(function VinextStableMemoProxy(props, ref) { + return createElement(target, Object.assign({}, props, { ref }) as never); + }) + : function VinextStableMemoProxy(props: Record) { + return createElement(target, props); + }; + + const namedProxy = proxy as { displayName?: string }; + namedProxy.displayName = + value.displayName ?? + (typeof value.type === "function" && value.type.name ? value.type.name : exportName); + + stableMemoExportCache.set(cacheKey, namedProxy); + return namedProxy; +} + +function stabilizeClientModuleExports(normalizedId: string, moduleExports: unknown): unknown { + const cached = stableModuleExportCache.get(normalizedId); + if (cached) { + return cached; + } + + if (isMemoLikeValue(moduleExports)) { + const stable = createStableMemoProxy(normalizedId, "default", moduleExports); + stableModuleExportCache.set(normalizedId, stable); + return stable; + } + + if ( + !moduleExports || + (typeof moduleExports !== "object" && typeof moduleExports !== "function") + ) { + stableModuleExportCache.set(normalizedId, moduleExports); + return moduleExports; + } + + let changed = false; + const next = Object.create(Object.getPrototypeOf(moduleExports)); + const descriptors = Object.getOwnPropertyDescriptors(moduleExports); + + for (const [exportName, descriptor] of Object.entries(descriptors)) { + if ("value" in descriptor && isMemoLikeValue(descriptor.value)) { + descriptor.value = createStableMemoProxy(normalizedId, exportName, descriptor.value); + changed = true; + } + Object.defineProperty(next, exportName, descriptor); + } + + const stableModuleExports = changed ? next : moduleExports; + stableModuleExportCache.set(normalizedId, stableModuleExports); + return stableModuleExports; +} + +function loadClientReference(normalizedId: string): Promise { + if (!import.meta.env.__vite_rsc_build__) { + return __vite_rsc_raw_import__( + withTrailingSlash(import.meta.env.BASE_URL) + normalizedId.slice(1), + ).then((moduleExports) => stabilizeClientModuleExports(normalizedId, moduleExports)); + } + + const importReference = clientReferences.default[normalizedId] as + | (() => Promise) + | undefined; + if (!importReference) { + throw new Error(`client reference not found '${normalizedId}'`); + } + return importReference().then((moduleExports) => + stabilizeClientModuleExports(normalizedId, moduleExports), + ); +} + +function getTrackedClientModule(id: string): TrackedPromise { + const normalizedId = normalizeClientReferenceId(id); + const cached = clientModuleCache.get(normalizedId); + if (cached) { + return cached; + } + + const tracked = trackPromise(Promise.resolve().then(() => loadClientReference(normalizedId))); + clientModuleCache.set(normalizedId, tracked); + return tracked; +} + +export function installVinextBrowserClientLoader(): void { + if (installed) { + return; + } + + installed = true; + setRequireModule({ + load(id) { + return getTrackedClientModule(id); + }, + }); +} + +export async function prewarmClientReferencesFromSnapshot( + snapshot: CachedRscResponse, +): Promise { + const payload = textDecoder.decode(snapshot.buffer); + const pending: Promise[] = []; + + CLIENT_REFERENCE_ROW_PATTERN.lastIndex = 0; + let match: RegExpExecArray | null; + while ((match = CLIENT_REFERENCE_ROW_PATTERN.exec(payload))) { + try { + const id = JSON.parse(match[1]) as string; + pending.push(getTrackedClientModule(id)); + } catch { + // Ignore malformed rows and let the regular RSC decoder surface errors. + } + } + + if (pending.length === 0) { + return; + } + + await Promise.allSettled(pending); +} diff --git a/packages/vinext/src/server/app-browser-entry.ts b/packages/vinext/src/server/app-browser-entry.ts index dd74e35e8..d4c55be35 100644 --- a/packages/vinext/src/server/app-browser-entry.ts +++ b/packages/vinext/src/server/app-browser-entry.ts @@ -16,7 +16,7 @@ import { createTemporaryReferenceSet, encodeReply, setServerCallback, -} from "@vitejs/plugin-rsc/browser"; +} from "@vitejs/plugin-rsc/react/browser"; import { hydrateRoot } from "react-dom/client"; import "../client/instrumentation-client.js"; import { notifyAppRouterTransitionStart } from "../client/instrumentation-client-state.js"; @@ -46,6 +46,10 @@ import { createProgressiveRscStream, getVinextBrowserGlobal, } from "./app-browser-stream.js"; +import { + installVinextBrowserClientLoader, + prewarmClientReferencesFromSnapshot, +} from "./app-browser-client-loader.js"; type SearchParamInput = ConstructorParameters[0]; @@ -488,11 +492,15 @@ async function readInitialRscStream(): Promise> { restoreHydrationNavigationContext(window.location.pathname, window.location.search, params); - if (!rscResponse.body) { + const responseSnapshot = await snapshotRscResponse(rscResponse); + await prewarmClientReferencesFromSnapshot(responseSnapshot); + + const restoredResponse = restoreRscResponse(responseSnapshot, false); + if (!restoredResponse.body) { throw new Error("[vinext] Initial RSC response had no body"); } - return rscResponse.body; + return restoredResponse.body; } function registerServerActionCallback(): void { @@ -534,8 +542,10 @@ function registerServerActionCallback(): void { clearClientNavigationCaches(); + const responseSnapshot = await snapshotRscResponse(fetchResponse); + await prewarmClientReferencesFromSnapshot(responseSnapshot); const result = await createFromFetch( - Promise.resolve(fetchResponse), + Promise.resolve(restoreRscResponse(responseSnapshot)), { temporaryReferences }, ); @@ -573,6 +583,7 @@ function registerServerActionCallback(): void { } async function main(): Promise { + installVinextBrowserClientLoader(); registerServerActionCallback(); const rscStream = await readInitialRscStream(); @@ -642,6 +653,7 @@ async function main(): Promise { // wrapping only) — no stale-navigation recheck needed between here and the // next await. const cachedNavigationSnapshot = createClientNavigationRenderSnapshot(href, cachedParams); + await prewarmClientReferencesFromSnapshot(cachedRoute.response); const cachedPayload = await createFromFetch( Promise.resolve(restoreRscResponse(cachedRoute.response)), ); @@ -726,6 +738,10 @@ async function main(): Promise { if (navId !== activeNavigationId) return; + await prewarmClientReferencesFromSnapshot(responseSnapshot); + + if (navId !== activeNavigationId) return; + const rscPayload = await createFromFetch( Promise.resolve(restoreRscResponse(responseSnapshot)), ); @@ -801,8 +817,12 @@ async function main(): Promise { import.meta.hot.on("rsc:update", async () => { try { clearClientNavigationCaches(); + const responseSnapshot = await snapshotRscResponse( + await fetch(toRscUrl(window.location.pathname + window.location.search)), + ); + await prewarmClientReferencesFromSnapshot(responseSnapshot); const rscPayload = await createFromFetch( - fetch(toRscUrl(window.location.pathname + window.location.search)), + Promise.resolve(restoreRscResponse(responseSnapshot)), ); // HMR updates skip renderNavigationPayload — no snapshot activated. updateBrowserTree( From 5edf6cc7ba0bb88582ec8a7b201930d23539bb84 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:48:24 +0800 Subject: [PATCH 3/3] Revert "code" This reverts commit 7693c176efc002236d8af9c94543c633e99efc62. --- .../src/server/app-browser-client-loader.ts | 202 ------------------ .../vinext/src/server/app-browser-entry.ts | 30 +-- 2 files changed, 5 insertions(+), 227 deletions(-) delete mode 100644 packages/vinext/src/server/app-browser-client-loader.ts diff --git a/packages/vinext/src/server/app-browser-client-loader.ts b/packages/vinext/src/server/app-browser-client-loader.ts deleted file mode 100644 index eb795874c..000000000 --- a/packages/vinext/src/server/app-browser-client-loader.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { createElement, forwardRef } from "react"; -import { setRequireModule } from "@vitejs/plugin-rsc/core/browser"; -import * as clientReferences from "virtual:vite-rsc/client-references"; -import type { CachedRscResponse } from "../shims/navigation.js"; - -declare const __vite_rsc_raw_import__: (id: string) => Promise; - -type TrackedPromise = Promise & { - reason?: unknown; - status?: "fulfilled" | "pending" | "rejected"; - value?: T; -}; - -type MemoLikeValue = { - $$typeof: symbol; - displayName?: string; - type?: unknown; -}; - -const CLIENT_REFERENCE_ROW_PATTERN = /(?:^|\n)[0-9a-fA-F]+:I\[("(?:\\.|[^"\\])*")/g; -const REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"); -const REACT_MEMO_TYPE = Symbol.for("react.memo"); -const textDecoder = new TextDecoder(); -const clientModuleCache = new Map>(); -const stableMemoExportCache = new Map(); -const stableModuleExportCache = new Map(); -let installed = false; - -function withTrailingSlash(path: string): string { - return path[path.length - 1] === "/" ? path : `${path}/`; -} - -function normalizeClientReferenceId(id: string): string { - return id.split("$$cache=")[0]; -} - -function trackPromise(promise: Promise): TrackedPromise { - const tracked = promise as TrackedPromise; - if (tracked.status) { - return tracked; - } - - tracked.status = "pending"; - promise.then( - (value) => { - tracked.status = "fulfilled"; - tracked.value = value; - }, - (reason) => { - tracked.status = "rejected"; - tracked.reason = reason; - }, - ); - return tracked; -} - -function isObjectRecord(value: unknown): value is Record { - return !!value && typeof value === "object"; -} - -function isForwardRefLikeValue(value: unknown): value is { $$typeof: symbol } { - return isObjectRecord(value) && "$$typeof" in value && value.$$typeof === REACT_FORWARD_REF_TYPE; -} - -function isMemoLikeValue(value: unknown): value is MemoLikeValue { - return isObjectRecord(value) && "$$typeof" in value && value.$$typeof === REACT_MEMO_TYPE; -} - -function createStableMemoProxy( - normalizedId: string, - exportName: string, - value: MemoLikeValue, -): unknown { - const cacheKey = `${normalizedId}#${exportName}`; - const cached = stableMemoExportCache.get(cacheKey); - if (cached) { - return cached; - } - - const target = value as unknown as Parameters[0]; - const supportsRef = isForwardRefLikeValue(value.type); - - const proxy = supportsRef - ? forwardRef>(function VinextStableMemoProxy(props, ref) { - return createElement(target, Object.assign({}, props, { ref }) as never); - }) - : function VinextStableMemoProxy(props: Record) { - return createElement(target, props); - }; - - const namedProxy = proxy as { displayName?: string }; - namedProxy.displayName = - value.displayName ?? - (typeof value.type === "function" && value.type.name ? value.type.name : exportName); - - stableMemoExportCache.set(cacheKey, namedProxy); - return namedProxy; -} - -function stabilizeClientModuleExports(normalizedId: string, moduleExports: unknown): unknown { - const cached = stableModuleExportCache.get(normalizedId); - if (cached) { - return cached; - } - - if (isMemoLikeValue(moduleExports)) { - const stable = createStableMemoProxy(normalizedId, "default", moduleExports); - stableModuleExportCache.set(normalizedId, stable); - return stable; - } - - if ( - !moduleExports || - (typeof moduleExports !== "object" && typeof moduleExports !== "function") - ) { - stableModuleExportCache.set(normalizedId, moduleExports); - return moduleExports; - } - - let changed = false; - const next = Object.create(Object.getPrototypeOf(moduleExports)); - const descriptors = Object.getOwnPropertyDescriptors(moduleExports); - - for (const [exportName, descriptor] of Object.entries(descriptors)) { - if ("value" in descriptor && isMemoLikeValue(descriptor.value)) { - descriptor.value = createStableMemoProxy(normalizedId, exportName, descriptor.value); - changed = true; - } - Object.defineProperty(next, exportName, descriptor); - } - - const stableModuleExports = changed ? next : moduleExports; - stableModuleExportCache.set(normalizedId, stableModuleExports); - return stableModuleExports; -} - -function loadClientReference(normalizedId: string): Promise { - if (!import.meta.env.__vite_rsc_build__) { - return __vite_rsc_raw_import__( - withTrailingSlash(import.meta.env.BASE_URL) + normalizedId.slice(1), - ).then((moduleExports) => stabilizeClientModuleExports(normalizedId, moduleExports)); - } - - const importReference = clientReferences.default[normalizedId] as - | (() => Promise) - | undefined; - if (!importReference) { - throw new Error(`client reference not found '${normalizedId}'`); - } - return importReference().then((moduleExports) => - stabilizeClientModuleExports(normalizedId, moduleExports), - ); -} - -function getTrackedClientModule(id: string): TrackedPromise { - const normalizedId = normalizeClientReferenceId(id); - const cached = clientModuleCache.get(normalizedId); - if (cached) { - return cached; - } - - const tracked = trackPromise(Promise.resolve().then(() => loadClientReference(normalizedId))); - clientModuleCache.set(normalizedId, tracked); - return tracked; -} - -export function installVinextBrowserClientLoader(): void { - if (installed) { - return; - } - - installed = true; - setRequireModule({ - load(id) { - return getTrackedClientModule(id); - }, - }); -} - -export async function prewarmClientReferencesFromSnapshot( - snapshot: CachedRscResponse, -): Promise { - const payload = textDecoder.decode(snapshot.buffer); - const pending: Promise[] = []; - - CLIENT_REFERENCE_ROW_PATTERN.lastIndex = 0; - let match: RegExpExecArray | null; - while ((match = CLIENT_REFERENCE_ROW_PATTERN.exec(payload))) { - try { - const id = JSON.parse(match[1]) as string; - pending.push(getTrackedClientModule(id)); - } catch { - // Ignore malformed rows and let the regular RSC decoder surface errors. - } - } - - if (pending.length === 0) { - return; - } - - await Promise.allSettled(pending); -} diff --git a/packages/vinext/src/server/app-browser-entry.ts b/packages/vinext/src/server/app-browser-entry.ts index d4c55be35..dd74e35e8 100644 --- a/packages/vinext/src/server/app-browser-entry.ts +++ b/packages/vinext/src/server/app-browser-entry.ts @@ -16,7 +16,7 @@ import { createTemporaryReferenceSet, encodeReply, setServerCallback, -} from "@vitejs/plugin-rsc/react/browser"; +} from "@vitejs/plugin-rsc/browser"; import { hydrateRoot } from "react-dom/client"; import "../client/instrumentation-client.js"; import { notifyAppRouterTransitionStart } from "../client/instrumentation-client-state.js"; @@ -46,10 +46,6 @@ import { createProgressiveRscStream, getVinextBrowserGlobal, } from "./app-browser-stream.js"; -import { - installVinextBrowserClientLoader, - prewarmClientReferencesFromSnapshot, -} from "./app-browser-client-loader.js"; type SearchParamInput = ConstructorParameters[0]; @@ -492,15 +488,11 @@ async function readInitialRscStream(): Promise> { restoreHydrationNavigationContext(window.location.pathname, window.location.search, params); - const responseSnapshot = await snapshotRscResponse(rscResponse); - await prewarmClientReferencesFromSnapshot(responseSnapshot); - - const restoredResponse = restoreRscResponse(responseSnapshot, false); - if (!restoredResponse.body) { + if (!rscResponse.body) { throw new Error("[vinext] Initial RSC response had no body"); } - return restoredResponse.body; + return rscResponse.body; } function registerServerActionCallback(): void { @@ -542,10 +534,8 @@ function registerServerActionCallback(): void { clearClientNavigationCaches(); - const responseSnapshot = await snapshotRscResponse(fetchResponse); - await prewarmClientReferencesFromSnapshot(responseSnapshot); const result = await createFromFetch( - Promise.resolve(restoreRscResponse(responseSnapshot)), + Promise.resolve(fetchResponse), { temporaryReferences }, ); @@ -583,7 +573,6 @@ function registerServerActionCallback(): void { } async function main(): Promise { - installVinextBrowserClientLoader(); registerServerActionCallback(); const rscStream = await readInitialRscStream(); @@ -653,7 +642,6 @@ async function main(): Promise { // wrapping only) — no stale-navigation recheck needed between here and the // next await. const cachedNavigationSnapshot = createClientNavigationRenderSnapshot(href, cachedParams); - await prewarmClientReferencesFromSnapshot(cachedRoute.response); const cachedPayload = await createFromFetch( Promise.resolve(restoreRscResponse(cachedRoute.response)), ); @@ -738,10 +726,6 @@ async function main(): Promise { if (navId !== activeNavigationId) return; - await prewarmClientReferencesFromSnapshot(responseSnapshot); - - if (navId !== activeNavigationId) return; - const rscPayload = await createFromFetch( Promise.resolve(restoreRscResponse(responseSnapshot)), ); @@ -817,12 +801,8 @@ async function main(): Promise { import.meta.hot.on("rsc:update", async () => { try { clearClientNavigationCaches(); - const responseSnapshot = await snapshotRscResponse( - await fetch(toRscUrl(window.location.pathname + window.location.search)), - ); - await prewarmClientReferencesFromSnapshot(responseSnapshot); const rscPayload = await createFromFetch( - Promise.resolve(restoreRscResponse(responseSnapshot)), + fetch(toRscUrl(window.location.pathname + window.location.search)), ); // HMR updates skip renderNavigationPayload — no snapshot activated. updateBrowserTree(