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 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 all 47+ React best practice rules. Run this at your project root:
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` | `20` | Node.js version to use |
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 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
- -y, --yes skip prompts, scan all workspace projects
90
- --project <name> select workspace project (comma-separated for multiple)
91
- --diff [base] scan only files changed vs base branch
92
- -h, --help display help for command
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 | Type | Default | Description |
133
- | ----------------- | -------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
134
- | `ignore.rules` | `string[]` | `[]` | Rules to suppress, using the `plugin/rule` format shown in diagnostic output (e.g. `react/no-danger`, `knip/exports`, `knip/types`) |
135
- | `ignore.files` | `string[]` | `[]` | File paths to exclude, supports glob patterns (`src/generated/**`, `**/*.test.tsx`) |
136
- | `lint` | `boolean` | `true` | Enable/disable lint checks (same as `--no-lint`) |
137
- | `deadCode` | `boolean` | `true` | Enable/disable dead code detection (same as `--no-dead-code`) |
138
- | `verbose` | `boolean` | `false` | Show file details per rule (same as `--verbose`) |
139
- | `diff` | `boolean \| string` | — | Force diff mode (`true`) or pin a base branch (`"main"`). Set to `false` to disable auto-detection. |
140
- | `failOn` | `"error" \| "warning" \| "none"` | `"none"` | Exit with error code on diagnostics of the given severity or above |
141
- | `customRulesOnly` | `boolean` | `false` | Disable built-in react/jsx-a11y/compiler rules, keeping only `react-doctor/*` plugin rules |
142
- | `share` | `boolean` | `true` | Show the share-your-results URL after scanning |
143
- | `textComponents` | `string[]` | `[]` | React Native only. Component names whose children should not trigger `rn-no-raw-text` (e.g. `["MyText", "Label.Bold"]`) |
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: "Good" } or null
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
- ## [Scores for popular open-source projects](https://react.doctor/leaderboard)
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
- | Project | Score | Share |
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 -r run build
339
+ pnpm build
212
340
  ```
213
341
 
214
342
  Run locally:
215
343
 
216
344
  ```bash
217
- node packages/react-doctor/dist/cli.js /path/to/your/react-project
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 GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
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 >= SCORE_GOOD_THRESHOLD) return "Great";
17
- if (score >= SCORE_OK_THRESHOLD) return "Needs work";
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(PERFECT_SCORE - penalty));
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) return null;
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 calculateScore = async (diagnostics) => await tryScoreFromApi(diagnostics, fetch) ?? calculateScoreLocally(diagnostics);
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) => Array.isArray(userConfig?.ignore?.files) ? userConfig.ignore.files.map(compileGlobPattern) : [];
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("No React dependency found in package.json");
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("No React dependency found in package.json");
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 [lintDiagnostics, deadCodeDiagnostics] = await Promise.all([lintPromise, deadCodePromise]);
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 buildDiagnoseResult({
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
- //# sourceMappingURL=process-browser-diagnostics-DpaZeYLI.js.map
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
- //#region src/adapters/browser/diagnose-browser.d.ts
118
- interface DiagnoseBrowserInput {
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 processBrowserDiagnostics, c as diagnoseCore, d as diagnose, f as calculateScore, g as ReactDoctorConfig, h as ProjectInfo, i as ProcessBrowserDiagnosticsResult, l as BrowserDiagnoseInput, m as Diagnostic, n as diagnoseBrowser, o as DiagnoseCoreOptions, p as calculateScoreLocally, r as ProcessBrowserDiagnosticsInput, s as DiagnoseCoreResult, t as DiagnoseBrowserInput, u as BrowserDiagnoseResult } from "./diagnose-browser-B17IqMa3.js";
2
- export { type BrowserDiagnoseInput, type BrowserDiagnoseResult, type DiagnoseBrowserInput, type DiagnoseCoreOptions, type DiagnoseCoreResult, type Diagnostic, type ProcessBrowserDiagnosticsInput, type ProcessBrowserDiagnosticsResult, type ProjectInfo, type ReactDoctorConfig, type ScoreResult, calculateScore, calculateScoreLocally, diagnose, diagnoseBrowser, diagnoseCore, processBrowserDiagnostics };
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 "./process-browser-diagnostics-DpaZeYLI.js";
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
@@ -1,2 +1 @@
1
- #!/usr/bin/env node
2
1
  export { };