diff --git a/.changeset/odd-numbers-film.md b/.changeset/odd-numbers-film.md new file mode 100644 index 00000000000..1e129194f69 --- /dev/null +++ b/.changeset/odd-numbers-film.md @@ -0,0 +1,32 @@ +--- +"@remix-run/react": patch +"@remix-run/server-runtime": patch +--- + +Add `future.v2_errorBoundary` flag to opt-into v2 `ErrorBoundary` behavior. This removes the separate `CatchBoundary` and `ErrorBoundary` and consolidates them into a single `ErrorBoundary` following the logic used by `errorElement` in React Router. You can then use `isRouteErrorResponse` to differentiate between thrown `Response`/`Error` instances. + +```jsx +// Current (Remix v1 default) +import { useCatch } from "@remix-run/react"; + +export function CatchBoundary() { + let caught = useCatch(); + return

{caught.status} {caught.data}

; +} + +export function ErrorBoundary({ error }) { + return

{error.message}

; +} + + +// Using future.v2_errorBoundary +import { isRouteErrorResponse, useRouteError } from "@remix-run/react"; + +export function ErrorBoundary() { + let error = useRouteError(); + + return isRouteErrorResponse(error) ? +

{error.status} {error.data}

: +

{error.message}

; +} +``` \ No newline at end of file diff --git a/integration/error-boundary-v2-test.ts b/integration/error-boundary-v2-test.ts new file mode 100644 index 00000000000..bbacdfa3359 --- /dev/null +++ b/integration/error-boundary-v2-test.ts @@ -0,0 +1,237 @@ +import type { Page } from "@playwright/test"; +import { test, expect } from "@playwright/test"; +import { ServerMode } from "@remix-run/server-runtime/mode"; + +import { createAppFixture, createFixture, js } from "./helpers/create-fixture"; +import type { Fixture, AppFixture } from "./helpers/create-fixture"; +import { PlaywrightFixture } from "./helpers/playwright-fixture"; + +test.describe("V2 Singular ErrorBoundary (future.v2_errorBoundary)", () => { + let fixture: Fixture; + let appFixture: AppFixture; + let oldConsoleError: () => void; + + test.beforeAll(async () => { + fixture = await createFixture({ + future: { + v2_errorBoundary: true, + }, + files: { + "app/root.jsx": js` + import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; + + export default function Root() { + return ( + + + + + + +
+ +
+ + + + ); + } + `, + + "app/routes/parent.jsx": js` + import { + Link, + Outlet, + isRouteErrorResponse, + useLoaderData, + useRouteError, + } from "@remix-run/react"; + + export function loader() { + return "PARENT LOADER"; + } + + export default function Component() { + return ( +
+ +

{useLoaderData()}

+ +
+ ) + } + + export function ErrorBoundary() { + let error = useRouteError(); + return isRouteErrorResponse(error) ? +

{error.status + ' ' + error.data}

: +

{error.message}

; + } + `, + + "app/routes/parent/child-with-boundary.jsx": js` + import { + isRouteErrorResponse, + useLoaderData, + useLocation, + useRouteError, + } from "@remix-run/react"; + + export function loader({ request }) { + let errorType = new URL(request.url).searchParams.get('type'); + if (errorType === 'response') { + throw new Response('Loader Response', { status: 418 }); + } else if (errorType === 'error') { + throw new Error('Loader Error'); + } + return "CHILD LOADER"; + } + + export default function Component() {; + let data = useLoaderData(); + if (new URLSearchParams(useLocation().search).get('type') === "render") { + throw new Error("Render Error"); + } + return

{data}

; + } + + export function ErrorBoundary() { + let error = useRouteError(); + return isRouteErrorResponse(error) ? +

{error.status + ' ' + error.data}

: +

{error.message}

; + } + `, + + "app/routes/parent/child-without-boundary.jsx": js` + import { useLoaderData, useLocation } from "@remix-run/react"; + + export function loader({ request }) { + let errorType = new URL(request.url).searchParams.get('type'); + if (errorType === 'response') { + throw new Response('Loader Response', { status: 418 }); + } else if (errorType === 'error') { + throw new Error('Loader Error'); + } + return "CHILD LOADER"; + } + + export default function Component() {; + let data = useLoaderData(); + if (new URLSearchParams(useLocation().search).get('type') === "render") { + throw new Error("Render Error"); + } + return

{data}

; + } + `, + }, + }); + + appFixture = await createAppFixture(fixture, ServerMode.Development); + }); + + test.afterAll(() => { + appFixture.close(); + }); + + test.beforeEach(({ page }) => { + oldConsoleError = console.error; + console.error = () => {}; + }); + + test.afterEach(() => { + console.error = oldConsoleError; + }); + + test.describe("without JavaScript", () => { + test.use({ javaScriptEnabled: false }); + runBoundaryTests(); + }); + + test.describe("with JavaScript", () => { + test.use({ javaScriptEnabled: true }); + runBoundaryTests(); + }); + + function runBoundaryTests() { + // Shorthand util to wait for an element to appear before asserting it + async function waitForAndAssert( + page: Page, + app: PlaywrightFixture, + selector: string, + match: string + ) { + await page.waitForSelector(selector); + expect(await app.getHtml(selector)).toMatch(match); + } + + test("No errors", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/parent"); + await app.clickLink("/parent/child-with-boundary"); + await waitForAndAssert(page, app, "#child-data", "CHILD LOADER"); + }); + + test("Throwing a Response to own boundary", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/parent"); + await app.clickLink("/parent/child-with-boundary?type=response"); + await waitForAndAssert( + page, + app, + "#child-error-response", + "418 Loader Response" + ); + }); + + test("Throwing an Error to own boundary", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/parent"); + await app.clickLink("/parent/child-with-boundary?type=error"); + await waitForAndAssert(page, app, "#child-error", "Loader Error"); + }); + + test("Throwing a render error to own boundary", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/parent"); + await app.clickLink("/parent/child-with-boundary?type=render"); + await waitForAndAssert(page, app, "#child-error", "Render Error"); + }); + + test("Throwing a Response to parent boundary", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/parent"); + await app.clickLink("/parent/child-without-boundary?type=response"); + await waitForAndAssert( + page, + app, + "#parent-error-response", + "418 Loader Response" + ); + }); + + test("Throwing an Error to parent boundary", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/parent"); + await app.clickLink("/parent/child-without-boundary?type=error"); + await waitForAndAssert(page, app, "#parent-error", "Loader Error"); + }); + + test("Throwing a render error to parent boundary", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/parent"); + await app.clickLink("/parent/child-without-boundary?type=render"); + await waitForAndAssert(page, app, "#parent-error", "Render Error"); + }); + } +}); diff --git a/integration/helpers/create-fixture.ts b/integration/helpers/create-fixture.ts index 7cda8189132..4238d6a3b75 100644 --- a/integration/helpers/create-fixture.ts +++ b/integration/helpers/create-fixture.ts @@ -7,6 +7,7 @@ import stripIndent from "strip-indent"; import { sync as spawnSync } from "cross-spawn"; import type { JsonObject } from "type-fest"; import type { ServerMode } from "@remix-run/server-runtime/mode"; +import type { FutureConfig } from "@remix-run/server-runtime/entry"; import type { ServerBuild } from "../../build/node_modules/@remix-run/server-runtime"; import { createRequestHandler } from "../../build/node_modules/@remix-run/server-runtime"; @@ -20,6 +21,7 @@ interface FixtureInit { files?: { [filename: string]: string }; template?: "cf-template" | "deno-template" | "node-template"; setup?: "node" | "cloudflare"; + future?: Partial; } export type Fixture = Awaited>; @@ -174,6 +176,22 @@ export async function createFixtureProject( ); } } + + if (init.future) { + let contents = fse.readFileSync( + path.join(projectDir, "remix.config.js"), + "utf-8" + ); + if (!contents.includes("future: {},")) { + throw new Error("Invalid formatted remix.config.js in template"); + } + contents = contents.replace( + "future: {},", + "future: " + JSON.stringify(init.future) + "," + ); + fse.writeFileSync(path.join(projectDir, "remix.config.js"), contents); + } + await writeTestFiles(init, projectDir); build(projectDir, init.buildStdio, init.sourcemap); diff --git a/integration/helpers/node-template/remix.config.js b/integration/helpers/node-template/remix.config.js index adf2a0b5d3e..b7f693265aa 100644 --- a/integration/helpers/node-template/remix.config.js +++ b/integration/helpers/node-template/remix.config.js @@ -5,4 +5,8 @@ module.exports = { // assetsBuildDirectory: "public/build", // serverBuildPath: "build/index.js", // publicPath: "/build/", + + // !!! Don't adust this without changing the code that overwrites this + // in createFixtureProject() + future: {}, }; diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index a88bb37f599..38cbb33b315 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -26,6 +26,7 @@ describe("readConfig", () => { unstable_cssModules: expect.any(Boolean), unstable_cssSideEffectImports: expect.any(Boolean), unstable_vanillaExtract: expect.any(Boolean), + v2_errorBoundary: expect.any(Boolean), v2_meta: expect.any(Boolean), v2_routeConvention: expect.any(Boolean), }, @@ -43,6 +44,7 @@ describe("readConfig", () => { "unstable_cssModules": Any, "unstable_cssSideEffectImports": Any, "unstable_vanillaExtract": Any, + "v2_errorBoundary": Any, "v2_meta": Any, "v2_routeConvention": Any, }, diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 1733a292604..143c9f6fee1 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -36,6 +36,7 @@ interface FutureConfig { unstable_cssModules: boolean; unstable_cssSideEffectImports: boolean; unstable_vanillaExtract: boolean; + v2_errorBoundary: boolean; v2_meta: boolean; v2_routeConvention: boolean; } @@ -495,6 +496,7 @@ export async function readConfig( unstable_cssSideEffectImports: appConfig.future?.unstable_cssSideEffectImports === true, unstable_vanillaExtract: appConfig.future?.unstable_vanillaExtract === true, + v2_errorBoundary: appConfig.future?.v2_errorBoundary === true, v2_meta: appConfig.future?.v2_meta === true, v2_routeConvention: appConfig.future?.v2_routeConvention === true, }; diff --git a/packages/remix-react/browser.tsx b/packages/remix-react/browser.tsx index ce280c23ca2..38110fe4cb0 100644 --- a/packages/remix-react/browser.tsx +++ b/packages/remix-react/browser.tsx @@ -39,7 +39,8 @@ export function RemixBrowser(_props: RemixBrowserProps): ReactElement { if (!router) { let routes = createClientRoutes( window.__remixManifest.routes, - window.__remixRouteModules + window.__remixRouteModules, + window.__remixContext.future ); let hydrationData = window.__remixContext.state; diff --git a/packages/remix-react/components.tsx b/packages/remix-react/components.tsx index fadac712ac0..191f7579150 100644 --- a/packages/remix-react/components.tsx +++ b/packages/remix-react/components.tsx @@ -42,9 +42,9 @@ import type { AppData } from "./data"; import type { EntryContext, RemixContextObject } from "./entry"; import { RemixRootDefaultErrorBoundary, - RemixErrorBoundary, RemixRootDefaultCatchBoundary, RemixCatchBoundary, + V2_RemixRootDefaultErrorBoundary, } from "./errorBoundaries"; import invariant from "./invariant"; import { @@ -141,7 +141,7 @@ export function RemixRoute({ id }: { id: string }) { } export function RemixRouteError({ id }: { id: string }) { - let { routeModules } = useRemixContext(); + let { future, routeModules } = useRemixContext(); // This checks prevent cryptic error messages such as: 'Cannot read properties of undefined (reading 'root')' invariant( @@ -151,17 +151,20 @@ export function RemixRouteError({ id }: { id: string }) { ); let error = useRouteError(); - let location = useLocation(); let { CatchBoundary, ErrorBoundary } = routeModules[id]; - // POC for potential v2 error boundary handling - // if (future.v2_errorBoundary) { - // // Provide defaults for the root route if they are not present - // if (id === "root") { - // ErrorBoundary ||= RemixRootDefaultNewErrorBoundary; - // } - // return - // } + if (future.v2_errorBoundary) { + // Provide defaults for the root route if they are not present + if (id === "root") { + ErrorBoundary ||= V2_RemixRootDefaultErrorBoundary; + } + if (ErrorBoundary) { + // TODO: Unsure if we can satisfy the typings here + // @ts-expect-error + return ; + } + throw error; + } // Provide defaults for the root route if they are not present if (id === "root") { @@ -177,13 +180,7 @@ export function RemixRouteError({ id }: { id: string }) { ErrorBoundary ) { // Internal framework-thrown ErrorResponses - return ( - - ); + return ; } if (CatchBoundary) { // User-thrown ErrorResponses @@ -198,13 +195,7 @@ export function RemixRouteError({ id }: { id: string }) { if (error instanceof Error && ErrorBoundary) { // User- or framework-thrown Errors - return ( - - ); + return ; } throw error; diff --git a/packages/remix-react/entry.ts b/packages/remix-react/entry.ts index 77f17b4988b..33bef61688f 100644 --- a/packages/remix-react/entry.ts +++ b/packages/remix-react/entry.ts @@ -21,6 +21,7 @@ export interface FutureConfig { unstable_cssModules: boolean; unstable_cssSideEffectImports: boolean; unstable_vanillaExtract: boolean; + v2_errorBoundary: boolean; v2_meta: boolean; } diff --git a/packages/remix-react/errorBoundaries.tsx b/packages/remix-react/errorBoundaries.tsx index f2f8916e860..7b1419dfd55 100644 --- a/packages/remix-react/errorBoundaries.tsx +++ b/packages/remix-react/errorBoundaries.tsx @@ -1,6 +1,6 @@ import React, { useContext } from "react"; -import type { ErrorResponse } from "@remix-run/router"; -import type { Location } from "react-router-dom"; +import type { ErrorResponse, Location } from "@remix-run/router"; +import { isRouteErrorResponse, useRouteError } from "react-router-dom"; import type { CatchBoundaryComponent, @@ -109,6 +109,23 @@ export function RemixRootDefaultErrorBoundary({ error }: { error: Error }) { ); } +export function V2_RemixRootDefaultErrorBoundary() { + let error = useRouteError(); + if (isRouteErrorResponse(error)) { + return ; + } else if (error instanceof Error) { + return ; + } else { + let errorString = + error == null + ? "Unknown Error" + : typeof error === "object" && "toString" in error + ? error.toString() + : JSON.stringify(error); + return ; + } +} + let RemixCatchContext = React.createContext( undefined ); @@ -150,6 +167,14 @@ export function RemixCatchBoundary({ */ export function RemixRootDefaultCatchBoundary() { let caught = useCatch(); + return ; +} + +function RemixRootDefaultCatchBoundaryImpl({ + caught, +}: { + caught: ThrownResponse; +}) { return ( diff --git a/packages/remix-react/index.tsx b/packages/remix-react/index.tsx index 5af30425e82..5bad944d4da 100644 --- a/packages/remix-react/index.tsx +++ b/packages/remix-react/index.tsx @@ -13,6 +13,7 @@ export type { export { Form, Outlet, + isRouteErrorResponse, useBeforeUnload, useFormAction, useHref, @@ -25,6 +26,7 @@ export { useParams, useResolvedPath, useRevalidator, + useRouteError, useRouteLoaderData, useSearchParams, useSubmit, diff --git a/packages/remix-react/routeModules.ts b/packages/remix-react/routeModules.ts index db5496ce288..454b33cc4c5 100644 --- a/packages/remix-react/routeModules.ts +++ b/packages/remix-react/routeModules.ts @@ -17,7 +17,7 @@ export interface RouteModules { export interface RouteModule { CatchBoundary?: CatchBoundaryComponent; - ErrorBoundary?: ErrorBoundaryComponent; + ErrorBoundary?: ErrorBoundaryComponent | V2_ErrorBoundaryComponent; default: RouteComponent; handle?: RouteHandle; links?: LinksFunction; @@ -43,6 +43,13 @@ export type CatchBoundaryComponent = ComponentType<{}>; */ export type ErrorBoundaryComponent = ComponentType<{ error: Error }>; +/** + * V2 version of the ErrorBoundary that eliminates the distinction between + * Error and Catch Boundaries and behaves like RR 6.4 errorElement and captures + * errors with useRouteError() + */ +export type V2_ErrorBoundaryComponent = ComponentType; + /** * A function that defines `` tags to be inserted into the `` of * the document on route transitions. diff --git a/packages/remix-react/routes.tsx b/packages/remix-react/routes.tsx index 15c237a7da3..0b342f808fa 100644 --- a/packages/remix-react/routes.tsx +++ b/packages/remix-react/routes.tsx @@ -10,6 +10,7 @@ import { redirect } from "react-router-dom"; import type { RouteModules } from "./routeModules"; import { loadRouteModule } from "./routeModules"; import { fetchData, isCatchResponse, isRedirectResponse } from "./data"; +import type { FutureConfig } from "./entry"; import { prefetchStyleLinks } from "./links"; import invariant from "./invariant"; import { RemixRoute, RemixRouteError } from "./components"; @@ -41,13 +42,18 @@ export interface EntryRoute extends Route { export function createServerRoutes( manifest: RouteManifest, routeModules: RouteModules, + future: FutureConfig, parentId?: string ): DataRouteObject[] { return Object.values(manifest) .filter((route) => route.parentId === parentId) .map((route) => { let hasErrorBoundary = - route.id === "root" || route.hasErrorBoundary || route.hasCatchBoundary; + future.v2_errorBoundary === true + ? route.id === "root" || route.hasErrorBoundary + : route.id === "root" || + route.hasCatchBoundary || + route.hasErrorBoundary; let dataRoute: DataRouteObject = { caseSensitive: route.caseSensitive, element: , @@ -62,7 +68,12 @@ export function createServerRoutes( // since they're for a static render }; - let children = createServerRoutes(manifest, routeModules, route.id); + let children = createServerRoutes( + manifest, + routeModules, + future, + route.id + ); if (children.length > 0) dataRoute.children = children; return dataRoute; }); @@ -71,13 +82,18 @@ export function createServerRoutes( export function createClientRoutes( manifest: RouteManifest, routeModulesCache: RouteModules, + future: FutureConfig, parentId?: string ): DataRouteObject[] { return Object.values(manifest) .filter((entryRoute) => entryRoute.parentId === parentId) .map((route) => { let hasErrorBoundary = - route.id === "root" || route.hasErrorBoundary || route.hasCatchBoundary; + future.v2_errorBoundary === true + ? route.id === "root" || route.hasErrorBoundary + : route.id === "root" || + route.hasCatchBoundary || + route.hasErrorBoundary; let dataRoute: DataRouteObject = { caseSensitive: route.caseSensitive, @@ -95,7 +111,12 @@ export function createClientRoutes( action: createDataFunction(route, routeModulesCache, true), shouldRevalidate: createShouldRevalidate(route, routeModulesCache), }; - let children = createClientRoutes(manifest, routeModulesCache, route.id); + let children = createClientRoutes( + manifest, + routeModulesCache, + future, + route.id + ); if (children.length > 0) dataRoute.children = children; return dataRoute; }); diff --git a/packages/remix-react/server.tsx b/packages/remix-react/server.tsx index 61bc6816c47..1f378692480 100644 --- a/packages/remix-react/server.tsx +++ b/packages/remix-react/server.tsx @@ -29,7 +29,11 @@ export function RemixServer({ context, url }: RemixServerProps): ReactElement { } let { manifest, routeModules, serverHandoffString } = context; - let routes = createServerRoutes(manifest.routes, routeModules); + let routes = createServerRoutes( + manifest.routes, + routeModules, + context.future + ); let router = createStaticRouter(routes, context.staticHandlerContext); return ( diff --git a/packages/remix-server-runtime/__tests__/data-test.ts b/packages/remix-server-runtime/__tests__/data-test.ts index fca71a1f9fe..5b64f3c7118 100644 --- a/packages/remix-server-runtime/__tests__/data-test.ts +++ b/packages/remix-server-runtime/__tests__/data-test.ts @@ -21,9 +21,10 @@ describe("loaders", () => { }, }, entry: { module: {} }, + future: {}, } as unknown as ServerBuild; - let handler = createRequestHandler(build, {}); + let handler = createRequestHandler(build); let request = new Request( "http://example.com/random?_data=routes/random&foo=bar", @@ -59,9 +60,10 @@ describe("loaders", () => { }, }, entry: { module: {} }, + future: {}, } as unknown as ServerBuild; - let handler = createRequestHandler(build, {}); + let handler = createRequestHandler(build); let request = new Request( "http://example.com/random?_data=routes/random&foo=bar", @@ -93,9 +95,10 @@ describe("loaders", () => { }, }, entry: { module: {} }, + future: {}, } as unknown as ServerBuild; - let handler = createRequestHandler(build, {}); + let handler = createRequestHandler(build); let request = new Request( "http://example.com/random?_data=routes/random&index&foo=bar", @@ -127,9 +130,10 @@ describe("loaders", () => { }, }, entry: { module: {} }, + future: {}, } as unknown as ServerBuild; - let handler = createRequestHandler(build, {}); + let handler = createRequestHandler(build); let request = new Request( "http://example.com/random?_data=routes/random&index&foo=bar&index=test", diff --git a/packages/remix-server-runtime/__tests__/handler-test.ts b/packages/remix-server-runtime/__tests__/handler-test.ts index bb676a8e9d3..8b0e05688ef 100644 --- a/packages/remix-server-runtime/__tests__/handler-test.ts +++ b/packages/remix-server-runtime/__tests__/handler-test.ts @@ -16,6 +16,10 @@ describe("createRequestHandler", () => { }, assets: {} as any, entry: { module: {} as any }, + future: { + v2_errorBoundary: false, + v2_meta: false, + }, }); let response = await handler( diff --git a/packages/remix-server-runtime/__tests__/server-test.ts b/packages/remix-server-runtime/__tests__/server-test.ts index 6834bd4b4bd..e4d1951728d 100644 --- a/packages/remix-server-runtime/__tests__/server-test.ts +++ b/packages/remix-server-runtime/__tests__/server-test.ts @@ -55,6 +55,7 @@ describe("server", () => { }, }, }, + future: {}, } as unknown as ServerBuild; describe("createRequestHandler", () => { diff --git a/packages/remix-server-runtime/__tests__/utils.ts b/packages/remix-server-runtime/__tests__/utils.ts index 9a75e5ce5c2..21223d7d5d0 100644 --- a/packages/remix-server-runtime/__tests__/utils.ts +++ b/packages/remix-server-runtime/__tests__/utils.ts @@ -80,6 +80,7 @@ export function mockServerBuild( }, {} ), + future: {}, }; } diff --git a/packages/remix-server-runtime/entry.ts b/packages/remix-server-runtime/entry.ts index 152bf2ad13a..7dddbb38061 100644 --- a/packages/remix-server-runtime/entry.ts +++ b/packages/remix-server-runtime/entry.ts @@ -15,6 +15,7 @@ export interface FutureConfig { unstable_cssModules: true; unstable_cssSideEffectImports: boolean; unstable_vanillaExtract: boolean; + v2_errorBoundary: boolean; v2_meta: boolean; } diff --git a/packages/remix-server-runtime/routes.ts b/packages/remix-server-runtime/routes.ts index 19a48e589f4..6c63ae870b1 100644 --- a/packages/remix-server-runtime/routes.ts +++ b/packages/remix-server-runtime/routes.ts @@ -5,6 +5,7 @@ import type { } from "@remix-run/router"; import { callRouteActionRR, callRouteLoaderRR } from "./data"; +import type { FutureConfig } from "./entry"; import type { ServerRouteModule } from "./routeModules"; export interface RouteManifest { @@ -54,17 +55,21 @@ export function createRoutes( // createStaticHandler export function createStaticHandlerDataRoutes( manifest: ServerRouteManifest, + future: FutureConfig, parentId?: string ): AgnosticDataRouteObject[] { return Object.values(manifest) .filter((route) => route.parentId === parentId) .map((route) => { + let hasErrorBoundary = + future.v2_errorBoundary === true + ? route.id === "root" || route.module.ErrorBoundary != null + : route.id === "root" || + route.module.CatchBoundary != null || + route.module.ErrorBoundary != null; let commonRoute = { // Always include root due to default boundaries - hasErrorBoundary: - route.id === "root" || - route.module.CatchBoundary != null || - route.module.ErrorBoundary != null, + hasErrorBoundary, id: route.id, path: route.path, loader: route.module.loader @@ -97,7 +102,7 @@ export function createStaticHandlerDataRoutes( } : { caseSensitive: route.caseSensitive, - children: createStaticHandlerDataRoutes(manifest, route.id), + children: createStaticHandlerDataRoutes(manifest, future, route.id), ...commonRoute, }; }); diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index 7c0142760cc..d62f8a19efd 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -34,7 +34,7 @@ export const createRequestHandler: CreateRequestHandlerFunction = ( mode ) => { let routes = createRoutes(build.routes); - let dataRoutes = createStaticHandlerDataRoutes(build.routes); + let dataRoutes = createStaticHandlerDataRoutes(build.routes, build.future); let serverMode = isServerMode(mode) ? mode : ServerMode.Production; let staticHandler = createStaticHandler(dataRoutes); @@ -230,7 +230,9 @@ async function handleDocumentRequestRR( } // Restructure context.errors to the right Catch/Error Boundary - differentiateCatchVersusErrorBoundaries(build, context); + if (build.future.v2_errorBoundary !== true) { + differentiateCatchVersusErrorBoundaries(build, context); + } let headers = getDocumentHeadersRR(build, context); @@ -266,7 +268,9 @@ async function handleDocumentRequestRR( ); // Restructure context.errors to the right Catch/Error Boundary - differentiateCatchVersusErrorBoundaries(build, context); + if (build.future.v2_errorBoundary !== true) { + differentiateCatchVersusErrorBoundaries(build, context); + } // Update entryContext for the second render pass entryContext = { diff --git a/packages/remix-testing/create-remix-stub.tsx b/packages/remix-testing/create-remix-stub.tsx index fef6ea3021b..e84ec5bf288 100644 --- a/packages/remix-testing/create-remix-stub.tsx +++ b/packages/remix-testing/create-remix-stub.tsx @@ -68,10 +68,11 @@ export function createRemixStub(routes: AgnosticDataRouteObject[]) { if (remixContextRef.current == null) { remixContextRef.current = { future: { - v2_meta: false, unstable_cssModules: false, unstable_cssSideEffectImports: false, unstable_vanillaExtract: false, + v2_errorBoundary: false, + v2_meta: false, ...remixConfigFuture, }, manifest: createManifest(routes),