react-doctor 0.0.41 → 0.0.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -41
- package/bin/react-doctor.js +13 -0
- package/dist/{process-browser-diagnostics-DpaZeYLI.js → browser-BOxs7MrK.js} +39 -45
- package/dist/{diagnose-browser-B17IqMa3.d.ts → browser-Dcq3yn-p.d.ts} +32 -17
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +2 -3
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +1470 -517
- package/dist/index.d.ts +119 -12
- package/dist/index.js +1178 -363
- package/dist/react-doctor-plugin.js +2339 -169
- package/dist/worker.d.ts +2 -2
- package/dist/worker.js +2 -3
- package/package.json +35 -13
- package/dist/cli.js.map +0 -1
- package/dist/diagnose-browser-B17IqMa3.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/process-browser-diagnostics-DpaZeYLI.js.map +0 -1
- package/dist/react-doctor-plugin.d.ts.map +0 -1
- package/dist/react-doctor-plugin.js.map +0 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ npx -y react-doctor@latest . --verbose
|
|
|
40
40
|
|
|
41
41
|
## Install for your coding agent
|
|
42
42
|
|
|
43
|
-
Teach your coding agent
|
|
43
|
+
Teach your coding agent React best practices. Run this at your project root:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
npx -y react-doctor@latest install
|
|
@@ -71,7 +71,7 @@ Supports Claude Code, Codex, GitHub Copilot, Gemini CLI, Cursor, OpenCode, Facto
|
|
|
71
71
|
| `github-token` | | When set on `pull_request` events, posts findings as a PR comment |
|
|
72
72
|
| `fail-on` | `error` | Exit with error code on diagnostics: `error`, `warning`, `none` |
|
|
73
73
|
| `offline` | `false` | Skip sending diagnostics to the react.doctor API |
|
|
74
|
-
| `node-version` | `
|
|
74
|
+
| `node-version` | `22` | Node.js version to use |
|
|
75
75
|
|
|
76
76
|
The action outputs a `score` (0–100) you can use in subsequent steps.
|
|
77
77
|
|
|
@@ -81,15 +81,73 @@ The action outputs a `score` (0–100) you can use in subsequent steps.
|
|
|
81
81
|
Usage: react-doctor [directory] [options]
|
|
82
82
|
|
|
83
83
|
Options:
|
|
84
|
-
-v, --version
|
|
85
|
-
--no-lint
|
|
86
|
-
--no-dead-code
|
|
87
|
-
--verbose
|
|
88
|
-
--score
|
|
89
|
-
|
|
90
|
-
--
|
|
91
|
-
--
|
|
92
|
-
|
|
84
|
+
-v, --version display the version number
|
|
85
|
+
--no-lint skip linting
|
|
86
|
+
--no-dead-code skip dead code detection
|
|
87
|
+
--verbose show file details per rule
|
|
88
|
+
--score output only the score
|
|
89
|
+
--json output a single structured JSON report (suppresses other output)
|
|
90
|
+
-y, --yes skip prompts, scan all workspace projects
|
|
91
|
+
--full skip prompts, always run a full scan (decline diff-only)
|
|
92
|
+
--project <name> select workspace project (comma-separated for multiple)
|
|
93
|
+
--diff [base] scan only files changed vs base branch
|
|
94
|
+
--offline skip telemetry (anonymous, not stored, only used to calculate score)
|
|
95
|
+
--staged scan only staged (git index) files for pre-commit hooks
|
|
96
|
+
--fail-on <level> exit with error code on diagnostics: error, warning, none
|
|
97
|
+
--annotations output diagnostics as GitHub Actions annotations
|
|
98
|
+
-h, --help display help for command
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## JSON output
|
|
102
|
+
|
|
103
|
+
Pass `--json` to get a single, parsable JSON object on stdout. All human-readable output, prompts, and the share link are suppressed; pipe straight into `jq`, `node`, or any other tool:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npx -y react-doctor@latest . --json | jq '.summary'
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Exit code is `0` on success and `1` if the scan throws or `--fail-on` is triggered. Errors still produce a JSON object with `ok: false`, so the stdout is always a valid document.
|
|
110
|
+
|
|
111
|
+
### Schema
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
interface JsonReport {
|
|
115
|
+
schemaVersion: 1;
|
|
116
|
+
version: string; // react-doctor version
|
|
117
|
+
ok: boolean; // false when an error was thrown
|
|
118
|
+
directory: string; // resolved root passed to the CLI
|
|
119
|
+
mode: "full" | "diff" | "staged";
|
|
120
|
+
diff: {
|
|
121
|
+
baseBranch: string;
|
|
122
|
+
currentBranch: string;
|
|
123
|
+
changedFileCount: number;
|
|
124
|
+
isCurrentChanges: boolean;
|
|
125
|
+
} | null;
|
|
126
|
+
projects: Array<{
|
|
127
|
+
directory: string;
|
|
128
|
+
project: ProjectInfo;
|
|
129
|
+
diagnostics: Diagnostic[];
|
|
130
|
+
score: { score: number; label: string } | null;
|
|
131
|
+
skippedChecks: string[];
|
|
132
|
+
elapsedMilliseconds: number;
|
|
133
|
+
}>;
|
|
134
|
+
diagnostics: Diagnostic[]; // flattened across all scanned projects
|
|
135
|
+
summary: {
|
|
136
|
+
errorCount: number;
|
|
137
|
+
warningCount: number;
|
|
138
|
+
affectedFileCount: number;
|
|
139
|
+
totalDiagnosticCount: number;
|
|
140
|
+
score: number | null; // worst project score, when available
|
|
141
|
+
scoreLabel: string | null;
|
|
142
|
+
};
|
|
143
|
+
elapsedMilliseconds: number; // total wall time across all projects
|
|
144
|
+
error: {
|
|
145
|
+
message: string;
|
|
146
|
+
name: string;
|
|
147
|
+
chain: string[]; // outer error message first, every `error.cause`
|
|
148
|
+
// unwrapped after; chain[0] always equals `message`
|
|
149
|
+
} | null; // null on success, populated when ok=false
|
|
150
|
+
}
|
|
93
151
|
```
|
|
94
152
|
|
|
95
153
|
## Browser API
|
|
@@ -127,20 +185,65 @@ You can also use the `"reactDoctor"` key in your `package.json` instead:
|
|
|
127
185
|
|
|
128
186
|
If both exist, `react-doctor.config.json` takes precedence.
|
|
129
187
|
|
|
188
|
+
### Inline suppressions
|
|
189
|
+
|
|
190
|
+
Suppress a rule on a specific line with `// react-doctor-disable-line` or the next line with `// react-doctor-disable-next-line`:
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
// react-doctor-disable-next-line react-doctor/no-cascading-set-state
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
setA(value);
|
|
196
|
+
setB(value);
|
|
197
|
+
setC(value);
|
|
198
|
+
}, [value]);
|
|
199
|
+
|
|
200
|
+
const value = expensiveComputation(); // react-doctor-disable-line react-doctor/no-usememo-simple-expression
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Comma- or space-separate multiple rule ids on the same comment. With no rule id, the comment suppresses every diagnostic on that line.
|
|
204
|
+
|
|
205
|
+
### Respecting your existing project ignores
|
|
206
|
+
|
|
207
|
+
By default, React Doctor honors all of the ignore-style files your project already has, so you don't need to maintain a separate "what should react-doctor skip" list:
|
|
208
|
+
|
|
209
|
+
| File | What gets skipped |
|
|
210
|
+
| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
211
|
+
| `.gitignore` | files git ignores (oxlint default) |
|
|
212
|
+
| `.eslintignore` | files eslint skips (oxlint default) |
|
|
213
|
+
| `.oxlintignore` | files oxlint skips (added via `--ignore-pattern` so `.eslintignore` still applies) |
|
|
214
|
+
| `.prettierignore` | files prettier skips — typically vendored code, generated builds, and lockfiles |
|
|
215
|
+
| `.gitattributes` (`linguist-vendored`, `linguist-generated`) | paths GitHub's linguist library hides from language stats; if it's not "your" code by GitHub's reckoning, it shouldn't be audited as your code by react-doctor either |
|
|
216
|
+
|
|
217
|
+
React Doctor also respects inline lint suppressions in source files:
|
|
218
|
+
|
|
219
|
+
- `// oxlint-disable`, `// oxlint-disable-line`, `// oxlint-disable-next-line` — with or without rule ids.
|
|
220
|
+
- `// eslint-disable`, `// eslint-disable-line`, `// eslint-disable-next-line` — oxlint reads both prefixes interchangeably.
|
|
221
|
+
|
|
222
|
+
> Note: `.editorconfig` is intentionally NOT consulted. It describes editor settings (indent size, charset, end-of-line) and has no concept of "files to skip" — there's nothing in it that would change what react-doctor lints.
|
|
223
|
+
|
|
224
|
+
If you want React Doctor to ignore those inline suppressions and audit your codebase for everything (useful for one-off "what does my project actually score?" runs), set:
|
|
225
|
+
|
|
226
|
+
```jsonc
|
|
227
|
+
{ "respectInlineDisables": false }
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
This only neutralizes the inline `// eslint-disable*` / `// oxlint-disable*` comments — the file-level ignore lists above are always honored, even in audit mode, because they typically point at vendored or generated code that genuinely shouldn't be linted.
|
|
231
|
+
|
|
130
232
|
### Config options
|
|
131
233
|
|
|
132
|
-
| Key
|
|
133
|
-
|
|
|
134
|
-
| `ignore.rules`
|
|
135
|
-
| `ignore.files`
|
|
136
|
-
| `lint`
|
|
137
|
-
| `deadCode`
|
|
138
|
-
| `verbose`
|
|
139
|
-
| `diff`
|
|
140
|
-
| `failOn`
|
|
141
|
-
| `customRulesOnly`
|
|
142
|
-
| `share`
|
|
143
|
-
| `textComponents`
|
|
234
|
+
| Key | Type | Default | Description |
|
|
235
|
+
| ----------------------- | -------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
236
|
+
| `ignore.rules` | `string[]` | `[]` | Rules to suppress, using the `plugin/rule` format shown in diagnostic output (e.g. `react/no-danger`, `knip/exports`, `knip/types`) |
|
|
237
|
+
| `ignore.files` | `string[]` | `[]` | File paths to exclude, supports glob patterns (`src/generated/**`, `**/*.test.tsx`) |
|
|
238
|
+
| `lint` | `boolean` | `true` | Enable/disable lint checks (same as `--no-lint`) |
|
|
239
|
+
| `deadCode` | `boolean` | `true` | Enable/disable dead code detection (same as `--no-dead-code`) |
|
|
240
|
+
| `verbose` | `boolean` | `false` | Show file details per rule (same as `--verbose`) |
|
|
241
|
+
| `diff` | `boolean \| string` | — | Force diff mode (`true`) or pin a base branch (`"main"`). Set to `false` to disable auto-detection. |
|
|
242
|
+
| `failOn` | `"error" \| "warning" \| "none"` | `"none"` | Exit with error code on diagnostics of the given severity or above |
|
|
243
|
+
| `customRulesOnly` | `boolean` | `false` | Disable built-in react/jsx-a11y/compiler rules, keeping only `react-doctor/*` plugin rules |
|
|
244
|
+
| `share` | `boolean` | `true` | Show the share-your-results URL after scanning |
|
|
245
|
+
| `textComponents` | `string[]` | `[]` | React Native only. Component names whose children should not trigger `rn-no-raw-text` (e.g. `["MyText", "Label.Bold"]`) |
|
|
246
|
+
| `respectInlineDisables` | `boolean` | `true` | Respect inline `// eslint-disable*` / `// oxlint-disable*` comments. Set `false` for audit mode. File-level ignores (`.gitignore`, `.eslintignore`, `.oxlintignore`, `.prettierignore`, `.gitattributes` linguist annotations) are always respected. |
|
|
144
247
|
|
|
145
248
|
CLI flags always override config values.
|
|
146
249
|
|
|
@@ -153,7 +256,7 @@ import { diagnose } from "react-doctor/api";
|
|
|
153
256
|
|
|
154
257
|
const result = await diagnose("./path/to/your/react-project");
|
|
155
258
|
|
|
156
|
-
console.log(result.score); // { score: 82, label: "
|
|
259
|
+
console.log(result.score); // { score: 82, label: "Great" } or null
|
|
157
260
|
console.log(result.diagnostics); // Array of Diagnostic objects
|
|
158
261
|
console.log(result.project); // Detected framework, React version, etc.
|
|
159
262
|
```
|
|
@@ -183,22 +286,47 @@ interface Diagnostic {
|
|
|
183
286
|
}
|
|
184
287
|
```
|
|
185
288
|
|
|
186
|
-
|
|
289
|
+
To produce the same structured output the `--json` CLI flag emits, use `toJsonReport`:
|
|
290
|
+
|
|
291
|
+
```js
|
|
292
|
+
import { diagnose, toJsonReport, summarizeDiagnostics } from "react-doctor/api";
|
|
293
|
+
|
|
294
|
+
const result = await diagnose(".");
|
|
295
|
+
|
|
296
|
+
const report = toJsonReport(result, { version: "1.0.0" });
|
|
297
|
+
console.log(JSON.stringify(report, null, 2));
|
|
298
|
+
|
|
299
|
+
const counts = summarizeDiagnostics(result.diagnostics);
|
|
300
|
+
console.log(`${counts.errorCount} errors, ${counts.warningCount} warnings`);
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`react-doctor/api` also re-exports the `JsonReport`, `JsonReportSummary`, `JsonReportProjectEntry`, and `JsonReportMode` types, plus the lower-level `buildJsonReport` and `buildJsonReportError` builders if you need to assemble reports from multiple `diagnose()` calls.
|
|
304
|
+
|
|
305
|
+
## Use the oxlint plugin standalone
|
|
306
|
+
|
|
307
|
+
If you already use oxlint and just want React Doctor's rule set, register the plugin directly in your `.oxlintrc.json`:
|
|
308
|
+
|
|
309
|
+
```jsonc
|
|
310
|
+
{
|
|
311
|
+
"jsPlugins": [
|
|
312
|
+
{
|
|
313
|
+
"name": "react-doctor",
|
|
314
|
+
"specifier": "react-doctor/oxlint-plugin",
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
"rules": {
|
|
318
|
+
"react-doctor/no-fetch-in-effect": "warn",
|
|
319
|
+
"react-doctor/no-derived-state-effect": "warn",
|
|
320
|
+
// ...pick the rules you want
|
|
321
|
+
},
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The full rule list is in [`oxlint-config.ts`](https://github.com/millionco/react-doctor/blob/main/packages/react-doctor/src/oxlint-config.ts).
|
|
326
|
+
|
|
327
|
+
## Scores for popular open-source projects
|
|
187
328
|
|
|
188
|
-
|
|
189
|
-
| ------------------------------------------------------ | ------ | --------------------------------------------------------------------------------------- |
|
|
190
|
-
| [tldraw](https://github.com/tldraw/tldraw) | **84** | [view](https://www.react.doctor/share?p=tldraw&s=84&e=98&w=139&f=40) |
|
|
191
|
-
| [excalidraw](https://github.com/excalidraw/excalidraw) | **84** | [view](https://www.react.doctor/share?p=%40excalidraw%2Fexcalidraw&s=84&e=2&w=196&f=80) |
|
|
192
|
-
| [twenty](https://github.com/twentyhq/twenty) | **78** | [view](https://www.react.doctor/share?p=twenty-front&s=78&e=99&w=293&f=268) |
|
|
193
|
-
| [plane](https://github.com/makeplane/plane) | **78** | [view](https://www.react.doctor/share?p=web&s=78&e=7&w=525&f=292) |
|
|
194
|
-
| [formbricks](https://github.com/formbricks/formbricks) | **75** | [view](https://www.react.doctor/share?p=%40formbricks%2Fweb&s=75&e=15&w=389&f=242) |
|
|
195
|
-
| [posthog](https://github.com/PostHog/posthog) | **72** | [view](https://www.react.doctor/share?p=%40posthog%2Ffrontend&s=72&e=82&w=1177&f=585) |
|
|
196
|
-
| [supabase](https://github.com/supabase/supabase) | **69** | [view](https://www.react.doctor/share?p=studio&s=69&e=74&w=1087&f=566) |
|
|
197
|
-
| [onlook](https://github.com/onlook-dev/onlook) | **69** | [view](https://www.react.doctor/share?p=%40onlook%2Fweb-client&s=69&e=64&w=418&f=178) |
|
|
198
|
-
| [payload](https://github.com/payloadcms/payload) | **68** | [view](https://www.react.doctor/share?p=%40payloadcms%2Fui&s=68&e=139&w=408&f=298) |
|
|
199
|
-
| [sentry](https://github.com/getsentry/sentry) | **64** | [view](https://www.react.doctor/share?p=sentry&s=64&e=94&w=1345&f=818) |
|
|
200
|
-
| [cal.com](https://github.com/calcom/cal.com) | **63** | [view](https://www.react.doctor/share?p=%40calcom%2Fweb&s=63&e=31&w=558&f=311) |
|
|
201
|
-
| [dub](https://github.com/dubinc/dub) | **62** | [view](https://www.react.doctor/share?p=web&s=62&e=52&w=966&f=457) |
|
|
329
|
+
See the live leaderboard at [react.doctor/leaderboard](https://react.doctor/leaderboard) for current scores across React projects.
|
|
202
330
|
|
|
203
331
|
## Contributing
|
|
204
332
|
|
|
@@ -208,13 +336,13 @@ Want to contribute? Check out the codebase and submit a PR.
|
|
|
208
336
|
git clone https://github.com/millionco/react-doctor
|
|
209
337
|
cd react-doctor
|
|
210
338
|
pnpm install
|
|
211
|
-
pnpm
|
|
339
|
+
pnpm build
|
|
212
340
|
```
|
|
213
341
|
|
|
214
342
|
Run locally:
|
|
215
343
|
|
|
216
344
|
```bash
|
|
217
|
-
node packages/react-doctor/
|
|
345
|
+
node packages/react-doctor/bin/react-doctor.js /path/to/your/react-project
|
|
218
346
|
```
|
|
219
347
|
|
|
220
348
|
### License
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import module from "node:module";
|
|
4
|
+
|
|
5
|
+
if (module.enableCompileCache && !process.env.NODE_DISABLE_COMPILE_CACHE) {
|
|
6
|
+
try {
|
|
7
|
+
module.enableCompileCache();
|
|
8
|
+
} catch {
|
|
9
|
+
// Ignore compile-cache errors.
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
await import("../dist/cli.js");
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
//#region src/constants.ts
|
|
2
2
|
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
3
|
-
const PERFECT_SCORE = 100;
|
|
4
|
-
const SCORE_GOOD_THRESHOLD = 75;
|
|
5
|
-
const SCORE_OK_THRESHOLD = 50;
|
|
6
3
|
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
7
4
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
8
|
-
const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
9
5
|
const ERROR_RULE_PENALTY = 1.5;
|
|
10
6
|
const WARNING_RULE_PENALTY = .75;
|
|
11
|
-
const
|
|
12
|
-
|
|
7
|
+
const buildNoReactDependencyError = (directory) => `No React dependency found in ${directory}/package.json. Add "react" to dependencies (or peerDependencies) and re-run.`;
|
|
13
8
|
//#endregion
|
|
14
9
|
//#region src/core/calculate-score-locally.ts
|
|
15
10
|
const getScoreLabel = (score) => {
|
|
16
|
-
if (score >=
|
|
17
|
-
if (score >=
|
|
11
|
+
if (score >= 75) return "Great";
|
|
12
|
+
if (score >= 50) return "Needs work";
|
|
18
13
|
return "Critical";
|
|
19
14
|
};
|
|
20
15
|
const countUniqueRules = (diagnostics) => {
|
|
@@ -32,7 +27,7 @@ const countUniqueRules = (diagnostics) => {
|
|
|
32
27
|
};
|
|
33
28
|
const scoreFromRuleCounts = (errorRuleCount, warningRuleCount) => {
|
|
34
29
|
const penalty = errorRuleCount * ERROR_RULE_PENALTY + warningRuleCount * WARNING_RULE_PENALTY;
|
|
35
|
-
return Math.max(0, Math.round(
|
|
30
|
+
return Math.max(0, Math.round(100 - penalty));
|
|
36
31
|
};
|
|
37
32
|
const calculateScoreLocally = (diagnostics) => {
|
|
38
33
|
const { errorRuleCount, warningRuleCount } = countUniqueRules(diagnostics);
|
|
@@ -42,7 +37,6 @@ const calculateScoreLocally = (diagnostics) => {
|
|
|
42
37
|
label: getScoreLabel(score)
|
|
43
38
|
};
|
|
44
39
|
};
|
|
45
|
-
|
|
46
40
|
//#endregion
|
|
47
41
|
//#region src/core/try-score-from-api.ts
|
|
48
42
|
const parseScoreResult = (value) => {
|
|
@@ -56,29 +50,40 @@ const parseScoreResult = (value) => {
|
|
|
56
50
|
label: labelValue
|
|
57
51
|
};
|
|
58
52
|
};
|
|
53
|
+
const stripFilePaths = (diagnostics) => diagnostics.map(({ filePath: _filePath, ...rest }) => rest);
|
|
54
|
+
const isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
55
|
+
const describeFailure = (error) => {
|
|
56
|
+
if (isAbortError(error)) return `timed out after ${FETCH_TIMEOUT_MS / 1e3}s`;
|
|
57
|
+
if (error instanceof Error && error.message) return error.message;
|
|
58
|
+
return String(error);
|
|
59
|
+
};
|
|
59
60
|
const tryScoreFromApi = async (diagnostics, fetchImplementation) => {
|
|
61
|
+
if (typeof fetchImplementation !== "function") return null;
|
|
60
62
|
const controller = new AbortController();
|
|
61
63
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
62
64
|
try {
|
|
63
65
|
const response = await fetchImplementation(SCORE_API_URL, {
|
|
64
66
|
method: "POST",
|
|
65
67
|
headers: { "Content-Type": "application/json" },
|
|
66
|
-
body: JSON.stringify({ diagnostics }),
|
|
68
|
+
body: JSON.stringify({ diagnostics: stripFilePaths(diagnostics) }),
|
|
67
69
|
signal: controller.signal
|
|
68
70
|
});
|
|
69
|
-
if (!response.ok)
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
console.warn(`[react-doctor] Score API returned ${response.status} ${response.statusText} — using local scoring`);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
70
75
|
return parseScoreResult(await response.json());
|
|
71
|
-
} catch {
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.warn(`[react-doctor] Score API unreachable (${describeFailure(error)}) — using local scoring`);
|
|
72
78
|
return null;
|
|
73
79
|
} finally {
|
|
74
80
|
clearTimeout(timeoutId);
|
|
75
81
|
}
|
|
76
82
|
};
|
|
77
|
-
|
|
78
83
|
//#endregion
|
|
79
84
|
//#region src/utils/calculate-score-browser.ts
|
|
80
|
-
const
|
|
81
|
-
|
|
85
|
+
const getGlobalFetch = () => typeof fetch === "function" ? fetch : void 0;
|
|
86
|
+
const calculateScore = async (diagnostics, fetchImplementation = getGlobalFetch()) => await tryScoreFromApi(diagnostics, fetchImplementation) ?? calculateScoreLocally(diagnostics);
|
|
82
87
|
//#endregion
|
|
83
88
|
//#region src/utils/match-glob-pattern.ts
|
|
84
89
|
const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\]\\]/g;
|
|
@@ -106,7 +111,6 @@ const compileGlobPattern = (pattern) => {
|
|
|
106
111
|
regexSource += "$";
|
|
107
112
|
return new RegExp(regexSource);
|
|
108
113
|
};
|
|
109
|
-
|
|
110
114
|
//#endregion
|
|
111
115
|
//#region src/utils/is-ignored-file.ts
|
|
112
116
|
const toRelativePath = (filePath, rootDirectory) => {
|
|
@@ -115,13 +119,16 @@ const toRelativePath = (filePath, rootDirectory) => {
|
|
|
115
119
|
if (normalizedFilePath.startsWith(normalizedRoot)) return normalizedFilePath.slice(normalizedRoot.length);
|
|
116
120
|
return normalizedFilePath.replace(/^\.\//, "");
|
|
117
121
|
};
|
|
118
|
-
const compileIgnoredFilePatterns = (userConfig) =>
|
|
122
|
+
const compileIgnoredFilePatterns = (userConfig) => {
|
|
123
|
+
const files = userConfig?.ignore?.files;
|
|
124
|
+
if (!Array.isArray(files)) return [];
|
|
125
|
+
return files.filter((entry) => typeof entry === "string").map(compileGlobPattern);
|
|
126
|
+
};
|
|
119
127
|
const isFileIgnoredByPatterns = (filePath, rootDirectory, patterns) => {
|
|
120
128
|
if (patterns.length === 0) return false;
|
|
121
129
|
const relativePath = toRelativePath(filePath, rootDirectory);
|
|
122
130
|
return patterns.some((pattern) => pattern.test(relativePath));
|
|
123
131
|
};
|
|
124
|
-
|
|
125
132
|
//#endregion
|
|
126
133
|
//#region src/utils/filter-diagnostics.ts
|
|
127
134
|
const resolveCandidateReadPath = (rootDirectory, filePath) => {
|
|
@@ -157,9 +164,9 @@ const isRuleSuppressed = (commentRules, ruleId) => {
|
|
|
157
164
|
return commentRules.split(/[,\s]+/).some((rule) => rule.trim() === ruleId);
|
|
158
165
|
};
|
|
159
166
|
const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory, readFileLinesSync) => {
|
|
160
|
-
const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules : []);
|
|
167
|
+
const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules.filter((rule) => typeof rule === "string") : []);
|
|
161
168
|
const ignoredFilePatterns = compileIgnoredFilePatterns(config);
|
|
162
|
-
const textComponentNames = new Set(Array.isArray(config.textComponents) ? config.textComponents : []);
|
|
169
|
+
const textComponentNames = new Set(Array.isArray(config.textComponents) ? config.textComponents.filter((name) => typeof name === "string") : []);
|
|
163
170
|
const hasTextComponents = textComponentNames.size > 0;
|
|
164
171
|
const getFileLines = createFileLinesCache(rootDirectory, readFileLinesSync);
|
|
165
172
|
return diagnostics.filter((diagnostic) => {
|
|
@@ -195,13 +202,11 @@ const filterInlineSuppressions = (diagnostics, rootDirectory, readFileLinesSync)
|
|
|
195
202
|
return true;
|
|
196
203
|
});
|
|
197
204
|
};
|
|
198
|
-
|
|
199
205
|
//#endregion
|
|
200
206
|
//#region src/utils/merge-and-filter-diagnostics.ts
|
|
201
207
|
const mergeAndFilterDiagnostics = (mergedDiagnostics, directory, userConfig, readFileLinesSync) => {
|
|
202
208
|
return filterInlineSuppressions(userConfig ? filterIgnoredDiagnostics(mergedDiagnostics, userConfig, directory, readFileLinesSync) : mergedDiagnostics, directory, readFileLinesSync);
|
|
203
209
|
};
|
|
204
|
-
|
|
205
210
|
//#endregion
|
|
206
211
|
//#region src/core/build-result.ts
|
|
207
212
|
const buildDiagnoseTimedResult = async (input) => {
|
|
@@ -213,7 +218,6 @@ const buildDiagnoseTimedResult = async (input) => {
|
|
|
213
218
|
elapsedMilliseconds
|
|
214
219
|
};
|
|
215
220
|
};
|
|
216
|
-
|
|
217
221
|
//#endregion
|
|
218
222
|
//#region src/adapters/browser/create-browser-read-file-lines.ts
|
|
219
223
|
const normalizeKey = (rootDirectory, filePath) => {
|
|
@@ -229,11 +233,10 @@ const createBrowserReadFileLinesSync = (rootDirectory, projectFiles) => {
|
|
|
229
233
|
return content.split("\n");
|
|
230
234
|
};
|
|
231
235
|
};
|
|
232
|
-
|
|
233
236
|
//#endregion
|
|
234
237
|
//#region src/adapters/browser/diagnose.ts
|
|
235
238
|
const diagnose = async (input) => {
|
|
236
|
-
if (!input.project.reactVersion) throw new Error(
|
|
239
|
+
if (!input.project.reactVersion) throw new Error(buildNoReactDependencyError(input.rootDirectory));
|
|
237
240
|
const readFileLinesSync = createBrowserReadFileLinesSync(input.rootDirectory, input.projectFiles);
|
|
238
241
|
const userConfig = input.userConfig ?? null;
|
|
239
242
|
const deadCodeDiagnostics = input.deadCodeDiagnostics ?? [];
|
|
@@ -255,20 +258,9 @@ const diagnose = async (input) => {
|
|
|
255
258
|
elapsedMilliseconds: timed.elapsedMilliseconds
|
|
256
259
|
};
|
|
257
260
|
};
|
|
258
|
-
|
|
259
|
-
//#endregion
|
|
260
|
-
//#region src/core/build-diagnose-result.ts
|
|
261
|
-
const buildDiagnoseResult = (params) => ({
|
|
262
|
-
diagnostics: params.diagnostics,
|
|
263
|
-
score: params.score,
|
|
264
|
-
project: params.project,
|
|
265
|
-
elapsedMilliseconds: params.elapsedMilliseconds
|
|
266
|
-
});
|
|
267
|
-
|
|
268
261
|
//#endregion
|
|
269
262
|
//#region src/utils/jsx-include-paths.ts
|
|
270
263
|
const computeJsxIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath)) : void 0;
|
|
271
|
-
|
|
272
264
|
//#endregion
|
|
273
265
|
//#region src/core/diagnose-core.ts
|
|
274
266
|
const diagnoseCore = async (deps, options = {}) => {
|
|
@@ -280,7 +272,7 @@ const diagnoseCore = async (deps, options = {}) => {
|
|
|
280
272
|
const userConfig = deps.loadUserConfig();
|
|
281
273
|
const effectiveLint = options.lint ?? userConfig?.lint ?? true;
|
|
282
274
|
const effectiveDeadCode = options.deadCode ?? userConfig?.deadCode ?? true;
|
|
283
|
-
if (!projectInfo.reactVersion) throw new Error(
|
|
275
|
+
if (!projectInfo.reactVersion) throw new Error(buildNoReactDependencyError(deps.rootDirectory));
|
|
284
276
|
const lintIncludePaths = options.lintIncludePaths !== void 0 ? options.lintIncludePaths : computeJsxIncludePaths(includePaths);
|
|
285
277
|
const { runLint, runDeadCode } = deps.createRunners({
|
|
286
278
|
resolvedDirectory,
|
|
@@ -298,7 +290,11 @@ const diagnoseCore = async (deps, options = {}) => {
|
|
|
298
290
|
console.error("Dead code analysis failed:", error);
|
|
299
291
|
return emptyDiagnostics;
|
|
300
292
|
}) : Promise.resolve(emptyDiagnostics);
|
|
301
|
-
const [
|
|
293
|
+
const [lintSettled, deadCodeSettled] = await Promise.allSettled([lintPromise, deadCodePromise]);
|
|
294
|
+
const lintDiagnostics = lintSettled.status === "fulfilled" ? lintSettled.value : emptyDiagnostics;
|
|
295
|
+
const deadCodeDiagnostics = deadCodeSettled.status === "fulfilled" ? deadCodeSettled.value : emptyDiagnostics;
|
|
296
|
+
if (lintSettled.status === "rejected") console.error("Lint rejected:", lintSettled.reason);
|
|
297
|
+
if (deadCodeSettled.status === "rejected") console.error("Dead code rejected:", deadCodeSettled.reason);
|
|
302
298
|
const environmentDiagnostics = deps.getExtraDiagnostics?.() ?? [];
|
|
303
299
|
const timed = await buildDiagnoseTimedResult({
|
|
304
300
|
mergedDiagnostics: [
|
|
@@ -312,14 +308,13 @@ const diagnoseCore = async (deps, options = {}) => {
|
|
|
312
308
|
startTime,
|
|
313
309
|
calculateDiagnosticsScore: deps.calculateDiagnosticsScore
|
|
314
310
|
});
|
|
315
|
-
return
|
|
311
|
+
return {
|
|
316
312
|
diagnostics: timed.diagnostics,
|
|
317
313
|
score: timed.score,
|
|
318
314
|
project: projectInfo,
|
|
319
315
|
elapsedMilliseconds: timed.elapsedMilliseconds
|
|
320
|
-
}
|
|
316
|
+
};
|
|
321
317
|
};
|
|
322
|
-
|
|
323
318
|
//#endregion
|
|
324
319
|
//#region src/adapters/browser/diagnose-browser.ts
|
|
325
320
|
const diagnoseBrowser = async (input, options = {}) => {
|
|
@@ -339,7 +334,6 @@ const diagnoseBrowser = async (input, options = {}) => {
|
|
|
339
334
|
})
|
|
340
335
|
}, options);
|
|
341
336
|
};
|
|
342
|
-
|
|
343
337
|
//#endregion
|
|
344
338
|
//#region src/adapters/browser/process-browser-diagnostics.ts
|
|
345
339
|
const processBrowserDiagnostics = async (input) => {
|
|
@@ -359,7 +353,7 @@ const processBrowserDiagnostics = async (input) => {
|
|
|
359
353
|
score: timed.score
|
|
360
354
|
};
|
|
361
355
|
};
|
|
362
|
-
|
|
363
356
|
//#endregion
|
|
364
357
|
export { calculateScore as a, diagnose as i, diagnoseBrowser as n, calculateScoreLocally as o, diagnoseCore as r, processBrowserDiagnostics as t };
|
|
365
|
-
|
|
358
|
+
|
|
359
|
+
//# sourceMappingURL=browser-BOxs7MrK.js.map
|
|
@@ -8,6 +8,7 @@ interface ProjectInfo {
|
|
|
8
8
|
framework: Framework;
|
|
9
9
|
hasTypeScript: boolean;
|
|
10
10
|
hasReactCompiler: boolean;
|
|
11
|
+
hasTanStackQuery: boolean;
|
|
11
12
|
sourceFileCount: number;
|
|
12
13
|
}
|
|
13
14
|
interface Diagnostic {
|
|
@@ -20,7 +21,6 @@ interface Diagnostic {
|
|
|
20
21
|
line: number;
|
|
21
22
|
column: number;
|
|
22
23
|
category: string;
|
|
23
|
-
weight?: number;
|
|
24
24
|
}
|
|
25
25
|
interface ScoreResult {
|
|
26
26
|
score: number;
|
|
@@ -40,13 +40,28 @@ interface ReactDoctorConfig {
|
|
|
40
40
|
customRulesOnly?: boolean;
|
|
41
41
|
share?: boolean;
|
|
42
42
|
textComponents?: string[];
|
|
43
|
+
/**
|
|
44
|
+
* Whether to respect inline `// eslint-disable*` / `// oxlint-disable*`
|
|
45
|
+
* comments in source files. Default: `true`.
|
|
46
|
+
*
|
|
47
|
+
* File-level ignores (`.gitignore`, `.eslintignore`, `.oxlintignore`,
|
|
48
|
+
* `.prettierignore`, `.gitattributes` `linguist-vendored` /
|
|
49
|
+
* `linguist-generated`) are ALWAYS honored regardless of this option
|
|
50
|
+
* — they typically point at vendored or generated code that
|
|
51
|
+
* genuinely shouldn't be linted at all.
|
|
52
|
+
*
|
|
53
|
+
* Set to `false` for "audit mode": every inline suppression is
|
|
54
|
+
* neutralized so react-doctor reports every diagnostic regardless
|
|
55
|
+
* of historical hide-comments.
|
|
56
|
+
*/
|
|
57
|
+
respectInlineDisables?: boolean;
|
|
43
58
|
}
|
|
44
59
|
//#endregion
|
|
45
60
|
//#region src/core/calculate-score-locally.d.ts
|
|
46
61
|
declare const calculateScoreLocally: (diagnostics: Diagnostic[]) => ScoreResult;
|
|
47
62
|
//#endregion
|
|
48
63
|
//#region src/utils/calculate-score-browser.d.ts
|
|
49
|
-
declare const calculateScore: (diagnostics: Diagnostic[]) => Promise<ScoreResult | null>;
|
|
64
|
+
declare const calculateScore: (diagnostics: Diagnostic[], fetchImplementation?: typeof fetch | undefined) => Promise<ScoreResult | null>;
|
|
50
65
|
//#endregion
|
|
51
66
|
//#region src/adapters/browser/diagnose.d.ts
|
|
52
67
|
interface BrowserDiagnoseInput {
|
|
@@ -100,6 +115,19 @@ interface DiagnoseCoreDeps {
|
|
|
100
115
|
}
|
|
101
116
|
declare const diagnoseCore: (deps: DiagnoseCoreDeps, options?: DiagnoseCoreOptions) => Promise<DiagnoseCoreResult>;
|
|
102
117
|
//#endregion
|
|
118
|
+
//#region src/adapters/browser/diagnose-browser.d.ts
|
|
119
|
+
interface DiagnoseBrowserInput {
|
|
120
|
+
rootDirectory: string;
|
|
121
|
+
project: ProjectInfo;
|
|
122
|
+
projectFiles: Record<string, string>;
|
|
123
|
+
userConfig?: ReactDoctorConfig | null;
|
|
124
|
+
runOxlint: (input: {
|
|
125
|
+
lintIncludePaths: string[] | undefined;
|
|
126
|
+
customRulesOnly: boolean;
|
|
127
|
+
}) => Promise<Diagnostic[]>;
|
|
128
|
+
}
|
|
129
|
+
declare const diagnoseBrowser: (input: DiagnoseBrowserInput, options?: DiagnoseCoreOptions) => Promise<DiagnoseCoreResult>;
|
|
130
|
+
//#endregion
|
|
103
131
|
//#region src/adapters/browser/process-browser-diagnostics.d.ts
|
|
104
132
|
interface ProcessBrowserDiagnosticsInput {
|
|
105
133
|
rootDirectory: string;
|
|
@@ -114,18 +142,5 @@ interface ProcessBrowserDiagnosticsResult {
|
|
|
114
142
|
}
|
|
115
143
|
declare const processBrowserDiagnostics: (input: ProcessBrowserDiagnosticsInput) => Promise<ProcessBrowserDiagnosticsResult>;
|
|
116
144
|
//#endregion
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
rootDirectory: string;
|
|
120
|
-
project: ProjectInfo;
|
|
121
|
-
projectFiles: Record<string, string>;
|
|
122
|
-
userConfig?: ReactDoctorConfig | null;
|
|
123
|
-
runOxlint: (input: {
|
|
124
|
-
lintIncludePaths: string[] | undefined;
|
|
125
|
-
customRulesOnly: boolean;
|
|
126
|
-
}) => Promise<Diagnostic[]>;
|
|
127
|
-
}
|
|
128
|
-
declare const diagnoseBrowser: (input: DiagnoseBrowserInput, options?: DiagnoseCoreOptions) => Promise<DiagnoseCoreResult>;
|
|
129
|
-
//#endregion
|
|
130
|
-
export { ScoreResult as _, processBrowserDiagnostics as a, diagnoseCore as c, diagnose as d, calculateScore as f, ReactDoctorConfig as g, ProjectInfo as h, ProcessBrowserDiagnosticsResult as i, BrowserDiagnoseInput as l, Diagnostic as m, diagnoseBrowser as n, DiagnoseCoreOptions as o, calculateScoreLocally as p, ProcessBrowserDiagnosticsInput as r, DiagnoseCoreResult as s, DiagnoseBrowserInput as t, BrowserDiagnoseResult as u };
|
|
131
|
-
//# sourceMappingURL=diagnose-browser-B17IqMa3.d.ts.map
|
|
145
|
+
export { ScoreResult as _, diagnoseBrowser as a, diagnoseCore as c, diagnose as d, calculateScore as f, ReactDoctorConfig as g, ProjectInfo as h, DiagnoseBrowserInput as i, BrowserDiagnoseInput as l, Diagnostic as m, ProcessBrowserDiagnosticsResult as n, DiagnoseCoreOptions as o, calculateScoreLocally as p, processBrowserDiagnostics as r, DiagnoseCoreResult as s, ProcessBrowserDiagnosticsInput as t, BrowserDiagnoseResult as u };
|
|
146
|
+
//# sourceMappingURL=browser-Dcq3yn-p.d.ts.map
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as ScoreResult, a as
|
|
2
|
-
export {
|
|
1
|
+
import { _ as ScoreResult, a as diagnoseBrowser, c as diagnoseCore, d as diagnose, f as calculateScore, g as ReactDoctorConfig, h as ProjectInfo, i as DiagnoseBrowserInput, l as BrowserDiagnoseInput, m as Diagnostic, n as ProcessBrowserDiagnosticsResult, o as DiagnoseCoreOptions, p as calculateScoreLocally, r as processBrowserDiagnostics, s as DiagnoseCoreResult, t as ProcessBrowserDiagnosticsInput, u as BrowserDiagnoseResult } from "./browser-Dcq3yn-p.js";
|
|
2
|
+
export { BrowserDiagnoseInput, BrowserDiagnoseResult, DiagnoseBrowserInput, DiagnoseCoreOptions, DiagnoseCoreResult, Diagnostic, ProcessBrowserDiagnosticsInput, ProcessBrowserDiagnosticsResult, ProjectInfo, ReactDoctorConfig, ScoreResult, calculateScore, calculateScoreLocally, diagnose, diagnoseBrowser, diagnoseCore, processBrowserDiagnostics };
|
package/dist/browser.js
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import { a as calculateScore, i as diagnose, n as diagnoseBrowser, o as calculateScoreLocally, r as diagnoseCore, t as processBrowserDiagnostics } from "./
|
|
2
|
-
|
|
3
|
-
export { calculateScore, calculateScoreLocally, diagnose, diagnoseBrowser, diagnoseCore, processBrowserDiagnostics };
|
|
1
|
+
import { a as calculateScore, i as diagnose, n as diagnoseBrowser, o as calculateScoreLocally, r as diagnoseCore, t as processBrowserDiagnostics } from "./browser-BOxs7MrK.js";
|
|
2
|
+
export { calculateScore, calculateScoreLocally, diagnose, diagnoseBrowser, diagnoseCore, processBrowserDiagnostics };
|
package/dist/cli.d.ts
CHANGED