├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .prettierignore ├── .tool-versions ├── .vscode └── settings.json ├── .yarnrc.yml ├── README.md ├── build.tsconfig.json ├── package.json ├── src ├── JSResource.ts ├── Link.tsx ├── index.ts ├── routes │ ├── EntryPointRoute.tsx │ ├── create-entry-point-route.ts │ ├── entry-point-route-object.types.ts │ ├── entry-point.types.ts │ ├── internal-preload-symbol.ts │ └── prepare-preloadable-routes.ts ├── useLinkDataLoadHandler.ts ├── useLinkEntryPointLoadHandler.ts └── useLinkResourceLoadHandler.ts ├── tsconfig.json └── yarn.lock /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to npm 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version: 20 13 | registry-url: "https://registry.npmjs.org" 14 | scope: "@loop-payments" 15 | - run: corepack enable 16 | - run: yarn install --immutable 17 | - name: Build 18 | run: yarn build 19 | - name: Publish 20 | run: yarn npm publish 21 | env: 22 | YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .yarn -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.11.0 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.useTabs": false, 3 | "editor.tabSize": 2, 4 | "editor.formatOnSave": true 5 | } -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | npmPublishAccess: public 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @loop-payments/react-router-relay 2 | 3 | Utilities and components to take advantage of Relay's preloaded queries when using react-router's data routers. This follows Relay's entrypoint pattern. 4 | 5 | ## Usage 6 | 7 | Entrypoints work by defining the component, generally using a preloaded query, and a corresponding entrypoint. 8 | 9 | ### MyPage.tsx 10 | 11 | ```typescript 12 | import type { SimpleEntryPointProps } from '@loop-payments/react-router-relay'; 13 | import { usePreloadedQuery, graphql } from 'react-relay'; 14 | 15 | import type MyPageQuery from './__generated__/MyPageQuery.graphql'; 16 | 17 | type Props = SimpleEntryPointProps<{ 18 | query: MyPageQuery, 19 | }>; 20 | 21 | export default MyPage({ queries }: Props) { 22 | const data = usePreloadedQuery(graphql` 23 | query MyPageQuery($someId: ID!) @preloadable { 24 | node(id: $someId) { 25 | __typename 26 | } 27 | } 28 | `, queries.query); 29 | 30 | return <>You found a {data.node?.__typename ?? 'nothing'}; 31 | } 32 | ``` 33 | 34 | ### MyPage.entrypoint.ts 35 | 36 | ```typescript 37 | import { 38 | type SimpleEntryPoint, 39 | JSResource, 40 | } from "@loop-payments/react-router-relay"; 41 | import nullthrows from "nullthrows"; 42 | 43 | import type MyPage from "./MyPage"; 44 | import MyPageQueryParameters from "./__generated__/MyPageQuery$parameters"; 45 | 46 | const entryPoint: SimpleEntryPoint = { 47 | root: JSResource("MyPage", () => import("./MyPage")), 48 | getPreloadProps({ params }) { 49 | return { 50 | queries: { 51 | query: { 52 | parameters: MyPageQueryParameters, 53 | variables: { 54 | someId: nullthrows(params.someId), 55 | }, 56 | }, 57 | }, 58 | }; 59 | }, 60 | }; 61 | 62 | export default entryPoint; 63 | ``` 64 | 65 | #### Note for Relay < 16.2 66 | 67 | If you're using relay prior to 16.2.0 you won't be able to use the `@preloadable` annotation and thus won't be able to generate `$parameters` files. You can still use entry points, but they'll need to import concrete request objects from the `.graphql` files instead. 68 | 69 | ```ts 70 | import MyPageQuery from "./__generated__/MyPageQuery.graphql"; 71 | 72 | const entryPoint: SimpleEntryPoint = { 73 | root: JSResource("MyPage", () => import("./MyPage")), 74 | getPreloadProps({ params }) { 75 | return { 76 | queries: { 77 | query: { 78 | parameters: MyPageQuery, 79 | variables: { 80 | someId: nullthrows(params.someId), 81 | }, 82 | }, 83 | }, 84 | }; 85 | }, 86 | }; 87 | ``` 88 | 89 | ### MyRouter.tsx 90 | 91 | You need to use one of react-router's data routers and pre-process the routes via `preparePreloadableRoutes` before passing them into the router. 92 | 93 | ```typescript 94 | import { 95 | type EntryPointRouteObject, 96 | preparePreloadableRoutes, 97 | } from "@loop-payments/react-router-relay"; 98 | import { useMemo, useRef } from "react"; 99 | import { createBrowserRouter, RouterProvider } from "react-router-dom"; 100 | import { useRelayEnvironment } from "react-relay"; 101 | 102 | import MyPageEntryPoint from "./MyPage.entrypoint"; 103 | 104 | const MY_ROUTES: EntryPointRouteObject[] = [ 105 | { 106 | path: ":someId", 107 | entryPoint: MyPageEntryPoint, 108 | }, 109 | ]; 110 | 111 | export default function MyRouter() { 112 | const environment = useRelayEnvironment(); 113 | // Potentially unnecessary if you never change your environment 114 | const environmentRef = useRef(environment); 115 | environmentRef.current = environment; 116 | 117 | const router = useMemo(() => { 118 | const routes = preparePreloadableRoutes(MY_ROUTES, { 119 | getEnvironment() { 120 | return environmentRef.current; 121 | }, 122 | }); 123 | 124 | return createBrowserRouter(routes); 125 | }, []); 126 | 127 | return ; 128 | } 129 | ``` 130 | 131 | ## Link 132 | 133 | This package includes a wrapper around `react-router-dom`'s `Link` component. Using this component is optional. This adds a basic pre-fetch to the link that will load the JSResources for the destination on hover or focus events, and start fetching data on mouse down. 134 | 135 | ## A note on JSResource 136 | 137 | Loading data for entrypoints depends on having a JSResource implementation to coordinate and cache loads of the same resource. This package does not depend on using the internal JSResource implementation if you wish to use a different one in your entrypoints. 138 | -------------------------------------------------------------------------------- /build.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*"], 4 | "compilerOptions": { 5 | "outDir": "./dist/", 6 | "noEmit": false 7 | } 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@loop-payments/react-router-relay", 3 | "version": "2.1.3", 4 | "description": "Relay integration for react-router", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "type": "module", 10 | "types": "./dist/index.d.ts", 11 | "scripts": { 12 | "build": "tsc -p build.tsconfig.json" 13 | }, 14 | "author": "engineering@loop.com", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@types/react-relay": "^18.2.0", 18 | "prettier": "^3.5.3", 19 | "react": "^19.1.0", 20 | "react-relay": "^18.2.0", 21 | "react-router-dom": "^7.4.1", 22 | "typescript": "^5.8.2" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/loop-payments/react-router-relay.git" 27 | }, 28 | "keywords": [ 29 | "graphql", 30 | "relay", 31 | "react-router" 32 | ], 33 | "bugs": { 34 | "url": "https://github.com/loop-payments/react-router-relay/issues" 35 | }, 36 | "homepage": "https://github.com/loop-payments/react-router-relay#readme", 37 | "peerDependencies": { 38 | "react-relay": ">=15.0.0", 39 | "react-router-dom": ">=6.4.0" 40 | }, 41 | "packageManager": "yarn@4.8.1" 42 | } 43 | -------------------------------------------------------------------------------- /src/JSResource.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Sourced from: 3 | * https://github.com/relayjs/relay-examples/blob/2e9a42d808f1ac52ac51282ea7c5e1c33144c031/issue-tracker/src/JSResource.js 4 | * with some modifications to add types and conform to the current 5 | * JSResourceReference interface in react-relay. 6 | */ 7 | 8 | import type { JSResourceReference } from "react-relay"; 9 | 10 | /** 11 | * A cache of resources to avoid loading the same module twice. This is important 12 | * because Webpack dynamic imports only expose an asynchronous API for loading 13 | * modules, so to be able to access already-loaded modules synchronously we 14 | * must have stored the previous result somewhere. 15 | */ 16 | const resourceMap = new Map>(); 17 | 18 | type Loader = () => Promise; 19 | 20 | /** 21 | * A generic resource: given some method to asynchronously load a value - the loader() 22 | * argument - it allows accessing the state of the resource. 23 | */ 24 | class Resource implements JSResourceReference { 25 | #error: Error | null = null; 26 | #promise: Promise | null = null; 27 | #result: TModule | null = null; 28 | #moduleId: string; 29 | #loader: Loader; 30 | 31 | constructor(moduleId: string, loader: Loader) { 32 | this.#moduleId = moduleId; 33 | this.#loader = loader; 34 | } 35 | 36 | /** 37 | * Loads the resource if necessary. 38 | */ 39 | load(): Promise { 40 | let promise = this.#promise; 41 | if (promise == null) { 42 | promise = this.#loader() 43 | .then((result) => { 44 | // This is a hack to support both modules with default exports (the 45 | // common case) and loaders that need to export something directly. 46 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 47 | // @ts-ignore 48 | if (result.default) { 49 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 50 | // @ts-ignore 51 | this.#result = result.default as TModule; 52 | } else { 53 | this.#result = result as TModule; 54 | } 55 | return this.#result; 56 | }) 57 | .catch((error) => { 58 | this.#error = error; 59 | throw error; 60 | }); 61 | this.#promise = promise; 62 | } 63 | return promise; 64 | } 65 | 66 | /** 67 | * Returns the result, if available. This can be useful to check if the value 68 | * is resolved yet. 69 | */ 70 | getModuleIfRequired(): TModule | null { 71 | return this.#result; 72 | } 73 | 74 | /** 75 | * This is the key method for integrating with React Suspense. Read will: 76 | * - "Suspend" if the resource is still pending (currently implemented as 77 | * throwing a Promise, though this is subject to change in future 78 | * versions of React) 79 | * - Throw an error if the resource failed to load. 80 | * - Return the data of the resource if available. 81 | */ 82 | read() { 83 | if (this.#result != null) { 84 | return this.#result; 85 | } else if (this.#error != null) { 86 | throw this.#error; 87 | } else { 88 | throw this.load(); 89 | } 90 | } 91 | 92 | /** 93 | * 94 | */ 95 | getModuleId(): string { 96 | return this.#moduleId; 97 | } 98 | } 99 | 100 | /** 101 | * A helper method to create a resource, intended for dynamically loading code. 102 | * 103 | * Example: 104 | * ``` 105 | * // Before rendering, ie in an event handler: 106 | * const resource = JSResource('Foo', () => import('./Foo.js)); 107 | * resource.load(); 108 | * 109 | * // in a React component: 110 | * const Foo = resource.read(); 111 | * return ; 112 | * ``` 113 | * 114 | * @param {*} moduleId A globally unique identifier for the resource used for caching 115 | * @param {*} loader A method to load the resource's data if necessary 116 | */ 117 | export default function JSResource( 118 | moduleId: string, 119 | loader: Loader, 120 | ): Resource { 121 | let resource = resourceMap.get(moduleId); 122 | if (resource == null) { 123 | resource = new Resource(moduleId, loader); 124 | resourceMap.set(moduleId, resource); 125 | } 126 | return resource; 127 | } 128 | -------------------------------------------------------------------------------- /src/Link.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | type MouseEvent, 3 | type FocusEvent, 4 | useCallback, 5 | useEffect, 6 | } from "react"; 7 | import { Link as RouterLink, type LinkProps } from "react-router-dom"; 8 | import { useLinkResourceLoadHandler } from "./useLinkResourceLoadHandler.ts"; 9 | import { useLinkDataLoadHandler } from "./useLinkDataLoadHandler.ts"; 10 | import { useLinkEntryPointLoadHandler } from "./useLinkEntryPointLoadHandler.ts"; 11 | 12 | type Props = LinkProps; 13 | 14 | /** 15 | * A wrapper around react-router-dom's Link to preload the target routes. 16 | * This will load the target JSResources on hover or focus, and will trigger 17 | * the loader, which will start requesting graphql data, on mouse down. 18 | */ 19 | export default function Link({ 20 | onMouseEnter, 21 | onFocus, 22 | onMouseDown, 23 | ...props 24 | }: Props) { 25 | const fetchEntryPoint = useLinkEntryPointLoadHandler(); 26 | const fetchResources = useLinkResourceLoadHandler(); 27 | const fetchData = useLinkDataLoadHandler(props.to); 28 | 29 | useEffect(() => { 30 | if ("requestIdleCallback" in window) { 31 | const id = requestIdleCallback(() => fetchEntryPoint(props.to)); 32 | return () => cancelIdleCallback(id); 33 | } else { 34 | const id = requestAnimationFrame(() => fetchEntryPoint(props.to)); 35 | return () => cancelAnimationFrame(id); 36 | } 37 | }, [fetchEntryPoint, props.to]); 38 | 39 | const handleMouseEnter = useCallback( 40 | (e: MouseEvent) => { 41 | fetchResources(props.to); 42 | onMouseEnter?.(e); 43 | }, 44 | [onMouseEnter, fetchResources, props.to], 45 | ); 46 | const handleFocus = useCallback( 47 | (e: FocusEvent) => { 48 | fetchResources(props.to); 49 | onFocus?.(e); 50 | }, 51 | [onFocus, fetchResources, props.to], 52 | ); 53 | const handleMouseDown = useCallback( 54 | (e: MouseEvent) => { 55 | fetchResources(props.to); 56 | fetchData(); 57 | onMouseDown?.(e); 58 | }, 59 | [onMouseDown, fetchResources, fetchData, props.to], 60 | ); 61 | 62 | return ( 63 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { preparePreloadableRoutes } from "./routes/prepare-preloadable-routes.ts"; 2 | export { useLinkDataLoadHandler } from "./useLinkDataLoadHandler.ts"; 3 | export { useLinkResourceLoadHandler } from "./useLinkResourceLoadHandler.ts"; 4 | 5 | export type * from "./routes/entry-point.types.ts"; 6 | export type * from "./routes/entry-point-route-object.types.ts"; 7 | 8 | import Link from "./Link.tsx"; 9 | import JSResource from "./JSResource.ts"; 10 | 11 | export { Link, JSResource }; 12 | -------------------------------------------------------------------------------- /src/routes/EntryPointRoute.tsx: -------------------------------------------------------------------------------- 1 | import { type ComponentType, useEffect } from "react"; 2 | import type { 3 | EntryPoint, 4 | EntryPointProps, 5 | JSResourceReference, 6 | } from "react-relay"; 7 | import { useLoaderData } from "react-router-dom"; 8 | import type { OperationType } from "relay-runtime"; 9 | 10 | import type { 11 | BaseEntryPointComponent, 12 | SimpleEntryPoint, 13 | } from "./entry-point.types.ts"; 14 | import { InternalPreload } from "./internal-preload-symbol.ts"; 15 | 16 | const preloadsToDispose = new Set(); 17 | 18 | export type PreloadableComponent = ComponentType & { 19 | [InternalPreload]?: { 20 | entryPoint: () => Promise>; 21 | resource: () => Promise; 22 | }; 23 | }; 24 | 25 | export default function EntryPointRoute( 26 | entryPoint: 27 | | SimpleEntryPoint 28 | | JSResourceReference>, 29 | ): ComponentType { 30 | const Hoc: PreloadableComponent = () => { 31 | const data = useLoaderData() as EntryPointProps< 32 | Record, 33 | Record | undefined>, 34 | Record, 35 | Record 36 | >; 37 | 38 | // We need to dispose of preloaded queries when changing routes. React 39 | // router doesn't provide a mechanism for actually accomplishing this so 40 | // we have this effect which attempts to do a deferred cleanup. We use a 41 | // timeout to delay the cleanup to avoid issues when unmounting and re- 42 | // mounting the same component without a new call to the loader function. 43 | useEffect(() => { 44 | if (data.queries == null) { 45 | return; 46 | } 47 | 48 | Object.values(data.queries).forEach((preloadedQuery) => { 49 | preloadsToDispose.delete(preloadedQuery); 50 | }); 51 | 52 | return () => { 53 | Object.values(data.queries).forEach((preloadedQuery) => { 54 | preloadsToDispose.add(preloadedQuery); 55 | }); 56 | 57 | setTimeout(() => { 58 | Object.values(data.queries).forEach((preloadedQuery) => { 59 | if (preloadsToDispose.delete(preloadedQuery)) { 60 | preloadedQuery.dispose(); 61 | } 62 | }); 63 | }, 10); 64 | }; 65 | }, [data.queries]); 66 | 67 | const loadedEntryPoint = (() => { 68 | if (!("load" in entryPoint)) return entryPoint; 69 | const loadedEntryPoint = entryPoint.getModuleIfRequired(); 70 | if (!loadedEntryPoint) throw entryPoint.load(); 71 | return loadedEntryPoint; 72 | })(); 73 | const resource = loadedEntryPoint.root; 74 | const Component = resource.getModuleIfRequired(); 75 | if (!Component) throw resource.load(); 76 | return ; 77 | }; 78 | Hoc.displayName = `EntryPointRoute(${ 79 | "load" in entryPoint 80 | ? entryPoint.getModuleId() 81 | : entryPoint.root.getModuleId() 82 | })`; 83 | 84 | // This would be much better if it injected a modulepreload link. Unfortunately 85 | // we don't have a mechanism for getting the right bundle file name to put into 86 | // the href. We might be able to do it by building a rollup plugin. 87 | Hoc[InternalPreload] = { 88 | async entryPoint() { 89 | return "load" in entryPoint ? entryPoint.load() : entryPoint; 90 | }, 91 | async resource() { 92 | const entryPoint = await this.entryPoint(); 93 | return entryPoint.root.load(); 94 | }, 95 | }; 96 | 97 | return Hoc; 98 | } 99 | -------------------------------------------------------------------------------- /src/routes/create-entry-point-route.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IEnvironmentProvider, 3 | JSResourceReference, 4 | PreloadOptions, 5 | GraphQLTaggedNode, 6 | EnvironmentProviderOptions, 7 | } from "react-relay"; 8 | import Relay from "react-relay"; 9 | import type { PreloadableConcreteRequest } from "relay-runtime"; 10 | import type { LoaderFunction, LoaderFunctionArgs } from "react-router-dom"; 11 | import type { ComponentType } from "react"; 12 | 13 | import type { 14 | BaseEntryPointComponent, 15 | PreloaderContextProvider, 16 | SimpleEntryPoint, 17 | } from "./entry-point.types.ts"; 18 | import EntryPointRoute from "./EntryPointRoute.tsx"; 19 | 20 | // Workaround for ESM compatibility 21 | const { loadQuery } = Relay; 22 | 23 | type EntryPointRouteProperties = { 24 | loader: LoaderFunction; 25 | Component: ComponentType>; 26 | handle?: unknown; 27 | }; 28 | 29 | export function createEntryPointRoute< 30 | Component extends BaseEntryPointComponent, 31 | PreloaderContext = undefined, 32 | >( 33 | entryPoint: 34 | | SimpleEntryPoint 35 | | JSResourceReference>, 36 | environmentProvider: IEnvironmentProvider, 37 | contextProvider?: PreloaderContextProvider, 38 | ): EntryPointRouteProperties { 39 | async function loader(args: LoaderFunctionArgs): Promise { 40 | const loadedEntryPoint = 41 | "load" in entryPoint ? await entryPoint.load() : entryPoint; 42 | const { queries: queryArgs, ...props } = loadedEntryPoint.getPreloadProps({ 43 | ...args, 44 | preloaderContext: contextProvider?.getPreloaderContext() as any, 45 | }); 46 | let queries = undefined; 47 | if (queryArgs) { 48 | queries = Object.fromEntries( 49 | Object.entries(queryArgs).map( 50 | ([ 51 | key, 52 | { parameters, variables, options, environmentProviderOptions }, 53 | ]: [ 54 | string, 55 | { 56 | parameters: GraphQLTaggedNode | PreloadableConcreteRequest; 57 | variables: any; 58 | options: PreloadOptions | null | undefined; 59 | environmentProviderOptions: 60 | | EnvironmentProviderOptions 61 | | null 62 | | undefined; 63 | }, 64 | ]) => [ 65 | key, 66 | // This can leak if we fail to mount the EntryPointRoute HOC. Not 67 | // sure if we can handle this better without improved support in 68 | // react-router. 69 | loadQuery( 70 | environmentProvider.getEnvironment(null), 71 | parameters, 72 | variables, 73 | options ?? undefined, 74 | environmentProviderOptions ?? undefined, 75 | ), 76 | ], 77 | ), 78 | ); 79 | } 80 | 81 | return { 82 | ...props, 83 | queries, 84 | }; 85 | } 86 | 87 | // Entrypoints that are JSResourceReferences cannot have a handle. 88 | const handle = "load" in entryPoint ? undefined : entryPoint.handle; 89 | 90 | return { 91 | loader, 92 | Component: EntryPointRoute(entryPoint), 93 | // Only add the handle if it's defined. This makes spreading the object 94 | // easier. 95 | ...(handle !== undefined ? { handle } : {}), 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /src/routes/entry-point-route-object.types.ts: -------------------------------------------------------------------------------- 1 | import type { JSResourceReference, EntryPointComponent } from "react-relay"; 2 | import type { 3 | IndexRouteObject, 4 | NonIndexRouteObject, 5 | RouteObject, 6 | } from "react-router-dom"; 7 | import type { EntryPointParams } from "./entry-point.types.ts"; 8 | 9 | type BadEntryPointType = { 10 | readonly root: JSResourceReference>; 11 | readonly getPreloadProps: (entryPointParams: EntryPointParams) => any; 12 | // Handle will be propagated to the route if the entrypoint is referenced directly. 13 | readonly handle?: unknown; 14 | }; 15 | 16 | export interface EntryPointIndexRouteObject 17 | extends Omit< 18 | IndexRouteObject, 19 | "loader" | "action" | "element" | "Component" | "lazy" 20 | > { 21 | entryPoint: BadEntryPointType | JSResourceReference; 22 | } 23 | 24 | export interface EntryPointNonIndexRouteObject 25 | extends Omit< 26 | NonIndexRouteObject, 27 | "loader" | "action" | "element" | "Component" | "lazy" 28 | > { 29 | entryPoint: BadEntryPointType | JSResourceReference; 30 | children?: Array; 31 | } 32 | 33 | export type EntryPointRouteObject = 34 | | EntryPointIndexRouteObject 35 | | EntryPointNonIndexRouteObject 36 | | RouteObject; 37 | -------------------------------------------------------------------------------- /src/routes/entry-point.types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | EntryPoint, 3 | EntryPointComponent, 4 | EntryPointProps, 5 | } from "react-relay"; 6 | import type { LoaderFunctionArgs, Params } from "react-router-dom"; 7 | import type { OperationType } from "relay-runtime"; 8 | 9 | export type BaseEntryPointComponent = EntryPointComponent< 10 | Record, 11 | Record | undefined> 12 | >; 13 | 14 | export interface EntryPointParams extends LoaderFunctionArgs { 15 | request: Request; 16 | params: Params; 17 | preloaderContext: PreloaderContext; 18 | } 19 | 20 | export interface SimpleEntryPoint< 21 | Component = BaseEntryPointComponent, 22 | PreloaderContext = undefined, 23 | > extends EntryPoint> { 24 | // If you define a handle on your entrypoint we will propagate it to the 25 | // corresponding route. 26 | handle?: unknown; 27 | } 28 | 29 | export type SimpleEntryPointProps< 30 | Queries extends Record, 31 | ExtraProps = Record, 32 | > = EntryPointProps< 33 | Queries, 34 | Record | undefined>, 35 | any, 36 | ExtraProps 37 | >; 38 | 39 | export interface PreloaderContextProvider { 40 | getPreloaderContext(): T; 41 | } 42 | -------------------------------------------------------------------------------- /src/routes/internal-preload-symbol.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Symbol used for the preload method on entrypoint components. This is 3 | * meant to be a private implementation detail. 4 | */ 5 | export const InternalPreload = Symbol("react-router-relay-preload"); 6 | -------------------------------------------------------------------------------- /src/routes/prepare-preloadable-routes.ts: -------------------------------------------------------------------------------- 1 | import type { IEnvironmentProvider } from "react-relay"; 2 | import type { RouteObject } from "react-router-dom"; 3 | 4 | import type { PreloaderContextProvider } from "./entry-point.types.ts"; 5 | import { createEntryPointRoute } from "./create-entry-point-route.ts"; 6 | import type { 7 | EntryPointIndexRouteObject, 8 | EntryPointNonIndexRouteObject, 9 | EntryPointRouteObject, 10 | } from "./entry-point-route-object.types.ts"; 11 | 12 | /** 13 | * Prepare a set of routes that that use entry points for use in react-router. 14 | * This transforms the entryPoint property into a Component and loader that 15 | * react-router can understand. 16 | * @param routes 17 | * @param environmentProvider a provider for the relay environment 18 | * @param preloaderContextProvider an optional provider for additional context data for your entrypoints 19 | * @returns a list of RouteObjects compatible with react-router's data routers 20 | */ 21 | export function preparePreloadableRoutes( 22 | routes: Array, 23 | environmentProvider: IEnvironmentProvider, 24 | preloaderContextProvider?: PreloaderContextProvider, 25 | ): Array { 26 | return routes.map((route) => { 27 | let newRoute; 28 | if (isEntryPoint(route)) { 29 | const { entryPoint, ...rest } = route; 30 | newRoute = { 31 | ...rest, 32 | ...createEntryPointRoute( 33 | entryPoint, 34 | environmentProvider, 35 | preloaderContextProvider, 36 | ), 37 | }; 38 | } else { 39 | newRoute = route; 40 | } 41 | 42 | if (newRoute.children) { 43 | return { 44 | ...newRoute, 45 | children: preparePreloadableRoutes( 46 | newRoute.children, 47 | environmentProvider, 48 | preloaderContextProvider, 49 | ), 50 | }; 51 | } 52 | 53 | return newRoute; 54 | }); 55 | } 56 | 57 | function isEntryPoint( 58 | route: EntryPointRouteObject, 59 | ): route is EntryPointIndexRouteObject | EntryPointNonIndexRouteObject { 60 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 61 | // @ts-ignore 62 | return route.entryPoint != null; 63 | } 64 | -------------------------------------------------------------------------------- /src/useLinkDataLoadHandler.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useCallback } from "react"; 2 | import { 3 | type To, 4 | UNSAFE_DataRouterContext, 5 | matchRoutes, 6 | useResolvedPath, 7 | } from "react-router-dom"; 8 | 9 | /** 10 | * Returns a handler for triggering data loading for a target. This is used 11 | * by Link to preload graphql data for entrypoints on mouse down events. You 12 | * can use this to build your own Link component if necessary. 13 | */ 14 | export function useLinkDataLoadHandler(to: To): () => void { 15 | const routes = useContext(UNSAFE_DataRouterContext)?.router.routes ?? []; 16 | 17 | // Trigger data fetching for any entrypoints 18 | const resolvedPath = useResolvedPath(to); 19 | const fetchData = useCallback(() => { 20 | const matches = matchRoutes([...routes], to); 21 | if (!matches) { 22 | return; 23 | } 24 | 25 | const url = new URL( 26 | resolvedPath.pathname + resolvedPath.search, 27 | window.location.origin, 28 | ); 29 | const request = new Request(url); 30 | for (const match of matches) { 31 | const { loader } = match.route; 32 | if (typeof loader !== "function") { 33 | return; 34 | } 35 | 36 | try { 37 | loader?.({ 38 | params: match.params, 39 | request, 40 | context: undefined, 41 | }); 42 | } catch (e: unknown) { 43 | console.warn( 44 | `[react-router-relay] failed to preload ${ 45 | match.pathname 46 | } data for route ${JSON.stringify(to)}`, 47 | e, 48 | ); 49 | } 50 | } 51 | }, [routes, to, resolvedPath]); 52 | 53 | return fetchData; 54 | } 55 | -------------------------------------------------------------------------------- /src/useLinkEntryPointLoadHandler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useContext, 3 | useCallback, 4 | isValidElement, 5 | type ComponentType, 6 | } from "react"; 7 | import { 8 | type To, 9 | UNSAFE_DataRouterContext, 10 | matchRoutes, 11 | } from "react-router-dom"; 12 | import { InternalPreload } from "./routes/internal-preload-symbol.ts"; 13 | import type { PreloadableComponent } from "./routes/EntryPointRoute.ts"; 14 | 15 | /** 16 | * Returns a handler for triggering entrypoint loading for a target. This is used 17 | * by Link to preload entrypoints on render. You 18 | * can use this to build your own Link component if necessary. 19 | */ 20 | export function useLinkEntryPointLoadHandler(): (to: To) => void; 21 | /** @deprecated move the `to` argument to the callback */ 22 | export function useLinkEntryPointLoadHandler(to: To): () => void; 23 | export function useLinkEntryPointLoadHandler( 24 | deprecatedTo?: To, 25 | ): (to?: To) => void { 26 | const routes = useContext(UNSAFE_DataRouterContext)?.router.routes ?? []; 27 | 28 | // Fetch the entrypoint 29 | const fetchEntrypoint = useCallback( 30 | (to: To | undefined) => { 31 | to ??= deprecatedTo; 32 | // This shouldn't happen, at least one of to or deprecatedTo should be 33 | // defined. 34 | if (to == null) { 35 | return; 36 | } 37 | const matches = matchRoutes([...routes], to); 38 | if (!matches) { 39 | return; 40 | } 41 | for (const match of matches) { 42 | const route: any = match.route; 43 | const { Component, element } = route; 44 | let maybePreloadableComponent: 45 | | ComponentType 46 | | PreloadableComponent 47 | | undefined; 48 | if (Component) { 49 | maybePreloadableComponent = Component; 50 | } else if ( 51 | isValidElement(element) && 52 | typeof element.type !== "string" 53 | ) { 54 | maybePreloadableComponent = element.type; 55 | } 56 | 57 | if (maybePreloadableComponent) { 58 | try { 59 | if (InternalPreload in maybePreloadableComponent) { 60 | maybePreloadableComponent[InternalPreload]?.entryPoint().catch( 61 | (e: unknown) => { 62 | console.warn( 63 | `[react-router-relay] failed to preload ${ 64 | match.pathname 65 | } entrypoint for route ${JSON.stringify(to)}`, 66 | e, 67 | ); 68 | }, 69 | ); 70 | } 71 | } catch (e: unknown) { 72 | console.warn( 73 | `[react-router-relay] failed to call entrypoint preloader ${ 74 | match.pathname 75 | } for route ${JSON.stringify(to)}`, 76 | e, 77 | ); 78 | } 79 | } 80 | } 81 | }, 82 | [routes, deprecatedTo], 83 | ); 84 | 85 | return fetchEntrypoint; 86 | } 87 | -------------------------------------------------------------------------------- /src/useLinkResourceLoadHandler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useContext, 3 | useCallback, 4 | isValidElement, 5 | type ComponentType, 6 | } from "react"; 7 | import { 8 | type To, 9 | UNSAFE_DataRouterContext, 10 | matchRoutes, 11 | } from "react-router-dom"; 12 | import { InternalPreload } from "./routes/internal-preload-symbol.ts"; 13 | import type { PreloadableComponent } from "./routes/EntryPointRoute.ts"; 14 | 15 | /** 16 | * Returns a handler for triggering resource loading for a target. This is used 17 | * by Link to preload JSResources for entrypoints on hover or focus events. You 18 | * can use this to build your own Link component if necessary. 19 | */ 20 | export function useLinkResourceLoadHandler(): (to: To) => void; 21 | /** @deprecated move the `to` argument to the callback */ 22 | export function useLinkResourceLoadHandler(to: To): () => void; 23 | export function useLinkResourceLoadHandler( 24 | deprecatedTo?: To, 25 | ): (to?: To) => void { 26 | const routes = useContext(UNSAFE_DataRouterContext)?.router.routes ?? []; 27 | 28 | // Fetch the static JS resources of any entrypoints 29 | const fetchResources = useCallback( 30 | (to: To | undefined) => { 31 | to ??= deprecatedTo; 32 | // This shouldn't happen, at least one of to or deprecatedTo should be 33 | // defined. 34 | if (to == null) { 35 | return; 36 | } 37 | 38 | const matches = matchRoutes([...routes], to); 39 | if (!matches) { 40 | return; 41 | } 42 | for (const match of matches) { 43 | const route: any = match.route; 44 | const { Component, element } = route; 45 | let maybePreloadableComponent: 46 | | ComponentType 47 | | PreloadableComponent 48 | | undefined; 49 | if (Component) { 50 | maybePreloadableComponent = Component; 51 | } else if ( 52 | isValidElement(element) && 53 | typeof element.type !== "string" 54 | ) { 55 | maybePreloadableComponent = element.type; 56 | } 57 | 58 | if (maybePreloadableComponent) { 59 | try { 60 | if (InternalPreload in maybePreloadableComponent) { 61 | maybePreloadableComponent[InternalPreload]?.resource().catch( 62 | (e: unknown) => { 63 | console.warn( 64 | `[react-router-relay] failed to preload ${ 65 | match.pathname 66 | } resource for route ${JSON.stringify(to)}`, 67 | e, 68 | ); 69 | }, 70 | ); 71 | } 72 | } catch (e: unknown) { 73 | console.warn( 74 | `[react-router-relay] failed to call resource preloader ${ 75 | match.pathname 76 | } for route ${JSON.stringify(to)}`, 77 | e, 78 | ); 79 | } 80 | } 81 | } 82 | }, 83 | [routes, deprecatedTo], 84 | ); 85 | 86 | return fetchResources; 87 | } 88 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*", "test/**/*"], 3 | "compilerOptions": { 4 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 5 | 6 | /* Projects */ 7 | // "incremental": true, /* Enable incremental compilation */ 8 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 9 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 10 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 11 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 12 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 13 | 14 | /* Language and Environment */ 15 | "target": "ES2019" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 16 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 17 | "jsx": "react-jsx" /* Specify what JSX code is generated. */, 18 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 19 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 20 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 21 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 22 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 23 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 24 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 25 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 26 | 27 | /* Modules */ 28 | "module": "ESNext" /* Specify what module code is generated. */, 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "resolveJsonModule": true, /* Enable importing .json files */ 38 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 39 | 40 | /* JavaScript Support */ 41 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 42 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 43 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 44 | 45 | /* Emit */ 46 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 47 | "declarationMap": true /* Create sourcemaps for d.ts files. */, 48 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 49 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 50 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 51 | // "outDir": "./" /* Specify an output folder for all emitted files. */, 52 | // "removeComments": true, /* Disable emitting comments. */ 53 | "noEmit": true /* Disable emitting files from a compilation. */, 54 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 55 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 56 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 57 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 60 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 61 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 62 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 63 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 64 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 65 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 66 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 67 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 68 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 69 | "rewriteRelativeImportExtensions": true, 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 77 | 78 | /* Type Checking */ 79 | "strict": true /* Enable all strict type-checking options. */, 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 81 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 86 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # This file is generated by running "yarn install" inside your project. 2 | # Manual changes might be lost - proceed with caution! 3 | 4 | __metadata: 5 | version: 8 6 | cacheKey: 10 7 | 8 | "@babel/runtime@npm:^7.25.0": 9 | version: 7.26.0 10 | resolution: "@babel/runtime@npm:7.26.0" 11 | dependencies: 12 | regenerator-runtime: "npm:^0.14.0" 13 | checksum: 10/9f4ea1c1d566c497c052d505587554e782e021e6ccd302c2ad7ae8291c8e16e3f19d4a7726fb64469e057779ea2081c28b7dbefec6d813a22f08a35712c0f699 14 | languageName: node 15 | linkType: hard 16 | 17 | "@loop-payments/react-router-relay@workspace:.": 18 | version: 0.0.0-use.local 19 | resolution: "@loop-payments/react-router-relay@workspace:." 20 | dependencies: 21 | "@types/react-relay": "npm:^18.2.0" 22 | prettier: "npm:^3.5.3" 23 | react: "npm:^19.1.0" 24 | react-relay: "npm:^18.2.0" 25 | react-router-dom: "npm:^7.4.1" 26 | typescript: "npm:^5.8.2" 27 | peerDependencies: 28 | react-relay: ">=15.0.0" 29 | react-router-dom: ">=6.4.0" 30 | languageName: unknown 31 | linkType: soft 32 | 33 | "@types/cookie@npm:^0.6.0": 34 | version: 0.6.0 35 | resolution: "@types/cookie@npm:0.6.0" 36 | checksum: 10/b883348d5bf88695fbc2c2276b1c49859267a55cae3cf11ea1dccc1b3be15b466e637ce3242109ba27d616c77c6aa4efe521e3d557110b4fdd9bc332a12445c2 37 | languageName: node 38 | linkType: hard 39 | 40 | "@types/prop-types@npm:*": 41 | version: 15.7.5 42 | resolution: "@types/prop-types@npm:15.7.5" 43 | checksum: 10/5b43b8b15415e1f298243165f1d44390403bb2bd42e662bca3b5b5633fdd39c938e91b7fce3a9483699db0f7a715d08cef220c121f723a634972fdf596aec980 44 | languageName: node 45 | linkType: hard 46 | 47 | "@types/react-relay@npm:^18.2.0": 48 | version: 18.2.0 49 | resolution: "@types/react-relay@npm:18.2.0" 50 | dependencies: 51 | "@types/react": "npm:*" 52 | "@types/relay-runtime": "npm:*" 53 | checksum: 10/fb5642274314dd64bf2e02ea680088fc771ade9623339f2935f32e00d36b1a0e0dcd0b0ee6ee9ea115658594ba3bfb6b3cb50aea213d8bc9994fd5bf06fe710d 54 | languageName: node 55 | linkType: hard 56 | 57 | "@types/react@npm:*": 58 | version: 18.2.14 59 | resolution: "@types/react@npm:18.2.14" 60 | dependencies: 61 | "@types/prop-types": "npm:*" 62 | "@types/scheduler": "npm:*" 63 | csstype: "npm:^3.0.2" 64 | checksum: 10/9a8e9a6e6d39dba32a449ac986add435ee30d1f5af336efa7b0cb0e8e31eefa2b85ab2ef921284b373e820c47d184c7161f826cc77f1e528cc13642be3596110 65 | languageName: node 66 | linkType: hard 67 | 68 | "@types/relay-runtime@npm:*": 69 | version: 18.2.1 70 | resolution: "@types/relay-runtime@npm:18.2.1" 71 | checksum: 10/69bc739c101c99fb502882411006a168132811da67187ec04556e61e9acf5bb8590040222768b54ababad8c1a6b559441563a667bc058f94aadd6e5558e16014 72 | languageName: node 73 | linkType: hard 74 | 75 | "@types/scheduler@npm:*": 76 | version: 0.16.3 77 | resolution: "@types/scheduler@npm:0.16.3" 78 | checksum: 10/2b0aec39c24268e3ce938c5db2f2e77f5c3dd280e05c262d9c2fe7d890929e4632a6b8e94334017b66b45e4f92a5aa42ba3356640c2a1175fa37bef2f5200767 79 | languageName: node 80 | linkType: hard 81 | 82 | "asap@npm:~2.0.3": 83 | version: 2.0.6 84 | resolution: "asap@npm:2.0.6" 85 | checksum: 10/b244c0458c571945e4b3be0b14eb001bea5596f9868cc50cc711dc03d58a7e953517d3f0dad81ccde3ff37d1f074701fa76a6f07d41aaa992d7204a37b915dda 86 | languageName: node 87 | linkType: hard 88 | 89 | "cookie@npm:^1.0.1": 90 | version: 1.0.2 91 | resolution: "cookie@npm:1.0.2" 92 | checksum: 10/f5817cdc84d8977761b12549eba29435e675e65c7fef172bc31737788cd8adc83796bf8abe6d950554e7987325ad2d9ac2971c5bd8ff0c4f81c145f82e4ab1be 93 | languageName: node 94 | linkType: hard 95 | 96 | "cross-fetch@npm:^3.1.5": 97 | version: 3.1.8 98 | resolution: "cross-fetch@npm:3.1.8" 99 | dependencies: 100 | node-fetch: "npm:^2.6.12" 101 | checksum: 10/ac8c4ca87d2ac0e17a19b6a293a67ee8934881aee5ec9a5a8323c30e9a9a60a0f5291d3c0d633ec2a2f970cbc60978d628804dfaf03add92d7e720b6d37f392c 102 | languageName: node 103 | linkType: hard 104 | 105 | "csstype@npm:^3.0.2": 106 | version: 3.1.2 107 | resolution: "csstype@npm:3.1.2" 108 | checksum: 10/1f39c541e9acd9562996d88bc9fb62d1cb234786ef11ed275567d4b2bd82e1ceacde25debc8de3d3b4871ae02c2933fa02614004c97190711caebad6347debc2 109 | languageName: node 110 | linkType: hard 111 | 112 | "fbjs-css-vars@npm:^1.0.0": 113 | version: 1.0.2 114 | resolution: "fbjs-css-vars@npm:1.0.2" 115 | checksum: 10/72baf6d22c45b75109118b4daecb6c8016d4c83c8c0f23f683f22e9d7c21f32fff6201d288df46eb561e3c7d4bb4489b8ad140b7f56444c453ba407e8bd28511 116 | languageName: node 117 | linkType: hard 118 | 119 | "fbjs@npm:^3.0.2": 120 | version: 3.0.5 121 | resolution: "fbjs@npm:3.0.5" 122 | dependencies: 123 | cross-fetch: "npm:^3.1.5" 124 | fbjs-css-vars: "npm:^1.0.0" 125 | loose-envify: "npm:^1.0.0" 126 | object-assign: "npm:^4.1.0" 127 | promise: "npm:^7.1.1" 128 | setimmediate: "npm:^1.0.5" 129 | ua-parser-js: "npm:^1.0.35" 130 | checksum: 10/71252595b00b06fb0475a295c74d81ada1cc499b7e11f2cde51fef04618affa568f5b7f4927f61720c23254b9144be28f8acb2086a5001cf65df8eec87c6ca5c 131 | languageName: node 132 | linkType: hard 133 | 134 | "invariant@npm:^2.2.4": 135 | version: 2.2.4 136 | resolution: "invariant@npm:2.2.4" 137 | dependencies: 138 | loose-envify: "npm:^1.0.0" 139 | checksum: 10/cc3182d793aad82a8d1f0af697b462939cb46066ec48bbf1707c150ad5fad6406137e91a262022c269702e01621f35ef60269f6c0d7fd178487959809acdfb14 140 | languageName: node 141 | linkType: hard 142 | 143 | "js-tokens@npm:^3.0.0 || ^4.0.0": 144 | version: 4.0.0 145 | resolution: "js-tokens@npm:4.0.0" 146 | checksum: 10/af37d0d913fb56aec6dc0074c163cc71cd23c0b8aad5c2350747b6721d37ba118af35abdd8b33c47ec2800de07dedb16a527ca9c530ee004093e04958bd0cbf2 147 | languageName: node 148 | linkType: hard 149 | 150 | "loose-envify@npm:^1.0.0": 151 | version: 1.4.0 152 | resolution: "loose-envify@npm:1.4.0" 153 | dependencies: 154 | js-tokens: "npm:^3.0.0 || ^4.0.0" 155 | bin: 156 | loose-envify: cli.js 157 | checksum: 10/6517e24e0cad87ec9888f500c5b5947032cdfe6ef65e1c1936a0c48a524b81e65542c9c3edc91c97d5bddc806ee2a985dbc79be89215d613b1de5db6d1cfe6f4 158 | languageName: node 159 | linkType: hard 160 | 161 | "node-fetch@npm:^2.6.12": 162 | version: 2.6.12 163 | resolution: "node-fetch@npm:2.6.12" 164 | dependencies: 165 | whatwg-url: "npm:^5.0.0" 166 | peerDependencies: 167 | encoding: ^0.1.0 168 | peerDependenciesMeta: 169 | encoding: 170 | optional: true 171 | checksum: 10/370ed4d906edad9709a81b54a0141d37d2973a27dc80c723d8ac14afcec6dc67bc6c70986a96992b64ec75d08159cc4b65ce6aa9063941168ea5ac73b24df9f8 172 | languageName: node 173 | linkType: hard 174 | 175 | "nullthrows@npm:^1.1.1": 176 | version: 1.1.1 177 | resolution: "nullthrows@npm:1.1.1" 178 | checksum: 10/c7cf377a095535dc301d81cf7959d3784d090a609a2a4faa40b6121a0c1d7f70d3a3aa534a34ab852e8553b66848ec503c28f2c19efd617ed564dc07dfbb6d33 179 | languageName: node 180 | linkType: hard 181 | 182 | "object-assign@npm:^4.1.0": 183 | version: 4.1.1 184 | resolution: "object-assign@npm:4.1.1" 185 | checksum: 10/fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f 186 | languageName: node 187 | linkType: hard 188 | 189 | "prettier@npm:^3.5.3": 190 | version: 3.5.3 191 | resolution: "prettier@npm:3.5.3" 192 | bin: 193 | prettier: bin/prettier.cjs 194 | checksum: 10/7050c08f674d9e49fbd9a4c008291d0715471f64e94cc5e4b01729affce221dfc6875c8de7e66b728c64abc9352eefb7eaae071b5f79d30081be207b53774b78 195 | languageName: node 196 | linkType: hard 197 | 198 | "promise@npm:^7.1.1": 199 | version: 7.3.1 200 | resolution: "promise@npm:7.3.1" 201 | dependencies: 202 | asap: "npm:~2.0.3" 203 | checksum: 10/37dbe58ca7b0716cc881f0618128f1fd6ff9c46cdc529a269fd70004e567126a449a94e9428e2d19b53d06182d11b45d0c399828f103e06b2bb87643319bd2e7 204 | languageName: node 205 | linkType: hard 206 | 207 | "react-relay@npm:^18.2.0": 208 | version: 18.2.0 209 | resolution: "react-relay@npm:18.2.0" 210 | dependencies: 211 | "@babel/runtime": "npm:^7.25.0" 212 | fbjs: "npm:^3.0.2" 213 | invariant: "npm:^2.2.4" 214 | nullthrows: "npm:^1.1.1" 215 | relay-runtime: "npm:18.2.0" 216 | peerDependencies: 217 | react: ^16.9.0 || ^17 || ^18 218 | checksum: 10/9f6c908bb38e4aa03ebfd8597906d38967d120c857fdcd7a4d631a3b260b842026520915fcee81caeac6db5f8361e51481068d3d994e4dc0e3b318eb6fda095c 219 | languageName: node 220 | linkType: hard 221 | 222 | "react-router-dom@npm:^7.4.1": 223 | version: 7.4.1 224 | resolution: "react-router-dom@npm:7.4.1" 225 | dependencies: 226 | react-router: "npm:7.4.1" 227 | peerDependencies: 228 | react: ">=18" 229 | react-dom: ">=18" 230 | checksum: 10/5e839da47ca55eefb36766f115bf9dfca133fa7e1a573a590c34d3bbe980431edee8d3cc553a901e5869ecddda22a5af80987fcf8e59babb6853d945c7a6600b 231 | languageName: node 232 | linkType: hard 233 | 234 | "react-router@npm:7.4.1": 235 | version: 7.4.1 236 | resolution: "react-router@npm:7.4.1" 237 | dependencies: 238 | "@types/cookie": "npm:^0.6.0" 239 | cookie: "npm:^1.0.1" 240 | set-cookie-parser: "npm:^2.6.0" 241 | turbo-stream: "npm:2.4.0" 242 | peerDependencies: 243 | react: ">=18" 244 | react-dom: ">=18" 245 | peerDependenciesMeta: 246 | react-dom: 247 | optional: true 248 | checksum: 10/5873187abcd1d898f4bf7c0f31af48d7a867ec35bfa030d582c03bfb390fdf6ebfb252732a200d2212771319133124279b7c8ba034df0cde77daf9140345a1c6 249 | languageName: node 250 | linkType: hard 251 | 252 | "react@npm:^19.1.0": 253 | version: 19.1.0 254 | resolution: "react@npm:19.1.0" 255 | checksum: 10/d0180689826fd9de87e839c365f6f361c561daea397d61d724687cae88f432a307d1c0f53a0ee95ddbe3352c10dac41d7ff1ad85530fb24951b27a39e5398db4 256 | languageName: node 257 | linkType: hard 258 | 259 | "regenerator-runtime@npm:^0.14.0": 260 | version: 0.14.1 261 | resolution: "regenerator-runtime@npm:0.14.1" 262 | checksum: 10/5db3161abb311eef8c45bcf6565f4f378f785900ed3945acf740a9888c792f75b98ecb77f0775f3bf95502ff423529d23e94f41d80c8256e8fa05ed4b07cf471 263 | languageName: node 264 | linkType: hard 265 | 266 | "relay-runtime@npm:18.2.0": 267 | version: 18.2.0 268 | resolution: "relay-runtime@npm:18.2.0" 269 | dependencies: 270 | "@babel/runtime": "npm:^7.25.0" 271 | fbjs: "npm:^3.0.2" 272 | invariant: "npm:^2.2.4" 273 | checksum: 10/cfa7d4af0a13526c89c39f6b392647d33ce38435a9d5de95ca89e8ccf01612e682bca5defb1767f7ffc8896811e2acb90e2c73e577498efa37a7b050f31dcf64 274 | languageName: node 275 | linkType: hard 276 | 277 | "set-cookie-parser@npm:^2.6.0": 278 | version: 2.7.1 279 | resolution: "set-cookie-parser@npm:2.7.1" 280 | checksum: 10/c92b1130032693342bca13ea1b1bc93967ab37deec4387fcd8c2a843c0ef2fd9a9f3df25aea5bb3976cd05a91c2cf4632dd6164d6e1814208fb7d7e14edd42b4 281 | languageName: node 282 | linkType: hard 283 | 284 | "setimmediate@npm:^1.0.5": 285 | version: 1.0.5 286 | resolution: "setimmediate@npm:1.0.5" 287 | checksum: 10/76e3f5d7f4b581b6100ff819761f04a984fa3f3990e72a6554b57188ded53efce2d3d6c0932c10f810b7c59414f85e2ab3c11521877d1dea1ce0b56dc906f485 288 | languageName: node 289 | linkType: hard 290 | 291 | "tr46@npm:~0.0.3": 292 | version: 0.0.3 293 | resolution: "tr46@npm:0.0.3" 294 | checksum: 10/8f1f5aa6cb232f9e1bdc86f485f916b7aa38caee8a778b378ffec0b70d9307873f253f5cbadbe2955ece2ac5c83d0dc14a77513166ccd0a0c7fe197e21396695 295 | languageName: node 296 | linkType: hard 297 | 298 | "turbo-stream@npm:2.4.0": 299 | version: 2.4.0 300 | resolution: "turbo-stream@npm:2.4.0" 301 | checksum: 10/7079bbc82b58340f783144cd669cc7e598288523103a8d68bb8a4c6bb28c64eccb71d389b33aab07788d3a9030638b795709e15cb8486f722b1cdac59cb58afc 302 | languageName: node 303 | linkType: hard 304 | 305 | "typescript@npm:^5.8.2": 306 | version: 5.8.2 307 | resolution: "typescript@npm:5.8.2" 308 | bin: 309 | tsc: bin/tsc 310 | tsserver: bin/tsserver 311 | checksum: 10/dbc2168a55d56771f4d581997be52bab5cbc09734fec976cfbaabd787e61fb4c6cf9125fd48c6f98054ce549c77ecedefc7f64252a830dd8e9c3381f61fbeb78 312 | languageName: node 313 | linkType: hard 314 | 315 | "typescript@patch:typescript@npm%3A^5.8.2#optional!builtin": 316 | version: 5.8.2 317 | resolution: "typescript@patch:typescript@npm%3A5.8.2#optional!builtin::version=5.8.2&hash=5786d5" 318 | bin: 319 | tsc: bin/tsc 320 | tsserver: bin/tsserver 321 | checksum: 10/97920a082ffc57583b1cb6bc4faa502acc156358e03f54c7fc7fdf0b61c439a717f4c9070c449ee9ee683d4cfc3bb203127c2b9794b2950f66d9d307a4ff262c 322 | languageName: node 323 | linkType: hard 324 | 325 | "ua-parser-js@npm:^1.0.35": 326 | version: 1.0.35 327 | resolution: "ua-parser-js@npm:1.0.35" 328 | checksum: 10/b69c99c20f90e1d441939be591a3e4c848d12b88671953fc0de7664bdcdb660f4e9db236099ae966cfb20504d8894825bbdee0fcc31326f2823bf439eadfc02c 329 | languageName: node 330 | linkType: hard 331 | 332 | "webidl-conversions@npm:^3.0.0": 333 | version: 3.0.1 334 | resolution: "webidl-conversions@npm:3.0.1" 335 | checksum: 10/b65b9f8d6854572a84a5c69615152b63371395f0c5dcd6729c45789052296df54314db2bc3e977df41705eacb8bc79c247cee139a63fa695192f95816ed528ad 336 | languageName: node 337 | linkType: hard 338 | 339 | "whatwg-url@npm:^5.0.0": 340 | version: 5.0.0 341 | resolution: "whatwg-url@npm:5.0.0" 342 | dependencies: 343 | tr46: "npm:~0.0.3" 344 | webidl-conversions: "npm:^3.0.0" 345 | checksum: 10/f95adbc1e80820828b45cc671d97da7cd5e4ef9deb426c31bcd5ab00dc7103042291613b3ef3caec0a2335ed09e0d5ed026c940755dbb6d404e2b27f940fdf07 346 | languageName: node 347 | linkType: hard 348 | --------------------------------------------------------------------------------