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({