Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(remix-testing): utilize react-router 6.5+ #4915

Merged
merged 9 commits into from
Dec 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 [email protected] 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/[email protected]":
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/[email protected]":
version "1.2.0"
resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.2.0.tgz#54eff8306938b64c521f4a9ed313d33a91ef019a"
Expand Down