diff --git a/docs/guide/pages-routing/errors.md b/docs/guide/pages-routing/errors.md
index e2cae34c3..9e3345e03 100644
--- a/docs/guide/pages-routing/errors.md
+++ b/docs/guide/pages-routing/errors.md
@@ -90,6 +90,26 @@ In middleware, `c.notFound()` shows the root `_not-found` without running a page
Action errors that reach a client boundary keep their HTTP status and public message. Redirects, authentication responses, and API errors keep their usual behavior.
+## HTTP exceptions
+
+Throw an `HTTPException` with a 4xx/5xx status and a public message to show a deliberate HTTP error inside your layouts:
+
+```ts
+// pages/account.server.ts
+import { HTTPException } from 'hono/http-exception';
+import { defineHandler } from 'void/handler';
+
+export const loader = defineHandler(() => {
+ throw new HTTPException(403, { message: 'Your account is suspended' });
+});
+```
+
+The nearest `_error` receives `{ status: 403, message: 'Your account is suspended' }`. The HTML document or client-navigation response uses HTTP 403. For exceptions thrown in application middleware before the loader, Void uses the root boundary without running the loader. A 404 selects `_not-found`. If no matching boundary exists, the exception's response is returned.
+
+This applies to page GET and HEAD requests. Redirects, API routes, actions, returned responses, and exceptions with an explicit `res` keep their existing behavior. Only include messages safe to show to users. Checks that determine the HTTP status must finish before deferred streaming starts.
+
+Use [`c.status()`](./loaders.md#response-status) instead when you want an error status while rendering the requested page with its normal props.
+
## Nested errors
Put an error component in a subdirectory to customize that section. Void uses the closest one and keeps its surrounding layouts. If a layout fails, an error component outside that layout handles it. If an error component fails, Void tries an outer one, then its built-in error page.
diff --git a/docs/guide/pages-routing/loaders.md b/docs/guide/pages-routing/loaders.md
index 2ecbbf3d2..0b5425fc4 100644
--- a/docs/guide/pages-routing/loaders.md
+++ b/docs/guide/pages-routing/loaders.md
@@ -31,6 +31,23 @@ Return data directly to preserve values such as `Date`, `Map`, `Set`, `bigint`,
Returning `Response.json()` uses JSON conversion instead. Functions and custom class instances can't be sent as page data.
+## Response Status
+
+Use `c.status()` to render the current page with an HTTP error status, such as `403` or `503`. Return the page's props as usual:
+
+```ts
+// pages/maintenance.server.ts
+import { defineHandler } from 'void/handler';
+
+export const loader = defineHandler((c) => {
+ c.status(503);
+ c.header('Retry-After', '120');
+ return { message: 'Down for maintenance' };
+});
+```
+
+The status and headers apply to both the HTML document and client-side navigation responses. Client visits still display the page with its props and layouts. Middleware can also set the status before `await next()`. For pages with deferred props, set the status in the loader before streaming starts.
+
## Using the Data in Page Components
The page component receives the loader's result as props. Export the inferred type from the server file so the component stays in sync:
diff --git a/packages/void/AGENTS.md b/packages/void/AGENTS.md
index 49af70172..04641cbf2 100644
--- a/packages/void/AGENTS.md
+++ b/packages/void/AGENTS.md
@@ -53,6 +53,8 @@ Pages and island actions opt in to `X-VoidPages-Redirect: manual`. The routing m
Pages `_not-found`/`_error` recovery uses finite render plans that retain logical layout slots. Never serialize original exceptions through framework hydration helpers, rerun a failed loader/action, or cache an error document. Native client boundaries reset on every visit/refresh and preserve healthy ancestor layouts.
+Recover response-less 4xx/5xx Hono `HTTPException`s only for page GET/HEAD requests: nearest boundary for loaders/handler middleware, root boundary for application middleware. Preserve redirects, explicit responses, API routes, actions, and existing 404 recovery. Middleware recovery must identify the actual matched endpoint without loading its module or replaying request code.
+
**Hono** is used as the runtime framework. The generated virtual module creates a Hono app with file-based routes and middleware. Route handlers receive a Hono `Context` object (`c`) and can use the full Hono API (`c.json()`, `c.text()`, `c.req.param()`, etc.) plus the Hono middleware ecosystem.
- `defineHandler(fn)` — wraps a route handler `(c: CloudContext) => R`, returns `TypedHandler<{}, R>`
diff --git a/packages/void/src/pages/protocol.ts b/packages/void/src/pages/protocol.ts
index 0f90a3deb..ba8ff3b98 100644
--- a/packages/void/src/pages/protocol.ts
+++ b/packages/void/src/pages/protocol.ts
@@ -1,5 +1,6 @@
import { PAGES_PROTOCOL_VERSION } from '@void/edge';
import type { Context } from 'hono';
+import type { HTTPException } from 'hono/http-exception';
import { executeComposedHandler, getHandlerComposition } from '../runtime/handler-composition.ts';
import type { CloudBindings, CloudContext } from '../runtime/handler.ts';
import { isConvertedResponse } from '../runtime/response.ts';
@@ -103,6 +104,7 @@ function legacyPageResponse(
c: CloudContext,
errors: Record | null = null,
): Response {
+ c.header('Cache-Control', 'no-store');
const headers = pageResponseHeaders(c, {
'Content-Type': errors ? 'application/json' : 'text/plain; charset=utf-8',
'Cache-Control': 'no-store',
@@ -290,6 +292,11 @@ async function renderPageHtml(
function pageJsonResponse(c: CloudContext, pageObj: PageObject): Response {
return new Response(serializePageData(pageObj), {
+ // Hono's lazy c.res getter ignores the private status set by c.status().
+ // Use its response factory to read that status without changing our header merging.
+ // https://github.com/honojs/hono/blob/v4.13.17/src/context.ts#L403-L407
+ // https://github.com/honojs/hono/blob/v4.13.17/src/context.ts#L652-L659
+ status: c.newResponse(null).status,
headers: pageResponseHeaders(c, {
'X-VoidPages': 'true',
'X-VoidPages-Version': PAGES_PROTOCOL_VERSION,
@@ -397,6 +404,38 @@ async function runLoader(
});
}
+function isRecoverableHttpException(error: unknown): error is HTTPException {
+ // Application middleware may use a different installed copy of Hono.
+ const exception = error as HTTPException | null | undefined;
+ return (
+ !!exception &&
+ typeof exception.getResponse === 'function' &&
+ !exception.res &&
+ typeof exception.status === 'number' &&
+ exception.status >= 400 &&
+ exception.status < 600
+ );
+}
+
+/** Recover HTTP failures outside the loader leaf without executing application code again. */
+export async function recoverPageHttpException(
+ error: unknown,
+ c: PageRequestContext,
+ renderFn: RenderPage,
+ getAssetTags: GetAssetTags,
+ configHead?: HeadConfig,
+ options?: PageRenderOptions,
+): Promise {
+ if (!isRecoverableHttpException(error) || c.finalized) {
+ return undefined;
+ }
+ const context = asCloudContext(c);
+ if (options?.renderMode !== 'island' && needsPageReload(context)) {
+ return legacyPageResponse(context);
+ }
+ return renderPageFailure(context, error, undefined, renderFn, getAssetTags, configHead, options);
+}
+
/** Recover one completed GET operation without replaying its middleware or loader. */
async function renderPageFailure(
c: CloudContext,
@@ -413,7 +452,11 @@ async function renderPageFailure(
'getResponse' in error &&
typeof error.getResponse === 'function';
const explicit = httpError ? (error as { getResponse(): Response }).getResponse() : original;
- if (explicit && (explicit.status !== 404 || explicit.headers.has('Content-Disposition'))) {
+ const recoverHttpError = isRecoverableHttpException(error);
+ if (
+ explicit &&
+ ((!recoverHttpError && explicit.status !== 404) || explicit.headers.has('Content-Disposition'))
+ ) {
return undefined;
}
const notFound = explicit?.status === 404;
@@ -426,19 +469,22 @@ async function renderPageFailure(
if (!candidates?.[0]) {
return undefined;
}
+ c.header('Cache-Control', 'no-store');
if (options?.renderMode !== 'island') {
varyPagesResponse(c);
}
- let status = notFound ? 404 : 500;
+ let status = recoverHttpError ? error.status : notFound ? 404 : 500;
let message = notFound
? httpError && error instanceof Error
? error.message || 'Not Found'
: 'Not Found'
- : 'Internal Server Error';
- if (!notFound && options?.dev && error instanceof Error) {
+ : recoverHttpError
+ ? error.message || explicit?.statusText || 'HTTP Error'
+ : 'Internal Server Error';
+ if (!notFound && !recoverHttpError && options?.dev && error instanceof Error) {
message = error.message;
}
- if (error && !notFound) {
+ if (error && !notFound && !recoverHttpError) {
console.error(error);
}
const shared = (c.get('shared') as Record | undefined) ?? {};
@@ -465,7 +511,13 @@ async function renderPageFailure(
errors: null,
head: {
...head,
- title: head?.title ?? (status === 404 ? 'Not Found' : 'Internal Server Error'),
+ title:
+ head?.title ??
+ (status === 404
+ ? 'Not Found'
+ : recoverHttpError && status !== 500
+ ? message
+ : 'Internal Server Error'),
},
};
try {
@@ -599,6 +651,11 @@ function renderedResponse(
assetTags?: AssetTags,
): Response {
return new Response(result, {
+ // Hono's lazy c.res getter ignores the private status set by c.status().
+ // Use its response factory to read that status without changing our header merging.
+ // https://github.com/honojs/hono/blob/v4.13.17/src/context.ts#L403-L407
+ // https://github.com/honojs/hono/blob/v4.13.17/src/context.ts#L652-L659
+ status: c.newResponse(null).status,
headers: adjustDevCssCsp(
pageResponseHeaders(c, {
'content-type': contentType,
diff --git a/packages/void/src/router/compile.ts b/packages/void/src/router/compile.ts
index a9860a215..7ec323bfb 100644
--- a/packages/void/src/router/compile.ts
+++ b/packages/void/src/router/compile.ts
@@ -21,7 +21,7 @@ function pushDefaultErrorPageHelpers(lines: Array, options: CompileOptio
lines.push(
`const __errorRenderer = __createErrorRenderer(${JSON.stringify({ dev: options.dev === true, projectRoot: options.projectRoot, homeDir: options.homeDir })});`,
);
- lines.push('app.onError(__errorRenderer.onError);');
+ lines.push('app.onError(__createPageErrorHandler(__matchedRoutes, __errorRenderer.onError));');
}
export type AuthCompileOptions = {
@@ -196,6 +196,11 @@ export function compileRouteTable(
lines.push(
`import { createRoutingRuntime as __createRoutingRuntime, createHttpRouteHandler as __createHttpRouteHandler, createErrorRenderer as __createErrorRenderer, createPageGetHandler as __createPageGetHandler, createStaticNotFound as __createStaticNotFound, createPageMutationHandler as __createPageMutationHandler, createNamedActionMarker as __createNamedActionMarker, markRouteHandler as __markRouteHandler, createPagesFallback as __createPagesFallback, createModuleLoader as __createModuleLoader, createClientAssetTags as __createClientAssetTags, createManifestLoader as __createManifestLoader, createSsrFallback as __createSsrFallback, createAssetNotFound as __createAssetNotFound, buildPrerenderPath as __buildPrerenderPath } from ${JSON.stringify(ROUTING_RUNTIME_PATH)};`,
);
+ if (pages) {
+ lines.push(
+ `import { createPageErrorHandler as __createPageErrorHandler } from ${JSON.stringify(ROUTING_RUNTIME_PATH)};`,
+ );
+ }
if (options.nodeTarget) {
lines.push(`const withRuntimeEnv = (_env, fn) => fn();`);
} else {
@@ -306,11 +311,13 @@ export function compileRouteTable(
// After routing rules, which ignore the action key as platform dispatch does,
// and before auth and user middleware, so their c.redirect() drops it too.
- if (pages && scan.pages.pages.some((page) => page.methods.includes('actions'))) {
+ if (pages) {
lines.push(
`import { matchedRoutes as __matchedRoutes } from ${JSON.stringify(options.honoRoutePath)};`,
);
- lines.push('app.use(__createNamedActionMarker(__matchedRoutes));');
+ if (scan.pages.pages.some((page) => page.methods.includes('actions'))) {
+ lines.push('app.use(__createNamedActionMarker(__matchedRoutes));');
+ }
}
const headerRules = emitEdgeRules ? options.headerRules : undefined;
@@ -1144,7 +1151,8 @@ function compilePageRoutes(
const trailingCatchAll = options.trailingSlash && page.catchAll;
const pattern = trailingCatchAll ? page.pattern.slice(0, -1) : page.pattern;
const catchAllParam = page.pattern.match(/:([^/{}]+)\{\.\+\}\/$/)?.[1];
- const routeOptions = `{ ${serverImportPath ? 'load: _pload_' + safeId + ', ' : ''}${mdHeadRef ? 'head: ' + mdHeadRef + ', ' : ''}${trailingCatchAll ? `trailingSlash: true, catchAllParam: ${JSON.stringify(catchAllParam)}, ` : ''}componentId: ${JSON.stringify(page.componentId)}, render: ${renderer}, assets: ${assetTags}, headConfig: __voidHeadConfig, renderOptions: ${JSON.stringify({ renderMode: page.renderMode, ...(options.dev === true ? { dev: true } : {}), ...(boundaryManifest.pages[page.componentId] && { boundaries: boundaryManifest.pages[page.componentId] }) })} }`;
+ const middlewareBoundaries = `middlewareBoundaries: ${JSON.stringify({ notFound: boundaryManifest.root, errors: (boundaryManifest.pages[page.componentId]?.errors ?? []).filter((id) => !boundaryManifest.plans[id].directory) })}, `;
+ const routeOptions = `{ ${serverImportPath ? 'load: _pload_' + safeId + ', ' : ''}${mdHeadRef ? 'head: ' + mdHeadRef + ', ' : ''}${trailingCatchAll ? `trailingSlash: true, catchAllParam: ${JSON.stringify(catchAllParam)}, ` : ''}${middlewareBoundaries}componentId: ${JSON.stringify(page.componentId)}, render: ${renderer}, assets: ${assetTags}, headConfig: __voidHeadConfig, renderOptions: ${JSON.stringify({ renderMode: page.renderMode, ...(options.dev === true ? { dev: true } : {}), ...(boundaryManifest.pages[page.componentId] && { boundaries: boundaryManifest.pages[page.componentId] }) })} }`;
lines.push(`app.get(${JSON.stringify(pattern)}, __createPageGetHandler(${routeOptions}));`);
if (page.methods.includes('action') || page.methods.includes('actions')) {
lines.push(
diff --git a/packages/void/src/runtime/routing-pages.ts b/packages/void/src/runtime/routing-pages.ts
index f912ed21b..0cafe8112 100644
--- a/packages/void/src/runtime/routing-pages.ts
+++ b/packages/void/src/runtime/routing-pages.ts
@@ -5,7 +5,7 @@ import {
searchWithoutPreservedQuery,
} from '../pages/action-key.ts';
import { handlesPageLoaderNotFound } from '../pages/not-found-context.ts';
-import { handlePageGet, handlePageMutation } from '../pages/protocol.ts';
+import { handlePageGet, handlePageMutation, recoverPageHttpException } from '../pages/protocol.ts';
import { decodePagePath } from '../pages/url.ts';
import type { ClientAssetTags } from './routing-assets.ts';
import type { ErrorRenderer } from './routing-errors.ts';
@@ -28,6 +28,7 @@ export type PageRouteOptions = {
assets: PageGetArguments[5];
headConfig: PageGetArguments[6] | null;
renderOptions: PageGetArguments[7];
+ middlewareBoundaries?: NonNullable['boundaries'];
namedActions?: boolean;
trailingSlash?: boolean;
catchAllParam?: string;
@@ -59,11 +60,61 @@ function actionNameOf(request: Request): string | undefined {
}
const routeHandlers = new WeakSet();
const namedActionHandlers = new WeakSet();
+const pageGetOptions = new WeakMap();
/** Records a handler that answers a route, so the marker can tell it from middleware. */
export function markRouteHandler(handler: T): T {
routeHandlers.add(handler);
return handler;
}
+
+/** Hono catches middleware throws itself, before the page endpoint can run. */
+export function createPageErrorHandler(
+ matchedRoutes: (c: Context) => Array<{ handler: Function }>,
+ fallback: ErrorRenderer['onError'],
+): ErrorRenderer['onError'] {
+ const recovering = new WeakSet();
+ return async (error, c) => {
+ if ((c.req.method !== 'GET' && c.req.method !== 'HEAD') || recovering.has(c)) {
+ return fallback(error, c);
+ }
+ // Use the first endpoint, not a broad Pages pattern that follows an API route.
+ const routes = matchedRoutes(c);
+ const endpointIndex = routes.findIndex((route) => routeHandlers.has(route.handler));
+ const options =
+ endpointIndex < 0 ? undefined : pageGetOptions.get(routes[endpointIndex].handler);
+ // The endpoint already tried its nearest boundary; do not retry it as a middleware failure.
+ if (c.req.routeIndex >= endpointIndex) {
+ return fallback(error, c);
+ }
+ if (!options || (options.trailingSlash && !c.req.path.endsWith('/'))) {
+ return fallback(error, c);
+ }
+ recovering.add(c);
+ const routeIndex = c.req.routeIndex;
+ try {
+ // Params are scoped to Hono's current route, which is still the failed middleware.
+ c.req.routeIndex = endpointIndex;
+ const response = await recoverPageHttpException(
+ error,
+ c,
+ options.render,
+ options.assets,
+ options.headConfig ?? undefined,
+ { ...options.renderOptions, boundaries: options.middlewareBoundaries ?? { errors: [] } },
+ );
+ return response ?? fallback(error, c);
+ } catch (failure) {
+ return fallback(
+ failure instanceof Error
+ ? failure
+ : new Error('Unable to render Pages error boundary', { cause: failure }),
+ c,
+ );
+ } finally {
+ c.req.routeIndex = routeIndex;
+ }
+ };
+}
/**
* Runs before auth and user middleware, so a c.redirect() there leaves the
* action key out too. It marks the request only when a page with named actions
@@ -86,7 +137,7 @@ export function createNamedActionMarker(
};
}
export function createPageGetHandler(options: PageRouteOptions): (c: Context) => Promise {
- return markRouteHandler(async (context: Context) => {
+ const handler = markRouteHandler(async (context: Context) => {
if (!preparePageRequest(context, options)) {
return context.notFound();
}
@@ -100,17 +151,34 @@ export function createPageGetHandler(options: PageRouteOptions): (c: Context) =>
},
};
}
- return handlePageGet(
- mod.loader,
- mod.head,
- context as PageGetArguments[2],
- options.componentId,
- options.render,
- options.assets,
- options.headConfig ?? undefined,
- options.renderOptions,
- );
+ try {
+ return await handlePageGet(
+ mod.loader,
+ mod.head,
+ context as PageGetArguments[2],
+ options.componentId,
+ options.render,
+ options.assets,
+ options.headConfig ?? undefined,
+ options.renderOptions,
+ );
+ } catch (error) {
+ const response = await recoverPageHttpException(
+ error,
+ context,
+ options.render,
+ options.assets,
+ options.headConfig ?? undefined,
+ options.renderOptions,
+ );
+ if (response) {
+ return response;
+ }
+ throw error;
+ }
});
+ pageGetOptions.set(handler, options);
+ return handler;
}
/** Build-only rendering bypasses application middleware and serializes no request data. */
diff --git a/packages/void/src/runtime/routing.ts b/packages/void/src/runtime/routing.ts
index 81dcc5354..49826fe66 100644
--- a/packages/void/src/runtime/routing.ts
+++ b/packages/void/src/runtime/routing.ts
@@ -573,6 +573,7 @@ export function createHttpRouteHandler(
export {
createPageGetHandler,
+ createPageErrorHandler,
createStaticNotFound,
createPageMutationHandler,
createPagesFallback,
diff --git a/packages/void/test/integration/http-errors-playground.test.ts b/packages/void/test/integration/http-errors-playground.test.ts
new file mode 100644
index 000000000..3f7a9bb6f
--- /dev/null
+++ b/packages/void/test/integration/http-errors-playground.test.ts
@@ -0,0 +1,370 @@
+import { join } from 'node:path';
+import spawn from 'cross-spawn';
+import type { Browser, Page } from 'playwright';
+import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vite-plus/test';
+import {
+ createIsolatedPlayground,
+ killChild,
+ startServer,
+ type IsolatedPlayground,
+ type StartedServer,
+} from '../../../../test/integration-helpers.ts';
+import { launchPagesBrowser } from '../../../../test/pages-browser.ts';
+import { PAGES_PROTOCOL_VERSION } from '../../../edge/src/pages-protocol.ts';
+import { deserializePageData } from '../../src/pages/serialize.ts';
+
+const repository = join(import.meta.dirname, '../../../..');
+const pagesHeaders = {
+ 'X-VoidPages': 'true',
+ 'X-VoidPages-Version': PAGES_PROTOCOL_VERSION,
+};
+const renderedExamples = [
+ {
+ path: '/loaders/status/403',
+ status: 403,
+ heading: 'Status set in a loader',
+ message: 'Your account is banned',
+ retryAfter: '120',
+ },
+ {
+ path: '/loaders/status/503',
+ status: 503,
+ heading: 'Status set in a loader',
+ message: 'Down for maintenance',
+ retryAfter: '120',
+ },
+ {
+ path: '/loaders/validated',
+ status: 503,
+ heading: 'Status set in a validated loader',
+ message: 'Down for maintenance',
+ retryAfter: null,
+ },
+ {
+ path: '/loaders/deferred',
+ status: 429,
+ heading: 'Status set before deferred streaming',
+ message: 'Too many requests',
+ retryAfter: '120',
+ },
+ {
+ path: '/middleware/application',
+ status: 503,
+ heading: 'Status set in application middleware',
+ message: 'Down for maintenance',
+ retryAfter: '120',
+ },
+ {
+ path: '/middleware/handler',
+ status: 403,
+ heading: 'Status set in handler middleware',
+ message: 'Your account is banned',
+ retryAfter: null,
+ },
+ {
+ path: '/loaders/unexpected',
+ status: 500,
+ heading: 'Root error boundary',
+ message: 'Internal Server Error',
+ retryAfter: null,
+ },
+ {
+ path: '/loaders/not-found',
+ status: 404,
+ heading: 'Not-found boundary',
+ message: 'This page does not exist',
+ retryAfter: null,
+ },
+ ...(
+ [
+ { status: 401, message: 'Please sign in' },
+ { status: 403, message: 'Your account is banned' },
+ { status: 410, message: 'This page is permanently gone' },
+ { status: 429, message: 'Too many requests' },
+ { status: 503, message: 'Down for maintenance' },
+ ] as const
+ ).map(({ status, message }) => ({
+ path: `/loaders/errors/${status}`,
+ status,
+ heading: 'Nested error boundary',
+ message,
+ retryAfter: null,
+ })),
+ ...(
+ [
+ { status: 403, message: 'Your account is banned' },
+ { status: 503, message: 'Down for maintenance' },
+ ] as const
+ ).map(({ status, message }) => ({
+ path: `/middleware/errors/${status}`,
+ status,
+ heading: 'Root error boundary',
+ message,
+ retryAfter: null,
+ })),
+] as const;
+
+type PageData = {
+ component: string;
+ props: Record;
+ url: string;
+ deferredKeys?: Record;
+};
+
+describe.each(['dev', 'production'] as const)('HTTP errors playground (%s)', (mode) => {
+ let playground: IsolatedPlayground;
+ let server: StartedServer;
+ let browser: Browser;
+ let base: string;
+
+ function expectedMessage(example: (typeof renderedExamples)[number]) {
+ return mode === 'dev' && example.path === '/loaders/unexpected'
+ ? 'Unexpected loader failure'
+ : example.message;
+ }
+
+ beforeAll(async () => {
+ playground = createIsolatedPlayground(repository, 'playground/http-errors');
+ if (mode === 'production') {
+ const build = spawn.sync('vp', ['build'], {
+ cwd: playground.root,
+ encoding: 'utf8',
+ timeout: 60_000,
+ maxBuffer: 10 * 1024 * 1024,
+ env: { ...process.env, NODE_ENV: 'production' },
+ });
+ expect(build.status, `${build.stdout ?? ''}${build.stderr ?? ''}`).toBe(0);
+ }
+ server = await startServer(
+ (port) =>
+ mode === 'dev'
+ ? spawn('vp', ['dev', '--host', '127.0.0.1', '--port', String(port), '--strictPort'], {
+ cwd: playground.root,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ })
+ : spawn(process.execPath, ['dist/ssr/index.js'], {
+ cwd: playground.root,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ env: { ...process.env, PORT: String(port), NODE_ENV: 'production' },
+ }),
+ 30_000,
+ { expectBody: 'HTTP Errors Example' },
+ );
+ base = server.base;
+ browser = await launchPagesBrowser();
+ });
+
+ afterAll(async () => {
+ await browser?.close();
+ if (server) {
+ await killChild(server.child);
+ }
+ playground?.cleanup();
+ });
+
+ for (const representation of ['HTML', 'Pages'] as const) {
+ it.each(renderedExamples)(
+ `${representation}: $path renders $heading with HTTP $status`,
+ async (example) => {
+ const pages = representation === 'Pages';
+ const response = await fetch(base + example.path, { headers: pages ? pagesHeaders : {} });
+ expect(response.status).toBe(example.status);
+ expect(response.headers.get('Retry-After')).toBe(example.retryAfter);
+ const body = await response.text();
+ if (!pages) {
+ expect(response.headers.get('content-type')).toContain('text/html');
+ expect(body).toContain(example.heading);
+ expect(body).toContain(expectedMessage(example));
+ expect(body).toContain('aria-label="Example sections"');
+ expect(body).toContain('example-page-loaded-toast');
+ if (example.path === '/loaders/deferred') {
+ expect(body).toContain('Waiting one second for deferred props');
+ expect(body).toContain('window.__resolveDeferred("details"');
+ }
+ } else {
+ expect(response.headers.get('X-VoidPages')).toBe('true');
+ const lines = body.trim().split('\n');
+ const data = deserializePageData(lines[0]);
+ expect(data.url).toBe(example.path);
+ if (example.path === '/loaders/deferred') {
+ expect(response.headers.get('content-type')).toContain('application/x-ndjson');
+ expect(data.deferredKeys).toEqual({ details: 'details' });
+ expect(lines).toHaveLength(2);
+ expect(deserializePageData(lines[1])).toMatchObject({
+ __deferred: 'details',
+ data: example.message,
+ });
+ } else {
+ expect(response.headers.get('content-type')).toContain('application/json');
+ expect(data.props).toMatchObject(
+ example.path.includes('/errors/') || example.status === 404 || example.status === 500
+ ? { error: { status: example.status, message: expectedMessage(example) } }
+ : { status: example.status, message: example.message },
+ );
+ }
+ }
+ if (example.path.startsWith('/middleware/errors/')) {
+ expect(server.output()).not.toContain('Middleware-error page loader ran');
+ }
+ },
+ );
+ }
+
+ describe('browser navigation', () => {
+ let page: Page;
+
+ beforeEach(async () => {
+ page = await browser.newPage();
+ page.setDefaultTimeout(10_000);
+ });
+ afterEach(async () => {
+ await page?.close();
+ });
+
+ async function openHome() {
+ await page.goto(base);
+ // The layout effect is also the example's observable hydration signal.
+ await page.waitForSelector('.example-page-loaded-toast:popover-open');
+ await page.evaluate(() => Reflect.set(globalThis, '__HTTP_ERRORS_DOCUMENT__', true));
+ await page
+ .locator('header')
+ .evaluate((element) => element.setAttribute('data-retained', 'yes'));
+ }
+
+ for (const navigation of ['Document load', 'Client navigation'] as const) {
+ it.each(renderedExamples)(
+ `${navigation}: $path renders $heading with HTTP $status`,
+ async (example) => {
+ await openHome();
+ const responsePromise = page.waitForResponse(
+ (response) =>
+ response.url() === base + example.path &&
+ (navigation === 'Document load'
+ ? response.request().isNavigationRequest()
+ : response.request().headers()['x-voidpages'] === 'true'),
+ );
+ await page
+ .locator(`a[href="${example.path}"]`)
+ .getByText(navigation, { exact: true })
+ .click();
+ const response = await responsePromise;
+ expect(response.status()).toBe(example.status);
+ expect(response.headers()['retry-after'] ?? null).toBe(example.retryAfter);
+ if (navigation === 'Document load') {
+ expect(response.headers()['content-type']).toContain('text/html');
+ await page.waitForSelector('.example-page-loaded-toast:popover-open');
+ } else {
+ expect(response.headers()['x-voidpages']).toBe('true');
+ }
+ await expect.poll(() => page.locator('main h1').textContent()).toBe(example.heading);
+ await expect
+ .poll(() => page.locator('main').textContent(), { timeout: 10_000 })
+ .toContain(expectedMessage(example));
+ expect(await page.locator('header').count()).toBe(1);
+ if (example.path.startsWith('/loaders/errors/')) {
+ expect(
+ await page
+ .getByText('Nested layout for HTTP exception examples.', { exact: true })
+ .count(),
+ ).toBe(1);
+ expect(
+ await page
+ .getByRole('heading', { name: 'Loader exception example', exact: true })
+ .count(),
+ ).toBe(0);
+ }
+ if (example.path.startsWith('/middleware/errors/')) {
+ expect(server.output()).not.toContain('Middleware-error page loader ran');
+ expect(
+ await page
+ .getByRole('heading', { name: 'Middleware exception example', exact: true })
+ .count(),
+ ).toBe(0);
+ }
+ const retained = navigation === 'Client navigation';
+ expect(
+ await page.evaluate(() => Reflect.get(globalThis, '__HTTP_ERRORS_DOCUMENT__')),
+ ).toBe(retained ? true : undefined);
+ expect(await page.locator('header').getAttribute('data-retained')).toBe(
+ retained ? 'yes' : null,
+ );
+ },
+ );
+ }
+
+ it('offers document and client navigation for every example', async () => {
+ await openHome();
+ const paths = renderedExamples.map(({ path }) => path).sort();
+ for (const name of ['Document load', 'Client navigation']) {
+ const links = await page.getByRole('link', { name, exact: true }).all();
+ const targets = await Promise.all(links.map((link) => link.getAttribute('href')));
+ expect(targets.sort()).toEqual(paths);
+ }
+ });
+
+ it.each(['/loaders/errors/403', '/middleware/errors/403'])(
+ 'navigates away from %s and restores it through history without reloading',
+ async (path) => {
+ await openHome();
+ const heading = path.startsWith('/loaders/')
+ ? 'Nested error boundary'
+ : 'Root error boundary';
+ await page
+ .locator(`a[href="${path}"]`)
+ .getByText('Client navigation', { exact: true })
+ .click();
+ await expect.poll(() => page.locator('main h1').textContent()).toBe(heading);
+ await page.getByRole('link', { name: 'HTTP Errors Example', exact: true }).click();
+ await expect.poll(() => page.locator('main h1').textContent()).toBe('HTTP Errors Example');
+ await page.goBack();
+ await expect.poll(() => page.locator('main h1').textContent()).toBe(heading);
+ await page.goForward();
+ await expect.poll(() => page.locator('main h1').textContent()).toBe('HTTP Errors Example');
+ expect(await page.locator('header').getAttribute('data-retained')).toBe('yes');
+ expect(await page.evaluate(() => Reflect.get(globalThis, '__HTTP_ERRORS_DOCUMENT__'))).toBe(
+ true,
+ );
+ },
+ );
+
+ it.each(['hover', 'focus'] as const)(
+ 'prefetches complete deferred props on %s without navigating',
+ async (interaction) => {
+ await openHome();
+ const responsePromise = page.waitForResponse(
+ (response) =>
+ response.url() === base + '/loaders/deferred' &&
+ response.request().headers().purpose === 'prefetch',
+ );
+ const link = page.getByRole('link', { name: 'Hover or focus to prefetch deferred data' });
+ await link[interaction]();
+ const response = await responsePromise;
+ expect(response.status()).toBe(429);
+ expect(response.headers()['retry-after']).toBe('120');
+ expect(response.headers()['content-type']).toContain('application/json');
+ expect(response.request().headers()['x-voidpages-version']).toBe(PAGES_PROTOCOL_VERSION);
+ expect(deserializePageData(await response.text())).toMatchObject({
+ props: { details: 'Too many requests' },
+ });
+ expect(page.url()).toBe(base + '/');
+ expect(await page.locator('header').getAttribute('data-retained')).toBe('yes');
+ },
+ );
+
+ it('shows the document-load toast once and does not reopen it on client navigation', async () => {
+ await openHome();
+ await page.waitForSelector('.example-page-loaded-toast:popover-open', { state: 'hidden' });
+ await page
+ .locator('a[href="/loaders/status/403"]')
+ .getByText('Client navigation', { exact: true })
+ .click();
+ await expect.poll(() => page.locator('main h1').textContent()).toBe('Status set in a loader');
+ expect(await page.locator('.example-page-loaded-toast:popover-open').count()).toBe(0);
+ expect(await page.locator('header').getAttribute('data-retained')).toBe('yes');
+ await page.getByRole('link', { name: 'HTTP Errors Example', exact: true }).click();
+ await expect.poll(() => page.locator('main h1').textContent()).toBe('HTTP Errors Example');
+ expect(await page.locator('.example-page-loaded-toast:popover-open').count()).toBe(0);
+ expect(await page.title()).toBe('HTTP Errors Example');
+ });
+ });
+});
diff --git a/packages/void/test/integration/page-errors.test.ts b/packages/void/test/integration/page-errors.test.ts
index 5e93e8403..ebb549838 100644
--- a/packages/void/test/integration/page-errors.test.ts
+++ b/packages/void/test/integration/page-errors.test.ts
@@ -82,6 +82,21 @@ describe.each(frameworks)('Pages errors with %s', (framework) => {
`import { defineMiddleware } from 'void/handler'; export default defineMiddleware(async (c,next)=>{c.set('shared',{viewer:'alice',...(c.req.header('X-Test-No-Root') ? { noRoot: true } : {})}); c.header('X-Security','retained'); const guard=c.req.header('X-Test-Guard'); if(guard==='not-found') return c.notFound(); if(guard==='explicit') return c.text('Hidden by guard',404); await next(); if(guard==='after-not-found') c.res=await c.notFound();});`,
);
write('pages/style.css', 'nav { color: rgb(20, 80, 100); }');
+ write(
+ 'middleware/status.ts',
+ `import { defineMiddleware } from 'void/handler';
+import { HTTPException } from 'hono/http-exception';
+export default defineMiddleware(async (c, next) => {
+ const status = c.req.header('X-Test-Error-Status');
+ if (status === '403' || status === '503') {
+ c.header('Retry-After', '120');
+ throw new HTTPException(status === '403' ? 403 : 503, {
+ message: status === '403' ? 'Your account is banned' : 'Down for maintenance',
+ });
+ }
+ await next();
+});`,
+ );
const nav =
' Home Returned Thrown Unexpected Crash Nested Deep Layout Client only ';
let rootLayout = component(nav, '', true);
@@ -119,6 +134,16 @@ describe.each(frameworks)('Pages errors with %s', (framework) => {
write(`pages/_error.${extension}`, component('Error page {{error.message}}
'));
}
write(`pages/items/[id].${extension}`, component('Item '));
+ write(`pages/status/[code].${extension}`, component('Status page '));
+ write(
+ 'pages/status/[code].server.ts',
+ `import { defineHandler } from 'void/handler';
+export const loader = defineHandler(c => {
+ c.status(c.req.param('code') === '403' ? 403 : 503);
+ c.header('Retry-After', '120');
+ return {};
+});`,
+ );
for (const name of ['falsy', 'island-falsy.island']) {
write(`pages/${name}.${extension}`, component('Broken view ', 'throw undefined;'));
}
@@ -295,6 +320,46 @@ describe.each(frameworks)('Pages errors with %s', (framework) => {
expect(await response.text()).toContain('Missing island page');
});
+ it.each(
+ ([403, 503] as const).flatMap((status) =>
+ (['HTML', 'Pages JSON'] as const).map((format) => ({ status, format })),
+ ),
+ )(
+ 'renders middleware errors through the root page boundary with HTTP $status ($format)',
+ async ({ status, format }) => {
+ const message = status === 403 ? 'Your account is banned' : 'Down for maintenance';
+ // This route throws a different error if its loader is reached.
+ const response = await fetch(base + '/items/unexpected', {
+ headers: {
+ 'X-Test-Error-Status': String(status),
+ ...(format === 'Pages JSON'
+ ? { 'X-VoidPages': 'true', 'X-VoidPages-Version': PAGES_PROTOCOL_VERSION }
+ : { Accept: 'text/html' }),
+ },
+ });
+ const body = await response.text();
+ expect(response.status).toBe(status);
+ expect(body).not.toContain('SECRET_');
+ if (format === 'Pages JSON') {
+ expect(response.headers.get('X-VoidPages')).toBe('true');
+ expect(deserializePageData(body)).toMatchObject({
+ props: { error: { status, message } },
+ shared: { viewer: 'alice' },
+ url: '/items/unexpected',
+ });
+ } else {
+ expect(response.headers.get('Content-Type')).toContain('text/html');
+ expect(body).toContain('Error page');
+ expect(body).toContain(message);
+ expect(body).toContain('data-layout="root"');
+ expect(body).toContain('stylesheet');
+ }
+ expect(response.headers.get('Retry-After')).toBe('120');
+ expect(response.headers.get('X-Security')).toBe('retained');
+ expect(response.headers.get('Cache-Control')).toBe('no-store');
+ },
+ );
+
it('returns status-correct documents and marked error payloads without exposing secrets', async () => {
for (const [path, status] of [
['/unknown', 404],
@@ -344,6 +409,39 @@ describe.each(frameworks)('Pages errors with %s', (framework) => {
expect(response.status).toBe(404);
expect(await response.text()).toBe('API missing');
});
+ it('renders application-defined error statuses on document loads and client visits', async () => {
+ const page = await browser.newPage();
+ page.setDefaultTimeout(8_000);
+ try {
+ for (const status of [403, 503]) {
+ const path = `/status/${status}`;
+ const documentResponse = await fetch(base + path);
+ expect(documentResponse.status).toBe(status);
+ expect(documentResponse.headers.get('Retry-After')).toBe('120');
+ expect(await documentResponse.text()).toContain('Status page');
+
+ await page.goto(base);
+ await page.waitForSelector('#app[data-hydrated="true"]');
+ await page.evaluate(() =>
+ document.querySelector('nav')!.setAttribute('data-retained', 'yes'),
+ );
+ const navigation = page.waitForResponse(
+ (response) =>
+ response.url() === base + path &&
+ response.request().headers()['x-voidpages'] === 'true',
+ );
+ await page.evaluate(`window.testRouter.visit(${JSON.stringify(path)})`);
+ const response = await navigation;
+ expect(response.status()).toBe(status);
+ expect(response.headers()['x-voidpages']).toBe('true');
+ await expect.poll(() => page.locator('h1').textContent()).toBe('Status page');
+ expect(await page.locator('nav').getAttribute('data-retained')).toBe('yes');
+ }
+ } finally {
+ await page.close();
+ }
+ });
+
it('keeps the app and outer layout mounted across 404s and client errors', async () => {
const page = await browser.newPage();
const errors: Array = [];
diff --git a/packages/void/test/unit/page-boundaries.test.ts b/packages/void/test/unit/page-boundaries.test.ts
index 3609bad53..28b549767 100644
--- a/packages/void/test/unit/page-boundaries.test.ts
+++ b/packages/void/test/unit/page-boundaries.test.ts
@@ -167,6 +167,106 @@ describe('boundary render plans', () => {
});
describe('Pages loader boundary protocol', () => {
+ describe.each(['HTML', 'Pages JSON'] as const)(
+ 'HTTP exception recovery through page boundaries (%s)',
+ (format) => {
+ it.each([
+ [401, 'Please sign in'],
+ [403, 'Your account is banned'],
+ [410, 'This item was deleted'],
+ [429, 'Too many requests'],
+ [503, 'Down for maintenance'],
+ ] as const)('renders HTTP %s through the nearest error boundary', async (status, message) => {
+ const app = new Hono();
+ app.onError(createErrorRenderer({ projectRoot: '/project', homeDir: '/home' }).onError);
+ let loaderCalls = 0;
+ app.get(
+ '/items/1',
+ createPageGetHandler({
+ componentId: 'items/item',
+ load: async () => ({
+ loader: defineHandler((c) => {
+ loaderCalls++;
+ c.header('Retry-After', '120');
+ throw new HTTPException(status, { message });
+ }),
+ }),
+ render: async (page) =>
+ `${JSON.stringify(page.props)} `,
+ assets: async () => ({ css: '', body: '', preloads: '' }),
+ headConfig: null,
+ renderOptions: {
+ boundaries: { notFound: 'not-found-plan', errors: ['items-error', 'root-error'] },
+ },
+ }),
+ );
+ const response = await app.request('https://app.test/items/1', {
+ headers:
+ format === 'Pages JSON'
+ ? { 'X-VoidPages': 'true', 'X-VoidPages-Version': PAGES_PROTOCOL_VERSION }
+ : { Accept: 'text/html' },
+ });
+ const body = await response.text();
+ expect(loaderCalls).toBe(1);
+ expect(response.status).toBe(status);
+ if (format === 'Pages JSON') {
+ expect(response.headers.get('X-VoidPages')).toBe('true');
+ expect(deserializePageData(body)).toMatchObject({
+ component: 'items-error',
+ props: { error: { status, message } },
+ url: '/items/1',
+ });
+ } else {
+ expect(response.headers.get('Content-Type')).toContain('text/html');
+ expect(body).toContain('data-component="items-error"');
+ expect(body).toContain(JSON.stringify({ error: { status, message } }));
+ }
+ expect(response.headers.get('Retry-After')).toBe('120');
+ expect(response.headers.get('Cache-Control')).toBe('no-store');
+ });
+
+ it.each([302, 503] as const)(
+ 'passes through HTTP %s with an explicit response',
+ async (status) => {
+ const app = new Hono();
+ app.onError(createErrorRenderer({ projectRoot: '/project', homeDir: '/home' }).onError);
+ app.get(
+ '/',
+ createPageGetHandler({
+ componentId: 'index',
+ load: async () => ({
+ loader: () => {
+ throw new HTTPException(status, {
+ res: new Response('Explicit response', {
+ status,
+ headers: status === 302 ? { Location: '/login' } : { 'Retry-After': '120' },
+ }),
+ });
+ },
+ }),
+ render: async () => 'Error boundary ',
+ assets: async () => ({ css: '', body: '', preloads: '' }),
+ headConfig: null,
+ renderOptions: { boundaries: { errors: ['root-error'] } },
+ }),
+ );
+ const response = await app.request('https://app.test/', {
+ headers:
+ format === 'Pages JSON'
+ ? { 'X-VoidPages': 'true', 'X-VoidPages-Version': PAGES_PROTOCOL_VERSION }
+ : { Accept: 'text/html' },
+ });
+ expect(response.status).toBe(status);
+ expect(response.headers.has('X-VoidPages')).toBe(false);
+ expect(response.headers.get(status === 302 ? 'Location' : 'Retry-After')).toBe(
+ status === 302 ? '/login' : '120',
+ );
+ expect(await response.text()).toBe('Explicit response');
+ },
+ );
+ },
+ );
+
it.each(['returned', 'thrown', 'empty-message', 'unexpected'])(
'renders %s failures as marked, uncached Pages data',
async (kind) => {
diff --git a/packages/void/test/unit/page-http-errors.test.ts b/packages/void/test/unit/page-http-errors.test.ts
new file mode 100644
index 000000000..c3e4e54fa
--- /dev/null
+++ b/packages/void/test/unit/page-http-errors.test.ts
@@ -0,0 +1,337 @@
+import { Hono, type Context } from 'hono';
+import { HTTPException } from 'hono/http-exception';
+import { matchedRoutes } from 'hono/route';
+import { describe, expect, it, vi } from 'vite-plus/test';
+import { PAGES_PROTOCOL_VERSION } from '../../../edge/src/pages-protocol.ts';
+import { deserializePageData } from '../../src/pages/serialize.ts';
+import { defineHandler, defineMiddleware } from '../../src/runtime/handler.ts';
+import { createErrorRenderer } from '../../src/runtime/routing-errors.ts';
+import {
+ createPageErrorHandler,
+ createPageGetHandler,
+ createPageMutationHandler,
+ type PageRouteOptions,
+} from '../../src/runtime/routing-pages.ts';
+import { createHttpRouteHandler } from '../../src/runtime/routing.ts';
+
+const pagesHeaders = { 'X-VoidPages': 'true', 'X-VoidPages-Version': PAGES_PROTOCOL_VERSION };
+const origins = ['loader', 'handler middleware', 'application middleware'] as const;
+type Origin = (typeof origins)[number];
+
+function fixture({
+ origin = 'loader',
+ fail = () => {
+ throw new HTTPException(403, { message: 'Access denied' });
+ },
+ render = async (page) => `${page.component}:${JSON.stringify(page.props)} `,
+ boundaries = { notFound: 'nested-not-found', errors: ['nested-error', 'root-error'] },
+ middlewareBoundaries = { notFound: 'root-not-found', errors: ['root-error'] },
+}: {
+ origin?: Origin;
+ fail?: (c: Context) => unknown;
+ render?: PageRouteOptions['render'];
+ boundaries?: NonNullable['boundaries'];
+ middlewareBoundaries?: PageRouteOptions['middlewareBoundaries'];
+} = {}) {
+ const app = new Hono<{ Variables: { shared: Record } }>();
+ const fallback = createErrorRenderer({ projectRoot: '/project', homeDir: '/home' }).onError;
+ app.onError(createPageErrorHandler(matchedRoutes, fallback));
+ const loader = vi.fn(() => ({ ok: true }));
+ const moduleLoad = vi.fn();
+ const failure = vi.fn(fail);
+ app.use(async (c, next) => {
+ c.set('shared', { viewer: 'alice' });
+ c.header('X-Security', 'retained');
+ c.header('Set-Cookie', 'session=retained; Path=/');
+ c.header('Retry-After', '120');
+ c.header('Cache-Control', 'public, max-age=3600');
+ if (origin === 'application middleware') {
+ return failure(c) as Response;
+ }
+ await next();
+ });
+ const options: PageRouteOptions = {
+ componentId: 'items/item',
+ load: async () => {
+ moduleLoad();
+ return {
+ loader:
+ origin === 'loader'
+ ? defineHandler((c) => {
+ loader();
+ return failure(c);
+ })
+ : defineHandler(
+ defineMiddleware(async (c) => failure(c) as Response),
+ loader,
+ ),
+ action: (c) => failure(c),
+ };
+ },
+ render,
+ assets: async () => ({ css: '', body: '', preloads: '' }),
+ headConfig: null,
+ renderOptions: { boundaries },
+ middlewareBoundaries,
+ };
+ app.get('/items/:id', createPageGetHandler(options));
+ app.post('/items/:id', createPageMutationHandler(options));
+ // A broad page must not make an API endpoint eligible for recovery.
+ app.get(
+ '/api/profile',
+ createHttpRouteHandler(async () => ({ GET: (c) => failure(c) }), 'GET'),
+ );
+ app.get('/api/*', createPageGetHandler(options));
+ return { app, loader, moduleLoad, failure };
+}
+
+describe.each(origins)('Pages HTTP errors from %s', (origin) => {
+ it.each([false, true])('renders the appropriate boundary (Pages request: %s)', async (pages) => {
+ const { app, loader, moduleLoad, failure } = fixture({ origin });
+ const response = await app.request('/items/123?tab=details', {
+ headers: pages ? pagesHeaders : {},
+ });
+ expect(response.status).toBe(403);
+ expect(response.headers.get('Cache-Control')).toBe('no-store');
+ expect(response.headers.get('X-Security')).toBe('retained');
+ expect(response.headers.get('Retry-After')).toBe('120');
+ expect(response.headers.getSetCookie()).toEqual(['session=retained; Path=/']);
+ const component = origin === 'application middleware' ? 'root-error' : 'nested-error';
+ if (pages) {
+ expect(response.headers.get('X-VoidPages')).toBe('true');
+ expect(deserializePageData(await response.text())).toMatchObject({
+ component,
+ props: { error: { status: 403, message: 'Access denied' } },
+ params: { id: '123' },
+ shared: { viewer: 'alice' },
+ url: '/items/123?tab=details',
+ });
+ } else {
+ expect(response.headers.get('Content-Type')).toContain('text/html');
+ expect(await response.text()).toContain(component);
+ }
+ expect(failure).toHaveBeenCalledTimes(1);
+ expect(loader).toHaveBeenCalledTimes(origin === 'loader' ? 1 : 0);
+ expect(moduleLoad).toHaveBeenCalledTimes(origin === 'application middleware' ? 0 : 1);
+ });
+
+ it.each([302, 503] as const)('preserves an explicit HTTP %s response', async (status) => {
+ const { app } = fixture({
+ origin,
+ fail: () => {
+ throw new HTTPException(status, {
+ res: new Response('Explicit response', {
+ status,
+ headers: {
+ Location: '/login',
+ 'Content-Disposition': 'attachment; filename=error.txt',
+ },
+ }),
+ });
+ },
+ });
+ const response = await app.request('/items/123', { headers: pagesHeaders });
+ expect(response.status).toBe(status);
+ expect(response.headers.has('X-VoidPages')).toBe(false);
+ expect(response.headers.get('Location')).toBe('/login');
+ expect(response.headers.get('Content-Disposition')).toContain('attachment');
+ expect(await response.text()).toBe('Explicit response');
+ });
+
+ it('does not reinterpret a returned response', async () => {
+ const { app } = fixture({ origin, fail: (c) => c.text('Explicit response', 503) });
+ const response = await app.request('/items/123', { headers: pagesHeaders });
+ expect(response.status).toBe(503);
+ expect(response.headers.has('X-VoidPages')).toBe(false);
+ expect(await response.text()).toBe('Explicit response');
+ });
+
+ it('keeps action failures outside GET recovery', async () => {
+ const { app } = fixture({ origin });
+ const response = await app.request('/items/123', { method: 'POST', headers: pagesHeaders });
+ expect(response.status).toBe(403);
+ expect(response.headers.has('X-VoidPages')).toBe(false);
+ expect(await response.text()).toBe('Access denied');
+ });
+
+ it('keeps API exceptions outside Pages recovery', async () => {
+ const { app } = fixture({ origin });
+ const response = await app.request('/api/profile', { headers: pagesHeaders });
+ expect(response.status).toBe(403);
+ expect(response.headers.has('X-VoidPages')).toBe(false);
+ expect(await response.text()).toBe('Access denied');
+ });
+
+ it('returns a bodyless HEAD response with the recovered status', async () => {
+ const { app } = fixture({ origin });
+ const response = await app.request('/items/123', { method: 'HEAD' });
+ expect(response.status).toBe(403);
+ expect(response.headers.get('Content-Type')).toContain('text/html');
+ expect(await response.text()).toBe('');
+ });
+
+ it('keeps unsupported Pages clients on the document fallback', async () => {
+ const { app } = fixture({ origin });
+ const response = await app.request('/items/123', { headers: { 'X-VoidPages': 'true' } });
+ expect(response.status).toBe(200);
+ expect(response.headers.has('X-VoidPages')).toBe(false);
+ expect(response.headers.get('Cache-Control')).toBe('no-store');
+ expect(await response.text()).toBe('');
+ });
+});
+
+describe('Pages HTTP error recovery compatibility', () => {
+ it('recovers HTTP exceptions from a separate Hono constructor', async () => {
+ class OtherHTTPException extends Error {
+ readonly status = 403;
+ getResponse() {
+ return new Response(this.message, { status: this.status });
+ }
+ }
+ const error = new OtherHTTPException('Access denied');
+ expect(error).not.toBeInstanceOf(HTTPException);
+ const { app } = fixture({
+ fail: () => {
+ throw error;
+ },
+ });
+ const response = await app.request('/items/123', { headers: pagesHeaders });
+ expect(response.status).toBe(403);
+ expect(deserializePageData(await response.text())).toMatchObject({
+ component: 'nested-error',
+ props: { error: { status: 403, message: 'Access denied' } },
+ });
+ });
+
+ it('does not render a nested boundary for application middleware without a root boundary', async () => {
+ const render = vi.fn();
+ const { app } = fixture({
+ origin: 'application middleware',
+ middlewareBoundaries: { errors: [] },
+ render,
+ });
+ const response = await app.request('/items/123', { headers: pagesHeaders });
+ expect(response.status).toBe(403);
+ expect(await response.text()).toBe('Access denied');
+ expect(render).not.toHaveBeenCalled();
+ });
+
+ it('does not rerender a completed page after middleware next()', async () => {
+ const app = new Hono();
+ app.onError(
+ createPageErrorHandler(
+ matchedRoutes,
+ createErrorRenderer({ projectRoot: '/project', homeDir: '/home' }).onError,
+ ),
+ );
+ app.use(async (_c, next) => {
+ await next();
+ throw new HTTPException(403, { message: 'After next' });
+ });
+ const render = vi.fn(async () => 'Healthy page ');
+ app.get(
+ '/',
+ createPageGetHandler({
+ componentId: 'index',
+ render,
+ assets: async () => ({ css: '', body: '', preloads: '' }),
+ headConfig: null,
+ renderOptions: { boundaries: { errors: ['nested-error'] } },
+ middlewareBoundaries: { errors: ['root-error'] },
+ }),
+ );
+ const response = await app.request('/');
+ expect(response.status).toBe(403);
+ expect(await response.text()).toBe('After next');
+ expect(render).toHaveBeenCalledTimes(1);
+ });
+
+ it('recovers response-less HTTP exceptions without configuration', async () => {
+ const app = new Hono();
+ app.onError(createErrorRenderer({ projectRoot: '/project', homeDir: '/home' }).onError);
+ app.get(
+ '/',
+ createPageGetHandler({
+ componentId: 'index',
+ load: async () => ({
+ loader: () => {
+ throw new HTTPException(401, { message: 'Unauthorized' });
+ },
+ }),
+ render: async () => 'Error boundary ',
+ assets: async () => ({ css: '', body: '', preloads: '' }),
+ headConfig: null,
+ renderOptions: { boundaries: { errors: ['root-error'] } },
+ }),
+ );
+ const response = await app.request('/', { headers: pagesHeaders });
+ expect(response.status).toBe(401);
+ expect(response.headers.get('X-VoidPages')).toBe('true');
+ expect(deserializePageData(await response.text())).toMatchObject({
+ component: 'root-error',
+ props: { error: { status: 401, message: 'Unauthorized' } },
+ });
+ });
+
+ it('uses the root not-found boundary for an application-middleware 404', async () => {
+ const { app } = fixture({
+ origin: 'application middleware',
+ fail: () => {
+ throw new HTTPException(404);
+ },
+ });
+ const response = await app.request('/items/123', { headers: pagesHeaders });
+ expect(response.status).toBe(404);
+ expect(deserializePageData(await response.text())).toMatchObject({
+ component: 'root-not-found',
+ });
+ });
+
+ it('passes through exceptions when no error boundary exists', async () => {
+ const { app } = fixture({ boundaries: { errors: [] } });
+ const response = await app.request('/items/123', { headers: pagesHeaders });
+ expect(response.status).toBe(403);
+ expect(response.headers.has('X-VoidPages')).toBe(false);
+ expect(await response.text()).toBe('Access denied');
+ });
+
+ it('escalates a failed boundary to an uncached generic 500 without replaying the loader', async () => {
+ const log = vi.spyOn(console, 'error').mockImplementation(() => {});
+ try {
+ const render = vi.fn(async (page) => {
+ if (page.component === 'nested-error') {
+ throw new Error('SECRET');
+ }
+ return `${JSON.stringify(page.props)} `;
+ });
+ const { app, failure } = fixture({ render });
+ const response = await app.request('/items/123');
+ expect(response.status).toBe(500);
+ expect(response.headers.get('Cache-Control')).toBe('no-store');
+ expect(await response.text()).toContain('Internal Server Error');
+ expect(render).toHaveBeenCalledTimes(2);
+ expect(failure).toHaveBeenCalledTimes(1);
+ } finally {
+ log.mockRestore();
+ }
+ });
+
+ it('does not retry a failed application-middleware boundary', async () => {
+ const log = vi.spyOn(console, 'error').mockImplementation(() => {});
+ try {
+ const render = vi.fn(async () => {
+ throw new Error('SECRET');
+ });
+ const { app, moduleLoad, failure } = fixture({ origin: 'application middleware', render });
+ const response = await app.request('/items/123');
+ expect(response.status).toBe(500);
+ expect(response.headers.get('Cache-Control')).toBe('no-store');
+ expect(await response.text()).not.toContain('SECRET');
+ expect(render).toHaveBeenCalledTimes(1);
+ expect(moduleLoad).not.toHaveBeenCalled();
+ expect(failure).toHaveBeenCalledTimes(1);
+ } finally {
+ log.mockRestore();
+ }
+ });
+});
diff --git a/packages/void/test/unit/pages-compile.test.ts b/packages/void/test/unit/pages-compile.test.ts
index 05eabccba..61afe9362 100644
--- a/packages/void/test/unit/pages-compile.test.ts
+++ b/packages/void/test/unit/pages-compile.test.ts
@@ -92,7 +92,7 @@ describe('Pages metadata wiring', () => {
code.includes(
'import { matchedRoutes as __matchedRoutes } from "/resolved/hono/route.mjs";',
),
- ).toBe(marked);
+ ).toBe(true);
if (marked) {
expect(marker).toBeGreaterThan(code.indexOf('__routingRuntime.routingMiddleware('));
expect(marker).toBeLessThan(code.indexOf('app.use(middleware0);'));
@@ -105,6 +105,26 @@ describe('Pages metadata wiring', () => {
expect(prod).toContain('renderOptions: {"renderMode":"server"} }');
expect(prod).not.toContain('"renderMode":"server","dev"');
});
+ it('emits HTTP error recovery for pages without configuration or named actions', () => {
+ const code = compileRouteTable(
+ makeScan({ pages: [{ ...page, methods: ['loader'] }], layouts: [], namedLayouts: [] }),
+ '/routes',
+ '/middleware',
+ '/crons',
+ '/queues',
+ null,
+ testOptions,
+ pagesOptions,
+ );
+ expect(
+ code.includes(
+ 'app.onError(__createPageErrorHandler(__matchedRoutes, __errorRenderer.onError))',
+ ),
+ ).toBe(true);
+ expect(code).toContain('import { matchedRoutes as __matchedRoutes }');
+ expect(code).not.toContain('app.use(__createNamedActionMarker(__matchedRoutes))');
+ expect(parseSync('generated.js', code).errors).toEqual([]);
+ });
it('passes dev CSS metadata and a shared manifest loader to the runtime', () => {
const code = compile();
expect(code).toContain('getVoidPagesDevCss');
@@ -119,7 +139,9 @@ describe('Pages metadata wiring', () => {
expect(code).toContain(
'__createErrorRenderer({"dev":true,"projectRoot":"/project","homeDir":"/Users/test"})',
);
- expect(code).toContain('app.onError(__errorRenderer.onError)');
+ expect(code).toContain(
+ 'app.onError(__createPageErrorHandler(__matchedRoutes, __errorRenderer.onError))',
+ );
expect(code).toContain('app.notFound(__routingRuntime.trackNotFound(__createPagesFallback(');
expect(code).not.toContain('function __voidRenderDevErrorOverlay');
});
diff --git a/packages/void/test/unit/routing-runtime.test.ts b/packages/void/test/unit/routing-runtime.test.ts
index a63ab86d4..ce516144b 100644
--- a/packages/void/test/unit/routing-runtime.test.ts
+++ b/packages/void/test/unit/routing-runtime.test.ts
@@ -1855,6 +1855,112 @@ describe('page loaders and actions composed with defineHandler middleware', () =
expect(await html.text()).toContain('{"notes":["a"]}');
});
+ describe.each(['HTML', 'Pages JSON'] as const)(
+ 'Application-defined page status codes (%s)',
+ (format) => {
+ it.each(
+ ([403, 503] as const).flatMap((status) =>
+ (['loader', 'validated loader', 'app middleware', 'handler middleware'] as const).map(
+ (source) => ({
+ status,
+ source,
+ }),
+ ),
+ ),
+ )('renders the page with HTTP $status set by $source', async ({ status, source }) => {
+ const message = status === 403 ? 'Your account is banned' : 'Down for maintenance';
+ const setStatus = defineMiddleware(async (c, next) => {
+ c.status(status);
+ c.header('Retry-After', '120');
+ await next();
+ });
+ const loader = vi.fn((c: CloudContext) => {
+ if (source === 'loader' || source === 'validated loader') {
+ c.status(status);
+ c.header('Retry-After', '120');
+ }
+ return { message };
+ });
+ const app = createPageApp(
+ {
+ loader:
+ source === 'handler middleware'
+ ? defineHandler(setStatus, loader)
+ : source === 'validated loader'
+ ? defineHandler.withValidator({})(loader)
+ : defineHandler(loader),
+ },
+ async (c, next) => {
+ c.header('Set-Cookie', 'session=renewed; HttpOnly', { append: true });
+ c.header('Set-Cookie', 'locale=en', { append: true });
+ if (source === 'app middleware') {
+ return setStatus(c, next);
+ }
+ await next();
+ },
+ );
+ const response = await get(app, format === 'HTML' ? {} : undefined);
+ const body = await response.text();
+
+ expect(loader).toHaveBeenCalledTimes(1);
+ expect(response.headers.get('Retry-After')).toBe('120');
+ expect(response.headers.getSetCookie()).toEqual(['session=renewed; HttpOnly', 'locale=en']);
+ if (format === 'Pages JSON') {
+ expect(response.headers.get('X-VoidPages')).toBe('true');
+ expect(decodePageData(JSON.parse(body))).toMatchObject({
+ component: 'page',
+ props: { message },
+ });
+ } else {
+ expect(response.headers.get('Content-Type')).toContain('text/html');
+ expect(body).toContain(message);
+ }
+ expect(response.status).toBe(status);
+ });
+
+ it('preserves the loader status while streaming deferred props', async () => {
+ const app = createPageApp({
+ loader: defineHandler((c) => {
+ c.status(429);
+ return { slow: defer(async () => 'Try again later') };
+ }),
+ });
+ const response = await get(app, format === 'HTML' ? {} : undefined);
+ const body = await response.text();
+ expect(response.status).toBe(429);
+ if (format === 'Pages JSON') {
+ expect(response.headers.get('Content-Type')).toBe('application/x-ndjson');
+ expect(response.headers.get('X-VoidPages')).toBe('true');
+ expect(body).toContain(
+ serializePageData({ __deferred: 'slow', data: 'Try again later' }),
+ );
+ } else {
+ expect(body).toContain('__resolveDeferred');
+ expect(body).toContain('Try again later');
+ }
+ });
+ },
+ );
+
+ it('preserves the loader status when prefetching deferred page data', async () => {
+ const app = createPageApp({
+ loader: defineHandler((c) => {
+ c.status(503);
+ return { slow: defer(async () => 'Try again later') };
+ }),
+ });
+ const response = await get(app, {
+ 'X-VoidPages': 'true',
+ 'X-VoidPages-Version': PAGES_PROTOCOL_VERSION,
+ Purpose: 'prefetch',
+ });
+ expect(response.status).toBe(503);
+ expect(response.headers.get('Content-Type')).toBe('application/json');
+ expect(decodePageData(await response.json())).toMatchObject({
+ props: { slow: 'Try again later' },
+ });
+ });
+
it('re-renders the page after an action with middleware, or shows its field errors', async () => {
const action = (message?: string) =>
defineHandler(
diff --git a/playground/http-errors/README.md b/playground/http-errors/README.md
new file mode 100644
index 000000000..5c696e622
--- /dev/null
+++ b/playground/http-errors/README.md
@@ -0,0 +1,18 @@
+# HTTP errors
+
+Explore HTTP statuses, error boundaries, and navigation behavior through React Pages API examples.
+
+From the repository root:
+
+```sh
+vp run --filter ./playground/http-errors dev
+```
+
+Run the HTTP and browser scenarios from the repository root:
+
+```sh
+vp test run packages/void/test/integration/http-errors-playground.test.ts
+```
+
+The suite runs an isolated copy in development and production, covering response statuses,
+error boundaries, document loads, client navigation, deferred streaming, and prefetching.
diff --git a/playground/http-errors/middleware/http-errors.ts b/playground/http-errors/middleware/http-errors.ts
new file mode 100644
index 000000000..ae116daf3
--- /dev/null
+++ b/playground/http-errors/middleware/http-errors.ts
@@ -0,0 +1,20 @@
+import { HTTPException } from 'hono/http-exception';
+import { defineMiddleware } from 'void/handler';
+import { statusMessages } from '../src/utils.ts';
+
+export default defineMiddleware(async (c, next) => {
+ if (c.req.path === '/middleware/application') {
+ c.status(503);
+ c.header('Retry-After', '120');
+ }
+
+ if (c.req.path === '/middleware/errors/403') {
+ throw new HTTPException(403, { message: statusMessages[403] });
+ }
+
+ if (c.req.path === '/middleware/errors/503') {
+ throw new HTTPException(503, { message: statusMessages[503] });
+ }
+
+ await next();
+});
diff --git a/playground/http-errors/package.json b/playground/http-errors/package.json
new file mode 100644
index 000000000..a0471f52f
--- /dev/null
+++ b/playground/http-errors/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "playground-http-errors",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "vp build",
+ "dev": "vp dev",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@hono/node-server": "catalog:",
+ "hono": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:"
+ },
+ "devDependencies": {
+ "@types/react": "^19.3.0",
+ "@types/react-dom": "^19.3.0",
+ "@void/react": "workspace:*",
+ "typescript": "catalog:",
+ "vite": "catalog:",
+ "vite-plus": "catalog:",
+ "void": "workspace:*"
+ }
+}
diff --git a/playground/http-errors/pages/_error.tsx b/playground/http-errors/pages/_error.tsx
new file mode 100644
index 000000000..8da2a97d3
--- /dev/null
+++ b/playground/http-errors/pages/_error.tsx
@@ -0,0 +1,12 @@
+import type { ErrorPageProps } from '@void/react';
+
+export default function RootError({ error }: ErrorPageProps) {
+ return (
+
+ Root error boundary
+
+ {error.status}: {error.message}
+
+
+ );
+}
diff --git a/playground/http-errors/pages/_not-found.tsx b/playground/http-errors/pages/_not-found.tsx
new file mode 100644
index 000000000..6b82e4125
--- /dev/null
+++ b/playground/http-errors/pages/_not-found.tsx
@@ -0,0 +1,12 @@
+import type { ErrorPageProps } from '@void/react';
+
+export default function NotFound({ error }: ErrorPageProps) {
+ return (
+
+ Not-found boundary
+
+ {error.status}: {error.message}
+
+
+ );
+}
diff --git a/playground/http-errors/pages/index.tsx b/playground/http-errors/pages/index.tsx
new file mode 100644
index 000000000..a078787de
--- /dev/null
+++ b/playground/http-errors/pages/index.tsx
@@ -0,0 +1,224 @@
+import { Link } from '@void/react';
+import type { ReactNode } from 'react';
+import { SourceLink } from '../src/source-link.tsx';
+
+const loaderExamples = [
+ {
+ path: '/loaders/status/403',
+ title: 'Loader sets 403',
+ file: 'pages/loaders/status/[status].server.ts',
+ code: 'c.status(403);\nreturn { message: messages[403] };',
+ },
+ {
+ path: '/loaders/status/503',
+ title: 'Loader sets 503',
+ file: 'pages/loaders/status/[status].server.ts',
+ code: "c.status(503);\nc.header('Retry-After', '120');\nreturn { message: messages[503] };",
+ },
+ {
+ path: '/loaders/validated',
+ title: 'Validated loader sets 503',
+ file: 'pages/loaders/validated.server.ts',
+ code: 'export const loader = defineHandler.withValidator({})((c) => {\n c.status(503);\n return { message: messages[503] };\n});',
+ },
+ {
+ path: '/loaders/deferred',
+ title: 'Loader sets 429 before deferred streaming',
+ file: 'pages/loaders/deferred.server.ts',
+ code: 'c.status(429);\nreturn { details: defer(async () => {\n await new Promise(resolve => setTimeout(resolve, 1000));\n return messages[429];\n}) };',
+ },
+] as const;
+
+const middlewareExamples = [
+ {
+ path: '/middleware/application',
+ title: 'Application middleware sets 503',
+ file: 'middleware/http-errors.ts',
+ code: '// Inside application middleware, for this path:\nc.status(503);\nawait next();',
+ },
+ {
+ path: '/middleware/handler',
+ title: 'Handler middleware sets 403',
+ file: 'pages/middleware/handler.server.ts',
+ code: 'const guard = defineMiddleware(async (c, next) => {\n c.status(403);\n await next();\n});\nexport const loader = defineHandler(guard, () => ({ message: messages[403] }));',
+ },
+] as const;
+
+function Visits({ path }: { path: string }) {
+ return (
+
+ Document load | Client navigation
+
+ );
+}
+
+function Example({
+ title,
+ file,
+ code,
+ expected,
+ children,
+}: {
+ title: string;
+ file: string;
+ code?: string;
+ expected: ReactNode;
+ children: ReactNode;
+}) {
+ return (
+
+ {title}
+
+
+
+ {code && {code} }
+ Expected
+ {expected}
+ {children}
+
+ );
+}
+
+export default function Home() {
+ return (
+ <>
+ HTTP Errors Example
+
+ Explore how loader and middleware APIs affect response statuses, page content, and layouts.
+ Compare full document loads, client navigation, deferred streaming, and prefetching.
+
+
+ Related: {' '}
+ Issue #784 |{' '}
+ PR #788
+
+
+
+
+ Loaders
+
+ Use c.status() to choose the HTTP status while returning props for the requested page. An
+ error status does not select _error or stop rendering. Both document loads and client
+ navigation display the page with its props and layouts.
+
+
+ {loaderExamples.map(({ path, title, file, code }) => (
+
+ The response uses the requested status and renders the page with its props and
+ layouts. Client navigation keeps the app’s layout.
+
+ }
+ >
+
+
+ ))}
+
+
+ HTTP 429, Purpose: prefetch, and a complete JSON payload after the one-second
+ deferred task. Prefetching waits for deferred props rather than streaming them.
+
+ }
+ >
+
+ Hover or focus this link without clicking, then inspect the prefetch request in the
+ browser’s Network panel.
+
+
+ Hover or focus to prefetch deferred data
+
+
+
+ {[401, 403, 410, 429, 503].map((status) => (
+
+ Stops the loader and renders the nested _error boundary with the thrown HTTP
+ status and message. The root and nested layouts remain visible, and client
+ navigation keeps the app’s layout.
+
+ }
+ >
+
+
+ ))}
+
+
HTTP 500 with the root error boundary inside the app’s layout.}
+ >
+
+
+
HTTP 404 with the not-found boundary inside the app’s layout.}
+ >
+
+
+
+
+
+
+
+ Middleware
+
+ Application middleware runs before the route handler. Handler middleware runs as part of
+ defineHandler(). Both can set a status and call await next() to render the page normally.
+
+
+ {middlewareExamples.map(({ path, title, file, code }) => (
+
+ The loader runs and the response uses the middleware’s status, rendering the page
+ with its props and layouts.
+
+ }
+ >
+
+
+ ))}
+
+ {[403, 503].map((status) => (
+
+ Stops the request before its loader runs and renders the root _error boundary with
+ the thrown HTTP status and message. The root layout remains visible, and client
+ navigation keeps it mounted.
+
+ }
+ >
+
+
+ ))}
+
+
+ >
+ );
+}
diff --git a/playground/http-errors/pages/layout.tsx b/playground/http-errors/pages/layout.tsx
new file mode 100644
index 000000000..d1f63034c
--- /dev/null
+++ b/playground/http-errors/pages/layout.tsx
@@ -0,0 +1,35 @@
+import { Link } from '@void/react';
+import { useEffect, useRef, type ReactNode } from 'react';
+import './styles.css';
+
+export default function Layout({ children }: { children: ReactNode }) {
+ const toastRef = useRef(null);
+
+ useEffect(() => {
+ const toast = toastRef.current!;
+ toast.showPopover();
+ const timeout = window.setTimeout(() => toast.hidePopover(), 2500);
+ return () => {
+ window.clearTimeout(timeout);
+ toast.hidePopover();
+ };
+ }, []);
+
+ return (
+
+
+ Page loaded
+
+
+
+
+ HTTP Errors Example
+ {' '}
+ | Loaders | Middleware
+
+
+
+
{children}
+
+ );
+}
diff --git a/playground/http-errors/pages/loaders/deferred.server.ts b/playground/http-errors/pages/loaders/deferred.server.ts
new file mode 100644
index 000000000..b3bb57a65
--- /dev/null
+++ b/playground/http-errors/pages/loaders/deferred.server.ts
@@ -0,0 +1,15 @@
+import { defineHandler, defer, type InferProps } from 'void/handler';
+import { statusMessages } from '../../src/utils.ts';
+
+export const loader = defineHandler((c) => {
+ c.status(429);
+ c.header('Retry-After', '120');
+ return {
+ details: defer(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ return statusMessages[429];
+ }),
+ };
+});
+
+export type Props = InferProps;
diff --git a/playground/http-errors/pages/loaders/deferred.tsx b/playground/http-errors/pages/loaders/deferred.tsx
new file mode 100644
index 000000000..cb006c760
--- /dev/null
+++ b/playground/http-errors/pages/loaders/deferred.tsx
@@ -0,0 +1,18 @@
+import { Suspense, use } from 'react';
+import type { Props } from './deferred.server.ts';
+
+function Details({ details }: Props) {
+ return {use(details)}
;
+}
+
+export default function DeferredPage(props: Props) {
+ return (
+
+ Status set before deferred streaming
+ Expected HTTP 429 for both the HTML document and the client navigation stream.
+ Waiting one second for deferred props…
}>
+
+
+
+ );
+}
diff --git a/playground/http-errors/pages/loaders/errors/[status].server.ts b/playground/http-errors/pages/loaders/errors/[status].server.ts
new file mode 100644
index 000000000..e5c572323
--- /dev/null
+++ b/playground/http-errors/pages/loaders/errors/[status].server.ts
@@ -0,0 +1,9 @@
+import { HTTPException } from 'hono/http-exception';
+import { defineHandler } from 'void/handler';
+import { statusMessages, parseStatus } from '../../../src/utils.ts';
+
+export const loader = defineHandler((c) => {
+ const status = parseStatus(c);
+
+ throw new HTTPException(status, { message: statusMessages[status] });
+});
diff --git a/playground/http-errors/pages/loaders/errors/[status].tsx b/playground/http-errors/pages/loaders/errors/[status].tsx
new file mode 100644
index 000000000..f220ecaaf
--- /dev/null
+++ b/playground/http-errors/pages/loaders/errors/[status].tsx
@@ -0,0 +1,3 @@
+export default function LoaderPage() {
+ return Loader exception example ;
+}
diff --git a/playground/http-errors/pages/loaders/errors/_error.tsx b/playground/http-errors/pages/loaders/errors/_error.tsx
new file mode 100644
index 000000000..a7bcf3f44
--- /dev/null
+++ b/playground/http-errors/pages/loaders/errors/_error.tsx
@@ -0,0 +1,12 @@
+import type { ErrorPageProps } from '@void/react';
+
+export default function NestedError({ error }: ErrorPageProps) {
+ return (
+
+ Nested error boundary
+
+ {error.status}: {error.message}
+
+
+ );
+}
diff --git a/playground/http-errors/pages/loaders/errors/layout.tsx b/playground/http-errors/pages/loaders/errors/layout.tsx
new file mode 100644
index 000000000..50363c31f
--- /dev/null
+++ b/playground/http-errors/pages/loaders/errors/layout.tsx
@@ -0,0 +1,10 @@
+import type { ReactNode } from 'react';
+
+export default function LoaderErrorLayout({ children }: { children: ReactNode }) {
+ return (
+
+ Nested layout for HTTP exception examples.
+ {children}
+
+ );
+}
diff --git a/playground/http-errors/pages/loaders/not-found.server.ts b/playground/http-errors/pages/loaders/not-found.server.ts
new file mode 100644
index 000000000..ea0a6be9a
--- /dev/null
+++ b/playground/http-errors/pages/loaders/not-found.server.ts
@@ -0,0 +1,7 @@
+import { HTTPException } from 'hono/http-exception';
+import { defineHandler } from 'void/handler';
+import { statusMessages } from '../../src/utils.ts';
+
+export const loader = defineHandler(() => {
+ throw new HTTPException(404, { message: statusMessages[404] });
+});
diff --git a/playground/http-errors/pages/loaders/not-found.tsx b/playground/http-errors/pages/loaders/not-found.tsx
new file mode 100644
index 000000000..f962c7569
--- /dev/null
+++ b/playground/http-errors/pages/loaders/not-found.tsx
@@ -0,0 +1,3 @@
+export default function MissingPage() {
+ return Not-found example ;
+}
diff --git a/playground/http-errors/pages/loaders/status/[status].server.ts b/playground/http-errors/pages/loaders/status/[status].server.ts
new file mode 100644
index 000000000..a44e2f7a3
--- /dev/null
+++ b/playground/http-errors/pages/loaders/status/[status].server.ts
@@ -0,0 +1,13 @@
+import { defineHandler } from 'void/handler';
+import { statusMessages, parseStatus } from '../../../src/utils.ts';
+
+export const loader = defineHandler((c) => {
+ const status = parseStatus(c);
+ c.status(status);
+ c.header('Retry-After', '120');
+ return {
+ title: 'Status set in a loader',
+ status,
+ message: statusMessages[status],
+ };
+});
diff --git a/playground/http-errors/pages/loaders/status/[status].tsx b/playground/http-errors/pages/loaders/status/[status].tsx
new file mode 100644
index 000000000..d39705d6f
--- /dev/null
+++ b/playground/http-errors/pages/loaders/status/[status].tsx
@@ -0,0 +1 @@
+export { default } from '../../../src/status-page.tsx';
diff --git a/playground/http-errors/pages/loaders/unexpected.server.ts b/playground/http-errors/pages/loaders/unexpected.server.ts
new file mode 100644
index 000000000..9ac88ecfb
--- /dev/null
+++ b/playground/http-errors/pages/loaders/unexpected.server.ts
@@ -0,0 +1,6 @@
+import { defineHandler } from 'void/handler';
+import { statusMessages } from '../../src/utils.ts';
+
+export const loader = defineHandler(() => {
+ throw new Error(statusMessages[500]);
+});
diff --git a/playground/http-errors/pages/loaders/unexpected.tsx b/playground/http-errors/pages/loaders/unexpected.tsx
new file mode 100644
index 000000000..ec3f357b0
--- /dev/null
+++ b/playground/http-errors/pages/loaders/unexpected.tsx
@@ -0,0 +1,3 @@
+export default function UnexpectedPage() {
+ return Unexpected error example ;
+}
diff --git a/playground/http-errors/pages/loaders/validated.server.ts b/playground/http-errors/pages/loaders/validated.server.ts
new file mode 100644
index 000000000..c697cd8ef
--- /dev/null
+++ b/playground/http-errors/pages/loaders/validated.server.ts
@@ -0,0 +1,11 @@
+import { defineHandler } from 'void/handler';
+import { statusMessages } from '../../src/utils.ts';
+
+export const loader = defineHandler.withValidator({})((c) => {
+ c.status(503);
+ return {
+ title: 'Status set in a validated loader',
+ status: 503,
+ message: statusMessages[503],
+ };
+});
diff --git a/playground/http-errors/pages/loaders/validated.tsx b/playground/http-errors/pages/loaders/validated.tsx
new file mode 100644
index 000000000..68421b3c2
--- /dev/null
+++ b/playground/http-errors/pages/loaders/validated.tsx
@@ -0,0 +1 @@
+export { default } from '../../src/status-page.tsx';
diff --git a/playground/http-errors/pages/middleware/application.server.ts b/playground/http-errors/pages/middleware/application.server.ts
new file mode 100644
index 000000000..e91875349
--- /dev/null
+++ b/playground/http-errors/pages/middleware/application.server.ts
@@ -0,0 +1,8 @@
+import { defineHandler } from 'void/handler';
+import { statusMessages } from '../../src/utils.ts';
+
+export const loader = defineHandler(() => ({
+ title: 'Status set in application middleware',
+ status: 503,
+ message: statusMessages[503],
+}));
diff --git a/playground/http-errors/pages/middleware/application.tsx b/playground/http-errors/pages/middleware/application.tsx
new file mode 100644
index 000000000..68421b3c2
--- /dev/null
+++ b/playground/http-errors/pages/middleware/application.tsx
@@ -0,0 +1 @@
+export { default } from '../../src/status-page.tsx';
diff --git a/playground/http-errors/pages/middleware/errors/[status].server.ts b/playground/http-errors/pages/middleware/errors/[status].server.ts
new file mode 100644
index 000000000..b836335b4
--- /dev/null
+++ b/playground/http-errors/pages/middleware/errors/[status].server.ts
@@ -0,0 +1,6 @@
+import { defineHandler } from 'void/handler';
+
+export const loader = defineHandler(() => {
+ console.log('Middleware-error page loader ran. It should have been blocked.');
+ return {};
+});
diff --git a/playground/http-errors/pages/middleware/errors/[status].tsx b/playground/http-errors/pages/middleware/errors/[status].tsx
new file mode 100644
index 000000000..5a5370693
--- /dev/null
+++ b/playground/http-errors/pages/middleware/errors/[status].tsx
@@ -0,0 +1,3 @@
+export default function MiddlewarePage() {
+ return Middleware exception example ;
+}
diff --git a/playground/http-errors/pages/middleware/handler.server.ts b/playground/http-errors/pages/middleware/handler.server.ts
new file mode 100644
index 000000000..be09cf87d
--- /dev/null
+++ b/playground/http-errors/pages/middleware/handler.server.ts
@@ -0,0 +1,13 @@
+import { defineHandler, defineMiddleware } from 'void/handler';
+import { statusMessages } from '../../src/utils.ts';
+
+const setStatus = defineMiddleware(async (c, next) => {
+ c.status(403);
+ await next();
+});
+
+export const loader = defineHandler(setStatus, () => ({
+ title: 'Status set in handler middleware',
+ status: 403,
+ message: statusMessages[403],
+}));
diff --git a/playground/http-errors/pages/middleware/handler.tsx b/playground/http-errors/pages/middleware/handler.tsx
new file mode 100644
index 000000000..68421b3c2
--- /dev/null
+++ b/playground/http-errors/pages/middleware/handler.tsx
@@ -0,0 +1 @@
+export { default } from '../../src/status-page.tsx';
diff --git a/playground/http-errors/pages/styles.css b/playground/http-errors/pages/styles.css
new file mode 100644
index 000000000..c87b64a18
--- /dev/null
+++ b/playground/http-errors/pages/styles.css
@@ -0,0 +1,54 @@
+.example-container {
+ box-sizing: border-box;
+ max-width: 100rem;
+ margin-inline: auto;
+ padding: 1rem;
+}
+
+.example-container p {
+ max-width: 75ch;
+}
+
+.example-page-loaded-toast {
+ position: fixed;
+ inset: 1rem auto auto 50%;
+ transform: translateX(-50%);
+ margin: 0;
+ padding: 0.75rem 1rem;
+ border: 1px solid;
+ border-radius: 0.25rem;
+ background: Canvas;
+ color: CanvasText;
+}
+
+.examples {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ align-items: start;
+ gap: 1rem 2rem;
+}
+
+.example {
+ min-width: 0;
+}
+
+@media (max-width: 72rem) {
+ .examples {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 48rem) {
+ .examples {
+ grid-template-columns: minmax(0, 1fr);
+ }
+}
+
+.example pre {
+ white-space: pre-wrap;
+}
+
+.example pre,
+.example code {
+ overflow-wrap: anywhere;
+}
diff --git a/playground/http-errors/src/editor-url.ts b/playground/http-errors/src/editor-url.ts
new file mode 100644
index 000000000..3ecc05701
--- /dev/null
+++ b/playground/http-errors/src/editor-url.ts
@@ -0,0 +1,5 @@
+export function vscodeFileUrl(root: string, file: string): string {
+ const source = new URL(file.split('/').map(encodeURIComponent).join('/'), root);
+ const host = source.host ? `//${source.host}` : '';
+ return `vscode://file${host}${source.pathname}:1:1`;
+}
diff --git a/playground/http-errors/src/source-link.tsx b/playground/http-errors/src/source-link.tsx
new file mode 100644
index 000000000..8581f3084
--- /dev/null
+++ b/playground/http-errors/src/source-link.tsx
@@ -0,0 +1,9 @@
+import { vscodeFileUrl } from './editor-url.ts';
+
+export function SourceLink({ file }: { file: string }) {
+ return (
+
+ {file}
+
+ );
+}
diff --git a/playground/http-errors/src/status-page.tsx b/playground/http-errors/src/status-page.tsx
new file mode 100644
index 000000000..d2cbd5cff
--- /dev/null
+++ b/playground/http-errors/src/status-page.tsx
@@ -0,0 +1,18 @@
+export type StatusProps = {
+ title: string;
+ status: number;
+ message: string;
+};
+
+export default function StatusPage({ title, status, message }: StatusProps) {
+ return (
+
+ {title}
+ {message}
+ Requested HTTP status: {status}. Check the actual response in the Network panel.
+
+ This is the normal page, not an error boundary. Setting c.status() does not abort rendering.
+
+
+ );
+}
diff --git a/playground/http-errors/src/utils.ts b/playground/http-errors/src/utils.ts
new file mode 100644
index 000000000..ac59caf9f
--- /dev/null
+++ b/playground/http-errors/src/utils.ts
@@ -0,0 +1,15 @@
+import type { CloudContext } from 'void/handler';
+
+export const statusMessages = {
+ 401: 'Please sign in',
+ 403: 'Your account is banned',
+ 404: 'This page does not exist',
+ 410: 'This page is permanently gone',
+ 429: 'Too many requests',
+ 500: 'Unexpected loader failure',
+ 503: 'Down for maintenance',
+} as const;
+
+export function parseStatus(c: CloudContext) {
+ return parseInt(c.req.param('status')!) as keyof typeof statusMessages;
+}
diff --git a/playground/http-errors/src/vite-env.d.ts b/playground/http-errors/src/vite-env.d.ts
new file mode 100644
index 000000000..83d437882
--- /dev/null
+++ b/playground/http-errors/src/vite-env.d.ts
@@ -0,0 +1,3 @@
+///
+
+declare const __HTTP_ERRORS_SOURCE_ROOT__: string;
diff --git a/playground/http-errors/tsconfig.json b/playground/http-errors/tsconfig.json
new file mode 100644
index 000000000..18d0c6eb8
--- /dev/null
+++ b/playground/http-errors/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "strict": true,
+ "noUnusedLocals": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "jsx": "react-jsx",
+ "noEmit": true,
+ "allowImportingTsExtensions": true
+ },
+ "include": ["pages", "middleware", "src", "*.config.ts"]
+}
diff --git a/playground/http-errors/vite.config.ts b/playground/http-errors/vite.config.ts
new file mode 100644
index 000000000..48c1c0d91
--- /dev/null
+++ b/playground/http-errors/vite.config.ts
@@ -0,0 +1,10 @@
+import { voidReact } from '@void/react/plugin';
+import { defineConfig } from 'vite-plus';
+import { voidPlugin } from 'void';
+
+export default defineConfig({
+ define: {
+ __HTTP_ERRORS_SOURCE_ROOT__: JSON.stringify(new URL('./', import.meta.url).href),
+ },
+ plugins: [voidPlugin(), voidReact()],
+});
diff --git a/playground/http-errors/void.config.ts b/playground/http-errors/void.config.ts
new file mode 100644
index 000000000..83f948417
--- /dev/null
+++ b/playground/http-errors/void.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from 'void/config';
+
+export default defineConfig({
+ target: 'node',
+ head: {
+ title: 'HTTP Errors Example',
+ htmlAttrs: { lang: 'en' },
+ meta: [{ name: 'viewport', content: 'width=device-width, initial-scale=1' }],
+ },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d0f5c3463..49280dbf6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1360,6 +1360,43 @@ importers:
specifier: workspace:*
version: link:../../packages/void
+ playground/http-errors:
+ dependencies:
+ '@hono/node-server':
+ specifier: 'catalog:'
+ version: 2.1.6(hono@4.13.18)
+ hono:
+ specifier: 'catalog:'
+ version: 4.13.18
+ react:
+ specifier: 'catalog:'
+ version: 19.3.1
+ react-dom:
+ specifier: 'catalog:'
+ version: 19.3.1(react@19.3.1)
+ devDependencies:
+ '@types/react':
+ specifier: ^19.3.0
+ version: 19.3.0
+ '@types/react-dom':
+ specifier: ^19.3.0
+ version: 19.3.0(@types/react@19.3.0)
+ '@void/react':
+ specifier: workspace:*
+ version: link:../../packages/react
+ typescript:
+ specifier: 'catalog:'
+ version: 7.0.2
+ vite:
+ specifier: 'catalog:'
+ version: '@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1)'
+ vite-plus:
+ specifier: 'catalog:'
+ version: 1.6.0(@astrojs/compiler-rs@0.5.3)(@types/node@26.6.6)(@vitest/ui@5.1.0)(esbuild@0.28.2)(happy-dom@21.0.4)(jiti@2.7.0)(playwright@1.65.0)(sass-embedded@1.104.1)(sass@1.104.1)(svelte@5.57.2)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1)
+ void:
+ specifier: workspace:*
+ version: link:../../packages/void
+
playground/kitchen-sink:
dependencies:
hono:
@@ -1574,7 +1611,7 @@ importers:
dependencies:
nuxt:
specifier: 'catalog:'
- version: 4.6.4(28cabe179d6b5d1d97f7fc0415b8148e)
+ version: 4.6.4(bb0ed2b01b3594c95f4ec08c384cb46b)
devDependencies:
pathslash:
specifier: 'catalog:'
@@ -12406,10 +12443,10 @@ snapshots:
- magic-string
- unplugin
- '@nuxt/nitro-server@4.6.4(11c7add04b75266a45f152ffa44abd58)':
+ '@nuxt/nitro-server@4.6.4(4420c67e34a1beb2667caeefcf5ec4ed)':
dependencies:
'@nuxt/kit': 4.6.4(@nuxt/schema@4.6.4)(confbox@0.3.2)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.5)(mlly@1.8.2)(oxc-parser@0.156.0)(rolldown@1.3.1)(unplugin@3.7.0(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(rolldown@1.3.1)(rollup@4.68.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0)))
- '@unhead/vue': 3.4.3(@oxc-project/types@0.156.0)(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(lightningcss@1.33.0)(oxc-parser@0.156.0)(rolldown@1.3.1)(rollup@4.68.2)(vue@3.5.45(typescript@7.0.2))(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0))
+ '@unhead/vue': 3.4.3(@oxc-project/types@0.158.0)(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(lightningcss@1.33.0)(oxc-parser@0.156.0)(rolldown@1.3.1)(rollup@4.68.2)(vue@3.5.45(typescript@7.0.2))(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0))
consola: 3.4.2
defu: 6.1.7
destr: 2.0.5
@@ -12425,7 +12462,7 @@ snapshots:
my-bad: 0.2.12(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))
nitropack: 2.13.6(@electric-sql/pglite@0.5.16)(@parcel/watcher@2.6.0)(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(aws4fetch@1.0.20)(bare-buffer@3.7.1)(better-sqlite3@13.0.3)(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261110.1)(@electric-sql/pglite@0.5.16)(@types/better-sqlite3@9.6.0)(@types/pg@8.23.1)(better-sqlite3@13.0.3)(kysely@0.29.8)(mysql2@3.24.6(@types/node@26.6.6))(pg@8.23.1))(mysql2@3.24.6(@types/node@26.6.6))(oxc-parser@0.156.0)(rolldown@1.3.1)(srvx@1.0.8)(supports-color@10.2.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0))
nostics: 1.3.0
- nuxt: 4.6.4(28cabe179d6b5d1d97f7fc0415b8148e)
+ nuxt: 4.6.4(bb0ed2b01b3594c95f4ec08c384cb46b)
ohash: 2.0.12
pathe: 2.0.3
radix3: 1.1.2
@@ -12516,7 +12553,7 @@ snapshots:
rc9: 3.1.0
std-env: 4.3.0
- '@nuxt/vite-builder@4.6.4(926b91ff9d9fff80aa13715ef35bb050)':
+ '@nuxt/vite-builder@4.6.4(6fcbe3d3dcc90cf47c525e8ae510ae9b)':
dependencies:
'@nuxt/kit': 4.6.4(@nuxt/schema@4.6.4)(confbox@0.3.2)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.5)(mlly@1.8.2)(oxc-parser@0.156.0)(rolldown@1.3.1)(unplugin@3.7.0(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(rolldown@1.3.1)(rollup@4.68.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0)))
'@vitejs/plugin-vue': 6.0.10(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(vue@3.5.45(typescript@7.0.2))
@@ -12534,7 +12571,7 @@ snapshots:
knitwork: 1.3.0
mocked-exports: 0.1.1
my-bad: 0.2.12(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))
- nuxt: 4.6.4(28cabe179d6b5d1d97f7fc0415b8148e)
+ nuxt: 4.6.4(bb0ed2b01b3594c95f4ec08c384cb46b)
pathe: 2.0.3
pkg-types: 2.4.0
postcss: 8.5.32
@@ -12597,7 +12634,7 @@ snapshots:
- webpack
- yaml
- '@nuxt/vite-server@4.6.4(@nuxt/schema@4.6.4)(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(confbox@0.3.2)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.5)(mlly@1.8.2)(nuxt@4.6.4(28cabe179d6b5d1d97f7fc0415b8148e))(oxc-parser@0.156.0)(rolldown@1.3.1)(unplugin@3.7.0(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(rolldown@1.3.1)(rollup@4.68.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0)))':
+ '@nuxt/vite-server@4.6.4(@nuxt/schema@4.6.4)(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(confbox@0.3.2)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.5)(mlly@1.8.2)(nuxt@4.6.4(bb0ed2b01b3594c95f4ec08c384cb46b))(oxc-parser@0.156.0)(rolldown@1.3.1)(unplugin@3.7.0(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(rolldown@1.3.1)(rollup@4.68.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0)))':
dependencies:
'@nuxt/kit': 4.6.4(@nuxt/schema@4.6.4)(confbox@0.3.2)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.5)(mlly@1.8.2)(oxc-parser@0.156.0)(rolldown@1.3.1)(unplugin@3.7.0(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(rolldown@1.3.1)(rollup@4.68.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0)))
clickable-path: 0.1.1
@@ -12605,7 +12642,7 @@ snapshots:
exsolve: 1.2.0
mocked-exports: 0.1.1
my-bad: 0.2.12(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))
- nuxt: 4.6.4(28cabe179d6b5d1d97f7fc0415b8148e)
+ nuxt: 4.6.4(bb0ed2b01b3594c95f4ec08c384cb46b)
pathe: 2.0.3
rou3: 0.9.2
srvx: 1.0.5
@@ -13972,10 +14009,10 @@ snapshots:
'@ungap/structured-clone@1.4.0': {}
- '@unhead/bundler@3.4.3(@oxc-project/types@0.156.0)(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(lightningcss@1.33.0)(oxc-parser@0.156.0)(rolldown@1.3.1)(rollup@4.68.2)(unhead@3.4.3(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(rolldown@1.3.1)(rollup@4.68.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0)))(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0))':
+ '@unhead/bundler@3.4.3(@oxc-project/types@0.158.0)(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(lightningcss@1.33.0)(oxc-parser@0.156.0)(rolldown@1.3.1)(rollup@4.68.2)(unhead@3.4.3(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(rolldown@1.3.1)(rollup@4.68.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0)))(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0))':
dependencies:
magic-string: 1.4.5
- oxc-walker: 1.1.1(@oxc-project/types@0.156.0)(oxc-parser@0.156.0)(rolldown@1.3.1)
+ oxc-walker: 1.1.1(@oxc-project/types@0.158.0)(oxc-parser@0.156.0)(rolldown@1.3.1)
unhead: 3.4.3(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(rolldown@1.3.1)(rollup@4.68.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0))
unplugin: 3.7.0(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(rolldown@1.3.1)(rollup@4.68.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0))
optionalDependencies:
@@ -13994,9 +14031,9 @@ snapshots:
- rollup
- unloader
- '@unhead/vue@3.4.3(@oxc-project/types@0.156.0)(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(lightningcss@1.33.0)(oxc-parser@0.156.0)(rolldown@1.3.1)(rollup@4.68.2)(vue@3.5.45(typescript@7.0.2))(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0))':
+ '@unhead/vue@3.4.3(@oxc-project/types@0.158.0)(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(lightningcss@1.33.0)(oxc-parser@0.156.0)(rolldown@1.3.1)(rollup@4.68.2)(vue@3.5.45(typescript@7.0.2))(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0))':
dependencies:
- '@unhead/bundler': 3.4.3(@oxc-project/types@0.156.0)(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(lightningcss@1.33.0)(oxc-parser@0.156.0)(rolldown@1.3.1)(rollup@4.68.2)(unhead@3.4.3(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(rolldown@1.3.1)(rollup@4.68.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0)))(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0))
+ '@unhead/bundler': 3.4.3(@oxc-project/types@0.158.0)(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(lightningcss@1.33.0)(oxc-parser@0.156.0)(rolldown@1.3.1)(rollup@4.68.2)(unhead@3.4.3(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(rolldown@1.3.1)(rollup@4.68.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0)))(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0))
hookable: 6.1.2
unhead: 3.4.3(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(rolldown@1.3.1)(rollup@4.68.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0))
unplugin: 3.7.0(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(rolldown@1.3.1)(rollup@4.68.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0))
@@ -17057,7 +17094,7 @@ snapshots:
dependencies:
boolbase: 2.0.0
- nuxt@4.6.4(28cabe179d6b5d1d97f7fc0415b8148e):
+ nuxt@4.6.4(bb0ed2b01b3594c95f4ec08c384cb46b):
dependencies:
'@dxup/nuxt': 0.5.11(@nuxt/schema@4.6.4)(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(confbox@0.3.2)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(jiti@2.7.0)(mlly@1.8.2)(oxc-parser@0.156.0)(rolldown@1.3.1)(rollup@4.68.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0))
'@jridgewell/trace-mapping': 0.3.31
@@ -17065,12 +17102,12 @@ snapshots:
'@nuxt/devalue': 2.0.2
'@nuxt/devtools': 3.4.4(0696d073fe5f9c60158611c26666c741)
'@nuxt/kit': 4.6.4(@nuxt/schema@4.6.4)(confbox@0.3.2)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.5)(mlly@1.8.2)(oxc-parser@0.156.0)(rolldown@1.3.1)(unplugin@3.7.0(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(rolldown@1.3.1)(rollup@4.68.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0)))
- '@nuxt/nitro-server': 4.6.4(11c7add04b75266a45f152ffa44abd58)
+ '@nuxt/nitro-server': 4.6.4(4420c67e34a1beb2667caeefcf5ec4ed)
'@nuxt/schema': 4.6.4
'@nuxt/telemetry': 2.9.1(@nuxt/kit@4.6.4(@nuxt/schema@4.6.4)(confbox@0.3.2)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.5)(mlly@1.8.2)(oxc-parser@0.156.0)(rolldown@1.3.1)(unplugin@3.7.0(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(rolldown@1.3.1)(rollup@4.68.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0))))
- '@nuxt/vite-builder': 4.6.4(926b91ff9d9fff80aa13715ef35bb050)
- '@nuxt/vite-server': 4.6.4(@nuxt/schema@4.6.4)(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(confbox@0.3.2)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.5)(mlly@1.8.2)(nuxt@4.6.4(28cabe179d6b5d1d97f7fc0415b8148e))(oxc-parser@0.156.0)(rolldown@1.3.1)(unplugin@3.7.0(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(rolldown@1.3.1)(rollup@4.68.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0)))
- '@unhead/vue': 3.4.3(@oxc-project/types@0.156.0)(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(lightningcss@1.33.0)(oxc-parser@0.156.0)(rolldown@1.3.1)(rollup@4.68.2)(vue@3.5.45(typescript@7.0.2))(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0))
+ '@nuxt/vite-builder': 4.6.4(6fcbe3d3dcc90cf47c525e8ae510ae9b)
+ '@nuxt/vite-server': 4.6.4(@nuxt/schema@4.6.4)(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(confbox@0.3.2)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.5)(mlly@1.8.2)(nuxt@4.6.4(bb0ed2b01b3594c95f4ec08c384cb46b))(oxc-parser@0.156.0)(rolldown@1.3.1)(unplugin@3.7.0(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(rolldown@1.3.1)(rollup@4.68.2)(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0)))
+ '@unhead/vue': 3.4.3(@oxc-project/types@0.158.0)(@voidzero-dev/vite-plus-core@1.6.0(@types/node@26.6.6)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1))(esbuild@0.28.2)(lightningcss@1.33.0)(oxc-parser@0.156.0)(rolldown@1.3.1)(rollup@4.68.2)(vue@3.5.45(typescript@7.0.2))(webpack@5.112.0(cssnano@9.9.0(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@26.6.6))(svgo@4.1.0))
'@vue/shared': 3.5.45
chokidar: 5.0.0
clickable-path: 0.1.1
@@ -17100,7 +17137,7 @@ snapshots:
ofetch: 1.5.1
ohash: 2.0.12
on-change: 6.0.2
- oxc-walker: 1.1.1(@oxc-project/types@0.156.0)(oxc-parser@0.156.0)(rolldown@1.3.1)
+ oxc-walker: 1.1.1(@oxc-project/types@0.158.0)(oxc-parser@0.156.0)(rolldown@1.3.1)
pathe: 2.0.3
perfect-debounce: 2.1.0
picomatch: 4.0.7
@@ -17333,9 +17370,9 @@ snapshots:
'@oxc-parser/binding-win32-ia32-msvc': 0.156.0
'@oxc-parser/binding-win32-x64-msvc': 0.156.0
- oxc-walker@1.1.1(@oxc-project/types@0.156.0)(oxc-parser@0.156.0)(rolldown@1.3.1):
+ oxc-walker@1.1.1(@oxc-project/types@0.158.0)(oxc-parser@0.156.0)(rolldown@1.3.1):
optionalDependencies:
- '@oxc-project/types': 0.156.0
+ '@oxc-project/types': 0.158.0
oxc-parser: 0.156.0
rolldown: 1.3.1