Skip to content

Commit

Permalink
chore(remix-testing): utilize react-router 6.5+ (#4915)
Browse files Browse the repository at this point in the history
Signed-off-by: Logan McAnsh <logan@mcan.sh>
  • Loading branch information
mcansh authored Dec 22, 2022
1 parent 23c85e1 commit 722de31
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 219 deletions.
6 changes: 6 additions & 0 deletions .changeset/silver-ducks-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"remix": patch
"@remix-run/testing": patch
---

use react router apis directly
18 changes: 12 additions & 6 deletions packages/remix-react/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export {
Scripts,
Link,
NavLink,
RemixEntry,
PrefetchPageLinks,
LiveReload,
useTransition,
Expand All @@ -51,6 +50,7 @@ export {
useLoaderData,
useMatches,
useActionData,
RemixContext as UNSAFE_RemixContext,
} from "./components";

export type { FormMethod, FormEncType } from "./data";
Expand All @@ -61,8 +61,7 @@ export { useCatch } from "./errorBoundaries";
export type { HtmlLinkDescriptor } from "./links";
export type {
HtmlMetaDescriptor,
CatchBoundaryComponent,
RouteModules,
RouteModules as UNSAFE_RouteModules,
} from "./routeModules";

export { ScrollRestoration } from "./scroll-restoration";
Expand All @@ -72,6 +71,13 @@ export { RemixServer } from "./server";

export type { Fetcher } from "./transition";

export type { AssetsManifest, EntryContext } from "./entry";
export type { RouteData } from "./routeData";
export type { EntryRoute, RouteManifest } from "./routes";
export type {
FutureConfig as UNSAFE_FutureConfig,
AssetsManifest as UNSAFE_AssetsManifest,
RemixContextObject as UNSAFE_RemixContextObject,
} from "./entry";

export type {
EntryRoute as UNSAFE_EntryRoute,
RouteManifest as UNSAFE_RouteManifest,
} from "./routes";
250 changes: 44 additions & 206 deletions packages/remix-testing/create-remix-stub.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,16 @@
import * as React from "react";
import type { HydrationState, InitialEntry, Router } from "@remix-run/router";
import { UNSAFE_RemixContext as RemixContext } from "@remix-run/react";
import type {
AssetsManifest,
EntryContext,
EntryRoute,
RouteData,
RouteManifest,
RouteModules,
UNSAFE_FutureConfig as FutureConfig,
UNSAFE_AssetsManifest as AssetsManifest,
UNSAFE_EntryRoute as EntryRoute,
UNSAFE_RouteManifest as RouteManifest,
UNSAFE_RouteModules as RouteModules,
UNSAFE_RemixContextObject as RemixContextObject,
} from "@remix-run/react";
import { RemixEntry } from "@remix-run/react";
import type {
Action,
AgnosticDataRouteObject,
AgnosticIndexRouteObject,
AgnosticNonIndexRouteObject,
AgnosticRouteMatch,
InitialEntry,
Location,
MemoryHistory,
StaticHandler,
} from "@remix-run/router";
import {
createMemoryHistory,
matchRoutes,
unstable_createStaticHandler as createStaticHandler,
} from "@remix-run/router";
import { json } from "@remix-run/server-runtime";

type Update = {
action: Action;
location: Location;
};
import type { RouteObject } from "react-router-dom";
import { createMemoryRouter, RouterProvider } from "react-router-dom";

type RemixStubOptions = {
/**
Expand All @@ -41,16 +22,13 @@ type RemixStubOptions = {
initialEntries?: InitialEntry[];

/**
* Used to set the route's initial loader data.
* e.g. initialLoaderData={{ "/contact": { locale: "en-US" } }}
*/
initialLoaderData?: RouteData;

/**
* Used to set the route's initial action data.
* e.g. initialActionData={{ "/login": { errors: { email: "invalid email" } }}
* Used to set the route's initial loader and action data.
* e.g. hydrationData={{
* loaderData: { "/contact": { locale: "en-US" } },
* actionData: { "/login": { errors: { email: "invalid email" } }}
* }}
*/
initialActionData?: RouteData;
hydrationData?: HydrationState;

/**
* The initial index in the history stack to render. This allows you to start a test at a specific entry.
Expand All @@ -60,113 +38,48 @@ type RemixStubOptions = {
* initialIndex: 1 // start at "/events/123"
*/
initialIndex?: number;
};

type IndexStubRouteObject = AgnosticIndexRouteObject & {
element?: React.ReactNode;
children?: undefined;
};

type NonIndexStubRouteObject = AgnosticNonIndexRouteObject & {
element?: React.ReactNode;
children?: StubRouteObject[];
remixConfigFuture?: Partial<FutureConfig>;
};

// TODO: once Remix is on RR@6.4 we can just use the native type
type StubRouteObject = IndexStubRouteObject | NonIndexStubRouteObject;

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

export function createRemixStub(
routes: StubRouteObject[],
remixConfigFuture?: RemixConfigFuture
) {
// Setup request handler to handle requests to the mock routes
let { dataRoutes, queryRoute } = createStaticHandler(routes);
export function createRemixStub(routes: RouteObject[]) {
return function RemixStub({
initialEntries,
initialLoaderData = {},
initialActionData,
initialIndex,
hydrationData,
remixConfigFuture,
}: RemixStubOptions) {
let historyRef = React.useRef<MemoryHistory>();
if (historyRef.current == null) {
historyRef.current = createMemoryHistory({
let routerRef = React.useRef<Router>();
let remixContextRef = React.useRef<RemixContextObject>();

if (routerRef.current == null) {
routerRef.current = createMemoryRouter(routes, {
initialEntries,
initialIndex,
v5Compat: true,
hydrationData,
});
}

let history = historyRef.current;

let [state, dispatch] = React.useReducer(
(_: Update, update: Update) => update,
{ action: history.action, location: history.location }
);

React.useLayoutEffect(() => history.listen(dispatch), [history]);

// Convert path based ids in user supplied initial loader/action data to data route ids
let loaderData = convertRouteData(dataRoutes, initialLoaderData);
let actionData = convertRouteData(dataRoutes, initialActionData);

// Create mock remix context
let remixContext = createRemixContext(
dataRoutes,
state.location,
loaderData,
actionData,
remixConfigFuture
);

// Patch fetch so that mock routes can handle action/loader requests
monkeyPatchFetch(queryRoute, dataRoutes);
if (remixContextRef.current == null) {
remixContextRef.current = {
future: {
v2_meta: false,
...remixConfigFuture,
},
manifest: createManifest(routes),
routeModules: createRouteModules(routes),
};
}

return (
<RemixEntry
context={remixContext}
action={state.action}
location={state.location}
navigator={history}
/>
<RemixContext.Provider value={remixContextRef.current}>
<RouterProvider router={routerRef.current} />
</RemixContext.Provider>
);
};
}

function createRemixContext(
routes: AgnosticDataRouteObject[],
currentLocation: Location,
initialLoaderData?: RouteData,
initialActionData?: RouteData,
future?: RemixConfigFuture
): EntryContext {
let manifest = createManifest(routes);
let matches = matchRoutes(routes, currentLocation) || [];

return {
// TODO: Check with Logan on how to handle the update heree
// @ts-expect-error
actionData: initialActionData,
appState: {
trackBoundaries: true,
trackCatchBoundaries: true,
catchBoundaryRouteId: null,
renderBoundaryRouteId: null,
loaderBoundaryRouteId: null,
},
future: {
v2_meta: false,
...future,
},
matches: convertToEntryRouteMatch(matches),
routeData: initialLoaderData || {},
manifest,
routeModules: createRouteModules(routes),
};
}

function createManifest(routes: AgnosticDataRouteObject[]): AssetsManifest {
function createManifest(routes: RouteObject[]): AssetsManifest {
return {
routes: createRouteManifest(routes),
entry: { imports: [], module: "" },
Expand All @@ -176,7 +89,7 @@ function createManifest(routes: AgnosticDataRouteObject[]): AssetsManifest {
}

function createRouteManifest(
routes: AgnosticDataRouteObject[],
routes: RouteObject[],
manifest?: RouteManifest<EntryRoute>,
parentId?: string
): RouteManifest<EntryRoute> {
Expand All @@ -190,7 +103,7 @@ function createRouteManifest(
}

function createRouteModules(
routes: AgnosticDataRouteObject[],
routes: RouteObject[],
routeModules?: RouteModules
): RouteModules {
return routes.reduce((modules, route) => {
Expand All @@ -206,50 +119,14 @@ function createRouteModules(
handle: route.handle,
links: undefined,
meta: undefined,
// TODO: Check with Logan on how to handle the update here
// @ts-expect-error
unstable_shouldReload: undefined,
shouldRevalidate: undefined,
};
return modules;
}, routeModules || {});
}

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

function monkeyPatchFetch(
queryRoute: StaticHandler["queryRoute"],
dataRoutes: StaticHandler["dataRoutes"]
) {
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(dataRoutes, url);
if (matches && matches.length > 0) {
let response = await 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;
}

function convertToEntryRoute(
route: AgnosticDataRouteObject,
route: RouteObject,
parentId?: string
): EntryRoute {
return {
Expand All @@ -265,42 +142,3 @@ function convertToEntryRoute(
hasErrorBoundary: false,
};
}

function convertToEntryRouteMatch(
routes: AgnosticRouteMatch<string, AgnosticDataRouteObject>[]
) {
return routes.map((match) => {
return {
params: match.params,
pathname: match.pathname,
route: convertToEntryRoute(match.route),
};
});
}

// Converts route data from a path based index to a route id index value.
// e.g. { "/post/:postId": post } to { "0": post }
// TODO: may not need
function convertRouteData(
routes: AgnosticDataRouteObject[],
initialRouteData?: RouteData,
routeData: RouteData = {}
): RouteData | undefined {
if (!initialRouteData) return undefined;
return routes.reduce<RouteData>((data, route) => {
if (route.children) {
convertRouteData(route.children, initialRouteData, data);
}
// Check if any of the initial route data entries match this route
Object.keys(initialRouteData).forEach((routePath) => {
if (
routePath === route.path ||
// Let '/' refer to the root routes data
(routePath === "/" && route.id === "0" && !route.path)
) {
data[route.id!] = initialRouteData[routePath];
}
});
return data;
}, routeData);
}
4 changes: 2 additions & 2 deletions packages/remix-testing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
"dependencies": {
"@remix-run/node": "1.10.0-pre.1",
"@remix-run/react": "1.10.0-pre.1",
"@remix-run/router": "1.1.0",
"@remix-run/server-runtime": "1.10.0-pre.1",
"@remix-run/router": "1.2.0",
"react-router-dom": "6.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
Expand Down
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2174,11 +2174,6 @@
"@changesets/types" "^5.0.0"
dotenv "^8.1.0"

"@remix-run/router@1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.1.0.tgz#b48db8148c8a888e50580a8152b6f68161c49406"
integrity sha512-rGl+jH/7x1KBCQScz9p54p0dtPLNeKGb3e0wD2H5/oZj41bwQUnXdzbj2TbUAFhvD7cp9EyEQA4dEgpUFa1O7Q==

"@remix-run/router@1.2.0":
version "1.2.0"
resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.2.0.tgz#54eff8306938b64c521f4a9ed313d33a91ef019a"
Expand Down

0 comments on commit 722de31

Please sign in to comment.