Skip to content

Commit

Permalink
feat: support global-error.js (#596)
Browse files Browse the repository at this point in the history
  • Loading branch information
hi-ogawa authored Aug 4, 2024
1 parent 1b65749 commit 40aa862
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 33 deletions.
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;
}

0 comments on commit 40aa862

Please sign in to comment.