From 564956760aa2ade555f9aaffd17e3412d72dc295 Mon Sep 17 00:00:00 2001 From: Jannis Jorre Date: Fri, 13 Dec 2024 09:30:07 +0100 Subject: [PATCH] implement typesafe useRouteLoaderData enable via typesafeUseRouteLoaderData future flag --- contributors.yml | 1 + packages/react-router-dev/config/config.ts | 3 ++ packages/react-router-dev/typegen/generate.ts | 43 ++++++++++++++++++- packages/react-router-dev/typegen/index.ts | 21 ++++++++- packages/react-router-dev/typegen/paths.ts | 4 ++ packages/react-router/lib/hooks.tsx | 2 +- 6 files changed, 70 insertions(+), 4 deletions(-) diff --git a/contributors.yml b/contributors.yml index 441271bea7..d9aabb4c19 100644 --- a/contributors.yml +++ b/contributors.yml @@ -136,6 +136,7 @@ - jenseng - JeraldVin - JesusTheHun +- jeyj0 - jimniels - jmargeta - johnpangalos diff --git a/packages/react-router-dev/config/config.ts b/packages/react-router-dev/config/config.ts index a1e4a85036..9e8063e4fa 100644 --- a/packages/react-router-dev/config/config.ts +++ b/packages/react-router-dev/config/config.ts @@ -85,6 +85,7 @@ type ServerModuleFormat = "esm" | "cjs"; interface FutureConfig { unstable_optimizeDeps: boolean; + typesafeUseRouteLoaderData: boolean; } export type BuildManifest = DefaultBuildManifest | ServerBundlesBuildManifest; @@ -482,6 +483,8 @@ async function resolveConfig({ let future: FutureConfig = { unstable_optimizeDeps: reactRouterUserConfig.future?.unstable_optimizeDeps ?? false, + typesafeUseRouteLoaderData: + reactRouterUserConfig.future?.typesafeUseRouteLoaderData ?? false, }; let reactRouterConfig: ResolvedReactRouterConfig = deepFreeze({ diff --git a/packages/react-router-dev/typegen/generate.ts b/packages/react-router-dev/typegen/generate.ts index 3ac02209ab..4a214f3d3a 100644 --- a/packages/react-router-dev/typegen/generate.ts +++ b/packages/react-router-dev/typegen/generate.ts @@ -4,7 +4,7 @@ import * as Pathe from "pathe/utils"; import { type RouteManifest, type RouteManifestEntry } from "../config/routes"; import { type Context } from "./context"; -import { getTypesPath } from "./paths"; +import { getTypesDir, getTypesPath } from "./paths"; export function generate(ctx: Context, route: RouteManifestEntry): string { const lineage = getRouteLineage(ctx.config.routes, route); @@ -120,3 +120,44 @@ function parseParams(urlpath: string) { if (hasSplat) result["*"] = [false]; return result; } + +export function generateRouteManifest(ctx: Context) { + return ts` + export type RouteManifest = { + ${Object.entries(ctx.config.routes) + .map(([routeId, routeEntry], i) => { + const indent = i === 0 ? "" : " ".repeat(3); + const routeTypeFile = Path.relative( + getTypesDir(ctx), + getTypesPath(ctx, routeEntry) + ); + return `${indent}"${routeId}": import("./${routeTypeFile}").Info,`; + }) + .join("\n")} + }; + + export type RouteId = keyof RouteManifest; + `; +} + +export function generateUseRouteLoaderDataType(ctx: Context) { + if (ctx.config.future.typesafeUseRouteLoaderData) { + return ts` + import type { RouteId, RouteManifest } from "./routeManifest"; + + declare module "react-router" { + export function useRouteLoaderData( + id: RI, + ): RouteManifest[RI]["loaderData"]; + } + `; + } + + return ts` + declare module "react-router" { + export function useRouteLoaderData( + routeId: string + ): SerializeFrom | undefined; + } + `; +} diff --git a/packages/react-router-dev/typegen/index.ts b/packages/react-router-dev/typegen/index.ts index 06a1df81fc..825ff05d58 100644 --- a/packages/react-router-dev/typegen/index.ts +++ b/packages/react-router-dev/typegen/index.ts @@ -6,9 +6,13 @@ import type vite from "vite"; import { createConfigLoader } from "../config/config"; -import { generate } from "./generate"; +import { + generate, + generateRouteManifest, + generateUseRouteLoaderDataType, +} from "./generate"; import type { Context } from "./context"; -import { getTypesDir, getTypesPath } from "./paths"; +import { getTypesDir, getTypesPath, getGlobalTypesFilePath } from "./paths"; export async function run(rootDirectory: string) { const ctx = await createContext({ rootDirectory, watch: false }); @@ -81,4 +85,17 @@ async function writeAll(ctx: Context): Promise { fs.mkdirSync(Path.dirname(typesPath), { recursive: true }); fs.writeFileSync(typesPath, content); }); + + const useRouteLoaderDataContent = generateUseRouteLoaderDataType(ctx); + const useRouteLoaderDataTypesPath = getGlobalTypesFilePath( + ctx, + "useRouteLoaderData" + ); + fs.writeFileSync(useRouteLoaderDataTypesPath, useRouteLoaderDataContent); + + if (ctx.config.future.typesafeUseRouteLoaderData) { + const routeManifestContent = generateRouteManifest(ctx); + const routeManifestPath = getGlobalTypesFilePath(ctx, "routeManifest"); + fs.writeFileSync(routeManifestPath, routeManifestContent); + } } diff --git a/packages/react-router-dev/typegen/paths.ts b/packages/react-router-dev/typegen/paths.ts index bf9cceb2c1..047102d787 100644 --- a/packages/react-router-dev/typegen/paths.ts +++ b/packages/react-router-dev/typegen/paths.ts @@ -16,3 +16,7 @@ export function getTypesPath(ctx: Context, route: RouteManifestEntry) { "+types/" + Pathe.filename(route.file) + ".ts" ); } + +export function getGlobalTypesFilePath(ctx: Context, fileName: string) { + return Path.join(getTypesDir(ctx), Pathe.filename(fileName) + ".d.ts"); +} diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index 13027e2da9..04e9bf176d 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -1117,7 +1117,7 @@ export function useLoaderData(): SerializeFrom { @category Hooks */ export function useRouteLoaderData( - routeId: string + routeId: never // actually string after typegen; never allows typesafeUseRouteLoaderData future flag to work ): SerializeFrom | undefined { let state = useDataRouterState(DataRouterStateHook.UseRouteLoaderData); return state.loaderData[routeId] as SerializeFrom | undefined;