Skip to content

Commit fbd601e

Browse files
authored
automatic typegen when using vite plugin (#12296)
* automatic typegen for react router vite plugin * typegen: nicer logging * typegen: handle absolute paths for `route.file`
1 parent 62cf938 commit fbd601e

File tree

6 files changed

+74
-99
lines changed

6 files changed

+74
-99
lines changed

integration/typegen-test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,4 +304,38 @@ test.describe("typegen", () => {
304304
expect(proc.stderr.toString()).toBe("");
305305
expect(proc.status).toBe(0);
306306
});
307+
308+
test("route files with absolute paths", async () => {
309+
const cwd = await createProject({
310+
"vite.config.ts": viteConfig,
311+
"app/expect-type.ts": expectType,
312+
"app/routes.ts": tsx`
313+
import path from "node:path";
314+
import { type RouteConfig, route } from "@react-router/dev/routes";
315+
316+
export const routes: RouteConfig = [
317+
route("absolute/:id", path.resolve(__dirname, "routes/absolute.tsx")),
318+
];
319+
`,
320+
"app/routes/absolute.tsx": tsx`
321+
import { Expect, Equal } from "../expect-type"
322+
import type { Route } from "./+types/absolute"
323+
324+
export function loader({ params }: Route.LoaderArgs) {
325+
type Test = Expect<Equal<typeof params.id, string>>
326+
return { planet: "world" }
327+
}
328+
329+
export default function Component({ loaderData }: Route.ComponentProps) {
330+
type Test = Expect<Equal<typeof loaderData.planet, string>>
331+
return <h1>Hello, {loaderData.planet}!</h1>
332+
}
333+
`,
334+
});
335+
336+
const proc = typecheck(cwd);
337+
expect(proc.stdout.toString()).toBe("");
338+
expect(proc.stderr.toString()).toBe("");
339+
expect(proc.status).toBe(0);
340+
});
307341
});

packages/react-router-dev/cli/commands.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import { loadPluginContext } from "../vite/plugin";
1212
import { transpile as convertFileToJS } from "./useJavascript";
1313
import * as profiler from "../vite/profiler";
1414
import * as Typegen from "../typegen";
15+
import {
16+
importViteEsmSync,
17+
preloadViteEsm,
18+
} from "../vite/import-vite-esm-sync";
1519

1620
export async function routes(
1721
reactRouterRoot?: string,
@@ -201,7 +205,11 @@ export async function typegen(root: string, flags: { watch: boolean }) {
201205
root ??= process.cwd();
202206

203207
if (flags.watch) {
204-
await Typegen.watch(root);
208+
await preloadViteEsm();
209+
const vite = importViteEsmSync();
210+
const logger = vite.createLogger("info", { prefix: "[react-router]" });
211+
212+
await Typegen.watch(root, { logger });
205213
await new Promise(() => {}); // keep alive
206214
return;
207215
}

packages/react-router-dev/logger.ts

Lines changed: 0 additions & 53 deletions
This file was deleted.

packages/react-router-dev/typegen/index.ts

Lines changed: 19 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import fs from "node:fs";
22

33
import * as Path from "pathe";
44
import pc from "picocolors";
5+
import type vite from "vite";
56

6-
import * as Logger from "../logger";
77
import { createConfigLoader } from "../config/config";
88

99
import { generate } from "./generate";
@@ -15,46 +15,29 @@ export async function run(rootDirectory: string) {
1515
await writeAll(ctx);
1616
}
1717

18-
export async function watch(rootDirectory: string) {
19-
const watchStart = performance.now();
18+
export async function watch(
19+
rootDirectory: string,
20+
{ logger }: { logger?: vite.Logger } = {}
21+
) {
2022
const ctx = await createContext({ rootDirectory, watch: true });
21-
2223
await writeAll(ctx);
23-
Logger.info("generated initial types", {
24-
durationMs: performance.now() - watchStart,
25-
});
26-
27-
ctx.configLoader.onChange(
28-
async ({ result, routeConfigChanged, event, path }) => {
29-
const eventStart = performance.now();
30-
31-
if (result.ok) {
32-
ctx.config = result.value;
24+
logger?.info(pc.green("generated types"), { timestamp: true, clear: true });
3325

34-
if (routeConfigChanged) {
35-
await writeAll(ctx);
36-
Logger.info("changed route config", {
37-
durationMs: performance.now() - eventStart,
38-
});
26+
ctx.configLoader.onChange(async ({ result, routeConfigChanged }) => {
27+
if (!result.ok) {
28+
logger?.error(pc.red(result.error), { timestamp: true, clear: true });
29+
return;
30+
}
3931

40-
const route = findRoute(ctx, path);
41-
if (route && (event === "add" || event === "unlink")) {
42-
Logger.info(
43-
`${event === "add" ? "added" : "removed"} route ${pc.blue(
44-
route.file
45-
)}`,
46-
{ durationMs: performance.now() - eventStart }
47-
);
48-
return;
49-
}
50-
}
51-
} else {
52-
Logger.error(result.error, {
53-
durationMs: performance.now() - eventStart,
54-
});
55-
}
32+
ctx.config = result.value;
33+
if (routeConfigChanged) {
34+
await writeAll(ctx);
35+
logger?.info(pc.green("regenerated types"), {
36+
timestamp: true,
37+
clear: true,
38+
});
5639
}
57-
);
40+
});
5841
}
5942

6043
async function createContext({
@@ -80,18 +63,11 @@ async function createContext({
8063
};
8164
}
8265

83-
function findRoute(ctx: Context, path: string) {
84-
return Object.values(ctx.config.routes).find(
85-
(route) => path === Path.join(ctx.config.appDirectory, route.file)
86-
);
87-
}
88-
8966
async function writeAll(ctx: Context): Promise<void> {
9067
const typegenDir = getTypesDir(ctx);
9168

9269
fs.rmSync(typegenDir, { recursive: true, force: true });
9370
Object.values(ctx.config.routes).forEach((route) => {
94-
if (!fs.existsSync(Path.join(ctx.config.appDirectory, route.file))) return;
9571
const typesPath = getTypesPath(ctx, route);
9672
const content = generate(ctx, route);
9773
fs.mkdirSync(Path.dirname(typesPath), { recursive: true });

packages/react-router-dev/typegen/paths.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@ export function getTypesDir(ctx: Context) {
99
}
1010

1111
export function getTypesPath(ctx: Context, route: RouteManifestEntry) {
12+
const rel = Path.isAbsolute(route.file)
13+
? Path.relative(ctx.config.appDirectory, route.file)
14+
: route.file;
15+
1216
return Path.join(
1317
getTypesDir(ctx),
1418
Path.relative(ctx.rootDirectory, ctx.config.appDirectory),
15-
Path.dirname(route.file),
16-
"+types/" + Pathe.filename(route.file) + ".ts"
19+
Path.dirname(rel),
20+
"+types/" + Pathe.filename(rel) + ".ts"
1721
);
1822
}

packages/react-router-dev/vite/plugin.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import jsesc from "jsesc";
2424
import colors from "picocolors";
2525

26+
import * as Typegen from "../typegen";
2627
import { type RouteManifestEntry, type RouteManifest } from "../config/routes";
2728
import type { Manifest as ReactRouterManifest } from "../manifest";
2829
import invariant from "../invariant";
@@ -746,6 +747,11 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
746747
rootDirectory =
747748
viteUserConfig.root ?? process.env.REACT_ROUTER_ROOT ?? process.cwd();
748749

750+
Typegen.watch(rootDirectory, {
751+
// ignore `info` logs from typegen since they are redundant when Vite plugin logs are active
752+
logger: vite.createLogger("warn", { prefix: "[react-router]" }),
753+
});
754+
749755
reactRouterConfigLoader = await createConfigLoader({
750756
rootDirectory,
751757
watch: viteCommand === "serve",

0 commit comments

Comments
 (0)