- Home
-
-
- {/* TODO: not found error convention? */}
- {!e && <>Not Found : {slug}>}
-
- {e && (
-
-
-
-
{e.name.english}
-
{e.name.japanese}
-
Types: {e.type.join(", ")}
- {Object.entries(e.base).map(([k, v]) => (
-
- {k}: {v}
-
- ))}
+
+
+
+
{e.name.english}
+
{e.name.japanese}
+
Types: {e.type.join(", ")}
+ {Object.entries(e.base).map(([k, v]) => (
+
+ {k}: {v}
-
- )}
+ ))}
+
);
}
diff --git a/packages/react-server/examples/basic/src/routes/demo/waku_02/_client.tsx b/packages/react-server/examples/basic/src/routes/demo/waku_02/_client.tsx
new file mode 100644
index 000000000..e26c4d21f
--- /dev/null
+++ b/packages/react-server/examples/basic/src/routes/demo/waku_02/_client.tsx
@@ -0,0 +1,20 @@
+"use client";
+
+import { __global } from "@hiogawa/react-server";
+
+// TODO: server action + redirect
+export function SearchInput() {
+ return (
+
+ );
+}
diff --git a/packages/react-server/examples/basic/src/routes/demo/waku_02/layout.tsx b/packages/react-server/examples/basic/src/routes/demo/waku_02/layout.tsx
index 29c3fd1b8..45756d56c 100644
--- a/packages/react-server/examples/basic/src/routes/demo/waku_02/layout.tsx
+++ b/packages/react-server/examples/basic/src/routes/demo/waku_02/layout.tsx
@@ -1,3 +1,5 @@
+import { SearchInput } from "./_client";
+
export default async function Layout(props: React.PropsWithChildren) {
return (
@@ -11,6 +13,7 @@ export default async function Layout(props: React.PropsWithChildren) {
Waku
+
{props.children}
);
diff --git a/packages/react-server/examples/basic/src/routes/test/error.tsx b/packages/react-server/examples/basic/src/routes/test/error.tsx
new file mode 100644
index 000000000..6538d8f1b
--- /dev/null
+++ b/packages/react-server/examples/basic/src/routes/test/error.tsx
@@ -0,0 +1,15 @@
+"use client";
+
+import type { ErrorRouteProps } from "@hiogawa/react-server/server";
+
+export default function ErrorPage(props: ErrorRouteProps) {
+ return (
+
+
ErrorPage
+
+ server error:{" "}
+ {props.serverError ? JSON.stringify(props.serverError) : "(N/A)"}
+
+
+ );
+}
diff --git a/packages/react-server/examples/basic/src/routes/test/error/browser/_client.tsx b/packages/react-server/examples/basic/src/routes/test/error/browser/_client.tsx
new file mode 100644
index 000000000..896a95f8e
--- /dev/null
+++ b/packages/react-server/examples/basic/src/routes/test/error/browser/_client.tsx
@@ -0,0 +1,10 @@
+"use client";
+
+import React from "react";
+
+export function ClinetPage() {
+ React.useEffect(() => {
+ throw new Error("boom!");
+ }, []);
+ return
Error on Effect
;
+}
diff --git a/packages/react-server/examples/basic/src/routes/test/error/browser/page.tsx b/packages/react-server/examples/basic/src/routes/test/error/browser/page.tsx
new file mode 100644
index 000000000..7d562a747
--- /dev/null
+++ b/packages/react-server/examples/basic/src/routes/test/error/browser/page.tsx
@@ -0,0 +1,5 @@
+import { ClinetPage } from "./_client";
+
+export default function Page() {
+ return
;
+}
diff --git a/packages/react-server/examples/basic/src/routes/test/error/page.tsx b/packages/react-server/examples/basic/src/routes/test/error/page.tsx
new file mode 100644
index 000000000..1e006a6b6
--- /dev/null
+++ b/packages/react-server/examples/basic/src/routes/test/error/page.tsx
@@ -0,0 +1,26 @@
+import { Link } from "@hiogawa/react-server/client";
+
+export default async function Page() {
+ return (
+
+
+ Server 500
+
+
+ Server Custom
+
+
+ Browser
+
+
+ );
+}
diff --git a/packages/react-server/examples/basic/src/routes/test/error/server/page.tsx b/packages/react-server/examples/basic/src/routes/test/error/server/page.tsx
new file mode 100644
index 000000000..391eae254
--- /dev/null
+++ b/packages/react-server/examples/basic/src/routes/test/error/server/page.tsx
@@ -0,0 +1,15 @@
+import { type PageRouteProps, createError } from "@hiogawa/react-server/server";
+
+declare module "@hiogawa/react-server/server" {
+ interface ReactServerErrorContext {
+ customMessage?: string;
+ }
+}
+
+export default function Page(props: PageRouteProps) {
+ const url = new URL(props.request.url);
+ if (url.searchParams.has("custom")) {
+ throw createError({ status: 403, customMessage: "hello" });
+ }
+ throw new Error("boom!");
+}
diff --git a/packages/react-server/examples/basic/src/routes/test/layout.tsx b/packages/react-server/examples/basic/src/routes/test/layout.tsx
index de3f272e4..26bc099d2 100644
--- a/packages/react-server/examples/basic/src/routes/test/layout.tsx
+++ b/packages/react-server/examples/basic/src/routes/test/layout.tsx
@@ -1,7 +1,8 @@
+import type { LayoutRouteProps } from "@hiogawa/react-server/server";
import { NavMenu } from "../../components/nav-menu";
import { Hydrated } from "./_client";
-export default async function Layout(props: React.PropsWithChildren) {
+export default async function Layout(props: LayoutRouteProps) {
return (
);
diff --git a/packages/react-server/package.json b/packages/react-server/package.json
index 3aa34bdd4..1598cd02e 100644
--- a/packages/react-server/package.json
+++ b/packages/react-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@hiogawa/react-server",
- "version": "0.1.0-pre.8",
+ "version": "0.1.0-pre.9",
"license": "MIT",
"type": "module",
"exports": {
diff --git a/packages/react-server/src/client-internal.ts b/packages/react-server/src/client-internal.ts
index 6805e3c83..9d9792c86 100644
--- a/packages/react-server/src/client-internal.ts
+++ b/packages/react-server/src/client-internal.ts
@@ -1,3 +1,7 @@
"use client";
export { createServerReference } from "./lib/shared";
+export {
+ ErrorBoundary,
+ DefaultRootErrorPage,
+} from "./lib/components/error-boundary";
diff --git a/packages/react-server/src/entry/browser.tsx b/packages/react-server/src/entry/browser.tsx
index 84b4d9348..2960675aa 100644
--- a/packages/react-server/src/entry/browser.tsx
+++ b/packages/react-server/src/entry/browser.tsx
@@ -72,12 +72,21 @@ export async function start() {
return React.use(rsc);
}
- reactDomClient.hydrateRoot(
- document,
-
-
- ,
- );
+ // full client render on SSR error
+ if (document.documentElement.dataset["noHydate"]) {
+ reactDomClient.createRoot(document).render(
+
+
+ ,
+ );
+ } else {
+ reactDomClient.hydrateRoot(
+ document,
+
+
+ ,
+ );
+ }
// custom event for RSC reload
if (import.meta.hot) {
@@ -87,3 +96,10 @@ export async function start() {
});
}
}
+
+declare module "react-dom/client" {
+ // TODO: full document CSR works fine?
+ interface DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_CREATE_ROOT_CONTAINERS {
+ Document: Document;
+ }
+}
diff --git a/packages/react-server/src/entry/react-server.tsx b/packages/react-server/src/entry/react-server.tsx
index f912c03b7..f7306b21c 100644
--- a/packages/react-server/src/entry/react-server.tsx
+++ b/packages/react-server/src/entry/react-server.tsx
@@ -1,5 +1,7 @@
import { objectMapKeys } from "@hiogawa/utils";
import reactServerDomServer from "react-server-dom-webpack/server.edge";
+import { debug } from "../lib/debug";
+import { ReactServerDigestError, createError } from "../lib/error";
import { __global } from "../lib/global";
import { generateRouteTree, matchRoute, renderMatchRoute } from "../lib/router";
import { createBundlerConfig } from "../lib/rsc";
@@ -18,7 +20,6 @@ export type ReactServerHandlerResult =
| Response
| {
stream: ReadableStream
;
- status: number;
};
export const handler: ReactServerHandler = async ({ request }) => {
@@ -36,7 +37,7 @@ export const handler: ReactServerHandler = async ({ request }) => {
const rscOnlyRequest = unwrapRscRequest(request);
// rsc
- const { stream, status } = render({
+ const { stream } = await render({
request: rscOnlyRequest ?? request,
});
if (rscOnlyRequest) {
@@ -47,20 +48,33 @@ export const handler: ReactServerHandler = async ({ request }) => {
});
}
- return { stream, status };
+ return { stream };
};
//
// render RSC
//
-function render({ request }: { request: Request }) {
- const result = router.run(request);
+async function render({ request }: { request: Request }) {
+ const result = await router.run(request);
const stream = reactServerDomServer.renderToReadableStream(
result.node,
createBundlerConfig(),
+ {
+ onError(error, errorInfo) {
+ debug.rsc("[reactServerDomServer.renderToReadableStream]", {
+ error,
+ errorInfo,
+ });
+ const serverError =
+ error instanceof ReactServerDigestError
+ ? error
+ : createError({ status: 500 });
+ return serverError.digest;
+ },
+ },
);
- return { stream, status: result.match.notFound ? 404 : 200 };
+ return { stream };
}
//
@@ -72,7 +86,7 @@ const router = createRouter();
function createRouter() {
// for now hard code /src/routes as convention
const glob = import.meta.glob(
- "/src/routes/**/(page|layout).(js|jsx|ts|tsx)",
+ "/src/routes/**/(page|layout|error).(js|jsx|ts|tsx)",
{
eager: true,
},
@@ -81,13 +95,10 @@ function createRouter() {
objectMapKeys(glob, (_v, k) => k.slice("/src/routes".length)),
);
- function run(request: Request) {
+ async function run(request: Request) {
const url = new URL(request.url);
const match = matchRoute(url.pathname, tree);
- const node = renderMatchRoute(
- { request, match },
- Not Found: {url.pathname}
,
- );
+ const node = renderMatchRoute({ request, match });
return { node, match };
}
diff --git a/packages/react-server/src/entry/server.tsx b/packages/react-server/src/entry/server.tsx
index 695a787bd..b1496f455 100644
--- a/packages/react-server/src/entry/server.tsx
+++ b/packages/react-server/src/entry/server.tsx
@@ -1,6 +1,8 @@
import { splitFirst } from "@hiogawa/utils";
import reactDomServer from "react-dom/server.edge";
import { injectRSCPayload } from "rsc-html-stream/server";
+import { debug } from "../lib/debug";
+import { getErrorContext, getStatusText } from "../lib/error";
import { __global } from "../lib/global";
import {
createModuleMap,
@@ -19,9 +21,9 @@ export async function handler(request: Request): Promise {
}
// ssr rsc
- const htmlStream = await renderHtml(result.stream);
- return new Response(htmlStream, {
- status: result.status,
+ const ssrResult = await renderHtml(result.stream);
+ return new Response(ssrResult.htmlStream, {
+ status: ssrResult.status,
headers: {
"content-type": "text/html",
},
@@ -40,9 +42,7 @@ export async function importReactServer(): Promise<
}
}
-export async function renderHtml(
- rscStream: ReadableStream,
-): Promise {
+export async function renderHtml(rscStream: ReadableStream) {
await initDomWebpackSsr();
const { default: reactServerDomClient } = await import(
@@ -55,6 +55,7 @@ export async function renderHtml(
// (see src/lib/ssr.tsx for details)
const renderId = Math.random().toString(36).slice(2);
+ // TODO: Reac.use promise?
const rscNode = await reactServerDomClient.createFromReadableStream(
rscStream1,
{
@@ -73,16 +74,49 @@ export async function renderHtml(
}
const assets = (await import("virtual:ssr-assets" as string)).default;
- const ssrStream = await reactDomServer.renderToReadableStream(rscNode, {
- bootstrapModules: assets.bootstrapModules,
- });
+ // two pass SSR to re-render on error
+ let ssrStream: ReadableStream;
+ let status = 200;
+ try {
+ ssrStream = await reactDomServer.renderToReadableStream(rscNode, {
+ bootstrapModules: assets.bootstrapModules,
+ onError(error, errorInfo) {
+ // TODO: should handle SSR error which is not RSC error?
+ debug.ssr("renderToReadableStream", { error, errorInfo });
+ },
+ });
+ } catch (e) {
+ // render empty as error fallback and
+ // let browser render full CSR instead of hydration
+ // which will reply client error boudnary from RSC error
+ // TODO: proper two-pass SSR with error route tracking?
+ // TODO: meta tag system
+ status = getErrorContext(e)?.status ?? 500;
+ const errorRoot = (
+
+
+
+
+
+
+
+
+ );
+ ssrStream = await reactDomServer.renderToReadableStream(errorRoot, {
+ bootstrapModules: assets.bootstrapModules,
+ });
+ }
- return ssrStream
+ const htmlStream = ssrStream
.pipeThrough(invalidateImportCacheOnFinish(renderId))
.pipeThrough(new TextDecoderStream())
.pipeThrough(injectToHead(assets.head))
.pipeThrough(new TextEncoderStream())
.pipeThrough(injectRSCPayload(rscStream2));
+
+ return { htmlStream, status };
}
function injectToHead(data: string) {
diff --git a/packages/react-server/src/internal.ts b/packages/react-server/src/internal.ts
new file mode 100644
index 000000000..702de4ee8
--- /dev/null
+++ b/packages/react-server/src/internal.ts
@@ -0,0 +1,2 @@
+// expose for quick experimentation
+export { __global } from "./lib/global";
diff --git a/packages/react-server/src/lib/components/error-boundary.tsx b/packages/react-server/src/lib/components/error-boundary.tsx
new file mode 100644
index 000000000..6acb1d13f
--- /dev/null
+++ b/packages/react-server/src/lib/components/error-boundary.tsx
@@ -0,0 +1,76 @@
+"use client";
+
+import React from "react";
+import { getErrorContext, getStatusText } from "../error";
+import type { ErrorRouteProps } from "../router";
+
+// cf.
+// https://github.com/vercel/next.js/blob/33f8428f7066bf8b2ec61f025427ceb2a54c4bdf/packages/next/src/client/components/error-boundary.tsx
+// https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
+
+interface Props {
+ children?: React.ReactNode;
+ errorComponent: React.FC;
+ url: string;
+}
+
+interface State {
+ error: Error | null;
+ url: string;
+}
+
+export class ErrorBoundary extends React.Component {
+ constructor(props: Props) {
+ super(props);
+ this.state = { error: null, url: props.url };
+ }
+
+ static getDerivedStateFromError(error: Error) {
+ return { error };
+ }
+
+ // automatically reset on url change
+ static getDerivedStateFromProps(props: Props, state: State): State {
+ return {
+ ...state,
+ url: props.url,
+ error: props.url === state.url ? state.error : null,
+ };
+ }
+
+ reset = () => {
+ this.setState({ error: null });
+ };
+
+ override render() {
+ const error = this.state.error;
+ if (error) {
+ const Component = this.props.errorComponent;
+ return (
+
+ );
+ }
+ return this.props.children;
+ }
+}
+
+export function DefaultRootErrorPage(props: ErrorRouteProps) {
+ const status = props.serverError?.status;
+ return (
+
+
+ {status ? (
+
+ {status} {getStatusText(status)}
+
+ ) : (
+ Unexpected Error
+ )}
+
+
+ );
+}
diff --git a/packages/react-server/src/lib/csr.tsx b/packages/react-server/src/lib/csr.tsx
index d72ddc7ba..ec6ecf86f 100644
--- a/packages/react-server/src/lib/csr.tsx
+++ b/packages/react-server/src/lib/csr.tsx
@@ -37,6 +37,7 @@ export function initDomWebpackCsr() {
//
import { type RouterHistory, createBrowserHistory } from "@tanstack/history";
+import { __global } from "..";
// TODO: client context instead of global?
// TODO: create a wrapper to do `callServer` before actual client url update?
@@ -44,4 +45,5 @@ export let __history: RouterHistory;
export function initHistory() {
__history = createBrowserHistory();
+ __global.history = __history;
}
diff --git a/packages/react-server/src/lib/debug.ts b/packages/react-server/src/lib/debug.ts
index 360bbf58a..534d5807e 100644
--- a/packages/react-server/src/lib/debug.ts
+++ b/packages/react-server/src/lib/debug.ts
@@ -35,4 +35,9 @@ type Debug = ((...args: unknown[]) => void) & {
[k in K]: (...args: unknown[]) => void;
};
-export const debug = createDebug("react-server", ["plugin", "ssr", "browser"]);
+export const debug = createDebug("react-server", [
+ "plugin",
+ "ssr",
+ "rsc",
+ "browser",
+]);
diff --git a/packages/react-server/src/lib/error.tsx b/packages/react-server/src/lib/error.tsx
new file mode 100644
index 000000000..bdadc4cf7
--- /dev/null
+++ b/packages/react-server/src/lib/error.tsx
@@ -0,0 +1,49 @@
+import { debug } from "./debug";
+
+// TODO: accomodate redirection error convention?
+// TODO: custom (de)serialization?
+export interface ReactServerErrorContext {
+ status: number;
+}
+
+export class ReactServerDigestError extends Error {
+ constructor(public digest: string) {
+ super("ReactServerError");
+ }
+}
+
+export function createError(ctx: ReactServerErrorContext) {
+ const digest = `__REACT_SERVER_ERROR__:${JSON.stringify(ctx)}`;
+ return new ReactServerDigestError(digest);
+}
+
+export function getErrorContext(
+ error: unknown,
+): ReactServerErrorContext | undefined {
+ if (
+ error instanceof Error &&
+ "digest" in error &&
+ typeof error.digest === "string"
+ ) {
+ const m = error.digest.match(/^__REACT_SERVER_ERROR__:(.*)$/);
+ if (m && m[1]) {
+ try {
+ return JSON.parse(m[1]);
+ } catch (e) {
+ debug("[getErrorContext]", e);
+ }
+ }
+ }
+ return;
+}
+
+const STATUS_TEXT_MAP = new Map([
+ [400, "Bad Request"],
+ [403, "Forbidden"],
+ [404, "Not Found"],
+ [500, "Internal Server Error"],
+]);
+
+export function getStatusText(status: number) {
+ return STATUS_TEXT_MAP.get(status) ?? "Unknown Server Error";
+}
diff --git a/packages/react-server/src/lib/global.ts b/packages/react-server/src/lib/global.ts
index cd3aa7d17..070ce3155 100644
--- a/packages/react-server/src/lib/global.ts
+++ b/packages/react-server/src/lib/global.ts
@@ -1,3 +1,4 @@
+import type { RouterHistory } from "@tanstack/history";
import type { ViteDevServer } from "vite";
import type { CallServerCallback } from "./types";
@@ -8,6 +9,9 @@ export const __global: {
server: ViteDevServer;
reactServer: ViteDevServer;
};
+ history: RouterHistory;
callServer: CallServerCallback;
+ // see "virtual:self-reference-workaround"
+ clientInternal: typeof import("../client-internal");
serverInternal: typeof import("../server-internal");
} = ((globalThis as any).__REACT_SERVER_GLOBAL ??= {});
diff --git a/packages/react-server/src/lib/router.tsx b/packages/react-server/src/lib/router.tsx
index 176c904ed..8ac5f83bc 100644
--- a/packages/react-server/src/lib/router.tsx
+++ b/packages/react-server/src/lib/router.tsx
@@ -1,8 +1,7 @@
import { objectHas, tinyassert } from "@hiogawa/utils";
-import type React from "react";
-
-// cf. similar to vite-glob-routes
-// https://github.com/hi-ogawa/vite-plugins/blob/c2d22f9436ef868fc413f05f243323686a7aa143/packages/vite-glob-routes/src/react-router/route-utils.ts#L15-L22
+import React from "react";
+import { type ReactServerErrorContext, createError } from "./error";
+import { __global } from "./global";
// cf. https://nextjs.org/docs/app/building-your-application/routing#file-conventions
interface RouteEntry {
@@ -12,6 +11,10 @@ interface RouteEntry {
layout?: {
default: React.FC;
};
+ error?: {
+ // TODO: warn if no "use client"
+ default: React.FC;
+ };
}
type RouteTreeNode = TreeNode;
@@ -21,7 +24,7 @@ type RouteTreeNode = TreeNode;
export function generateRouteTree(globEntries: Record) {
const entries: Record = {};
for (const [k, v] of Object.entries(globEntries)) {
- const m = k.match(/^(.*)\/(page|layout)\.\w*$/);
+ const m = k.match(/^(.*)\/(page|layout|error)\.\w*$/);
tinyassert(m && 1 in m && 2 in m);
tinyassert(objectHas(v, "default"), `no deafult export found in '${k}'`);
((entries[m[1]] ??= {}) as any)[m[2]] = v;
@@ -70,15 +73,13 @@ export function matchRoute(
}
// TODO: separate react code in a different file
-export function renderMatchRoute(
- props: RouteProps,
- fallback: React.ReactNode,
-): React.ReactNode {
+export function renderMatchRoute(props: RouteProps) {
+ const { ErrorBoundary, DefaultRootErrorPage } = __global.clientInternal;
+
const nodes = [...props.match.nodes].reverse();
- let acc: React.ReactNode = fallback;
+ let acc: React.ReactNode = ;
if (!props.match.notFound) {
- // TODO: assert?
const Page = nodes[0]?.value?.page?.default;
if (Page) {
acc = ;
@@ -86,15 +87,37 @@ export function renderMatchRoute(
}
for (const node of nodes) {
+ const ErrorPage = node.value?.error?.default;
+ if (ErrorPage) {
+ // TODO: can we remove extra ?
+ acc = (
+
+ {acc}
+
+ );
+ }
const Layout = node.value?.layout?.default;
if (Layout) {
acc =
{acc};
}
}
+ acc = (
+
+ {acc}
+
+ );
+
return acc;
}
+const ThrowNotFound: React.FC = () => {
+ throw createError({ status: 404 });
+};
+
interface RouteProps {
request: Request;
match: MatchRouteResult;
@@ -102,6 +125,12 @@ interface RouteProps {
export interface PageRouteProps extends RouteProps {}
+export interface ErrorRouteProps {
+ error: Error;
+ serverError?: ReactServerErrorContext;
+ reset: () => void;
+}
+
export interface LayoutRouteProps extends React.PropsWithChildren
{}
function matchChild(input: string, node: RouteTreeNode) {
@@ -124,7 +153,8 @@ function matchChild(input: string, node: RouteTreeNode) {
}
//
-// general tree structure utils
+// general tree utils copied from vite-glob-routes
+// https://github.com/hi-ogawa/vite-plugins/blob/c2d22f9436ef868fc413f05f243323686a7aa143/packages/vite-glob-routes/src/react-router/route-utils.ts#L15-L22
//
type TreeNode = {
diff --git a/packages/react-server/src/lib/types-env.d.ts b/packages/react-server/src/lib/types-env.d.ts
index 1a4668840..d03fa7e98 100644
--- a/packages/react-server/src/lib/types-env.d.ts
+++ b/packages/react-server/src/lib/types-env.d.ts
@@ -7,7 +7,9 @@ declare module "react-server-dom-webpack/server.edge" {
export function renderToReadableStream(
node: React.ReactNode,
bundlerConfig: import("./types").BundlerConfig,
- opitons?: {},
+ opitons?: {
+ onError: import("react-dom/server").RenderToReadableStreamOptions["onError"];
+ },
): ReadableStream;
export function decodeReply(body: string | FormData): Promise;
diff --git a/packages/react-server/src/plugin/index.ts b/packages/react-server/src/plugin/index.ts
index ff53c293d..03046741a 100644
--- a/packages/react-server/src/plugin/index.ts
+++ b/packages/react-server/src/plugin/index.ts
@@ -149,18 +149,24 @@ export function vitePluginReactServer(options?: {
// TODO: workaround Vite self-reference import via global (Try Vite 5.2).
// https://github.com/vitejs/vite/pull/16068
- createVirtualPlugin("self-reference-workaround", () => {
+ createVirtualPlugin("self-reference-workaround1", () => {
return /* js */ `
import * as serverInternal from "@hiogawa/react-server/server-internal";
- globalThis.__REACT_SERVER_GLOBAL ??= {};
- globalThis.__REACT_SERVER_GLOBAL.serverInternal = serverInternal;
+ Object.assign(globalThis.__REACT_SERVER_GLOBAL ??= {}, { serverInternal });
+ `;
+ }),
+ createVirtualPlugin("self-reference-workaround2", () => {
+ return /* js */ `
+ import * as clientInternal from "@hiogawa/react-server/client-internal";
+ Object.assign(globalThis.__REACT_SERVER_GLOBAL ??= {}, { clientInternal });
`;
}),
createVirtualPlugin(
ENTRY_REACT_SERVER_WRAPPER.slice("virtual:".length),
() => {
return /* js */ `
- import "virtual:self-reference-workaround";
+ import "virtual:self-reference-workaround1";
+ import "virtual:self-reference-workaround2";
export * from "${ENTRY_REACT_SERVER}";
`;
},
diff --git a/packages/react-server/src/server.ts b/packages/react-server/src/server.ts
index 7aecf678f..27c937cea 100644
--- a/packages/react-server/src/server.ts
+++ b/packages/react-server/src/server.ts
@@ -1,2 +1,7 @@
// TODO: export type from root?
-export type { PageRouteProps, LayoutRouteProps } from "./lib/router";
+export type {
+ PageRouteProps,
+ LayoutRouteProps,
+ ErrorRouteProps,
+} from "./lib/router";
+export { createError, type ReactServerErrorContext } from "./lib/error";