diff --git a/.changeset/remix-testing-meta-links.md b/.changeset/remix-testing-meta-links.md
new file mode 100644
index 00000000000..3613d5b55d2
--- /dev/null
+++ b/.changeset/remix-testing-meta-links.md
@@ -0,0 +1,6 @@
+---
+"@remix-run/testing": minor
+---
+
+* `unstable_createRemixStub` now supports adding `meta`/`links` functions on stubbed Remix routes
+* ⚠️ `unstable_createRemixStub` no longer supports the `element`/`errorElement` properties on routes. You must use `Component`/`ErrorBoundary` to match what you would export from a Remix route module.
diff --git a/integration/meta-test.ts b/integration/meta-test.ts
deleted file mode 100644
index 40e1321741f..00000000000
--- a/integration/meta-test.ts
+++ /dev/null
@@ -1,273 +0,0 @@
-import { test, expect } from "@playwright/test";
-
-import {
- createAppFixture,
- createFixture,
- js,
-} from "./helpers/create-fixture.js";
-import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
-import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
-
-test.describe("meta", () => {
- let fixture: Fixture;
- let appFixture: AppFixture;
-
- // disable JS for all tests in this file
- // to only disable them for some, add another test.describe()
- // and move the following line there
- test.use({ javaScriptEnabled: false });
-
- test.beforeAll(async () => {
- fixture = await createFixture({
- config: {
- ignoredRouteFiles: ["**/.*"],
- },
- files: {
- "app/root.tsx": js`
- import { json } from "@remix-run/node";
- import { Meta, Links, Outlet, Scripts } from "@remix-run/react";
-
- export const loader = async () =>
- json({
- description: "This is a meta page",
- title: "Meta Page",
- });
-
- export const meta = ({ data }) => [
- { charSet: "utf-8" },
- { name: "description", content: data.description },
- { property: "og:image", content: "https://picsum.photos/200/200" },
- { property: "og:type", content: data.contentType }, // undefined
- { title: data.title },
- ];
-
- export default function Root() {
- return (
-
-
-
-
-
-
-
-
-
-
- );
- }
- `,
-
- "app/routes/_index.tsx": js`
- export const meta = ({ data, matches }) =>
- matches.flatMap((match) => match.meta);
-
- export default function Index() {
- return This is the index file
;
- }
- `,
-
- "app/routes/no-meta-export.tsx": js`
- export default function NoMetaExport() {
- return Parent meta here!
;
- }
- `,
-
- "app/routes/empty-meta-function.tsx": js`
- export const meta = () => [];
- export default function EmptyMetaFunction() {
- return No meta here!
;
- }
- `,
-
- "app/routes/authors.$authorId.tsx": js`
- import { json } from "@remix-run/node";
-
- export async function loader({ params }) {
- return json({
- author: {
- id: params.authorId,
- name: "Sonny Day",
- address: {
- streetAddress: "123 Sunset Cliffs Blvd",
- city: "San Diego",
- state: "CA",
- zip: "92107",
- },
- emails: [
- "sonnyday@fancymail.com",
- "surfergal@veryprofessional.org",
- ],
- },
- });
- }
-
- export function meta({ data }) {
- let { author } = data;
- return [
- { title: data.name + " Profile" },
- {
- tagName: "link",
- rel: "canonical",
- href: "https://website.com/authors/" + author.id,
- },
- {
- "script:ld+json": {
- "@context": "http://schema.org",
- "@type": "Person",
- "name": author.name,
- "address": {
- "@type": "PostalAddress",
- "streetAddress": author.address.streetAddress,
- "addressLocality": author.address.city,
- "addressRegion": author.address.state,
- "postalCode": author.address.zip,
- },
- "email": author.emails,
- },
- },
- ];
- }
- export default function AuthorBio() {
- return Bio here!
;
- }
- `,
-
- "app/routes/music.tsx": js`
- export function meta({ data, matches }) {
- let rootModule = matches.find(match => match.id === "root");
- let rootCharSet = rootModule.meta.find(meta => meta.charSet);
- return [
- rootCharSet,
- { title: "What's My Age Again?" },
- { property: "og:type", content: "music.song" },
- { property: "music:musician", content: "https://www.blink182.com/" },
- { property: "music:duration", content: 182 },
- ];
- }
-
- export default function Music() {
- return Music
;
- }
- `,
-
- "app/routes/error.tsx": js`
- import { Link, useRouteError } from '@remix-run/react'
-
- export function loader() {
- throw new Error('lol oops')
- }
-
- export const meta = (args) => {
- return [{ title: args.error ? "Oops!" : "Home"}]
- }
-
- export default function Error() {
- return Error
- }
-
- export function ErrorBoundary() {
- return Error boundary
- }
- `,
- },
- });
- appFixture = await createAppFixture(fixture);
- });
-
- test.afterAll(() => {
- appFixture.close();
- });
-
- test("no meta export renders meta from nearest route meta in the tree", async ({
- page,
- }) => {
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/no-meta-export");
- expect(await app.getHtml('meta[name="description"]')).toBeTruthy();
- });
-
- test("empty meta array does not render a tag", async ({ page }) => {
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/empty-meta-function");
- await expect(app.getHtml("title")).rejects.toThrowError(
- 'No element matches selector "title"'
- );
- });
-
- test("meta from `matches` renders meta tags", async ({ page }) => {
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/music");
- expect(await app.getHtml('meta[charset="utf-8"]')).toBeTruthy();
- });
-
- test("{ charSet } adds a ", async ({ page }) => {
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- expect(await app.getHtml('meta[charset="utf-8"]')).toBeTruthy();
- });
-
- test("{ title } adds a ", async ({ page }) => {
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- expect(await app.getHtml("title")).toBeTruthy();
- });
-
- test("{ property: 'og:*', content: '*' } adds a ", async ({
- page,
- }) => {
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- expect(await app.getHtml('meta[property="og:image"]')).toBeTruthy();
- });
-
- test("{ 'script:ld+json': {} } adds a ", async ({
- page,
- }) => {
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/authors/1");
- let scriptTag = await app.getHtml('script[type="application/ld+json"]');
- let scriptContents = scriptTag
- .replace('", "")
- .trim();
-
- expect(JSON.parse(scriptContents)).toEqual({
- "@context": "http://schema.org",
- "@type": "Person",
- name: "Sonny Day",
- address: {
- "@type": "PostalAddress",
- streetAddress: "123 Sunset Cliffs Blvd",
- addressLocality: "San Diego",
- addressRegion: "CA",
- postalCode: "92107",
- },
- email: ["sonnyday@fancymail.com", "surfergal@veryprofessional.org"],
- });
- });
-
- test("{ tagName: 'link' } adds a ", async ({ page }) => {
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/authors/1");
- expect(await app.getHtml('link[rel="canonical"]')).toBeTruthy();
- });
-
- test("loader errors are passed to meta", async ({ page }) => {
- let restoreErrors = hideErrors();
-
- new PlaywrightFixture(appFixture, page);
- let response = await fixture.requestDocument("/error");
- expect(await response.text()).toMatch("Oops!");
-
- restoreErrors();
- });
-});
-
-function hideErrors() {
- let oldConsoleError: any;
- oldConsoleError = console.error;
- console.error = () => {};
- return () => {
- console.error = oldConsoleError;
- };
-}
diff --git a/packages/remix-react/__tests__/integration/meta-test.tsx b/packages/remix-react/__tests__/integration/meta-test.tsx
new file mode 100644
index 00000000000..39ce5e06aca
--- /dev/null
+++ b/packages/remix-react/__tests__/integration/meta-test.tsx
@@ -0,0 +1,317 @@
+import * as React from "react";
+import { Meta, Outlet } from "@remix-run/react";
+import { unstable_createRemixStub } from "@remix-run/testing";
+import { prettyDOM, render } from "@testing-library/react";
+
+const getHtml = (c: HTMLElement) =>
+ prettyDOM(c, undefined, { highlight: false });
+
+describe("meta", () => {
+ it("no meta export renders meta from nearest route meta in the tree", () => {
+ let RemixStub = unstable_createRemixStub([
+ {
+ id: "root",
+ path: "/",
+ meta: ({ data }) => [
+ { name: "description", content: data.description },
+ { title: data.title },
+ ],
+ Component() {
+ return (
+ <>
+
+
+ >
+ );
+ },
+ children: [
+ {
+ index: true,
+ Component() {
+ return Parent meta here!
;
+ },
+ },
+ ],
+ },
+ ]);
+
+ let { container } = render(
+
+ );
+
+ expect(getHtml(container)).toMatchInlineSnapshot(`
+ "
+
+
+ Meta Page
+
+
+ Parent meta here!
+
+
"
+ `);
+ });
+
+ it("empty meta array does not render a tag", () => {
+ let RemixStub = unstable_createRemixStub([
+ {
+ path: "/",
+ meta: () => [],
+ Component() {
+ return (
+ <>
+
+ No meta here!
+ >
+ );
+ },
+ },
+ ]);
+
+ let { container } = render();
+
+ expect(getHtml(container)).toMatchInlineSnapshot(`
+ ""
+ `);
+ });
+
+ it("meta from `matches` renders meta tags", () => {
+ let RemixStub = unstable_createRemixStub([
+ {
+ id: "root",
+ path: "/",
+ meta: () => [{ charSet: "utf-8" }],
+ Component() {
+ return (
+ <>
+
+
+ >
+ );
+ },
+ children: [
+ {
+ index: true,
+ meta({ matches }) {
+ let rootModule = matches.find((match) => match.id === "root");
+ // @ts-expect-error
+ let rootCharSet = rootModule?.meta.find((meta) => meta.charSet);
+ return [rootCharSet, { title: "Child title" }];
+ },
+ Component() {
+ return Matches Meta
;
+ },
+ },
+ ],
+ },
+ ]);
+
+ let { container } = render();
+
+ expect(getHtml(container)).toMatchInlineSnapshot(`
+ "
+
+
+ Child title
+
+
+ Matches Meta
+
+
"
+ `);
+ });
+
+ it("{ charSet } adds a ", () => {
+ let RemixStub = unstable_createRemixStub([
+ {
+ path: "/",
+ meta: () => [{ charSet: "utf-8" }],
+ Component: Meta,
+ },
+ ]);
+
+ let { container } = render();
+
+ expect(getHtml(container)).toMatchInlineSnapshot(`
+ "
+
+
"
+ `);
+ });
+
+ it("{ title } adds a ", () => {
+ let RemixStub = unstable_createRemixStub([
+ {
+ path: "/",
+ meta: () => [{ title: "Document Title" }],
+ Component: Meta,
+ },
+ ]);
+
+ let { container } = render();
+
+ expect(getHtml(container)).toMatchInlineSnapshot(`
+ "
+
+ Document Title
+
+ "
+ `);
+ });
+
+ it("{ property: 'og:*', content: '*' } adds a ", () => {
+ let RemixStub = unstable_createRemixStub([
+ {
+ path: "/",
+ meta: () => [
+ { property: "og:image", content: "https://picsum.photos/200/200" },
+ { property: "og:type", content: undefined },
+ ],
+ Component: Meta,
+ },
+ ]);
+
+ let { container } = render();
+ expect(getHtml(container)).toMatchInlineSnapshot(`
+ "
+
+
+
"
+ `);
+ });
+
+ it("{ 'script:ld+json': {} } adds a ", () => {
+ let jsonLd = {
+ "@context": "http://schema.org",
+ "@type": "Person",
+ name: "Sonny Day",
+ address: {
+ "@type": "PostalAddress",
+ streetAddress: "123 Sunset Cliffs Blvd",
+ addressLocality: "San Diego",
+ addressRegion: "CA",
+ postalCode: "92107",
+ },
+ email: ["sonnyday@fancymail.com", "surfergal@veryprofessional.org"],
+ };
+
+ let RemixStub = unstable_createRemixStub([
+ {
+ path: "/",
+ meta: () => [
+ {
+ "script:ld+json": jsonLd,
+ },
+ ],
+ Component: Meta,
+ },
+ ]);
+
+ let { container } = render();
+
+ // For some reason, prettyDOM strips the script tag (maybe because of
+ // dangerouslySetInnerHTML), so we just parse the HTML out into JSON and assert that way
+ let scriptTagContents =
+ container.querySelector('script[type="application/ld+json"]')
+ ?.innerHTML || "{}";
+ expect(JSON.parse(scriptTagContents)).toEqual(jsonLd);
+ });
+
+ it("{ tagName: 'link' } adds a ", () => {
+ let RemixStub = unstable_createRemixStub([
+ {
+ path: "/",
+ meta: () => [
+ {
+ tagName: "link",
+ rel: "canonical",
+ href: "https://website.com/authors/1",
+ },
+ ],
+ Component: Meta,
+ },
+ ]);
+
+ let { container } = render();
+ expect(getHtml(container)).toMatchInlineSnapshot(`
+ "
+
+
"
+ `);
+ });
+
+ it("loader errors are passed to meta", () => {
+ let RemixStub = unstable_createRemixStub([
+ {
+ path: "/",
+ Component() {
+ return (
+ <>
+
+
+ >
+ );
+ },
+ children: [
+ {
+ id: "index",
+ index: true,
+ meta: ({ error }) => [
+ {
+ title: (error as Error)?.message || "Home",
+ },
+ ],
+ Component() {
+ return Page
;
+ },
+ ErrorBoundary() {
+ return Boundary
;
+ },
+ },
+ ],
+ },
+ ]);
+
+ let { container } = render(
+
+ );
+ expect(getHtml(container)).toMatchInlineSnapshot(`
+ "
+
+ Oh no!
+
+
+ Boundary
+
+ "
+ `);
+ });
+});
diff --git a/packages/remix-testing/__tests__/stub-test.tsx b/packages/remix-testing/__tests__/stub-test.tsx
index 7ba27d9efcd..bdc584839e5 100644
--- a/packages/remix-testing/__tests__/stub-test.tsx
+++ b/packages/remix-testing/__tests__/stub-test.tsx
@@ -9,7 +9,7 @@ test("renders a route", () => {
let RemixStub = unstable_createRemixStub([
{
path: "/",
- element: HOME
,
+ Component: () => HOME
,
},
]);
@@ -21,16 +21,18 @@ test("renders a route", () => {
test("renders a nested route", () => {
let RemixStub = unstable_createRemixStub([
{
- element: (
-
-
ROOT
-
-
- ),
+ Component() {
+ return (
+
+
ROOT
+
+
+ );
+ },
children: [
{
path: "/",
- element: INDEX
,
+ Component: () => INDEX
,
},
],
},
@@ -52,7 +54,7 @@ test("loaders work", async () => {
{
path: "/",
index: true,
- element: ,
+ Component: App,
loader() {
return json({ message: "hello" });
},
@@ -99,14 +101,14 @@ test("can pass context values", async () => {
[
{
path: "/",
- element: ,
+ Component: App,
loader({ context }) {
return json(context);
},
children: [
{
path: "hello",
- element: ,
+ Component: Hello,
loader({ context }) {
return json(context);
},
@@ -141,16 +143,18 @@ test("all routes have ids", () => {
let RemixStub = unstable_createRemixStub([
{
- element: (
-
-
ROOT
-
-
- ),
+ Component() {
+ return (
+
+
ROOT
+
+
+ );
+ },
children: [
{
path: "/",
- element: ,
+ Component: Home,
},
],
},
diff --git a/packages/remix-testing/create-remix-stub.tsx b/packages/remix-testing/create-remix-stub.tsx
index 0c297cb500b..a86ffb201ec 100644
--- a/packages/remix-testing/create-remix-stub.tsx
+++ b/packages/remix-testing/create-remix-stub.tsx
@@ -1,28 +1,61 @@
import * as React from "react";
-import type { HydrationState, InitialEntry, Router } from "@remix-run/router";
+import {
+ UNSAFE_convertRoutesToDataRoutes,
+ type ActionFunctionArgs,
+ type HydrationState,
+ type InitialEntry,
+ type LoaderFunctionArgs,
+ type Router,
+} from "@remix-run/router";
import { UNSAFE_RemixContext as RemixContext } from "@remix-run/react";
import type {
UNSAFE_FutureConfig as FutureConfig,
UNSAFE_AssetsManifest as AssetsManifest,
UNSAFE_EntryRoute as EntryRoute,
- UNSAFE_RouteManifest as RouteManifest,
UNSAFE_RouteModules as RouteModules,
UNSAFE_RemixContextObject as RemixContextObject,
+ MetaFunction,
} from "@remix-run/react";
import type {
DataRouteObject,
IndexRouteObject,
NonIndexRouteObject,
- RouteObject,
} from "react-router-dom";
-import { createMemoryRouter, RouterProvider } from "react-router-dom";
+import { createMemoryRouter, Outlet, RouterProvider } from "react-router-dom";
import type {
ActionFunction,
AppLoadContext,
+ LinksFunction,
LoaderFunction,
} from "@remix-run/server-runtime";
-type RemixStubOptions = {
+interface StubIndexRouteObject
+ extends Omit<
+ IndexRouteObject,
+ "loader" | "action" | "element" | "errorElement" | "children"
+ > {
+ loader?: LoaderFunction;
+ action?: ActionFunction;
+ children?: StubRouteObject[];
+ meta?: MetaFunction;
+ links?: LinksFunction;
+}
+
+interface StubNonIndexRouteObject
+ extends Omit<
+ NonIndexRouteObject,
+ "loader" | "action" | "element" | "errorElement" | "children"
+ > {
+ loader?: LoaderFunction;
+ action?: ActionFunction;
+ children?: StubRouteObject[];
+ meta?: MetaFunction;
+ links?: LinksFunction;
+}
+
+type StubRouteObject = StubIndexRouteObject | StubNonIndexRouteObject;
+
+export interface RemixStubProps {
/**
* The initial entries in the history stack. This allows you to start a test with
* multiple locations already in the history stack (for testing a back navigation, etc.)
@@ -31,6 +64,15 @@ type RemixStubOptions = {
*/
initialEntries?: InitialEntry[];
+ /**
+ * The initial index in the history stack to render. This allows you to start a test at a specific entry.
+ * It defaults to the last entry in initialEntries.
+ * e.g.
+ * initialEntries: ["/", "/events/123"]
+ * initialIndex: 1 // start at "/events/123"
+ */
+ initialIndex?: number;
+
/**
* Used to set the route's initial loader and action data.
* e.g. hydrationData={{
@@ -41,66 +83,13 @@ type RemixStubOptions = {
hydrationData?: HydrationState;
/**
- * The initial index in the history stack to render. This allows you to start a test at a specific entry.
- * It defaults to the last entry in initialEntries.
- * e.g.
- * initialEntries: ["/", "/events/123"]
- * initialIndex: 1 // start at "/events/123"
+ * Future flags mimicking the settings in remix.config.js
*/
- initialIndex?: number;
-
remixConfigFuture?: Partial;
-};
-
-function patchRoutesWithContext(
- routes: (StubRouteObject | StubDataRouteObject)[],
- context: AppLoadContext
-): (RouteObject | DataRouteObject)[] {
- return routes.map((route) => {
- if (route.loader) {
- let loader = route.loader;
- route.loader = (args) => loader({ ...args, context });
- }
-
- if (route.action) {
- let action = route.action;
- route.action = (args) => action({ ...args, context });
- }
-
- if (route.children) {
- return {
- ...route,
- children: patchRoutesWithContext(route.children, context),
- };
- }
-
- return route as RouteObject | DataRouteObject;
- }) as (RouteObject | DataRouteObject)[];
}
-interface StubIndexRouteObject
- extends Omit {
- loader?: LoaderFunction;
- action?: ActionFunction;
- children?: StubRouteObject[];
-}
-
-interface StubNonIndexRouteObject
- extends Omit {
- loader?: LoaderFunction;
- action?: ActionFunction;
- children?: StubRouteObject[];
-}
-
-type StubRouteObject = StubIndexRouteObject | StubNonIndexRouteObject;
-
-type StubDataRouteObject = StubRouteObject & {
- children?: DataRouteObject[];
- id: string;
-};
-
export function createRemixStub(
- routes: (StubRouteObject | StubDataRouteObject)[],
+ routes: StubRouteObject[],
context: AppLoadContext = {}
) {
return function RemixStub({
@@ -108,14 +97,31 @@ export function createRemixStub(
initialIndex,
hydrationData,
remixConfigFuture,
- }: RemixStubOptions) {
+ }: RemixStubProps) {
let routerRef = React.useRef();
let remixContextRef = React.useRef();
if (routerRef.current == null) {
- // update the routes to include context in the loader/action
- let patched = patchRoutesWithContext(routes, context);
+ remixContextRef.current = {
+ future: { ...remixConfigFuture },
+ manifest: {
+ routes: {},
+ entry: { imports: [], module: "" },
+ url: "",
+ version: "",
+ },
+ routeModules: {},
+ };
+ // Update the routes to include context in the loader/action and populate
+ // the manifest and routeModules during the walk
+ let patched = processRoutes(
+ // @ts-expect-error loader/action context types don't match :/
+ UNSAFE_convertRoutesToDataRoutes(routes, (r) => r),
+ context,
+ remixContextRef.current.manifest,
+ remixContextRef.current.routeModules
+ );
routerRef.current = createMemoryRouter(patched, {
initialEntries,
initialIndex,
@@ -123,16 +129,6 @@ export function createRemixStub(
});
}
- if (remixContextRef.current == null) {
- remixContextRef.current = {
- future: {
- ...remixConfigFuture,
- },
- manifest: createManifest(routerRef.current.routes),
- routeModules: createRouteModules(routerRef.current.routes),
- };
- }
-
return (
@@ -141,64 +137,71 @@ export function createRemixStub(
};
}
-function createManifest(routes: RouteObject[]): AssetsManifest {
- return {
- routes: createRouteManifest(routes),
- entry: { imports: [], module: "" },
- url: "",
- version: "",
- };
-}
-
-function createRouteManifest(
- routes: RouteObject[],
- manifest?: RouteManifest,
+function processRoutes(
+ routes: StubRouteObject[],
+ context: AppLoadContext,
+ manifest: AssetsManifest,
+ routeModules: RouteModules,
parentId?: string
-): RouteManifest {
- return routes.reduce((manifest, route) => {
- if (route.children) {
- createRouteManifest(route.children, manifest, route.id);
+): DataRouteObject[] {
+ return routes.map((route) => {
+ if (!route.id) {
+ throw new Error(
+ "Expected a route.id in @remix-run/testing processRoutes() function"
+ );
}
- manifest[route.id!] = convertToEntryRoute(route, parentId);
- return manifest;
- }, manifest || {});
-}
-function createRouteModules(
- routes: RouteObject[],
- routeModules?: RouteModules
-): RouteModules {
- return routes.reduce((modules, route) => {
- if (route.children) {
- createRouteModules(route.children, modules);
- }
+ // Patch in the Remix context to loaders/actions
+ let { loader, action } = route;
+ let newRoute: DataRouteObject = {
+ id: route.id,
+ path: route.path,
+ index: route.index,
+ Component: route.Component,
+ ErrorBoundary: route.ErrorBoundary,
+ action: action
+ ? (args: ActionFunctionArgs) => action!({ ...args, context })
+ : undefined,
+ loader: loader
+ ? (args: LoaderFunctionArgs) => loader!({ ...args, context })
+ : undefined,
+ handle: route.handle,
+ shouldRevalidate: route.shouldRevalidate,
+ };
+
+ // Add the EntryRoute to the manifest
+ let entryRoute: EntryRoute = {
+ id: route.id,
+ path: route.path,
+ index: route.index,
+ parentId,
+ hasAction: route.action != null,
+ hasLoader: route.loader != null,
+ hasErrorBoundary: route.ErrorBoundary != null,
+ module: "build/stub-path-to-module.js", // any need for this?
+ };
+ manifest.routes[newRoute.id] = entryRoute;
- modules[route.id!] = {
- ErrorBoundary: undefined,
- default: () => route.element,
+ // Add the route to routeModules
+ routeModules[route.id] = {
+ default: route.Component || Outlet,
+ ErrorBoundary: route.ErrorBoundary || undefined,
handle: route.handle,
- links: undefined,
- meta: undefined,
- shouldRevalidate: undefined,
+ links: route.links,
+ meta: route.meta,
+ shouldRevalidate: route.shouldRevalidate,
};
- return modules;
- }, routeModules || {});
-}
+ if (route.children) {
+ newRoute.children = processRoutes(
+ route.children,
+ context,
+ manifest,
+ routeModules,
+ newRoute.id
+ );
+ }
-function convertToEntryRoute(
- route: RouteObject,
- parentId?: string
-): EntryRoute {
- return {
- id: route.id!,
- index: route.index,
- caseSensitive: route.caseSensitive,
- path: route.path,
- parentId,
- hasAction: !!route.action,
- hasLoader: !!route.loader,
- module: "",
- hasErrorBoundary: false,
- };
+ return newRoute;
+ });
}
diff --git a/packages/remix-testing/index.ts b/packages/remix-testing/index.ts
index 8b0aed3c2eb..fe7a155819e 100644
--- a/packages/remix-testing/index.ts
+++ b/packages/remix-testing/index.ts
@@ -1 +1,2 @@
+export type { RemixStubProps } from "./create-remix-stub";
export { createRemixStub as unstable_createRemixStub } from "./create-remix-stub";