diff --git a/.changeset/yellow-carpets-own.md b/.changeset/yellow-carpets-own.md new file mode 100644 index 0000000000..1a212de4f5 --- /dev/null +++ b/.changeset/yellow-carpets-own.md @@ -0,0 +1,5 @@ +--- +"@react-router/dev": patch +--- + +Fix typegen support for routes outside appDirectory diff --git a/contributors.yml b/contributors.yml index c428444f01..73a88084a7 100644 --- a/contributors.yml +++ b/contributors.yml @@ -20,6 +20,7 @@ - alberto - AlemTuzlak - Aleuck +- alex-pex - alexandernanberg - alexanderson1993 - alexlbr diff --git a/integration/typegen-test.ts b/integration/typegen-test.ts index 9ffcc5e183..b6928cbc3c 100644 --- a/integration/typegen-test.ts +++ b/integration/typegen-test.ts @@ -1,5 +1,6 @@ import fs from "node:fs/promises"; +import { expect } from "@playwright/test"; import tsx from "dedent"; import * as Path from "pathe"; @@ -336,6 +337,57 @@ test.describe("typegen", () => { await $("pnpm typecheck"); }); + test("routes outside app dir", async ({ cwd, edit, $ }) => { + // Create the subdirectories + await fs.mkdir(Path.join(cwd, "app/router"), { recursive: true }); + await fs.mkdir(Path.join(cwd, "app/pages"), { recursive: true }); + + await edit({ + "react-router.config.ts": tsx` + export default { + appDirectory: "app/router", + } + `, + "app/router/routes.ts": tsx` + import { type RouteConfig, route } from "@react-router/dev/routes"; + + export default [ + route("products/:id", "../pages/product.tsx") + ] satisfies RouteConfig; + `, + "app/router/root.tsx": tsx` + import { Outlet } from "react-router"; + + export default function Root() { + return ; + } + `, + "app/pages/product.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/product" + + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return { planet: "world" } + } + + export default function Component({ loaderData }: Route.ComponentProps) { + type Test = Expect> + return

Hello, {loaderData.planet}!

+ } + `, + }); + await $("pnpm typecheck"); + + // Verify that the types file was generated in the correct location + const annotationPath = Path.join( + cwd, + ".react-router/types/app/pages/+types/product.ts", + ); + const annotation = await fs.readFile(annotationPath, "utf8"); + expect(annotation).toContain("export namespace Route"); + }); + test("matches", async ({ edit, $ }) => { await edit({ "app/routes.ts": tsx` diff --git a/packages/react-router-dev/typegen/generate.ts b/packages/react-router-dev/typegen/generate.ts index dcab03fd27..a0a557df55 100644 --- a/packages/react-router-dev/typegen/generate.ts +++ b/packages/react-router-dev/typegen/generate.ts @@ -119,7 +119,7 @@ export function generateRoutes(ctx: Context): Array { // **/+types/*.ts const allAnnotations: Array = Array.from(fileToRoutes.entries()) - .filter(([file]) => isInAppDirectory(ctx, file)) + .filter(([file]) => isInRootDirectory(ctx, file)) .map(([file, routeIds]) => getRouteAnnotations({ ctx, file, routeIds, lineages }), ); @@ -219,9 +219,9 @@ function routeModulesType(ctx: Context) { ); } -function isInAppDirectory(ctx: Context, routeFile: string): boolean { +function isInRootDirectory(ctx: Context, routeFile: string): boolean { const path = Path.resolve(ctx.config.appDirectory, routeFile); - return path.startsWith(ctx.config.appDirectory); + return path.startsWith(ctx.rootDirectory); } function getRouteAnnotations({