Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/vinext/src/entries/app-rsc-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ export default __createAppRscHandler({
getSourceRoute(sourceRouteIndex) {
return routes[sourceRouteIndex];
},
hasCustomGlobalError: ${globalErrorVar ? `Boolean(${globalErrorVar}?.default)` : "false"},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Boolean(${globalErrorVar}?.default) correctly treats a global-error file with no default export as "no custom global error" — so an app with a malformed global-error.tsx (file present, no default) would still get the __next_error__ recovery shell rather than the server-side boundary path. That's a reasonable choice, but worth confirming it matches Next.js, which I believe validates/throws on a global-error without a default export rather than silently falling back. Non-blocking, but a candidate for a follow-up parity check.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Boolean(${globalErrorVar}?.default) treats a global-error.tsx that exists but has no default export as "no custom global error", so a malformed global-error file would silently get the __next_error__ recovery shell instead of erroring. That's a reasonable default, but Next.js validates app-router special files and throws on a missing default export rather than silently falling back. Non-blocking — worth a follow-up parity check (also flagged in a prior round).

hasGenerateStaticParams: __generateStaticParams.length > 0,
hasPageDefaultExport: !!PageComponent,
hasPageModule: !!route.page,
Expand Down
24 changes: 21 additions & 3 deletions packages/vinext/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ import { createRequire } from "node:module";
import fs from "node:fs";
import { randomBytes, randomUUID } from "node:crypto";
import commonjs from "vite-plugin-commonjs";
import { normalizePathSeparators, stripViteModuleQuery } from "./utils/path.js";
import { normalizePathSeparators, stripJsExtension, stripViteModuleQuery } from "./utils/path.js";

// Install the process-level peer-disconnect backstop at module load.
// Vite plugin lifecycle hooks (config / configureServer) proved
Expand Down Expand Up @@ -2500,11 +2500,12 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] {

resolveId: {
// Hook filter: only invoke JS for handled Next/Vinext compatibility modules.
// Matches "next/navigation", "next/router.js", "virtual:vinext-rsc-entry",
// Matches "next/navigation", "next/router.js", internal
// "vinext/shims/*" package subpaths, "virtual:vinext-rsc-entry",
// direct @vercel/og imports in metadata routes, and \0-prefixed
// re-imports from @vitejs/plugin-rsc.
filter: {
id: /(?:next\/|virtual:vinext-|^@vercel\/og(?:\.js)?$)/,
id: /(?:next\/|vinext\/shims\/|virtual:vinext-|@vercel\/og(?:\.js)?$)/,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The meaningful change here besides adding vinext\/shims\/ is dropping the ^ anchor from the @vercel/og alternative (^@vercel\/og(?:\.js)?$ -> @vercel\/og(?:\.js)?$). That's a deliberate broadening so \0@vercel/og re-imports pass the filter (the new shims test asserts this), and it's correct — just flagging because prior review rounds described this diff as narrowing next/, which it does not. A one-line note in the filter comment that the @vercel/og alternative is intentionally unanchored (to admit \0-prefixed re-imports) would prevent someone re-adding ^ and silently breaking \0@vercel/og resolution.

},
handler(id, importer) {
// Strip \0 prefix if present — @vitejs/plugin-rsc's generated
Expand Down Expand Up @@ -2578,6 +2579,23 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] {
);
}

// Runtime helper modules embedded into generated entries import
// vinext's own shims by package subpath (for example
// `vinext/shims/headers`). Source checkouts also alias userland
// `next/*` imports to the local shim files. Resolve both forms
// through this plugin so request-scoped singleton state is not split
// between source shims and the package export copy. These internal
// package subpaths deliberately resolve to the plain shim today; if
// one gains an environment-specific variant, handle it here before
// returning rather than relying on the userland `next/*` map below.
const vinextShimPrefix = "vinext/shims/";
if (cleanId.startsWith(vinextShimPrefix)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch now intercepts every vinext/shims/* subpath before the _reactServerShims check below. That's correct for the current shim set (the react-server variants are keyed on userland next/* specifiers like next/navigation, not vinext/shims/navigation), so internal vinext/shims/navigation imports have always resolved to the plain shim. But if a future shim needs a .react-server variant when imported via its vinext/shims/* package subpath, this early return would silently shadow it. A short comment noting that this branch deliberately does not handle per-environment variants (and where to add that if needed) would prevent a subtle future regression.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new vinext/shims/ branch is reachable only because the resolveId filter regex (/(?:next\/|…)/ at index.ts:2507) happens to match vinext/shims/*vinext/ contains the substring next/. This is load-bearing but entirely implicit: if the filter is ever tightened to anchor next/ (e.g. ^next\/ or a word boundary), this branch silently stops firing and the singleton-state split this PR fixes would quietly regress, with no test catching it (the new shims test calls the handler directly, bypassing the filter).

Consider either (a) adding an explicit vinext\/shims\/ alternative to the filter regex so the match is intentional, or (b) a one-line comment here noting the branch depends on the incidental next/-substring match in the filter above.

return resolveShimModulePath(
_shimsDir,
stripJsExtension(stripViteModuleQuery(cleanId.slice(vinextShimPrefix.length))),
);
}

// Shims with react-server variants — resolve per-environment.
// These are NOT in resolve.alias (Vite's alias plugin runs
// before enforce:"pre" plugins and can't be overridden).
Expand Down
43 changes: 33 additions & 10 deletions packages/vinext/src/server/app-browser-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
setServerCallback,
} from "@vitejs/plugin-rsc/browser";
import { flushSync } from "react-dom";
import { hydrateRoot } from "react-dom/client";
import { createRoot, hydrateRoot } from "react-dom/client";
import "../client/instrumentation-client.js";
import { notifyAppRouterTransitionStart } from "../client/instrumentation-client-state.js";
import {
Expand Down Expand Up @@ -1603,16 +1603,36 @@ function bootstrapHydration(rscStream: ReadableStream<Uint8Array>): void {
onCaughtError: prodOnCaughtError,
onUncaughtError,
});
window.__VINEXT_RSC_ROOT__ = hydrateRootInTransition({
children: createElement(BrowserRoot, {
initialElements: root,
initialNavigationSnapshot,
}),
container: document,
hydrateRoot,
options: hydrateRootOptions,
startTransition,
const children = createElement(BrowserRoot, {
initialElements: root,
initialNavigationSnapshot,
});
const errorShellStyles = document.querySelectorAll("style[data-vinext-error-shell-style]");
// Only the SSR shell-error recovery document carries the style marker.
// Legacy server-rendered default-error pages also use `__next_error__`, but
// keep their existing hydration path so the serialized server error digest
// remains available to DefaultGlobalError.
if (document.documentElement.id === "__next_error__" && errorShellStyles.length > 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavioral regression worth confirming: unconditional client throw + no boundary + no custom global-error now leaves a blank page.

This createRoot recovery path runs on the initial bootstrap, where pendingNavigationRecoveryHref is still null (it's only set during an in-flight navigation, app-browser-entry.ts:304/573). So the prod onUncaughtError (createOnUncaughtError(() => pendingNavigationRecoveryHref), line 1593) returns early without navigating — it only console.errors (app-browser-error.ts:24-27).

Consider a client component that throws on both server and client (not the SSR-only typeof window === undefined pattern), with no local error.tsx and no custom global-error.tsx:

  • On main: the SSR shell rejects, handleSsr rejects, and renderAppPageHtmlStreamWithRecovery falls to renderErrorBoundaryResponse (app-page-stream.ts:366), serving the server-rendered default error card which then hydrates and stays visible.
  • With this PR: the recovery flag is set (no custom global-error), so handleSsr resolves with the error-document shell. The browser enters this createRoot branch, tears down the error card, re-renders — the component throws again, onUncaughtError has no recovery href, so the user is left on a blank, torn-down document instead of the error card.

The new spec only exercises the SSR-only throw (recovers) and the local-error.tsx cases (boundary catches). The unconditional-throw-no-boundary case is the one that regresses, and it's untested. Worth either (a) confirming this matches Next.js shell-error semantics for a genuinely always-failing component, or (b) adding a fixture route that throws unconditionally with no boundary and asserting the resulting UX, so the regression (if it is one) is caught.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked this on the current PR head and it does not leave a blank page. The shell-recovery path already lands on the default This page couldn’t load card when there is no local boundary. I pushed a2f481b9 to lock that behavior in with a dedicated no-boundary regression case in ssr-error-shell-recovery.browser.spec.ts, then re-ran PLAYWRIGHT_PROJECT=app-router-chrome-browser-specific pnpm run test:e2e -- tests/e2e/app-router/nextjs-compat/ssr-error-shell-recovery.browser.spec.ts.

// There is no server-rendered form to hydrate in this client-render path;
// reuse only the shared root error callbacks and related root options.
const { formState: _inertFormState, ...createRootOptions } = hydrateRootOptions;
for (const style of errorShellStyles) {
style.remove();
}
startTransition(() => {
const clientRoot = createRoot(document, createRootOptions);
clientRoot.render(children);
window.__VINEXT_RSC_ROOT__ = clientRoot;
});
} else {
window.__VINEXT_RSC_ROOT__ = hydrateRootInTransition({
children,
container: document,
hydrateRoot,
options: hydrateRootOptions,
startTransition,
});
}

const navigateRsc: NavigationRuntimeNavigate = async function navigateRsc(
href: string,
Expand Down Expand Up @@ -2190,6 +2210,9 @@ function bootstrapHydration(rscStream: ReadableStream<Uint8Array>): void {
// original document from there, so let the next HMR update reload the
// current URL. If the edit fixed the error the page comes back clean; if
// not, initial dev server errors re-populate the overlay.
//
// Reloading is safe for any default-error document because the dev
// server will render the current state of the source after the edit.
if (document.documentElement.id === "__next_error__") {
window.location.reload();
return;
Expand Down
7 changes: 7 additions & 0 deletions packages/vinext/src/server/app-page-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,11 @@ type DispatchAppPageOptions<TRoute extends AppPageDispatchRoute> = {
probeLayoutAt: (layoutIndex: number, layoutParamAccess?: AppLayoutParamAccessTracker) => unknown;
probePage: () => unknown;
expireSeconds?: number;
/** True when the app supplies a custom global-error.tsx (generated entry
* knows this at build time). Gates SSR shell-error recovery: without a
* custom global-error, shell errors serve the default `__next_error__`
* recovery document instead of a server-side boundary re-render. */
hasCustomGlobalError?: boolean;
renderErrorBoundaryPage: (error: unknown) => Promise<Response | null>;
renderHttpAccessFallbackPage: (
statusCode: number,
Expand Down Expand Up @@ -659,6 +664,7 @@ async function dispatchAppPageInner<TRoute extends AppPageDispatchRoute>(
reactMaxHeadersLength: options.reactMaxHeadersLength,
rootParams: options.rootParams,
waitForAllReady: true,
fallbackToErrorDocumentOnShellError: options.hasCustomGlobalError === false,
...(revalidatedRscCapture.sideStream
? {
sideStream: revalidatedRscCapture.sideStream,
Expand Down Expand Up @@ -979,6 +985,7 @@ async function dispatchAppPageInner<TRoute extends AppPageDispatchRoute>(
return renderPageSpecialError(options, specialError);
},
renderToReadableStream: options.renderToReadableStream,
hasCustomGlobalError: options.hasCustomGlobalError,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This threads the flag into the initial render lifecycle. The background-revalidation handleSsr call in this same file (~line 653, revalidatedSsrEntry.handleSsr(...) with waitForAllReady: true at line 666) does not get the equivalent fallbackToErrorDocumentOnShellError, so revalidation of a throw-to-opt-out route permanently rejects after the first render caches a 200 recovery shell. See the review summary for details — worth threading the flag there too or documenting the divergence.

routePattern: route.pattern,
runWithSuppressedHookWarning(probe) {
return options.runWithSuppressedHookWarning(probe);
Expand Down
5 changes: 5 additions & 0 deletions packages/vinext/src/server/app-page-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ type RenderAppPageLifecycleOptions = {
element: ReactNode | AppOutgoingElements,
options: { onError: AppPageBoundaryOnError },
) => ReadableStream<Uint8Array>;
/** True when the app supplies a custom global-error.tsx. When false, SSR
* shell render errors fall back to the default `__next_error__` recovery
* document instead of a server-side boundary re-render. */
hasCustomGlobalError?: boolean;
routePattern: string;
runWithSuppressedHookWarning<T>(probe: () => Promise<T>): Promise<T>;
scriptNonce?: string;
Expand Down Expand Up @@ -836,6 +840,7 @@ export async function renderAppPageLifecycle(
rscStream: rscForResponse,
scriptNonce: options.scriptNonce,
sideStream: rscCapture.sideStream,
hasCustomGlobalError: options.hasCustomGlobalError,
ssrHandler,
waitForAllReady: options.isPrerender,
});
Expand Down
11 changes: 11 additions & 0 deletions packages/vinext/src/server/app-page-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ export type AppPageSsrHandler = {
waitForAllReady?: boolean;
/** Dev-only: original server error to surface in the browser overlay. */
initialDevServerError?: unknown;
/** When true, an SSR-phase-only shell render error resolves to the
* default `__next_error__` error-document shell (with the original
* flight payload and bootstrap) instead of rejecting. See handleSsr. */
fallbackToErrorDocumentOnShellError?: boolean;
},
) => Promise<ReadableStream<Uint8Array> | AppSsrRenderResult>;
};
Expand Down Expand Up @@ -166,6 +170,10 @@ type RenderAppPageHtmlStreamOptions = {
waitForAllReady?: boolean;
/** Dev-only: original server error to surface in the browser overlay. */
initialDevServerError?: unknown;
/** True when the app supplies a custom global-error.tsx. Disables the
* default error-document shell fallback so SSR shell errors keep driving
* the server-rendered global-error boundary re-render. */
hasCustomGlobalError?: boolean;
};

type RenderAppPageHtmlResponseOptions = {
Expand Down Expand Up @@ -227,6 +235,9 @@ export async function renderAppPageHtmlStream(
capturedRscDataRef: options.capturedRscDataRef,
waitForAllReady: options.waitForAllReady,
initialDevServerError: options.initialDevServerError,
// Only when the caller affirmatively knows there is no custom
// global-error.tsx; undefined (unknown) keeps reject semantics.
fallbackToErrorDocumentOnShellError: options.hasCustomGlobalError === false,
};

const rawResult = await options.ssrHandler.handleSsr(
Expand Down
93 changes: 89 additions & 4 deletions packages/vinext/src/server/app-ssr-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { BfcacheStateKeyMapContext, ElementsContext, Slot } from "vinext/shims/s
import { AppRouterContext } from "vinext/shims/internal/app-router-context";
import { createClientReferencePreloader } from "./app-client-reference-preloader.js";
import { RSC_FORM_STATE_GLOBAL } from "./app-browser-hydration.js";
import DefaultGlobalError from "vinext/shims/default-global-error";

/**
* `@types/react-dom` does not yet type `maxHeadersLength` (it pairs with the
Expand Down Expand Up @@ -106,6 +107,58 @@ function getErrorMessage(error: unknown): string {
return Object.prototype.toString.call(error);
}

function createUtf8Stream(html: string): ReadableStream<Uint8Array> {
const encoder = new TextEncoder();
return new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(encoder.encode(html));
controller.close();
},
});
}

function buildBootstrapModuleScript(bootstrapModuleUrl?: string, nonce?: string): string {
if (!bootstrapModuleUrl) return "";
return (
`<script type="module"${createNonceAttribute(nonce)} src="` +
escapeHtmlAttr(bootstrapModuleUrl) +
'" id="_R_" async=""></script>'
);
}

function markErrorShellStyle(html: string): string {
// DefaultGlobalError intentionally emits one style tag. If that changes,
// update this marker logic without touching styles inserted later by the
// stream transforms.
return html.replace("<style>", '<style data-vinext-error-shell-style="">');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

markErrorShellStyle replaces only the first <style>. That's correct today because DefaultGlobalError renders exactly one <style dangerouslySetInnerHTML>, and any inline-CSS/server-inserted styles are added later by the transform (so they correctly stay unmarked and are preserved by the browser cleanup). This is fragile against future edits to DefaultGlobalError adding a second <style> — worth a one-line comment noting the single-style assumption, since the browser-side style[data-vinext-error-shell-style] removal depends on it.

}

function renderSsrErrorDocumentShell(
bootstrapModuleUrl?: string,
nonce?: string,
): ReadableStream<Uint8Array> {
const html = markErrorShellStyle(
renderToStaticMarkup(
createReactElement(DefaultGlobalError, {
error: null,
}),
),
);
const bootstrapScript = buildBootstrapModuleScript(bootstrapModuleUrl, nonce);
if (!bootstrapScript) {
return createUtf8Stream(`<!DOCTYPE html>${html}`);
}

const documentClose = "</body></html>";
if (!html.endsWith(documentClose)) {
return createUtf8Stream(`<!DOCTYPE html>${html}${bootstrapScript}`);
}

return createUtf8Stream(
`<!DOCTYPE html>${html.slice(0, -documentClose.length)}${bootstrapScript}${documentClose}`,
);
}

function renderInsertedHtml(insertedElements: readonly unknown[]): string {
let insertedHTML = "";

Expand Down Expand Up @@ -306,6 +359,12 @@ export async function handleSsr(
* to resolve before returning the HTML stream. Used for static prerender
* and ISR cache writes to avoid caching fallback content. */
waitForAllReady?: boolean;
/** When true, an SSR-phase shell render error (no RSC-originated `digest`)
* resolves to the default `__next_error__` error-document shell with the
* original flight payload and bootstrap module, instead of rejecting.
* Callers set this only when the app has no custom global-error.tsx, so
* boundary re-render semantics are unaffected. */
fallbackToErrorDocumentOnShellError?: boolean;
},
): Promise<AppSsrRenderResult> {
return runWithNavigationContext(async () => {
Expand Down Expand Up @@ -501,8 +560,6 @@ export async function handleSsr(
},
};

const htmlStream = await renderToReadableStream(ssrRoot, ssrRenderOptions);

// Populated before any SSR request runs: at prod-server startup
// (prod-server.ts) or via build-time bundle injection (index.ts). Left
// undefined in dev, which naturally disables inline CSS there.
Expand Down Expand Up @@ -560,8 +617,36 @@ export async function handleSsr(
const getBeforeInteractiveHeadHTML = (): string =>
renderBeforeInteractiveInlineScripts(beforeInteractiveInlineScripts);

if (options?.waitForAllReady === true) {
await htmlStream.allReady;
let renderedHtmlStream: Awaited<ReturnType<typeof renderToReadableStream>> | undefined;
let htmlStream: ReadableStream<Uint8Array>;
try {
renderedHtmlStream = await renderToReadableStream(ssrRoot, ssrRenderOptions);
if (options?.waitForAllReady === true) {
await renderedHtmlStream.allReady;
}
htmlStream = renderedHtmlStream;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the waitForAllReady (prerender) path, if renderedHtmlStream.allReady rejects, the already-created renderedHtmlStream is abandoned without being cancelled before we fall through to renderSsrErrorDocumentShell. Not a correctness bug, but it leaves an un-consumed/un-cancelled React stream behind. Consider void renderedHtmlStream.cancel().catch(() => {}) in the catch before building the error shell.

} catch (error) {
void renderedHtmlStream?.cancel().catch(() => {});
// Contract with renderAppPageHtmlStreamWithRecovery (app-page-stream.ts):
// a rejected handleSsr drives redirect()/notFound() responses and
// local/global error.tsx boundary re-renders. Errors that originate in
// the RSC render (rethrown here through the flight deserializer) carry
// a string `digest` and must always propagate so those semantics stay
// intact. Only an SSR-phase-only shell error — and only when the app
// has no custom global-error.tsx (the caller sets the flag) — falls
// back to the default `__next_error__` document shell: the original
// flight payload plus the bootstrap module are still emitted, so the
// browser recovers by re-rendering the real tree from the embedded
// RSC data (Next.js shell-error recovery semantics). This resolves
// as a successful shell render, so the caller preserves its normal
// status and ISR eligibility rather than treating it as a 500.
if (
options?.fallbackToErrorDocumentOnShellError !== true ||
typeof (error as { digest?: unknown } | null)?.digest === "string"
) {
throw error;
}
htmlStream = renderSsrErrorDocumentShell(bootstrapModuleUrl, options?.scriptNonce);
}

const finalStream = deferUntilStreamConsumed(
Expand Down
9 changes: 9 additions & 0 deletions packages/vinext/src/utils/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,12 @@ export function stripViteModuleQuery(id: string): string {
const queryIndex = id.search(/[?#]/);
return queryIndex === -1 ? id : id.slice(0, queryIndex);
}

/** Strip a trailing `.js` extension from a module specifier so
* `resolveShimModulePath` looks for the correct base name (e.g. `headers.js`
* → `headers`). Public and internal shim imports may carry extensionful
* subpaths; normalising before resolution prevents looking for non-existent
* files like `headers.js.ts`. */
export function stripJsExtension(name: string): string {
return name.endsWith(".js") ? name.slice(0, -3) : name;
}
6 changes: 6 additions & 0 deletions tests/app-page-dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ function createDispatchOptions(
dynamicConfig?: DispatchOptions["dynamicConfig"];
findIntercept?: DispatchOptions["findIntercept"];
generateStaticParams?: DispatchOptions["generateStaticParams"];
hasCustomGlobalError?: DispatchOptions["hasCustomGlobalError"];
formState?: DispatchOptions["formState"];
getSourceRoute?: DispatchOptions["getSourceRoute"];
actionError?: DispatchOptions["actionError"];
Expand Down Expand Up @@ -338,6 +339,7 @@ function createDispatchOptions(
},
getSourceRoute: overrides.getSourceRoute ?? (() => undefined),
hasGenerateStaticParams: typeof overrides.generateStaticParams === "function",
hasCustomGlobalError: overrides.hasCustomGlobalError,
hasPageDefaultExport: true,
hasPageModule: true,
handlerStart: 10,
Expand Down Expand Up @@ -1187,6 +1189,7 @@ describe("app page dispatch", () => {
scheduledRender = renderFn;
};
let capturedWaitForAllReady: boolean | undefined;
let capturedFallbackToErrorDocument: boolean | undefined;
const isrSet = vi.fn(async () => {});
const { options } = createDispatchOptions({
buildPageElement: async () => React.createElement("main", null, "fresh"),
Expand All @@ -1196,9 +1199,11 @@ describe("app page dispatch", () => {
buildISRCacheEntry(buildCachedAppPageValue("<html>stale</html>"), true),
),
isrSet,
hasCustomGlobalError: false,
loadSsrHandler: async () => ({
async handleSsr(_rscStream, _navigationContext, _fontData, captureOptions) {
capturedWaitForAllReady = captureOptions?.waitForAllReady;
capturedFallbackToErrorDocument = captureOptions?.fallbackToErrorDocumentOnShellError;
if (captureOptions?.capturedRscDataRef) {
captureOptions.capturedRscDataRef.value = Promise.resolve(
new TextEncoder().encode("fresh-flight").buffer,
Expand Down Expand Up @@ -1228,6 +1233,7 @@ describe("app page dispatch", () => {
await scheduledRender();

expect(capturedWaitForAllReady).toBe(true);
expect(capturedFallbackToErrorDocument).toBe(true);
expect(isrSet).toHaveBeenCalled();
});
});
Loading
Loading