Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support global-error.js #596

Merged
merged 13 commits into from
Aug 4, 2024
7 changes: 7 additions & 0 deletions packages/react-server/examples/basic/e2e/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1599,3 +1599,10 @@ test("mdx dynamic", async ({ page }) => {
await page.goto("/test/mdx/dynamic/no-such-post");
await page.getByText("Page not found :(").click();
});

test("global-error", async ({ page }) => {
await page.goto("/?test-error");
await page.getByRole("heading", { name: "Something went wrong." }).click();
await page.getByRole("link", { name: "Home" }).click();
await page.waitForURL("/");
});
36 changes: 36 additions & 0 deletions packages/react-server/examples/basic/src/routes/global-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";

export default function GlobalError() {
return (
<html>
<body>
<div
style={{
height: "100vh",
display: "flex",
flexDirection: "column",
placeContent: "center",
placeItems: "center",
gap: "4px",
fontSize: "16px",
}}
>
<h1>Something went wrong. Please try it again later.</h1>
<div style={{ fontSize: "14px" }}>
Back to{" "}
<a
href="/"
style={{
textDecoration: "underline",
textUnderlineOffset: "2px",
color: "#3451b2",
}}
>
Home
</a>
</div>
</div>
</body>
</html>
);
}
7 changes: 6 additions & 1 deletion packages/react-server/examples/basic/src/routes/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export default function Page() {
import type { PageProps } from "@hiogawa/react-server/server";

export default function Page(props: PageProps) {
if ("test-error" in props.searchParams) {
throw new Error("boom!");
}
return <div>Choose a page from the menu</div>;
}
12 changes: 9 additions & 3 deletions packages/react-server/src/entry/browser.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as virtualClientRoutes from "virtual:client-routes";
import { createDebug, memoize, tinyassert } from "@hiogawa/utils";
import type { RouterHistory } from "@tanstack/history";
import React from "react";
import ReactDOMClient from "react-dom/client";
import { initializeReactClientBrowser } from "../features/client-component/browser";
import { RootErrorBoundary } from "../features/error/error-boundary";
import { ErrorBoundary } from "../features/error/error-boundary";
import { DefaultGlobalErrorPage } from "../features/error/global-error";
import {
FlightDataContext,
LayoutRoot,
Expand Down Expand Up @@ -182,14 +184,18 @@ async function start() {
const routeManifest = await importRouteManifest();
let reactRootEl = (
<RouterContext.Provider value={router}>
<RootErrorBoundary>
<ErrorBoundary
errorComponent={
virtualClientRoutes.GlobalErrorPage ?? DefaultGlobalErrorPage
}
>
<FlightDataHandler>
<RouteManifestContext.Provider value={routeManifest}>
<RouteAssetLinks />
<LayoutRoot />
</RouteManifestContext.Provider>
</FlightDataHandler>
</RootErrorBoundary>
</ErrorBoundary>
</RouterContext.Provider>
);
if (!window.location.search.includes("__noStrict")) {
Expand Down
1 change: 1 addition & 0 deletions packages/react-server/src/features/assets/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export function vitePluginServerAssets({
collectStyle($__global.dev.server, {
entries: [
entryBrowser,
"virtual:client-routes",
// TODO: dev should also use RouteManifest to manage client css
...manager.clientReferenceMap.keys(),
],
Expand Down
30 changes: 1 addition & 29 deletions packages/react-server/src/features/error/error-boundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ import { tinyassert } from "@hiogawa/utils";
import React from "react";
import { useRouter } from "../router/client/router";
import type { ErrorPageProps } from "../router/server";
import {
getErrorContext,
getStatusText,
isNotFoundError,
isRedirectError,
} from "./shared";
import { getErrorContext, isNotFoundError, isRedirectError } from "./shared";

// cf.
// https://github.com/vercel/next.js/blob/33f8428f7066bf8b2ec61f025427ceb2a54c4bdf/packages/next/src/client/components/error-boundary.tsx
Expand Down Expand Up @@ -71,29 +66,6 @@ function ErrorAutoReset(props: Pick<ErrorPageProps, "reset">) {
return null;
}

export function RootErrorBoundary(props: React.PropsWithChildren) {
return <ErrorBoundary errorComponent={DefaultRootErrorPage} {...props} />;
}

// TODO: customizable
function DefaultRootErrorPage(props: ErrorPageProps) {
const status = props.serverError?.status;
const message = status
? `${status} ${getStatusText(status)}`
: "Unknown Error";
return (
<html>
<title>{message}</title>
<body>
<h1>{message}</h1>
<div>
Back to <a href="/">Home</a>
</div>
</body>
</html>
);
}

export class RedirectBoundary extends React.Component<React.PropsWithChildren> {
override state: { error: null } | { error: Error; redirectLocation: string } =
{ error: null };
Expand Down
30 changes: 30 additions & 0 deletions packages/react-server/src/features/error/global-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { ErrorPageProps } from "../../server";

// https://github.com/vercel/next.js/blob/677c9b372faef680d17e9ba224743f44e1107661/packages/next/src/build/webpack/loaders/next-app-loader.ts#L73
// https://github.com/vercel/next.js/blob/677c9b372faef680d17e9ba224743f44e1107661/packages/next/src/client/components/error-boundary.tsx#L145
export function DefaultGlobalErrorPage(props: ErrorPageProps) {
const message = props.serverError
? `Unknown Server Error (see server logs for the details)`
: `Unknown Client Error (see browser console for the details)`;
return (
<html>
<title>{message}</title>
<body
style={{
fontFamily:
'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
height: "100vh",
display: "flex",
flexDirection: "column",
placeContent: "center",
placeItems: "center",
fontSize: "14px",
fontWeight: 400,
lineHeight: "28px",
}}
>
<h2>{message}</h2>
</body>
</html>
);
}
7 changes: 7 additions & 0 deletions packages/react-server/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,13 @@ export function vitePluginReactServer(
return `export * from "/${outDir}/rsc/index.js";`;
}),

createVirtualPlugin("client-routes", () => {
return `
const glob = import.meta.glob("/${routeDir}/global-error.(js|jsx|ts|tsx)", { eager: true });
export const GlobalErrorPage = Object.values(glob)[0]?.default;
`;
}),

createVirtualPlugin(ENTRY_BROWSER_WRAPPER.slice("virtual:".length), () => {
// dev
if (!manager.buildType) {
Expand Down
4 changes: 4 additions & 0 deletions packages/react-server/src/plugin/virtual.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ declare module "virtual:server-routes" {
| import("../features/next/middleware").MiddlewareModule
| undefined;
}

declare module "virtual:client-routes" {
export const GlobalErrorPage: React.FC | undefined;
}