react-doctor 0.0.42 → 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");
@@ -4,6 +4,7 @@ const SCORE_API_URL = "https://www.react.doctor/api/score";
4
4
  const FETCH_TIMEOUT_MS = 1e4;
5
5
  const ERROR_RULE_PENALTY = 1.5;
6
6
  const WARNING_RULE_PENALTY = .75;
7
+ const buildNoReactDependencyError = (directory) => `No React dependency found in ${directory}/package.json. Add "react" to dependencies (or peerDependencies) and re-run.`;
7
8
  //#endregion
8
9
  //#region src/core/calculate-score-locally.ts
9
10
  const getScoreLabel = (score) => {
@@ -49,19 +50,31 @@ const parseScoreResult = (value) => {
49
50
  label: labelValue
50
51
  };
51
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
+ };
52
60
  const tryScoreFromApi = async (diagnostics, fetchImplementation) => {
61
+ if (typeof fetchImplementation !== "function") return null;
53
62
  const controller = new AbortController();
54
63
  const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
55
64
  try {
56
65
  const response = await fetchImplementation(SCORE_API_URL, {
57
66
  method: "POST",
58
67
  headers: { "Content-Type": "application/json" },
59
- body: JSON.stringify({ diagnostics }),
68
+ body: JSON.stringify({ diagnostics: stripFilePaths(diagnostics) }),
60
69
  signal: controller.signal
61
70
  });
62
- 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
+ }
63
75
  return parseScoreResult(await response.json());
64
- } catch {
76
+ } catch (error) {
77
+ console.warn(`[react-doctor] Score API unreachable (${describeFailure(error)}) — using local scoring`);
65
78
  return null;
66
79
  } finally {
67
80
  clearTimeout(timeoutId);
@@ -69,7 +82,8 @@ const tryScoreFromApi = async (diagnostics, fetchImplementation) => {
69
82
  };
70
83
  //#endregion
71
84
  //#region src/utils/calculate-score-browser.ts
72
- const calculateScore = async (diagnostics) => await tryScoreFromApi(diagnostics, fetch) ?? calculateScoreLocally(diagnostics);
85
+ const getGlobalFetch = () => typeof fetch === "function" ? fetch : void 0;
86
+ const calculateScore = async (diagnostics, fetchImplementation = getGlobalFetch()) => await tryScoreFromApi(diagnostics, fetchImplementation) ?? calculateScoreLocally(diagnostics);
73
87
  //#endregion
74
88
  //#region src/utils/match-glob-pattern.ts
75
89
  const REGEX_SPECIAL_CHARACTERS = /[.+^${}()|[\]\\]/g;
@@ -105,7 +119,11 @@ const toRelativePath = (filePath, rootDirectory) => {
105
119
  if (normalizedFilePath.startsWith(normalizedRoot)) return normalizedFilePath.slice(normalizedRoot.length);
106
120
  return normalizedFilePath.replace(/^\.\//, "");
107
121
  };
108
- 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
+ };
109
127
  const isFileIgnoredByPatterns = (filePath, rootDirectory, patterns) => {
110
128
  if (patterns.length === 0) return false;
111
129
  const relativePath = toRelativePath(filePath, rootDirectory);
@@ -146,9 +164,9 @@ const isRuleSuppressed = (commentRules, ruleId) => {
146
164
  return commentRules.split(/[,\s]+/).some((rule) => rule.trim() === ruleId);
147
165
  };
148
166
  const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory, readFileLinesSync) => {
149
- 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") : []);
150
168
  const ignoredFilePatterns = compileIgnoredFilePatterns(config);
151
- 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") : []);
152
170
  const hasTextComponents = textComponentNames.size > 0;
153
171
  const getFileLines = createFileLinesCache(rootDirectory, readFileLinesSync);
154
172
  return diagnostics.filter((diagnostic) => {
@@ -218,7 +236,7 @@ const createBrowserReadFileLinesSync = (rootDirectory, projectFiles) => {
218
236
  //#endregion
219
237
  //#region src/adapters/browser/diagnose.ts
220
238
  const diagnose = async (input) => {
221
- 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));
222
240
  const readFileLinesSync = createBrowserReadFileLinesSync(input.rootDirectory, input.projectFiles);
223
241
  const userConfig = input.userConfig ?? null;
224
242
  const deadCodeDiagnostics = input.deadCodeDiagnostics ?? [];
@@ -241,14 +259,6 @@ const diagnose = async (input) => {
241
259
  };
242
260
  };
243
261
  //#endregion
244
- //#region src/core/build-diagnose-result.ts
245
- const buildDiagnoseResult = (params) => ({
246
- diagnostics: params.diagnostics,
247
- score: params.score,
248
- project: params.project,
249
- elapsedMilliseconds: params.elapsedMilliseconds
250
- });
251
- //#endregion
252
262
  //#region src/utils/jsx-include-paths.ts
253
263
  const computeJsxIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter((filePath) => JSX_FILE_PATTERN.test(filePath)) : void 0;
254
264
  //#endregion
@@ -262,7 +272,7 @@ const diagnoseCore = async (deps, options = {}) => {
262
272
  const userConfig = deps.loadUserConfig();
263
273
  const effectiveLint = options.lint ?? userConfig?.lint ?? true;
264
274
  const effectiveDeadCode = options.deadCode ?? userConfig?.deadCode ?? true;
265
- if (!projectInfo.reactVersion) throw new Error("No React dependency found in package.json");
275
+ if (!projectInfo.reactVersion) throw new Error(buildNoReactDependencyError(deps.rootDirectory));
266
276
  const lintIncludePaths = options.lintIncludePaths !== void 0 ? options.lintIncludePaths : computeJsxIncludePaths(includePaths);
267
277
  const { runLint, runDeadCode } = deps.createRunners({
268
278
  resolvedDirectory,
@@ -280,7 +290,11 @@ const diagnoseCore = async (deps, options = {}) => {
280
290
  console.error("Dead code analysis failed:", error);
281
291
  return emptyDiagnostics;
282
292
  }) : Promise.resolve(emptyDiagnostics);
283
- 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);
284
298
  const environmentDiagnostics = deps.getExtraDiagnostics?.() ?? [];
285
299
  const timed = await buildDiagnoseTimedResult({
286
300
  mergedDiagnostics: [
@@ -294,12 +308,12 @@ const diagnoseCore = async (deps, options = {}) => {
294
308
  startTime,
295
309
  calculateDiagnosticsScore: deps.calculateDiagnosticsScore
296
310
  });
297
- return buildDiagnoseResult({
311
+ return {
298
312
  diagnostics: timed.diagnostics,
299
313
  score: timed.score,
300
314
  project: projectInfo,
301
315
  elapsedMilliseconds: timed.elapsedMilliseconds
302
- });
316
+ };
303
317
  };
304
318
  //#endregion
305
319
  //#region src/adapters/browser/diagnose-browser.ts
@@ -342,4 +356,4 @@ const processBrowserDiagnostics = async (input) => {
342
356
  //#endregion
343
357
  export { calculateScore as a, diagnose as i, diagnoseBrowser as n, calculateScoreLocally as o, diagnoseCore as r, processBrowserDiagnostics as t };
344
358
 
345
- //# sourceMappingURL=process-browser-diagnostics-BHiLPUJT.js.map
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 {
@@ -128,4 +143,4 @@ interface ProcessBrowserDiagnosticsResult {
128
143
  declare const processBrowserDiagnostics: (input: ProcessBrowserDiagnosticsInput) => Promise<ProcessBrowserDiagnosticsResult>;
129
144
  //#endregion
130
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 };
131
- //# sourceMappingURL=browser-DFbjNpPb.d.ts.map
146
+ //# sourceMappingURL=browser-Dcq3yn-p.d.ts.map
package/dist/browser.d.ts CHANGED
@@ -1,2 +1,2 @@
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-DFbjNpPb.js";
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
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,2 +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-BHiLPUJT.js";
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
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 { };