Skip to content

Commit

Permalink
chore: use createMemoryRouter + RouterProvider
Browse files Browse the repository at this point in the history
Signed-off-by: Logan McAnsh <logan@mcan.sh>
  • Loading branch information
mcansh committed Dec 20, 2022
1 parent 549caa1 commit d200972
Show file tree
Hide file tree
Showing 2 changed files with 16 additions and 171 deletions.
1 change: 0 additions & 1 deletion packages/remix-react/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ export {
useLoaderData,
useMatches,
useActionData,
RemixContext as UNSAFE_RemixContext,
} from "./components";

export type { FormMethod, FormEncType } from "./data";
Expand Down
186 changes: 16 additions & 170 deletions packages/remix-testing/create-remix-stub.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,9 @@
import * as React from "react";
import type {
AssetsManifest,
EntryContext,
EntryRoute,
RouteData,
RouteManifest,
RouteModules,
} from "@remix-run/react";
import { UNSAFE_RemixContext as RemixContext } from "@remix-run/react";
import { StaticRouterProvider } from "react-router-dom/server";
import type { RouteData } from "@remix-run/react";
import type { RouteObject } from "react-router-dom";
import { RouterProvider } from "react-router-dom";
import { createMemoryRouter } from "react-router-dom";
import { matchRoutes, json } from "react-router-dom";
import type {
AgnosticDataRouteObject,
InitialEntry,
StaticHandler,
StaticHandlerContext,
} from "@remix-run/router";
import type { InitialEntry } from "@remix-run/router";
import { createStaticHandler } from "@remix-run/router";

type RemixStubOptions = {
Expand All @@ -35,28 +21,12 @@ type RemixStubOptions = {
*/
initialLoaderData?: RouteData;

/**
* Used to set the route's initial loader headers.
* e.g. initialLoaderHeaders={{ "/contact": { "Content-Type": "application/json" } }}
*/
initialLoaderHeaders?: Record<string, Headers>;

/**
* Used to set the route's initial action data.
* e.g. initialActionData={{ "/login": { errors: { email: "invalid email" } }}
*/
initialActionData?: RouteData;

/**
* Used to set the route's initial action headers.
*/
initialActionHeaders?: Record<string, Headers>;

/**
* Used to set the route's initial status code.
*/
initialStatusCode?: number;

/**
* 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.
Expand All @@ -67,154 +37,30 @@ type RemixStubOptions = {
initialIndex?: number;
};

type RemixConfigFuture = Partial<EntryContext["future"]>;

export function createRemixStub(
routes: RouteObject[],
remixConfigFuture?: RemixConfigFuture
) {
export function createRemixStub(routes: RouteObject[]) {
// Setup request handler to handle requests to the mock routes
let staticHandler = createStaticHandler(routes);
return function RemixStub({
initialEntries,
initialIndex,
initialLoaderData = {},
initialActionData,
initialActionHeaders,
initialLoaderHeaders,
initialStatusCode: statusCode,
initialLoaderData,
}: RemixStubOptions) {
let memoryRouter = createMemoryRouter(staticHandler.dataRoutes, {
let router = createMemoryRouter(staticHandler.dataRoutes, {
initialEntries,
initialIndex,
hydrationData: {
actionData: initialActionData,
loaderData: initialLoaderData,
},
});

let manifest = createManifest(staticHandler.dataRoutes);
let matches = matchRoutes(routes, memoryRouter.state.location) || [];
let future: EntryContext["future"] = {
v2_meta: false,
...remixConfigFuture,
};
let routeModules = createRouteModules(staticHandler.dataRoutes);

let staticHandlerContext: StaticHandlerContext = {
actionData: initialActionData || null,
actionHeaders: initialActionHeaders || {},
loaderData: initialLoaderData || {},
loaderHeaders: initialLoaderHeaders || {},
basename: "",
errors: null,
location: memoryRouter.state.location,
// @ts-expect-error
matches,
statusCode: statusCode || 200,
};

// Patch fetch so that mock routes can handle action/loader requests
monkeyPatchFetch(staticHandler);

return (
<RemixContext.Provider value={{ manifest, routeModules, future }}>
<StaticRouterProvider
router={memoryRouter}
context={staticHandlerContext}
/>
</RemixContext.Provider>
);
};
}

function createManifest(routes: AgnosticDataRouteObject[]): AssetsManifest {
return {
routes: createRouteManifest(routes),
entry: { imports: [], module: "" },
url: "",
version: "",
};
}

function createRouteManifest(
routes: AgnosticDataRouteObject[],
manifest?: RouteManifest<EntryRoute>,
parentId?: string
): RouteManifest<EntryRoute> {
return routes.reduce((manifest, route) => {
if (route.children) {
createRouteManifest(route.children, manifest, route.id);
}
manifest[route.id!] = convertToEntryRoute(route, parentId);
return manifest;
}, manifest || {});
}

function createRouteModules(
routes: AgnosticDataRouteObject[],
routeModules?: RouteModules
): RouteModules {
return routes.reduce((modules, route) => {
if (route.children) {
createRouteModules(route.children, modules);
}

modules[route.id!] = {
CatchBoundary: undefined,
ErrorBoundary: undefined,
// @ts-expect-error - types are still `agnostic` here
default: () => route.element,
handle: route.handle,
links: undefined,
meta: undefined,
shouldRevalidate: undefined,
};
return modules;
}, routeModules || {});
}

const originalFetch =
typeof global !== "undefined" ? global.fetch : window.fetch;

function monkeyPatchFetch(staticHandler: StaticHandler) {
let fetchPatch = async (
input: RequestInfo | URL,
init: RequestInit = {}
): Promise<Response> => {
let request = new Request(input, init);
let url = new URL(request.url);

// if we have matches, send the request to mock routes via @remix-run/router rather than the normal
// @remix-run/server-runtime so that stubs can also be used in browser environments.
let matches = matchRoutes(staticHandler.dataRoutes, url);
if (matches && matches.length > 0) {
let response = await staticHandler.queryRoute(request);

if (response instanceof Response) {
return response;
}

return json(response);
}

// if no matches, passthrough to the original fetch as mock routes couldn't handle the request.
return originalFetch(request, init);
};

globalThis.fetch = fetchPatch;
}
React.useLayoutEffect(() => {
return router.subscribe((state) => {
console.log("state", state);
});
}, []);

function convertToEntryRoute(
route: AgnosticDataRouteObject,
parentId?: string
): EntryRoute {
return {
id: route.id!,
index: route.index,
caseSensitive: route.caseSensitive,
path: route.path,
parentId,
hasAction: !!route.action,
hasLoader: !!route.loader,
module: "",
hasCatchBoundary: false,
hasErrorBoundary: false,
return <RouterProvider router={router} />;
};
}

0 comments on commit d200972

Please sign in to comment.