diff --git a/.changeset/sour-cycles-lie.md b/.changeset/sour-cycles-lie.md new file mode 100644 index 0000000000..6cc890d54e --- /dev/null +++ b/.changeset/sour-cycles-lie.md @@ -0,0 +1,11 @@ +--- +"@react-router/dev": major +"react-router": major +--- + +- Consolidate types previously duplicated across `@remix-run/router`, `@remix-run/server-runtime`, and `@remix-run/react` now that they all live in `react-router` + - Examples: `LoaderFunction`, `LoaderFunctionArgs`, `ActionFunction`, `ActionFunctionArgs`, `DataFunctionArgs`, `RouteManifest`, `LinksFunction`, `Route`, `EntryRoute` + - The `RouteManifest` type used by the "remix" code is now slightly stricter because it is using the former `@remix-run/router` `RouteManifest` + - `Record -> Record` + - Removed `AppData` type in favor of inlining `unknown` in the few locations it was used + - Removed `ServerRuntimeMeta*` types in favor of the `Meta*` types they were duplicated from diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts index 2d87c13bfc..3f0bc19a3d 100644 --- a/packages/react-router-dev/vite/plugin.ts +++ b/packages/react-router-dev/vite/plugin.ts @@ -2023,11 +2023,13 @@ function groupRoutesByParentId(manifest: ServerBuild["routes"]) { let routes: Record[]> = {}; Object.values(manifest).forEach((route) => { - let parentId = route.parentId || ""; - if (!routes[parentId]) { - routes[parentId] = []; + if (route) { + let parentId = route.parentId || ""; + if (!routes[parentId]) { + routes[parentId] = []; + } + routes[parentId].push(route); } - routes[parentId].push(route); }); return routes; diff --git a/packages/react-router-dev/vite/styles.ts b/packages/react-router-dev/vite/styles.ts index 2db7e89287..1e16372135 100644 --- a/packages/react-router-dev/vite/styles.ts +++ b/packages/react-router-dev/vite/styles.ts @@ -171,14 +171,16 @@ const findDeps = async ( }; const groupRoutesByParentId = (manifest: ServerRouteManifest) => { - let routes: Record[]> = {}; + let routes: Record[]> = {}; Object.values(manifest).forEach((route) => { - let parentId = route.parentId || ""; - if (!routes[parentId]) { - routes[parentId] = []; + if (route) { + let parentId = route.parentId || ""; + if (!routes[parentId]) { + routes[parentId] = []; + } + routes[parentId].push(route); } - routes[parentId].push(route); }); return routes; @@ -189,11 +191,8 @@ const groupRoutesByParentId = (manifest: ServerRouteManifest) => { const createRoutes = ( manifest: ServerRouteManifest, parentId: string = "", - routesByParentId: Record< - string, - Omit[] - > = groupRoutesByParentId(manifest) -): ServerRoute[] => { + routesByParentId = groupRoutesByParentId(manifest) +): NonNullable[] => { return (routesByParentId[parentId] || []).map((route) => ({ ...route, children: createRoutes(manifest, route.id, routesByParentId), diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index 0e63a1d4b0..8edab6eacb 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -213,12 +213,6 @@ export { createRoutesStub } from "./lib/dom/ssr/routes-test-stub"; // Expose old @remix-run/server-runtime API, minus duplicate APIs export { createCookie, isCookie } from "./lib/server-runtime/cookies"; -// TODO: (v7) Clean up code paths for these exports -// export { -// json, -// redirect, -// redirectDocument, -// } from "./lib/server-runtime/responses"; export { createRequestHandler } from "./lib/server-runtime/server"; export { createSession, @@ -258,16 +252,6 @@ export type { } from "./lib/router/links"; export type { - // TODO: (v7) Clean up code paths for these exports - // ActionFunction, - // ActionFunctionArgs, - // LinksFunction, - // LoaderFunction, - // LoaderFunctionArgs, - // ServerRuntimeMetaArgs, - // ServerRuntimeMetaDescriptor, - // ServerRuntimeMetaFunction, - DataFunctionArgs, HeadersArgs, HeadersFunction, } from "./lib/server-runtime/routeModules"; diff --git a/packages/react-router/lib/dom-export/hydrated-router.tsx b/packages/react-router/lib/dom-export/hydrated-router.tsx index 295470be06..af8ae89b04 100644 --- a/packages/react-router/lib/dom-export/hydrated-router.tsx +++ b/packages/react-router/lib/dom-export/hydrated-router.tsx @@ -133,6 +133,7 @@ function createHydratedRouter(): DataRouter { // * or doesn't have a server loader and we have no data to render if ( route && + manifestRoute && shouldHydrateRouteLoader( manifestRoute, route, diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx index c0a5c5914e..e91636138f 100644 --- a/packages/react-router/lib/dom/ssr/components.tsx +++ b/packages/react-router/lib/dom/ssr/components.tsx @@ -366,7 +366,8 @@ function PrefetchPageLinksImpl({ let routesParams = new Set(); let foundOptOutRoute = false; nextMatches.forEach((m) => { - if (!manifest.routes[m.route.id].hasLoader) { + let manifestRoute = manifest.routes[m.route.id]; + if (!manifestRoute || !manifestRoute.hasLoader) { return; } @@ -376,7 +377,7 @@ function PrefetchPageLinksImpl({ routeModules[m.route.id]?.shouldRevalidate ) { foundOptOutRoute = true; - } else if (manifest.routes[m.route.id].hasClientLoader) { + } else if (manifestRoute.hasClientLoader) { foundOptOutRoute = true; } else { routesParams.add(m.route.id); @@ -682,7 +683,7 @@ ${matches .map( (match, index) => `import * as route${index} from ${JSON.stringify( - manifest.routes[match.route.id].module + manifest.routes[match.route.id]!.module )};` ) .join("\n")} @@ -728,7 +729,7 @@ import(${JSON.stringify(manifest.entry.module)});`; let routePreloads = matches .map((match) => { let route = manifest.routes[match.route.id]; - return (route.imports || []).concat([route.module]); + return route ? (route.imports || []).concat([route.module]) : []; }) .flat(1); diff --git a/packages/react-router/lib/dom/ssr/data.ts b/packages/react-router/lib/dom/ssr/data.ts index 0705ce47a5..bc0aeab8e9 100644 --- a/packages/react-router/lib/dom/ssr/data.ts +++ b/packages/react-router/lib/dom/ssr/data.ts @@ -1,10 +1,5 @@ import "../global"; -/** - * Data for a route that was returned from a `loader()`. - */ -export type AppData = unknown; - export async function createRequestInit( request: Request ): Promise { diff --git a/packages/react-router/lib/dom/ssr/entry.ts b/packages/react-router/lib/dom/ssr/entry.ts index 76e0f29c2d..bded38733d 100644 --- a/packages/react-router/lib/dom/ssr/entry.ts +++ b/packages/react-router/lib/dom/ssr/entry.ts @@ -1,14 +1,15 @@ import type { StaticHandlerContext } from "../../router/router"; -import type { RouteManifest, EntryRoute } from "./routes"; +import type { EntryRoute } from "./routes"; import type { RouteModules } from "./routeModules"; - -// Object passed to RemixContext.Provider +import type { RouteManifest } from "../../router/utils"; type SerializedError = { message: string; stack?: string; }; + +// Object passed to RemixContext.Provider export interface FrameworkContextObject { manifest: AssetsManifest; routeModules: RouteModules; diff --git a/packages/react-router/lib/dom/ssr/fog-of-war.ts b/packages/react-router/lib/dom/ssr/fog-of-war.ts index 366a4eb49f..a9bfa8de6d 100644 --- a/packages/react-router/lib/dom/ssr/fog-of-war.ts +++ b/packages/react-router/lib/dom/ssr/fog-of-war.ts @@ -1,9 +1,11 @@ import * as React from "react"; import type { PatchRoutesOnNavigationFunction } from "../../context"; import type { Router as DataRouter } from "../../router/router"; +import type { RouteManifest } from "../../router/utils"; import { matchRoutes } from "../../router/utils"; import type { AssetsManifest } from "./entry"; import type { RouteModules } from "./routeModules"; +import type { EntryRoute } from "./routes"; import { createClientRoutes } from "./routes"; declare global { @@ -233,13 +235,12 @@ export async function fetchAndApplyManifestPatches( // Patch routes we don't know about yet into the manifest let knownRoutes = new Set(Object.keys(manifest.routes)); - let patches: AssetsManifest["routes"] = Object.values(serverPatches).reduce( - (acc, route) => - !knownRoutes.has(route.id) - ? Object.assign(acc, { [route.id]: route }) - : acc, - {} - ); + let patches = Object.values(serverPatches).reduce((acc, route) => { + if (route && !knownRoutes.has(route.id)) { + acc[route.id] = route; + } + return acc; + }, {} as RouteManifest); Object.assign(manifest.routes, patches); // Track discovered paths so we don't have to fetch them again @@ -249,7 +250,7 @@ export async function fetchAndApplyManifestPatches( // in their new children let parentIds = new Set(); Object.values(patches).forEach((patch) => { - if (!patch.parentId || !patches[patch.parentId]) { + if (patch && (!patch.parentId || !patches[patch.parentId])) { parentIds.add(patch.parentId); } }); diff --git a/packages/react-router/lib/dom/ssr/links.ts b/packages/react-router/lib/dom/ssr/links.ts index 50bb073e92..860aaa8f89 100644 --- a/packages/react-router/lib/dom/ssr/links.ts +++ b/packages/react-router/lib/dom/ssr/links.ts @@ -26,7 +26,9 @@ export function getKeyedLinksForMatches( let module = routeModules[match.route.id]; let route = manifest.routes[match.route.id]; return [ - route.css ? route.css.map((href) => ({ rel: "stylesheet", href })) : [], + route && route.css + ? route.css.map((href) => ({ rel: "stylesheet", href })) + : [], module?.links?.() || [], ]; }) @@ -138,11 +140,12 @@ export async function getKeyedPrefetchLinks( ): Promise { let links = await Promise.all( matches.map(async (match) => { - let mod = await loadRouteModule( - manifest.routes[match.route.id], - routeModules - ); - return mod.links ? mod.links() : []; + let route = manifest.routes[match.route.id]; + if (route) { + let mod = await loadRouteModule(route, routeModules); + return mod.links ? mod.links() : []; + } + return []; }) ); @@ -197,7 +200,7 @@ export function getNewMatchesForLinks( if (mode === "data") { return nextMatches.filter((match, index) => { let manifestRoute = manifest.routes[match.route.id]; - if (!manifestRoute.hasLoader) { + if (!manifestRoute || !manifestRoute.hasLoader) { return false; } @@ -235,6 +238,7 @@ export function getModuleLinkHrefs( matches .map((match) => { let route = manifestPatch.routes[match.route.id]; + if (!route) return []; let hrefs = [route.module]; if (route.imports) { hrefs = hrefs.concat(route.imports); @@ -256,12 +260,11 @@ function getCurrentPageModulePreloadHrefs( matches .map((match) => { let route = manifest.routes[match.route.id]; + if (!route) return []; let hrefs = [route.module]; - if (route.imports) { hrefs = hrefs.concat(route.imports); } - return hrefs; }) .flat(1) diff --git a/packages/react-router/lib/dom/ssr/routeModules.ts b/packages/react-router/lib/dom/ssr/routeModules.ts index 45d2f0294d..5c3500faa4 100644 --- a/packages/react-router/lib/dom/ssr/routeModules.ts +++ b/packages/react-router/lib/dom/ssr/routeModules.ts @@ -1,17 +1,15 @@ import type { ComponentType, ReactElement } from "react"; import type { Location } from "../../router/history"; import type { - ActionFunction as RRActionFunction, - ActionFunctionArgs as RRActionFunctionArgs, - LoaderFunction as RRLoaderFunction, - LoaderFunctionArgs as RRLoaderFunctionArgs, + ActionFunction, + ActionFunctionArgs, + LoaderFunction, + LoaderFunctionArgs, Params, ShouldRevalidateFunction, - LoaderFunctionArgs, } from "../../router/utils"; import type { SerializeFrom } from "./components"; -import type { AppData } from "./data"; import type { EntryRoute } from "./routes"; import type { DataRouteMatch } from "../../context"; import type { LinkDescriptor } from "../../router/links"; @@ -38,13 +36,13 @@ export interface RouteModule { */ export type ClientActionFunction = ( args: ClientActionFunctionArgs -) => ReturnType; +) => ReturnType; /** * Arguments passed to a route `clientAction` function */ -export type ClientActionFunctionArgs = RRActionFunctionArgs & { - serverAction: () => Promise>; +export type ClientActionFunctionArgs = ActionFunctionArgs & { + serverAction: () => Promise>; }; /** @@ -52,15 +50,15 @@ export type ClientActionFunctionArgs = RRActionFunctionArgs & { */ export type ClientLoaderFunction = (( args: ClientLoaderFunctionArgs -) => ReturnType) & { +) => ReturnType) & { hydrate?: boolean; }; /** * Arguments passed to a route `clientLoader` function */ -export type ClientLoaderFunctionArgs = RRLoaderFunctionArgs & { - serverLoader: () => Promise>; +export type ClientLoaderFunctionArgs = LoaderFunctionArgs & { + serverLoader: () => Promise>; }; /** @@ -96,19 +94,6 @@ export interface LinksFunction { (): LinkDescriptor[]; } -// Loose copy from @react-router/server-runtime to avoid circular imports -type LoaderFunction = ( - args: LoaderFunctionArgs & { - // Context is always provided in Remix, and typed for module augmentation support. - context: unknown; - // TODO: (v7) Make this non-optional once single-fetch is the default - response?: { - status: number | undefined; - headers: Headers; - }; - } -) => ReturnType; - export interface MetaMatch< RouteId extends string = string, Loader extends LoaderFunction | unknown = unknown @@ -144,7 +129,7 @@ export interface MetaArgs< > > { data: - | (Loader extends LoaderFunction ? SerializeFrom : AppData) + | (Loader extends LoaderFunction ? SerializeFrom : unknown) | undefined; params: Params; location: Location; @@ -152,6 +137,56 @@ export interface MetaArgs< error?: unknown; } +/** + * A function that returns an array of data objects to use for rendering + * metadata HTML tags in a route. These tags are not rendered on descendant + * routes in the route hierarchy. In other words, they will only be rendered on + * the route in which they are exported. + * + * @param Loader - The type of the current route's loader function + * @param MatchLoaders - Mapping from a parent route's filepath to its loader + * function type + * + * Note that parent route filepaths are relative to the `app/` directory. + * + * For example, if this meta function is for `/sales/customers/$customerId`: + * + * ```ts + * // app/root.tsx + * const loader = () => ({ hello: "world" }) + * export type Loader = typeof loader + * + * // app/routes/sales.tsx + * const loader = () => ({ salesCount: 1074 }) + * export type Loader = typeof loader + * + * // app/routes/sales/customers.tsx + * const loader = () => ({ customerCount: 74 }) + * export type Loader = typeof loader + * + * // app/routes/sales/customers/$customersId.tsx + * import type { Loader as RootLoader } from "../../../root" + * import type { Loader as SalesLoader } from "../../sales" + * import type { Loader as CustomersLoader } from "../../sales/customers" + * + * const loader = () => ({ name: "Customer name" }) + * + * const meta: MetaFunction = ({ data, matches }) => { + * const { name } = data + * // ^? string + * const { customerCount } = matches.find((match) => match.id === "routes/sales/customers").data + * // ^? number + * const { salesCount } = matches.find((match) => match.id === "routes/sales").data + * // ^? number + * const { hello } = matches.find((match) => match.id === "root").data + * // ^? "world" + * } + * ``` + */ export interface MetaFunction< Loader extends LoaderFunction | unknown = unknown, MatchLoaders extends Record = Record< diff --git a/packages/react-router/lib/dom/ssr/routes.tsx b/packages/react-router/lib/dom/ssr/routes.tsx index d83664a189..375e4da6e3 100644 --- a/packages/react-router/lib/dom/ssr/routes.tsx +++ b/packages/react-router/lib/dom/ssr/routes.tsx @@ -4,6 +4,7 @@ import type { HydrationState } from "../../router/router"; import type { ActionFunctionArgs, LoaderFunctionArgs, + RouteManifest, ShouldRevalidateFunction, ShouldRevalidateFunctionArgs, } from "../../router/utils"; @@ -18,12 +19,7 @@ import invariant from "./invariant"; import { useRouteError } from "../../hooks"; import type { DataRouteObject } from "../../context"; -export interface RouteManifest { - [routeId: string]: Route; -} - -// NOTE: make sure to change the Route in server-runtime if you change this -interface Route { +export interface Route { index?: boolean; caseSensitive?: boolean; id: string; @@ -31,7 +27,6 @@ interface Route { path?: string; } -// NOTE: make sure to change the EntryRoute in server-runtime if you change this export interface EntryRoute extends Route { hasAction: boolean; hasLoader: boolean; @@ -50,11 +45,13 @@ function groupRoutesByParentId(manifest: RouteManifest) { let routes: Record[]> = {}; Object.values(manifest).forEach((route) => { - let parentId = route.parentId || ""; - if (!routes[parentId]) { - routes[parentId] = []; + if (route) { + let parentId = route.parentId || ""; + if (!routes[parentId]) { + routes[parentId] = []; + } + routes[parentId].push(route); } - routes[parentId].push(route); }); return routes; diff --git a/packages/react-router/lib/dom/ssr/server.tsx b/packages/react-router/lib/dom/ssr/server.tsx index f8ad22f6f7..1d91e31801 100644 --- a/packages/react-router/lib/dom/ssr/server.tsx +++ b/packages/react-router/lib/dom/ssr/server.tsx @@ -58,6 +58,7 @@ export function ServerRouter({ // * or doesn't have a server loader and we have no data to render if ( route && + manifestRoute && shouldHydrateRouteLoader(manifestRoute, route, context.isSpaMode) && (route.HydrateFallback || !manifestRoute.hasLoader) ) { diff --git a/packages/react-router/lib/dom/ssr/single-fetch.tsx b/packages/react-router/lib/dom/ssr/single-fetch.tsx index 0a04684019..7829b400ca 100644 --- a/packages/react-router/lib/dom/ssr/single-fetch.tsx +++ b/packages/react-router/lib/dom/ssr/single-fetch.tsx @@ -234,6 +234,8 @@ async function singleFetchLoaderNavigationStrategy( m.resolve(async (handler) => { routeDfds[i].resolve(); + let manifestRoute = manifest.routes[m.route.id]; + if (!m.shouldLoad) { // If we're not yet initialized and this is the initial load, respect // `shouldLoad` because we're only dealing with `clientLoader.hydrate` @@ -247,7 +249,8 @@ async function singleFetchLoaderNavigationStrategy( // via `shouldRevalidate` if ( m.route.id in router.state.loaderData && - manifest.routes[m.route.id].hasLoader && + manifestRoute && + manifestRoute.hasLoader && routeModules[m.route.id]?.shouldRevalidate ) { foundOptOutRoute = true; @@ -257,8 +260,8 @@ async function singleFetchLoaderNavigationStrategy( // When a route has a client loader, it opts out of the singular call and // calls it's server loader via `serverLoader()` using a `?_routes` param - if (manifest.routes[m.route.id].hasClientLoader) { - if (manifest.routes[m.route.id].hasLoader) { + if (manifestRoute && manifestRoute.hasClientLoader) { + if (manifestRoute.hasLoader) { foundOptOutRoute = true; } try { @@ -276,7 +279,7 @@ async function singleFetchLoaderNavigationStrategy( } // Load this route on the server if it has a loader - if (manifest.routes[m.route.id].hasLoader) { + if (manifestRoute && manifestRoute.hasLoader) { routesParams.add(m.route.id); } diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index a667a4d791..8a913a80db 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -337,7 +337,10 @@ export type AgnosticDataRouteObject = | AgnosticDataIndexRouteObject | AgnosticDataNonIndexRouteObject; -export type RouteManifest = Record; +export type RouteManifest = Record< + string, + R | undefined +>; // Recursive helper for finding path parameters in the absence of wildcards type _PathParam = diff --git a/packages/react-router/lib/server-runtime/build.ts b/packages/react-router/lib/server-runtime/build.ts index 64f351cd26..b81a326440 100644 --- a/packages/react-router/lib/server-runtime/build.ts +++ b/packages/react-router/lib/server-runtime/build.ts @@ -1,4 +1,4 @@ -import type { ActionFunctionArgs, LoaderFunctionArgs } from "./routeModules"; +import type { ActionFunctionArgs, LoaderFunctionArgs } from "../router/utils"; import type { AssetsManifest, EntryContext, diff --git a/packages/react-router/lib/server-runtime/data.ts b/packages/react-router/lib/server-runtime/data.ts index 6e80b95301..8fbc1eb442 100644 --- a/packages/react-router/lib/server-runtime/data.ts +++ b/packages/react-router/lib/server-runtime/data.ts @@ -1,10 +1,10 @@ -import { isDataWithResponseInit, isRedirectStatusCode } from "../router/router"; import type { - ActionFunction, - ActionFunctionArgs, LoaderFunction, + ActionFunction, LoaderFunctionArgs, -} from "./routeModules"; + ActionFunctionArgs, +} from "../router/utils"; +import { isDataWithResponseInit, isRedirectStatusCode } from "../router/router"; /** * An object of unknown type for route loaders and actions provided by the @@ -16,11 +16,6 @@ export interface AppLoadContext { [key: string]: unknown; } -/** - * Data for a route that was returned from a `loader()`. - */ -export type AppData = unknown; - // Need to use RR's version here to permit the optional context even // though we know it'll always be provided in remix export async function callRouteHandler( diff --git a/packages/react-router/lib/server-runtime/entry.ts b/packages/react-router/lib/server-runtime/entry.ts index f25c6fdedd..b4895260e6 100644 --- a/packages/react-router/lib/server-runtime/entry.ts +++ b/packages/react-router/lib/server-runtime/entry.ts @@ -5,7 +5,10 @@ export function createEntryRouteModules( manifest: ServerRouteManifest ): RouteModules { return Object.keys(manifest).reduce((memo, routeId) => { - memo[routeId] = manifest[routeId].module; + let route = manifest[routeId]; + if (route) { + memo[routeId] = route.module; + } return memo; }, {} as RouteModules); } diff --git a/packages/react-router/lib/server-runtime/headers.ts b/packages/react-router/lib/server-runtime/headers.ts index 3c8cfe04b6..142891d5cb 100644 --- a/packages/react-router/lib/server-runtime/headers.ts +++ b/packages/react-router/lib/server-runtime/headers.ts @@ -2,6 +2,7 @@ import { splitCookiesString } from "set-cookie-parser"; import type { ServerBuild } from "./build"; import type { StaticHandlerContext } from "../router/router"; +import invariant from "./invariant"; export function getDocumentHeaders( build: ServerBuild, @@ -37,7 +38,9 @@ export function getDocumentHeaders( return matches.reduce((parentHeaders, match, idx) => { let { id } = match.route; - let routeModule = build.routes[id].module; + let route = build.routes[id]; + invariant(route, `Route with id "${id}" not found in build`); + let routeModule = route.module; let loaderHeaders = context.loaderHeaders[id] || new Headers(); let actionHeaders = context.actionHeaders[id] || new Headers(); diff --git a/packages/react-router/lib/server-runtime/routeModules.ts b/packages/react-router/lib/server-runtime/routeModules.ts index 16c1633f2f..18e03a1b85 100644 --- a/packages/react-router/lib/server-runtime/routeModules.ts +++ b/packages/react-router/lib/server-runtime/routeModules.ts @@ -1,95 +1,15 @@ -import type { Location } from "../router/history"; +import type { ActionFunction, LoaderFunction } from "../router/utils"; import type { - ActionFunction as RRActionFunction, - ActionFunctionArgs as RRActionFunctionArgs, - AgnosticRouteMatch, - LoaderFunction as RRLoaderFunction, - LoaderFunctionArgs as RRLoaderFunctionArgs, - Params, -} from "../router/utils"; -import type { AppData, AppLoadContext } from "./data"; -import type { SerializeFrom } from "../dom/ssr/components"; -import type { LinkDescriptor } from "../router/links"; + ClientActionFunction, + ClientLoaderFunction, + LinksFunction, + MetaFunction, +} from "../dom/ssr/routeModules"; export interface RouteModules { [routeId: string]: RouteModule | undefined; } -/** - * @deprecated Use `LoaderFunctionArgs`/`ActionFunctionArgs` instead - */ -export type DataFunctionArgs = RRActionFunctionArgs & - RRLoaderFunctionArgs & { - // Context is always provided in Remix, and typed for module augmentation support. - // RR also doesn't export DataFunctionArgs, so we extend the two interfaces here - // even tough they're identical under the hood - context: AppLoadContext; - }; - -/** - * A function that handles data mutations for a route on the server - */ -export type ActionFunction = ( - args: ActionFunctionArgs -) => ReturnType; - -/** - * Arguments passed to a route `action` function - */ -export type ActionFunctionArgs = RRActionFunctionArgs & { - // Context is always provided in Remix, and typed for module augmentation support. - context: AppLoadContext; -}; - -/** - * A function that handles data mutations for a route on the client - * @private Public API is exported from @react-router/react - */ -type ClientActionFunction = ( - args: ClientActionFunctionArgs -) => ReturnType; - -/** - * Arguments passed to a route `clientAction` function - * @private Public API is exported from @react-router/react - */ -export type ClientActionFunctionArgs = RRActionFunctionArgs & { - serverAction: () => Promise>; -}; - -/** - * A function that loads data for a route on the server - */ -export type LoaderFunction = ( - args: LoaderFunctionArgs -) => ReturnType; - -/** - * Arguments passed to a route `loader` function - */ -export type LoaderFunctionArgs = RRLoaderFunctionArgs & { - // Context is always provided in Remix, and typed for module augmentation support. - context: AppLoadContext; -}; - -/** - * A function that loads data for a route on the client - * @private Public API is exported from @react-router/react - */ -type ClientLoaderFunction = (( - args: ClientLoaderFunctionArgs -) => ReturnType) & { - hydrate?: boolean; -}; - -/** - * Arguments passed to a route `clientLoader` function - * @private Public API is exported from @react-router/react - */ -export type ClientLoaderFunctionArgs = RRLoaderFunctionArgs & { - serverLoader: () => Promise>; -}; - export type HeadersArgs = { loaderHeaders: Headers; parentHeaders: Headers; @@ -105,129 +25,6 @@ export interface HeadersFunction { (args: HeadersArgs): Headers | HeadersInit; } -/** - * A function that defines `` tags to be inserted into the `` of - * the document on route transitions. - */ -export interface LinksFunction { - (): LinkDescriptor[]; -} - -/** - * A function that returns an array of data objects to use for rendering - * metadata HTML tags in a route. These tags are not rendered on descendant - * routes in the route hierarchy. In other words, they will only be rendered on - * the route in which they are exported. - * - * @param Loader - The type of the current route's loader function - * @param MatchLoaders - Mapping from a parent route's filepath to its loader - * function type - * - * Note that parent route filepaths are relative to the `app/` directory. - * - * For example, if this meta function is for `/sales/customers/$customerId`: - * - * ```ts - * // app/root.tsx - * const loader = () => ({ hello: "world" }) - * export type Loader = typeof loader - * - * // app/routes/sales.tsx - * const loader = () => ({ salesCount: 1074 }) - * export type Loader = typeof loader - * - * // app/routes/sales/customers.tsx - * const loader = () => ({ customerCount: 74 }) - * export type Loader = typeof loader - * - * // app/routes/sales/customers/$customersId.tsx - * import type { Loader as RootLoader } from "../../../root" - * import type { Loader as SalesLoader } from "../../sales" - * import type { Loader as CustomersLoader } from "../../sales/customers" - * - * const loader = () => ({ name: "Customer name" }) - * - * const meta: MetaFunction = ({ data, matches }) => { - * const { name } = data - * // ^? string - * const { customerCount } = matches.find((match) => match.id === "routes/sales/customers").data - * // ^? number - * const { salesCount } = matches.find((match) => match.id === "routes/sales").data - * // ^? number - * const { hello } = matches.find((match) => match.id === "root").data - * // ^? "world" - * } - * ``` - */ -export interface ServerRuntimeMetaFunction< - Loader extends LoaderFunction | unknown = unknown, - ParentsLoaders extends Record = Record< - string, - unknown - > -> { - ( - args: ServerRuntimeMetaArgs - ): ServerRuntimeMetaDescriptor[]; -} - -interface ServerRuntimeMetaMatch< - RouteId extends string = string, - Loader extends LoaderFunction | unknown = unknown -> { - id: RouteId; - pathname: AgnosticRouteMatch["pathname"]; - data: Loader extends LoaderFunction ? SerializeFrom : unknown; - handle?: RouteHandle; - params: AgnosticRouteMatch["params"]; - meta: ServerRuntimeMetaDescriptor[]; - error?: unknown; -} - -type ServerRuntimeMetaMatches< - MatchLoaders extends Record = Record< - string, - unknown - > -> = Array< - { - [K in keyof MatchLoaders]: ServerRuntimeMetaMatch< - Exclude, - MatchLoaders[K] - >; - }[keyof MatchLoaders] ->; - -export interface ServerRuntimeMetaArgs< - Loader extends LoaderFunction | unknown = unknown, - MatchLoaders extends Record = Record< - string, - unknown - > -> { - data: - | (Loader extends LoaderFunction ? SerializeFrom : AppData) - | undefined; - params: Params; - location: Location; - matches: ServerRuntimeMetaMatches; - error?: unknown; -} - -export type ServerRuntimeMetaDescriptor = - | { charSet: "utf-8" } - | { title: string } - | { name: string; content: string } - | { property: string; content: string } - | { httpEquiv: string; content: string } - | { "script:ld+json": LdJsonObject } - | { tagName: "meta" | "link"; [name: string]: string } - | { [name: string]: unknown }; - type LdJsonObject = { [Key in string]: LdJsonValue } & { [Key in string]?: LdJsonValue | undefined; }; @@ -249,7 +46,7 @@ export interface EntryRouteModule { default: any; // Weakly typed because server-runtime is not React-aware handle?: RouteHandle; links?: LinksFunction; - meta?: ServerRuntimeMetaFunction; + meta?: MetaFunction; } export interface ServerRouteModule extends EntryRouteModule { diff --git a/packages/react-router/lib/server-runtime/routes.ts b/packages/react-router/lib/server-runtime/routes.ts index 26fdd34a64..6023927eec 100644 --- a/packages/react-router/lib/server-runtime/routes.ts +++ b/packages/react-router/lib/server-runtime/routes.ts @@ -2,14 +2,12 @@ import type { AgnosticDataRouteObject, LoaderFunctionArgs as RRLoaderFunctionArgs, ActionFunctionArgs as RRActionFunctionArgs, + RouteManifest, } from "../router/utils"; import { callRouteHandler } from "./data"; import type { FutureConfig } from "../dom/ssr/entry"; -import type { - ActionFunctionArgs, - LoaderFunctionArgs, - ServerRouteModule, -} from "./routeModules"; +import type { Route } from "../dom/ssr/routes"; +import type { ServerRouteModule } from "./routeModules"; import type { SingleFetchResult, SingleFetchResults, @@ -17,34 +15,8 @@ import type { import { decodeViaTurboStream } from "../dom/ssr/single-fetch"; import invariant from "./invariant"; -export interface RouteManifest { - [routeId: string]: Route; -} - export type ServerRouteManifest = RouteManifest>; -// NOTE: make sure to change the Route in remix-react/react-router-dev if you change this -export interface Route { - index?: boolean; - caseSensitive?: boolean; - id: string; - parentId?: string; - path?: string; -} - -// NOTE: make sure to change the EntryRoute in react-router/react-router-dev if you change this -export interface EntryRoute extends Route { - hasAction: boolean; - hasLoader: boolean; - hasClientAction: boolean; - hasClientLoader: boolean; - hasErrorBoundary: boolean; - imports?: string[]; - css?: string[]; - module: string; - parentId?: string; -} - export interface ServerRoute extends Route { children: ServerRoute[]; module: ServerRouteModule; @@ -54,11 +26,13 @@ function groupRoutesByParentId(manifest: ServerRouteManifest) { let routes: Record[]> = {}; Object.values(manifest).forEach((route) => { - let parentId = route.parentId || ""; - if (!routes[parentId]) { - routes[parentId] = []; + if (route) { + let parentId = route.parentId || ""; + if (!routes[parentId]) { + routes[parentId] = []; + } + routes[parentId].push(route); } - routes[parentId].push(route); }); return routes; @@ -126,16 +100,13 @@ export function createStaticHandlerDataRoutes( invariant("data" in result, "Unable to process prerendered data"); return result.data; } - let val = await callRouteHandler( - route.module.loader!, - args as LoaderFunctionArgs - ); + let val = await callRouteHandler(route.module.loader!, args); return val; } : undefined, action: route.module.action ? (args: RRActionFunctionArgs) => - callRouteHandler(route.module.action!, args as ActionFunctionArgs) + callRouteHandler(route.module.action!, args) : undefined, handle: route.module.handle, }; diff --git a/packages/react-router/lib/server-runtime/server.ts b/packages/react-router/lib/server-runtime/server.ts index c94b9cab72..de0231ca88 100644 --- a/packages/react-router/lib/server-runtime/server.ts +++ b/packages/react-router/lib/server-runtime/server.ts @@ -15,7 +15,7 @@ import { sanitizeErrors, serializeError, serializeErrors } from "./errors"; import { ServerMode, isServerMode } from "./mode"; import type { RouteMatch } from "./routeMatching"; import { matchServerRoutes } from "./routeMatching"; -import type { EntryRoute, ServerRoute } from "./routes"; +import type { ServerRoute } from "./routes"; import { createStaticHandlerDataRoutes, createRoutes } from "./routes"; import { createServerHandoffString } from "./serverHandoff"; import { getDevServerHooks } from "./dev"; @@ -30,6 +30,7 @@ import { } from "./single-fetch"; import { getDocumentHeaders } from "./headers"; import invariant from "./invariant"; +import type { EntryRoute } from "../dom/ssr/routes"; export type RequestHandler = ( request: Request, @@ -244,7 +245,10 @@ async function handleManifestRequest( if (matches) { for (let match of matches) { let routeId = match.route.id; - patches[routeId] = build.assets.routes[routeId]; + let route = build.assets.routes[routeId]; + if (route) { + patches[routeId] = route; + } } } }