Skip to content

Commit 27c6fe8

Browse files
authored
typegen --watch (#12210)
* typegen: remove ts plugin * dev: switch from chalk to picocolors - `NO_COLOR` env var support without needing to wrap it - 42kb -> 6kb specifically, out-of-the-box support for `NO_COLOR` means we can remove our `colors.ts` module and just use picocolors directly * typegen: add `--watch` flag * typegen: logging for --watch * typegen: --config flag * typegen: allow .tsx, .js, .jsx extensions for route config * move typegen out of obsolete typescript/ dir * refactor * add typegen:watch script to each playground * typesafety docs * typegen: update decision doc * typegen: update changeset * typegen: update snapshot for --config flag * pr feedback
1 parent acb339f commit 27c6fe8

File tree

22 files changed

+371
-256
lines changed

22 files changed

+371
-256
lines changed

.changeset/typesafety.md

Lines changed: 3 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,6 @@ This initial implementation targets type inference for:
2424
- `LoaderData` : Loader data from `loader` and/or `clientLoader` within your route module
2525
- `ActionData` : Action data from `action` and/or `clientAction` within your route module
2626

27-
These types are then used to create types for route export args and props:
28-
29-
- `LoaderArgs`
30-
- `ClientLoaderArgs`
31-
- `ActionArgs`
32-
- `ClientActionArgs`
33-
- `HydrateFallbackProps`
34-
- `ComponentProps` (for the `default` export)
35-
- `ErrorBoundaryProps`
36-
3727
In the future, we plan to add types for the rest of the route module exports: `meta`, `links`, `headers`, `shouldRevalidate`, etc.
3828
We also plan to generate types for typesafe `Link`s:
3929

@@ -43,109 +33,7 @@ We also plan to generate types for typesafe `Link`s:
4333
// typesafe `to` and `params` based on the available routes in your app
4434
```
4535

46-
#### Setup
47-
48-
React Router will generate types into a `.react-router/` directory at the root of your app.
49-
This directory is fully managed by React Router and is derived based on your route config (`routes.ts`).
50-
51-
👉 **Add `.react-router/` to `.gitignore`**
52-
53-
```txt
54-
.react-router
55-
```
56-
57-
You should also ensure that generated types for routes are always present before running typechecking,
58-
especially for running typechecking in CI.
59-
60-
👉 **Add `react-router typegen` to your `typecheck` command in `package.json`**
61-
62-
```json
63-
{
64-
"scripts": {
65-
"typecheck": "react-router typegen && tsc"
66-
}
67-
}
68-
```
69-
70-
To get TypeScript to use those generated types, you'll need to add them to `include` in `tsconfig.json`.
71-
And to be able to import them as if they files next to your route modules, you'll also need to configure `rootDirs`.
72-
73-
👉 **Configure `tsconfig.json` for generated types**
74-
75-
```json
76-
{
77-
"include": [".react-router/types/**/*"],
78-
"compilerOptions": {
79-
"rootDirs": [".", "./.react-router/types"]
80-
}
81-
}
82-
```
83-
84-
#### `typegen` command
85-
86-
You can manually generate types with the new `typegen` command:
87-
88-
```sh
89-
react-router typegen
90-
```
91-
92-
However, manual type generation is tedious and types can get out of sync quickly if you ever forget to run `typegen`.
93-
Instead, we recommend that you setup our new TypeScript plugin which will automatically generate fresh types whenever routes change.
94-
That way, you'll always have up-to-date types.
95-
96-
#### TypeScript plugin
97-
98-
To get automatic type generation, you can use our new TypeScript plugin.
99-
100-
👉 **Add the TypeScript plugin to `tsconfig.json`**
101-
102-
```json
103-
{
104-
"compilerOptions": {
105-
"plugins": [{ "name": "@react-router/dev" }]
106-
}
107-
}
108-
```
109-
110-
We plan to add some other goodies to our TypeScript plugin soon, including:
111-
112-
- Automatic `jsdoc` for route exports that include links to official docs
113-
- Autocomplete for route exports
114-
- Warnings for non-HMR compliant exports
115-
116-
##### VSCode
117-
118-
TypeScript looks for plugins registered in `tsconfig.json` in the local `node_modules/`,
119-
but VSCode ships with its own copy of TypeScript that is installed outside of your project.
120-
For TypeScript plugins to work, you'll need to tell VSCode to use the local workspace version of TypeScript.
121-
For security reasons, [VSCode won't use the workspace version of TypeScript](https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-the-workspace-version-of-typescript) until you manually opt-in.
122-
123-
Your project should have a `.vscode/settings.json` with the following settings:
124-
125-
```json
126-
{
127-
"typescript.tsdk": "node_modules/typescript/lib",
128-
"typescript.enablePromptUseWorkspaceTsdk": true
129-
}
130-
```
131-
132-
That way [VSCode will ask you](https://code.visualstudio.com/updates/v1_45#_prompt-users-to-switch-to-the-workspace-version-of-typescript) if you want to use the workspace version of TypeScript the first time you open a TS file in that project.
133-
134-
> [!IMPORTANT]
135-
> You'll need to install dependencies first so that the workspace version of TypeScript is available.
136-
137-
👉 **Select "Allow" when VSCode asks if you want to use the workspace version of TypeScript**
138-
139-
Otherwise, you can also manually opt-in to the workspace version:
140-
141-
1. Open up any TypeScript file in your project
142-
2. Open up the VSCode Command Palette (<kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd>)
143-
3. Search for `Select TypeScript Version`
144-
4. Choose `Use Workspace Version`
145-
5. Quit and reopen VSCode
146-
147-
##### Troubleshooting
36+
Check out our docs for more:
14837

149-
In VSCode, open up any TypeScript file in your project and then use <kbd>CMD</kbd>+<kbd>SHIFT</kbd>+<kbd>P</kbd> to select `Open TS Server log`.
150-
There should be a log for `[react-router] setup` that indicates that the plugin was resolved correctly.
151-
Then look for any errors in the log.
38+
- [_Explanations > Type Safety_](https://reactrouter.com/dev/guides/explanation/type-safety)
39+
- [_How-To > Setting up type safety_](https://reactrouter.com/dev/guides/how-to/setting-up-type-safety)

.vscode/settings.json

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

decisions/0012-type-inference.md

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -167,25 +167,15 @@ import { LoaderArgs, DefaultProps } from "./+types.product";
167167
TypeScript will even give you import autocompletion for the typegen file and the `+` prefix helps to distinguish it as a special file.
168168
Big thanks to Svelte Kit for showing us that [`rootDirs` trick](https://svelte.dev/blog/zero-config-type-safety#virtual-files)!
169169

170-
### TypeScript plugin
170+
### Watch mode
171171

172172
Typegen solutions often receive criticism due to typegen'd files becoming out of sync during development.
173173
This happens because many typegen solutions require you to then rerun a script to update the typegen'd files.
174174

175-
Instead, our typegen will automatically run within a TypeScript plugin.
176-
That means you should never need to manually run a typegen command during development.
177-
It also means that you don't need to run our dev server for typegen to take effect.
178-
The only requirement is that your editor is open.
179-
180-
Additionally, TypeScript plugins work with any LSP-compatible editor.
181-
That means that this single plugin will work in VS Code, Neovim, or any other popular editor.
182-
183-
Even more exciting is that a TS plugin sets the stage for tons of other DX goodies:
175+
Instead, we'll provide a `--watch` flag for the `react-router typegen` command to automatically regenerate types as files change.
176+
It's also straightforward to automatically run commands like `react-router typegen --watch` when opening up any modern editors.
184177

185-
- jsdoc and links to official documentation when you hover a route export
186-
- Snippet-like autocomplete for route exports
187-
- In-editor warnings when you forget to name your React components, which would cause HMR to fail
188-
- ...and more...
178+
In the future, we may also kick off typegen watching as part of running a React Router dev server.
189179

190180
## Rejected solutions
191181

@@ -265,6 +255,27 @@ Initially, this seemed like a good fit for React Router too, but we ran into a c
265255
For Svelte Kit, this isn't as big of an issue since they already need their own typecheck command for the Svelte language: `svelte-check`.
266256
But since React Router is pure TypeScript, it would be more natural to invoke `tsc` directly in your `package.json` scripts.
267257

258+
### TypeScript plugin
259+
260+
Originally, we created a basic TypeScript plugin to automatically run typegen in watch mode.
261+
One nice thing about this approach is that it worked across all editors.
262+
263+
However, there were a couple drawbacks:
264+
265+
1. A TypeScript plugin will silently fail to run unless you have installed dependencies prior to opening up the project in your editor.
266+
267+
2. A TypeScript plugin requires your editor to use the local (workspace) version of TypeScript.
268+
But by default [VSCode won't use the workspace version of TypeScript](https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-the-workspace-version-of-typescript), forcing you to run the `Select TypeScript Version` command every time you open up a new project.
269+
You can workaround this via the `typescript.tsdk` and `typescript.enablePromptUseWorkspaceTsdk` options in `.vscode/settings.json`, but those only take effect when that _specific_ directory is opened as by VSCode.
270+
For example, if you added `.vscode/settings.json` to a subfolder of a monorepo those options would be ignored when opening the root of the monorepo with VSCode.
271+
272+
3. Debugging a TypeScript plugin is not straightforward as you need to know to run the `Open TS Server log` command in VSCode and sift through verbose logs.
273+
Without this knowledge, its hard to know if you've set up typegen correctly.
274+
And even if you do know this, its tedious to find out what went wrong.
275+
276+
After we decided not to pursue "zero-effort typesafety" (as described above), our TypeScript plugin was already a simple passthrough that kicked off typegen as a side-effect.
277+
This was an additional indication that maybe a TypeScript plugin was not the right place for our typegen.
278+
268279
## Summary
269280

270281
By leaning into automated typegen within a TypeScript plugin, we radically simplify React Router's runtime APIs while providing strong type inference across the entire framework.

docs/explanation/type-safety.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
title: Type safety
3+
---
4+
5+
# Type safety
6+
7+
React Router generates types for each route in your app that you can use to get type safety for each route module export.
8+
9+
For example, let's say you have a `products/:id` route configured:
10+
11+
```ts filename=app/routes.ts
12+
import {
13+
type RouteConfig,
14+
route,
15+
} from "@react-router/dev/routes";
16+
17+
export const routes: RouteConfig = [
18+
route("products/:id", "./routes/product.tsx"),
19+
];
20+
```
21+
22+
You can import route-specific types like so:
23+
24+
```tsx filename=app/routes/product.tsx
25+
import type * as Route from "./+types.product";
26+
// types generated for this route 👆
27+
28+
export function loader({ params }: Route.LoaderArgs) {
29+
// 👆 { id: string }
30+
return { planet: `world #${params.id}` };
31+
}
32+
33+
export default function Component({
34+
loaderData, // 👈 { planet: string }
35+
}: Route.ComponentProps) {
36+
return <h1>Hello, {loaderData.planet}!</h1>;
37+
}
38+
```
39+
40+
If you haven't done so already, check out our guide for [setting up type safety][setting-up-type-safety] in a new project.
41+
42+
## `typegen` command
43+
44+
You can manually generate types with the `typegen` command:
45+
46+
```sh
47+
react-router typegen
48+
```
49+
50+
You can also use `--watch` to automatically regenerate types as files change:
51+
52+
```sh
53+
react-router typegen --watch
54+
```
55+
56+
The following types are generated for each route:
57+
58+
- `LoaderArgs`
59+
- `ClientLoaderArgs`
60+
- `ActionArgs`
61+
- `ClientActionArgs`
62+
- `HydrateFallbackProps`
63+
- `ComponentProps` (for the `default` export)
64+
- `ErrorBoundaryProps`
65+
66+
## How it works
67+
68+
React Router's type generation executes your route config (`app/routes.ts` by default) to determine the routes for your app.
69+
It then generates a `+types.<route file>.d.ts` for each route within a special `.react-router/types/` directory.
70+
With [`rootDirs` configured][setting-up-type-safety], TypeScript can import these generated files as if they were right next to their corresponding route modules.
71+
72+
For a deeper dive into some of the design decisions, check out our [type inference decision doc](https://github.com/remix-run/react-router/blob/dev/decisions/0012-type-inference.md).
73+
74+
[setting-up-type-safety]: ../how-to/setting-up-type-safety.md
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
title: Setting up type safety
3+
---
4+
5+
To know more about how type safety works in React Router, check out our [dedicated explanation](../explanation//type-safety.md).
6+
7+
# Setting up type safety
8+
9+
React Router generates types into a `.react-router/` directory at the root of your app.
10+
This directory is fully managed by React Router and is derived from your route config (`app/routes.ts` by default), so it should be gitignore'd.
11+
12+
👉 **Add `.react-router/` to `.gitignore`**
13+
14+
```txt
15+
.react-router/
16+
```
17+
18+
Make sure generated types are always present before type checking,
19+
especially when running type checking in CI.
20+
21+
👉 **Add `react-router typegen` to your `typecheck` command in `package.json`**
22+
23+
```json
24+
{
25+
"scripts": {
26+
"typecheck": "react-router typegen && tsc"
27+
}
28+
}
29+
```
30+
31+
To get TypeScript to use those generated types, you'll need to add them to `include` in `tsconfig.json`.
32+
And to be able to import them as if they files next to your route modules, you'll also need to [configure `rootDirs`](https://www.typescriptlang.org/tsconfig/#rootDirs).
33+
34+
👉 **Configure `tsconfig.json` for generated types**
35+
36+
```json
37+
{
38+
"include": [".react-router/types/**/*"],
39+
"compilerOptions": {
40+
"rootDirs": [".", "./.react-router/types"]
41+
}
42+
}
43+
```
44+
45+
During development, its nice to have a dedicate `package.json` script to run type generation in watch mode.
46+
47+
👉 **Add a `typegen --watch` script** (optional)
48+
49+
```json filename=package.json
50+
{
51+
"scripts": {
52+
"typegen:watch": "react-router typegen --watch"
53+
}
54+
}
55+
```
56+
57+
## Automatic typegen in VSCode (optional)
58+
59+
If you'd rather not need to remember to run `react-router typegen --watch` every time you start working on your app, you can use [VSCode Tasks](https://code.visualstudio.com/docs/editor/tasks) to automate this.
60+
Let's make use of the `typegen:watch` script you added earlier.
61+
62+
👉 **Configure a VSCode task for `typegen:watch`**
63+
64+
```json filename=.vscode/tasks.json
65+
{
66+
"version": "2.0.0",
67+
"tasks": [
68+
{
69+
"label": "React Router: Typegen",
70+
"type": "shell",
71+
"command": "npm run typegen:watch",
72+
"problemMatcher": [],
73+
"isBackground": true,
74+
"presentation": {
75+
"echo": true,
76+
"reveal": "always",
77+
"focus": false,
78+
"panel": "dedicated"
79+
},
80+
"runOptions": {
81+
"runOn": "folderOpen"
82+
}
83+
}
84+
]
85+
}
86+
```
87+
88+
<docs-info>
89+
90+
You should create `.vscode/tasks.json` in the directory you plan to open with VSCode.
91+
For monorepos, you can create this file in the repo root and use the [`cwd` option for the task](https://code.visualstudio.com/docs/editor/tasks#_custom-tasks) to run the command within a subfolder.
92+
93+
</docs-info>
94+
95+
Now, VSCode will automatically run the `typegen:watch` script in a dedicated terminal anytime you open your project.

packages/react-router-dev/__tests__/cli-test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ describe("remix CLI", () => {
120120
\`reveal\` Options:
121121
--config, -c Use specified Vite config file (string)
122122
--no-typescript Generate plain JavaScript files
123+
\`typegen\` Options:
124+
--config, -c Use specified Vite config file (string)
125+
--watch Automatically regenerate types whenever route config (\`routes.ts\`) or route modules change
123126
124127
Build your project:
125128
@@ -146,7 +149,9 @@ describe("remix CLI", () => {
146149
147150
Generate types for route modules:
148151
149-
$ react-router typegen"
152+
$ react-router typegen
153+
$ react-router typegen --watch
154+
$ react-router typegen --config vite.react-router.config.ts"
150155
`);
151156
});
152157
});

0 commit comments

Comments
 (0)