react-doctor 0.2.1 → 0.2.2

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
@@ -70,7 +70,7 @@ jobs:
70
70
  github-token: ${{ secrets.GITHUB_TOKEN }}
71
71
  ```
72
72
 
73
- When `github-token` is set on `pull_request` events, findings are posted (and updated) as a sticky PR comment. The action also exposes a `score` output (0–100) you can use in subsequent steps.
73
+ When `github-token` is set on `pull_request` events, findings are posted (and updated) as a sticky PR comment. The action also exposes a `score` output (0–100) you can read in subsequent steps — see [PR blocking and exit codes](#pr-blocking-and-exit-codes) for a score-floor recipe.
74
74
 
75
75
  **Inputs:** `directory`, `verbose`, `project`, `diff`, `github-token`, `fail-on` (`error` / `warning` / `none`), `offline`, `annotations`, `node-version`. See [`action.yml`](https://github.com/millionco/react-doctor/blob/main/action.yml) for full descriptions.
76
76
 
@@ -143,7 +143,13 @@ Combine `--fail-on` with `--diff <base>` to scope the gate to the PR's changed f
143
143
  SCORE: ${{ steps.doctor.outputs.score }}
144
144
  FLOOR: "80"
145
145
  run: |
146
- if [ -n "$SCORE" ] && [ "$SCORE" -lt "$FLOOR" ]; then
146
+ # `score` is best-effort and may be empty (e.g. when offline is on).
147
+ # Skip the floor when it's empty so unrelated PRs aren't blocked.
148
+ if [ -z "$SCORE" ]; then
149
+ echo "::notice::React Doctor score unavailable — skipping floor check"
150
+ exit 0
151
+ fi
152
+ if [ "$SCORE" -lt "$FLOOR" ]; then
147
153
  echo "::error::React Doctor score $SCORE is below floor $FLOOR"
148
154
  exit 1
149
155
  fi
@@ -158,7 +164,7 @@ Create a `react-doctor.config.json` in your project root:
158
164
  ```json
159
165
  {
160
166
  "ignore": {
161
- "rules": ["react/no-danger", "jsx-a11y/no-autofocus"],
167
+ "rules": ["react-doctor/no-danger", "react-doctor/no-autofocus"],
162
168
  "files": ["src/generated/**"],
163
169
  "overrides": [
164
170
  {
@@ -167,7 +173,7 @@ Create a `react-doctor.config.json` in your project root:
167
173
  },
168
174
  {
169
175
  "files": ["components/search/HighlightedSnippet.tsx"],
170
- "rules": ["react/no-danger"]
176
+ "rules": ["react-doctor/no-danger"]
171
177
  }
172
178
  ]
173
179
  }
@@ -220,12 +226,13 @@ Per-rule wins over per-category. `"off"` short-circuits before the rule runs; `"
220
226
 
221
227
  #### Optional companion plugins
222
228
 
223
- When the following ESLint plugins are installed in the scanned project (or hoisted in your monorepo), React Doctor folds their rules into the same scan. Both are listed as **optional peer dependencies** — install only what you want.
229
+ When the following ESLint plugins are installed in the scanned project (or hoisted in your monorepo), React Doctor folds their rules into the same scan. Listed as **optional peer dependencies** — install only what you want.
230
+
231
+ | Plugin | Adds | Namespace |
232
+ | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ------------------ |
233
+ | [`eslint-plugin-react-hooks`](https://www.npmjs.com/package/eslint-plugin-react-hooks) (v6 or v7) | The React Compiler frontend's correctness rules — fired when a React Compiler is detected in the project. | `react-hooks-js/*` |
224
234
 
225
- | Plugin | Adds | Namespace |
226
- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
227
- | [`eslint-plugin-react-hooks`](https://www.npmjs.com/package/eslint-plugin-react-hooks) (v6 or v7) | The React Compiler frontend's correctness rules — fired when a React Compiler is detected in the project. | `react-hooks-js/*` |
228
- | [`eslint-plugin-react-you-might-not-need-an-effect`](https://github.com/nickjvandyke/eslint-plugin-react-you-might-not-need-an-effect) (v0.10+) | Complementary effects-as-anti-pattern rules (`no-derived-state`, `no-chain-state-updates`, `no-event-handler`, `no-pass-data-to-parent`, …) that run alongside React Doctor's native State & Effects rules. | `effect/*` |
235
+ The 8 rules from [`eslint-plugin-react-you-might-not-need-an-effect`](https://github.com/nickjvandyke/eslint-plugin-react-you-might-not-need-an-effect) (NickvanDyke, MIT) are now ported natively into React Doctor — they fire as `react-doctor/no-derived-state`, `react-doctor/no-chain-state-updates`, `react-doctor/no-event-handler`, `react-doctor/no-adjust-state-on-prop-change`, `react-doctor/no-reset-all-state-on-prop-change`, `react-doctor/no-pass-live-state-to-parent`, `react-doctor/no-pass-data-to-parent`, and `react-doctor/no-initialize-state`. No peer dependency required.
229
236
 
230
237
  ### Inline suppressions
231
238
 
@@ -258,7 +265,7 @@ Block comments work inside JSX:
258
265
 
259
266
  <!-- prettier-ignore -->
260
267
  ```tsx
261
- {/* react-doctor-disable-next-line react/no-danger */}
268
+ {/* react-doctor-disable-next-line react-doctor/no-danger */}
262
269
  <div dangerouslySetInnerHTML={{ __html }} />
263
270
  ```
264
271
 
@@ -355,7 +362,7 @@ When a suppression isn't working, `--explain <file:line>` (or its alias `--why <
355
362
 
356
363
  `ignore.tags` suppresses entire categories of rules by tag. For example, `"tags": ["design"]` disables all opinionated design rules (gradient text, pure black backgrounds, side tab borders, default Tailwind palettes). Available tags: `"design"`.
357
364
 
358
- `offline` skips the score API entirely — no score is shown and no share URL is generated. Automatically enabled in CI environments (GitHub Actions, GitLab CI, CircleCI) so CI runs don't depend on the network.
365
+ `offline` skips the score API entirely — no score is shown and no share URL is generated. CI runs (GitHub Actions, GitLab CI, CircleCI) are not offline by default; only the share URL is suppressed. Set `offline: true` (or `--offline`) explicitly when you want zero network.
359
366
 
360
367
  ### React Native rules in mixed monorepos
361
368
 
@@ -383,7 +390,7 @@ The walker stops at function and `Program` boundaries — JSX defined inside a c
383
390
 
384
391
  The health score formula: `100 - (unique_error_rules x 1.5) - (unique_warning_rules x 0.75)`.
385
392
 
386
- Scoring runs on react.doctor's API and is **network-dependent**: without a successful API round-trip (or under `--offline`) the score is omitted and the rest of the report still renders normally. Key details:
393
+ Scoring runs on react.doctor's API and is **network-dependent**: without a successful API round-trip (or under `--offline`) the score is omitted and the rest of the report still renders normally. Score-based automation must treat an empty value as a no-op (see the strict-threshold example above). Key details:
387
394
 
388
395
  - The score counts **unique rules triggered**, not total instances. Fixing 49 of 50 `no-barrel-import` violations does not change the score; fixing all 50 removes the 0.75 penalty for that rule.
389
396
  - Error-severity rules cost 1.5 points each. Warning-severity rules cost 0.75 points each.
@@ -405,6 +412,52 @@ When on a feature branch without explicit flags, you'll be prompted: "Only scan
405
412
 
406
413
  `--staged` and `--diff` cannot be combined.
407
414
 
415
+ ### Pre-commit hooks with Husky + lint-staged
416
+
417
+ The most common setup is [Husky](https://typicode.github.io/husky/) for the git hook and [lint-staged](https://github.com/lint-staged/lint-staged) to filter which files run through each tool. React Doctor's `--staged` mode is built for this: it reads file contents from the git **index** (not the working tree) and materializes them into a temp directory, so partially-staged files are scanned exactly as they will be committed.
418
+
419
+ Install both, then wire them up:
420
+
421
+ ```bash
422
+ npx ni -D husky lint-staged
423
+ npx husky init
424
+ ```
425
+
426
+ `husky init` creates `.husky/pre-commit`. Replace its contents with:
427
+
428
+ ```bash
429
+ npx lint-staged
430
+ ```
431
+
432
+ Then add a `lint-staged` block to your `package.json`. Because React Doctor already filters to the staged set via `--staged`, **do not pass the lint-staged-injected file list** — invoke it with a single command and let it discover the index itself:
433
+
434
+ ```json
435
+ {
436
+ "lint-staged": {
437
+ "*.{ts,tsx,js,jsx}": "react-doctor --staged --fail-on warning"
438
+ }
439
+ }
440
+ ```
441
+
442
+ A few notes that bite people:
443
+
444
+ - **Don't append `{staged-files}`** — lint-staged would otherwise pass the matched paths as positional arguments and you'd get the union (path filter + index scan) instead of the intent.
445
+ - **Use the function form when you only want the hook to run if any matching file is staged** but still want a single project-wide scan:
446
+
447
+ ```js
448
+ // lint-staged.config.js
449
+ export default {
450
+ "*.{ts,tsx,js,jsx}": () => "react-doctor --staged --fail-on warning",
451
+ };
452
+ ```
453
+
454
+ - **`--fail-on warning`** blocks the commit on any diagnostic. Use `--fail-on error` for a softer gate, or `--fail-on none` to lint advisory-only.
455
+ - **Index vs. working tree:** `--staged` reflects `git diff --cached`, not your editor buffer. If you `git add` half a file and keep typing, only the added half is scanned — the unstaged tail is ignored.
456
+ - **Skip in CI:** lint-staged is a pre-commit concern. In CI, use the GitHub Action (above) or `react-doctor --diff <base>` directly; running both does duplicate work.
457
+ - **Other hook managers:** the same `react-doctor --staged --fail-on warning` command works under [Lefthook](https://lefthook.dev/), [pre-commit](https://pre-commit.com/), or a hand-written `.git/hooks/pre-commit` — `--staged` is hook-manager-agnostic.
458
+
459
+ To bypass the hook for a one-off commit, use `git commit --no-verify`.
460
+
408
461
  ## Agent and CI integration
409
462
 
410
463
  React Doctor detects 50+ coding agents (Claude Code, Cursor, Codex, OpenCode, Windsurf, and more) and adapts its behavior automatically:
@@ -416,7 +469,7 @@ React Doctor detects 50+ coding agents (Claude Code, Cursor, Codex, OpenCode, Wi
416
469
  - **Exit codes**: `--fail-on error` (default) exits non-zero when error-severity diagnostics are found. Use `--fail-on warning` or `--fail-on none` to tune CI gating. See [PR blocking and exit codes](#pr-blocking-and-exit-codes) for the full model — including how to fail only on new regressions vs. fail on the baseline score.
417
470
  - **Programmatic API**: `import { diagnose } from "react-doctor/api"` for direct integration in scripts and automation.
418
471
 
419
- In CI environments, prompts are automatically skipped and `--offline` is implied (no network round-trip; score is omitted from the output).
472
+ In CI environments, prompts are automatically skipped. Pass `--offline` explicitly when you need zero network.
420
473
 
421
474
  ## Node.js API
422
475
 
@@ -0,0 +1,26 @@
1
+ import { createRequire } from "node:module";
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
+ key = keys[i];
13
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
21
+ value: mod,
22
+ enumerable: true
23
+ }) : target, mod));
24
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
25
+ //#endregion
26
+ export { __require as n, __toESM as r, __commonJSMin as t };