|
1 | 1 | # roto-rooter |
2 | 2 |
|
3 | | -Static analysis and functional verifier tool for React Router applications. |
| 3 | +A static analysis tool for [React Router](https://reactrouter.com/) apps. It catches common bugs -- broken links, missing loaders, hydration mismatches, disconnected UI elements, and incorrect database operations -- by reading your route definitions and cross-referencing them against your components. |
4 | 4 |
|
5 | | -## Installation |
6 | | - |
7 | | -```bash |
| 5 | +``` |
8 | 6 | npm install -g roto-rooter |
9 | 7 | ``` |
10 | 8 |
|
11 | | -## Usage |
| 9 | +## Running Checks |
12 | 10 |
|
13 | | -```bash |
14 | | -# Check all files in current directory |
15 | | -rr |
| 11 | +``` |
| 12 | +rr [OPTIONS] [FILES...] |
| 13 | +``` |
16 | 14 |
|
17 | | -# Check specific file(s) |
18 | | -rr app/routes/employees.tsx |
| 15 | +Point `rr` at your project and it scans your route files for issues. With no arguments it runs the **default checks** (links, loader, params, interactivity) against all files in the current directory. |
19 | 16 |
|
20 | | -# Run specific checks only |
21 | | -rr --check links,forms |
| 17 | +| Option | Description | |
| 18 | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
| 19 | +| `-c, --check <checks>` | Comma-separated checks to run. Use `defaults` for the default set, `all` for everything, or pick individual checks: `links`, `loader`, `params`, `interactivity`, `forms`, `hydration`, `drizzle` | |
| 20 | +| `-f, --format <format>` | Output format: `text` (default) or `json` | |
| 21 | +| `-r, --root <path>` | Project root containing the `app/` folder (default: cwd) | |
| 22 | +| `--fix` | Auto-fix issues where possible | |
| 23 | +| `--dry-run` | Preview fixes without writing files | |
| 24 | +| `--drizzle-schema <path>` | Path to Drizzle schema file (auto-discovered by default) | |
22 | 25 |
|
23 | | -# Run all checks (including optional ones) |
24 | | -rr --check all |
| 26 | +**Checks at a glance:** |
25 | 27 |
|
26 | | -# Run default checks plus specific optional checks |
27 | | -rr --check defaults,forms |
| 28 | +- **links** (default) -- validates `<Link>`, `redirect()`, `navigate()` targets exist as routes |
| 29 | +- **loader** (default) -- ensures `useLoaderData()` is backed by a loader; catches `clientLoader` importing server-only modules |
| 30 | +- **params** (default) -- ensures `useParams()` only accesses params defined in the route path |
| 31 | +- **interactivity** (default) -- catches "Save" buttons that don't save, "Delete" buttons that don't delete, and empty click handlers |
| 32 | +- **forms** (opt-in) -- validates `<Form>` targets have actions and that field names match `formData.get()` calls |
| 33 | +- **hydration** (opt-in) -- detects SSR/client mismatches from `new Date()`, `Math.random()`, `window` access in render |
| 34 | +- **drizzle** (opt-in) -- validates Drizzle ORM operations against your schema (missing columns, type mismatches, etc.) |
28 | 35 |
|
29 | | -# Output as JSON |
30 | | -rr --format json |
| 36 | +**Example output:** |
31 | 37 |
|
32 | | -# Set project root (the directory containing the app/ folder) |
33 | | -rr --root ./my-app |
| 38 | +``` |
| 39 | +$ rr --root my-app |
| 40 | +
|
| 41 | +rr found 5 issues: |
34 | 42 |
|
35 | | -# Automatically fix issues where possible |
36 | | -rr --fix |
| 43 | +[error] dashboard.tsx:12:7 |
| 44 | + href="/employeees" |
| 45 | + x No matching route |
| 46 | + -> Did you mean: /employees? |
37 | 47 |
|
38 | | -# Preview fixes without applying |
39 | | -rr --dry-run |
| 48 | +[error] tasks.tsx:6:16 |
| 49 | + useLoaderData() |
| 50 | + x useLoaderData() called but route has no loader |
| 51 | + -> Add a loader function or remove the hook |
40 | 52 |
|
41 | | -# Fix specific file(s) |
42 | | -rr --fix app/routes/dashboard.tsx |
| 53 | +[error] employees.$id.edit.tsx:7:32 |
| 54 | + useParams().invalidParam |
| 55 | + x useParams() accesses "invalidParam" but route has no :invalidParam parameter |
| 56 | + -> Available params: :id |
43 | 57 |
|
44 | | -# Enable Drizzle ORM persistence checking (auto-discovers schema) |
45 | | -rr --check drizzle |
| 58 | +[error] disconnected-dialog.tsx:27:11 |
| 59 | + <Button onClick={...}>Save Changes</Button> |
| 60 | + x "Save Changes" button in Dialog only closes dialog without saving data |
| 61 | + -> Wrap inputs in a <Form> component or use useFetcher.submit() to persist data |
46 | 62 |
|
47 | | -# Drizzle checking with explicit schema path |
48 | | -rr --check drizzle --drizzle-schema src/db/schema.ts |
| 63 | +[warning] disconnected-dialog.tsx:78:11 |
| 64 | + <Button onClick={...}>Add Item</Button> |
| 65 | + x "Add Item" button has an empty or stub onClick handler |
| 66 | + -> Implement the handler or remove the button if not needed |
49 | 67 |
|
50 | | -# Extract SQL queries from Drizzle ORM code |
51 | | -rr sql --drizzle |
| 68 | +Summary: 4 errors, 1 warning |
| 69 | +Run with --help for options. |
| 70 | +``` |
52 | 71 |
|
53 | | -# Extract queries from a specific file |
54 | | -rr sql --drizzle app/routes/users.tsx |
| 72 | +## Extracting SQL |
55 | 73 |
|
56 | | -# SQL output as JSON |
57 | | -rr sql --drizzle --format json |
58 | 74 | ``` |
| 75 | +rr sql --drizzle [OPTIONS] [FILES...] |
| 76 | +``` |
| 77 | + |
| 78 | +Reads your Drizzle ORM code and prints the equivalent SQL for every query it finds. Useful for reviewing what your app actually sends to the database. |
| 79 | + |
| 80 | +| Option | Description | |
| 81 | +| ------------------------- | -------------------------------------------------------- | |
| 82 | +| `--drizzle` | Required. Specifies the ORM to analyze. | |
| 83 | +| `-f, --format <format>` | Output format: `text` (default) or `json` | |
| 84 | +| `-r, --root <path>` | Project root directory (default: cwd) | |
| 85 | +| `--drizzle-schema <path>` | Path to Drizzle schema file (auto-discovered by default) | |
| 86 | + |
| 87 | +**Example output:** |
59 | 88 |
|
60 | | -## Checks |
61 | | - |
62 | | -**Default checks** (run automatically): |
63 | | - |
64 | | -- **links**: Validates `<Link>`, `redirect()`, and `navigate()` targets exist as defined routes. Suggests closest matching route when a typo is detected. Auto-fixable when a close match exists. |
65 | | -- **loader**: Validates `useLoaderData()` is only used in routes that export a loader function. Detects `clientLoader`/`clientAction` that import server-only modules (database drivers, `fs`, etc.) which will fail in the browser. Auto-fixable by renaming to `loader`/`action`. |
66 | | -- **params**: Validates `useParams()` accesses only params defined in the route path (e.g., `:id` in `/users/:id`). |
67 | | -- **interactivity**: Detects disconnected interactive elements: |
68 | | - - Dialog/modal forms where "Save" button only closes the dialog without persisting data |
69 | | - - "Delete" confirmation buttons that only close without performing the action |
70 | | - - Buttons with empty or stub onClick handlers (console.log only) |
71 | | - - Validates dialogs use React Router `<Form>` or `useFetcher.submit()` for data operations |
72 | | - |
73 | | -**Optional checks** (opt-in via `--check`): |
74 | | - |
75 | | -- **forms**: Validates `<Form>` components submit to routes with action exports, and that form fields match what the action reads via `formData.get()`. Supports intent-based dispatch patterns. Auto-fixable when targeting a mistyped route. |
76 | | -- **hydration**: Detects SSR hydration mismatch risks: |
77 | | - - Date/time operations without consistent timezone handling |
78 | | - - Locale-dependent formatting (e.g., `toLocaleString()`) without explicit `timeZone` option |
79 | | - - Random value generation during render (`Math.random()`, `uuid()`, `nanoid()`) |
80 | | - - Browser-only API access outside `useEffect` (`window`, `document`, `localStorage`) |
81 | | - |
82 | | - Some hydration issues are auto-fixable (e.g., adding `{ timeZone: "UTC" }` to locale methods, replacing `uuid()` with `useId()`). |
83 | | - |
84 | | -- **drizzle** (persistence): Validates database operations against Drizzle ORM schema. Auto-discovers schema from common locations (`db/schema.ts`, `src/db/schema.ts`, etc.) or use `--drizzle-schema` for custom paths. |
85 | | - - Unknown table or column references in `db.insert()`, `db.update()`, `db.delete()` |
86 | | - - Missing required columns on `db.insert()` calls |
87 | | - - Null literal assigned to `notNull` column (insert or update) |
88 | | - - Invalid enum literal values (checked against schema-defined allowed values) |
89 | | - - Type mismatches: string from `formData.get()` to integer, boolean, timestamp, or json column |
90 | | - - Writing to auto-generated columns (e.g., serial, auto-increment) on insert |
91 | | - - `DELETE` or `UPDATE` without `.where()` clause (affects all rows) |
92 | | - - Enum columns receiving unvalidated external input |
93 | | - |
94 | | -## SQL Query Extraction |
95 | | - |
96 | | -The `rr sql` command extracts database queries from ORM code and generates equivalent SQL statements. |
97 | | - |
98 | | -```bash |
99 | | -rr sql --drizzle # extract all SQL queries |
100 | | -rr sql --drizzle app/routes/users.tsx # extract from specific file |
101 | | -rr sql --drizzle --format json # JSON output |
102 | | -rr sql --drizzle --drizzle-schema db/schema.ts # explicit schema path |
103 | 89 | ``` |
| 90 | +$ rr sql --drizzle --root my-app |
| 91 | +
|
| 92 | +Found 6 SQL queries: |
104 | 93 |
|
105 | | -Supports SELECT, INSERT, UPDATE, and DELETE patterns with parameterized queries and column type inference from the schema. |
| 94 | +File: app/routes/users.tsx:13:26 |
| 95 | + SELECT * FROM users |
106 | 96 |
|
107 | | -## Programmatic API |
| 97 | +File: app/routes/users.tsx:16:9 |
| 98 | + SELECT id, name, email FROM users |
108 | 99 |
|
109 | | -```typescript |
110 | | -import { analyze, applyFixes } from 'roto-rooter'; |
| 100 | +File: app/routes/users.tsx:24:29 |
| 101 | + SELECT * FROM users WHERE status = 'active' |
111 | 102 |
|
112 | | -// Run analysis |
113 | | -const result = analyze({ |
114 | | - root: './my-app', |
115 | | - files: [], // empty = all files |
116 | | - checks: [], // empty = all checks |
117 | | - format: 'text', |
118 | | -}); |
| 103 | +File: app/routes/users.tsx:36:9 |
| 104 | + INSERT INTO users (name, email, status) VALUES ($1, $2, $3) |
| 105 | + Parameters: |
| 106 | + $1: name (text) |
| 107 | + $2: email (text) |
| 108 | + $3: status (enum) |
119 | 109 |
|
120 | | -console.log(result.issues); |
| 110 | +File: app/routes/orders.tsx:16:9 |
| 111 | + INSERT INTO orders (status, user_id, total) VALUES ($1, $2, $3) |
| 112 | + Parameters: |
| 113 | + $1: status (enum) |
| 114 | + $2: userId (integer) |
| 115 | + $3: total (integer) |
121 | 116 |
|
122 | | -// Apply auto-fixes |
123 | | -const fixResult = applyFixes(result.issues); |
124 | | -console.log(`Fixed ${fixResult.fixesApplied} issues`); |
| 117 | +File: app/routes/users.tsx:42:9 |
| 118 | + DELETE FROM users WHERE id = $1 |
| 119 | + Parameters: |
| 120 | + $1: Number(params.id) (serial) |
125 | 121 | ``` |
126 | 122 |
|
127 | 123 | ## Development |
128 | 124 |
|
129 | | -```bash |
130 | | -npm install # Install dependencies |
131 | | -npm test # Run tests |
132 | | -npm run build # Build for distribution |
| 125 | +``` |
| 126 | +npm install # install dependencies |
| 127 | +npm test # run tests |
| 128 | +npm run build # build for distribution |
133 | 129 | ``` |
0 commit comments