react-doctor 0.2.14-dev.938376 → 0.2.14-dev.9777f1a
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 +27 -2
- package/dist/cli.js +786 -62
- package/dist/index.d.ts +24 -2
- package/dist/index.js +92 -55
- package/dist/skills/doctor-explain/SKILL.md +75 -0
- package/dist/skills/react-doctor/SKILL.md +4 -0
- package/package.json +7 -4
package/dist/index.d.ts
CHANGED
|
@@ -5,13 +5,35 @@ import * as Cause from "effect/Cause";
|
|
|
5
5
|
//#region src/types/config.d.ts
|
|
6
6
|
type FailOnLevel = "error" | "warning" | "none";
|
|
7
7
|
interface ReactDoctorIgnoreOverride {
|
|
8
|
+
/** Glob patterns the override applies to (e.g. `["src/legacy/**"]`). */
|
|
8
9
|
files: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Rule keys to suppress for the matched files. Omit (or leave empty) to
|
|
12
|
+
* suppress every rule for those files.
|
|
13
|
+
*/
|
|
9
14
|
rules?: string[];
|
|
10
15
|
}
|
|
11
16
|
interface ReactDoctorIgnoreConfig {
|
|
17
|
+
/**
|
|
18
|
+
* Fully-qualified rule keys (`"<plugin>/<rule>"`) whose diagnostics are
|
|
19
|
+
* dropped AFTER linting. The rule still runs; its findings are filtered
|
|
20
|
+
* out. To stop a rule from running at all, set it to `"off"` in the
|
|
21
|
+
* top-level `rules` map instead. Prefer `react-doctor rules disable
|
|
22
|
+
* <rule>` to edit this safely.
|
|
23
|
+
*/
|
|
12
24
|
rules?: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Glob patterns whose files are excluded from scanning entirely (matched
|
|
27
|
+
* against paths relative to the scanned directory).
|
|
28
|
+
*/
|
|
13
29
|
files?: string[];
|
|
30
|
+
/** Per-path rule suppressions — narrower than the top-level `rules`/`files`. */
|
|
14
31
|
overrides?: ReactDoctorIgnoreOverride[];
|
|
32
|
+
/**
|
|
33
|
+
* Behavioral tags whose rules are disabled BEFORE linting, skipping a
|
|
34
|
+
* whole family at once (e.g. `["design", "test-noise", "migration-hint"]`).
|
|
35
|
+
* Prefer `react-doctor rules ignore-tag <tag>` to edit this safely.
|
|
36
|
+
*/
|
|
15
37
|
tags?: string[];
|
|
16
38
|
}
|
|
17
39
|
/**
|
|
@@ -122,7 +144,7 @@ interface ReactDoctorConfig {
|
|
|
122
144
|
* the redirect is stable no matter where the CLI / `diagnose()` is
|
|
123
145
|
* run from. Absolute paths are used as-is.
|
|
124
146
|
*
|
|
125
|
-
* Typical use: a monorepo root holds the only `
|
|
147
|
+
* Typical use: a monorepo root holds the only `doctor.config.*`
|
|
126
148
|
* (so editor tooling and child commands all find it), but the React
|
|
127
149
|
* app lives in `apps/web`. Setting `"rootDir": "apps/web"` makes
|
|
128
150
|
* every invocation that loads this config scan that subproject
|
|
@@ -444,7 +466,7 @@ interface DiagnoseResult {
|
|
|
444
466
|
* Scan options (`deadCode`, `lint`, etc.) are flat on the entry and
|
|
445
467
|
* layer on top of the global defaults — omitted fields fall through.
|
|
446
468
|
* `config` is a full `ReactDoctorConfig` override that replaces the
|
|
447
|
-
* on-disk `
|
|
469
|
+
* on-disk `doctor.config.*` for this project's scan.
|
|
448
470
|
*/
|
|
449
471
|
//#endregion
|
|
450
472
|
//#region src/types/inspect.d.ts
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,8 @@ import * as Otlp from "effect/unstable/observability/Otlp";
|
|
|
16
16
|
import * as Context from "effect/Context";
|
|
17
17
|
import os from "node:os";
|
|
18
18
|
import * as Console from "effect/Console";
|
|
19
|
+
import { parseJSON5 } from "confbox";
|
|
20
|
+
import { createJiti } from "jiti";
|
|
19
21
|
import * as Fiber from "effect/Fiber";
|
|
20
22
|
import * as Filter from "effect/Filter";
|
|
21
23
|
import * as Option from "effect/Option";
|
|
@@ -3289,7 +3291,14 @@ const STAGED_FILES_PROJECT_CONFIG_FILENAMES = [
|
|
|
3289
3291
|
"tsconfig.json",
|
|
3290
3292
|
"tsconfig.base.json",
|
|
3291
3293
|
"package.json",
|
|
3292
|
-
"
|
|
3294
|
+
"doctor.config.ts",
|
|
3295
|
+
"doctor.config.mts",
|
|
3296
|
+
"doctor.config.cts",
|
|
3297
|
+
"doctor.config.js",
|
|
3298
|
+
"doctor.config.mjs",
|
|
3299
|
+
"doctor.config.cjs",
|
|
3300
|
+
"doctor.config.json",
|
|
3301
|
+
"doctor.config.jsonc",
|
|
3293
3302
|
"oxlint.json",
|
|
3294
3303
|
".oxlintrc.json"
|
|
3295
3304
|
];
|
|
@@ -4481,43 +4490,80 @@ const validateConfigTypes = (config) => {
|
|
|
4481
4490
|
const warn = (message) => {
|
|
4482
4491
|
Effect.runSync(Console.warn(message));
|
|
4483
4492
|
};
|
|
4484
|
-
const
|
|
4493
|
+
const CONFIG_BASENAME = "doctor.config";
|
|
4494
|
+
const CONFIG_EXTENSIONS = [
|
|
4495
|
+
"ts",
|
|
4496
|
+
"mts",
|
|
4497
|
+
"cts",
|
|
4498
|
+
"js",
|
|
4499
|
+
"mjs",
|
|
4500
|
+
"cjs",
|
|
4501
|
+
"json",
|
|
4502
|
+
"jsonc"
|
|
4503
|
+
];
|
|
4504
|
+
const DATA_CONFIG_EXTENSIONS = new Set(["json", "jsonc"]);
|
|
4505
|
+
const PACKAGE_JSON_FILENAME = "package.json";
|
|
4485
4506
|
const PACKAGE_JSON_CONFIG_KEY = "reactDoctor";
|
|
4486
|
-
const
|
|
4487
|
-
|
|
4507
|
+
const LEGACY_CONFIG_FILENAME = "react-doctor.config.json";
|
|
4508
|
+
const jiti = createJiti(import.meta.url);
|
|
4509
|
+
const formatError = (error) => error instanceof Error ? error.message : String(error);
|
|
4510
|
+
const loadModuleConfig = async (filePath) => {
|
|
4511
|
+
const imported = await jiti.import(filePath);
|
|
4512
|
+
return imported?.default ?? imported;
|
|
4513
|
+
};
|
|
4514
|
+
const readDataConfig = (filePath) => parseJSON5(fs.readFileSync(filePath, "utf-8"));
|
|
4515
|
+
const readEmbeddedPackageJsonConfig = (directory) => {
|
|
4516
|
+
const packageJsonPath = path.join(directory, PACKAGE_JSON_FILENAME);
|
|
4517
|
+
if (!isFile(packageJsonPath)) return null;
|
|
4518
|
+
try {
|
|
4519
|
+
const packageJson = parseJSON5(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
4520
|
+
if (isPlainObject(packageJson)) {
|
|
4521
|
+
const embeddedConfig = packageJson[PACKAGE_JSON_CONFIG_KEY];
|
|
4522
|
+
if (isPlainObject(embeddedConfig)) return embeddedConfig;
|
|
4523
|
+
}
|
|
4524
|
+
} catch {}
|
|
4525
|
+
return null;
|
|
4526
|
+
};
|
|
4527
|
+
const loadPackageJsonConfig = (directory) => {
|
|
4528
|
+
const embeddedConfig = readEmbeddedPackageJsonConfig(directory);
|
|
4529
|
+
if (!embeddedConfig) return null;
|
|
4530
|
+
return {
|
|
4531
|
+
config: validateConfigTypes(embeddedConfig),
|
|
4532
|
+
sourceDirectory: directory,
|
|
4533
|
+
configFilePath: path.join(directory, PACKAGE_JSON_FILENAME),
|
|
4534
|
+
format: "package-json"
|
|
4535
|
+
};
|
|
4536
|
+
};
|
|
4537
|
+
const loadConfigFromDirectory = async (directory) => {
|
|
4488
4538
|
let sawBrokenConfigFile = false;
|
|
4489
|
-
|
|
4539
|
+
for (const extension of CONFIG_EXTENSIONS) {
|
|
4540
|
+
const filePath = path.join(directory, `${CONFIG_BASENAME}.${extension}`);
|
|
4541
|
+
if (!isFile(filePath)) continue;
|
|
4542
|
+
const isDataFile = DATA_CONFIG_EXTENSIONS.has(extension);
|
|
4490
4543
|
try {
|
|
4491
|
-
const
|
|
4492
|
-
const parsed = JSON.parse(fileContent);
|
|
4544
|
+
const parsed = isDataFile ? readDataConfig(filePath) : await loadModuleConfig(filePath);
|
|
4493
4545
|
if (isPlainObject(parsed)) return {
|
|
4494
4546
|
status: "found",
|
|
4495
4547
|
loaded: {
|
|
4496
4548
|
config: validateConfigTypes(parsed),
|
|
4497
|
-
sourceDirectory: directory
|
|
4549
|
+
sourceDirectory: directory,
|
|
4550
|
+
configFilePath: filePath,
|
|
4551
|
+
format: isDataFile ? "json" : "module"
|
|
4498
4552
|
}
|
|
4499
4553
|
};
|
|
4500
|
-
warn(`${
|
|
4554
|
+
warn(`${CONFIG_BASENAME}.${extension} must export an object, ignoring.`);
|
|
4555
|
+
sawBrokenConfigFile = true;
|
|
4501
4556
|
} catch (error) {
|
|
4502
|
-
warn(`Failed to
|
|
4557
|
+
warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${formatError(error)}`);
|
|
4558
|
+
sawBrokenConfigFile = true;
|
|
4503
4559
|
}
|
|
4504
|
-
sawBrokenConfigFile = true;
|
|
4505
4560
|
}
|
|
4506
|
-
const
|
|
4507
|
-
if (
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
if (isPlainObject(embeddedConfig)) return {
|
|
4513
|
-
status: "found",
|
|
4514
|
-
loaded: {
|
|
4515
|
-
config: validateConfigTypes(embeddedConfig),
|
|
4516
|
-
sourceDirectory: directory
|
|
4517
|
-
}
|
|
4518
|
-
};
|
|
4519
|
-
}
|
|
4520
|
-
} catch {}
|
|
4561
|
+
const packageJsonConfig = loadPackageJsonConfig(directory);
|
|
4562
|
+
if (packageJsonConfig) return {
|
|
4563
|
+
status: "found",
|
|
4564
|
+
loaded: packageJsonConfig
|
|
4565
|
+
};
|
|
4566
|
+
if (isFile(path.join(directory, LEGACY_CONFIG_FILENAME))) warn(`${LEGACY_CONFIG_FILENAME} is no longer read — rename it to ${CONFIG_BASENAME}.json (or author a ${CONFIG_BASENAME}.ts).`);
|
|
4521
4567
|
return {
|
|
4522
4568
|
status: sawBrokenConfigFile ? "invalid" : "absent",
|
|
4523
4569
|
loaded: null
|
|
@@ -4527,34 +4573,26 @@ const cachedConfigs = /* @__PURE__ */ new Map();
|
|
|
4527
4573
|
const clearConfigCache = () => {
|
|
4528
4574
|
cachedConfigs.clear();
|
|
4529
4575
|
};
|
|
4530
|
-
const
|
|
4531
|
-
const
|
|
4532
|
-
if (
|
|
4533
|
-
|
|
4534
|
-
if (localResult.status === "found") {
|
|
4535
|
-
cachedConfigs.set(rootDirectory, localResult.loaded);
|
|
4536
|
-
return localResult.loaded;
|
|
4537
|
-
}
|
|
4538
|
-
if (localResult.status === "invalid" || isProjectBoundary(rootDirectory)) {
|
|
4539
|
-
cachedConfigs.set(rootDirectory, null);
|
|
4540
|
-
return null;
|
|
4541
|
-
}
|
|
4576
|
+
const loadConfigWalkingUp = async (rootDirectory) => {
|
|
4577
|
+
const localResult = await loadConfigFromDirectory(rootDirectory);
|
|
4578
|
+
if (localResult.status === "found") return localResult.loaded;
|
|
4579
|
+
if (localResult.status === "invalid" || isProjectBoundary(rootDirectory)) return null;
|
|
4542
4580
|
let ancestorDirectory = path.dirname(rootDirectory);
|
|
4543
4581
|
while (ancestorDirectory !== path.dirname(ancestorDirectory)) {
|
|
4544
|
-
const ancestorResult = loadConfigFromDirectory(ancestorDirectory);
|
|
4545
|
-
if (ancestorResult.status === "found")
|
|
4546
|
-
|
|
4547
|
-
return ancestorResult.loaded;
|
|
4548
|
-
}
|
|
4549
|
-
if (isProjectBoundary(ancestorDirectory)) {
|
|
4550
|
-
cachedConfigs.set(rootDirectory, null);
|
|
4551
|
-
return null;
|
|
4552
|
-
}
|
|
4582
|
+
const ancestorResult = await loadConfigFromDirectory(ancestorDirectory);
|
|
4583
|
+
if (ancestorResult.status === "found") return ancestorResult.loaded;
|
|
4584
|
+
if (isProjectBoundary(ancestorDirectory)) return null;
|
|
4553
4585
|
ancestorDirectory = path.dirname(ancestorDirectory);
|
|
4554
4586
|
}
|
|
4555
|
-
cachedConfigs.set(rootDirectory, null);
|
|
4556
4587
|
return null;
|
|
4557
4588
|
};
|
|
4589
|
+
const loadConfigWithSource = (rootDirectory) => {
|
|
4590
|
+
const cached = cachedConfigs.get(rootDirectory);
|
|
4591
|
+
if (cached !== void 0) return cached;
|
|
4592
|
+
const loadPromise = loadConfigWalkingUp(rootDirectory);
|
|
4593
|
+
cachedConfigs.set(rootDirectory, loadPromise);
|
|
4594
|
+
return loadPromise;
|
|
4595
|
+
};
|
|
4558
4596
|
const resolveConfigRootDir = (config, configSourceDirectory) => {
|
|
4559
4597
|
if (!config || !configSourceDirectory) return null;
|
|
4560
4598
|
const rawRootDir = config.rootDir;
|
|
@@ -4582,8 +4620,7 @@ const resolveDiagnoseTarget = (directory, options = {}) => {
|
|
|
4582
4620
|
* (`inspect()`, `diagnose()`, and the CLI's `inspectAction`):
|
|
4583
4621
|
*
|
|
4584
4622
|
* 1. Resolve the requested directory to absolute.
|
|
4585
|
-
* 2. Load `
|
|
4586
|
-
* if present.
|
|
4623
|
+
* 2. Load `doctor.config.*` / `package.json#reactDoctor` if present.
|
|
4587
4624
|
* 3. Honor `config.rootDir` to redirect the scan to a nested
|
|
4588
4625
|
* project root, if configured.
|
|
4589
4626
|
* 4. Walk into a nested React subproject when the requested
|
|
@@ -4601,9 +4638,9 @@ const resolveDiagnoseTarget = (directory, options = {}) => {
|
|
|
4601
4638
|
* via its own cache). Routing through `resolveScanTarget` keeps every
|
|
4602
4639
|
* shell in agreement on what "the scan directory" means.
|
|
4603
4640
|
*/
|
|
4604
|
-
const resolveScanTarget = (requestedDirectory, options = {}) => {
|
|
4641
|
+
const resolveScanTarget = async (requestedDirectory, options = {}) => {
|
|
4605
4642
|
const absoluteRequested = path.resolve(requestedDirectory);
|
|
4606
|
-
const loadedConfig = loadConfigWithSource(absoluteRequested);
|
|
4643
|
+
const loadedConfig = await loadConfigWithSource(absoluteRequested);
|
|
4607
4644
|
const userConfig = loadedConfig?.config ?? null;
|
|
4608
4645
|
const configSourceDirectory = loadedConfig?.sourceDirectory ?? null;
|
|
4609
4646
|
const redirectedDirectory = resolveConfigRootDir(userConfig, configSourceDirectory);
|
|
@@ -5653,8 +5690,8 @@ var Config = class Config extends Context.Service()("react-doctor/Config") {
|
|
|
5653
5690
|
const cache = yield* Cache.make({
|
|
5654
5691
|
capacity: 16,
|
|
5655
5692
|
timeToLive: CONFIG_CACHE_TTL_MS,
|
|
5656
|
-
lookup: (directory) => Effect.
|
|
5657
|
-
const loaded = loadConfigWithSource(directory);
|
|
5693
|
+
lookup: (directory) => Effect.promise(async () => {
|
|
5694
|
+
const loaded = await loadConfigWithSource(directory);
|
|
5658
5695
|
const redirected = resolveConfigRootDir(loaded?.config ?? null, loaded?.sourceDirectory ?? null);
|
|
5659
5696
|
return {
|
|
5660
5697
|
config: loaded?.config ?? null,
|
|
@@ -8414,7 +8451,7 @@ const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
|
|
|
8414
8451
|
};
|
|
8415
8452
|
const diagnose = async (directory, options = {}) => {
|
|
8416
8453
|
const startTime = globalThis.performance.now();
|
|
8417
|
-
const program = buildInspectProgram(resolveScanTarget(directory), options);
|
|
8454
|
+
const program = buildInspectProgram(await resolveScanTarget(directory), options);
|
|
8418
8455
|
return outputToDiagnoseResult(await Effect.runPromise(restoreLegacyThrow(program.pipe(Effect.provide(buildDiagnoseLayer()), Effect.provide(layerOtlp)))), globalThis.performance.now() - startTime);
|
|
8419
8456
|
};
|
|
8420
8457
|
//#endregion
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: doctor-explain
|
|
3
|
+
description: Explain React Doctor rules and configure which ones run via doctor.config.* (or package.json#reactDoctor). Use when the user types `/doctor-explain` or `/doctor-config`, asks why a rule fired, disagrees with a rule, wants to disable/enable a rule, silence a category or tag, tune CI/PR noise, or asks "what does this rule mean". Covers the `react-doctor rules` CLI (list, explain, set, enable, disable, category, ignore-tag) and how config layers combine: ignore.tags disables matching rules before linting, rules over categories sets severity, surfaces controls visibility only.
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Doctor Explain
|
|
8
|
+
|
|
9
|
+
Explains React Doctor rules and edits `doctor.config.*` safely. Use this when a user wants to understand a rule or change which rules run — not for fixing diagnostics (that is the `react-doctor` skill / `/doctor`).
|
|
10
|
+
|
|
11
|
+
Triggers: `/doctor-explain`, `/doctor-config`, "why did this rule fire", "I disagree with this rule", "turn this rule off", "stop flagging X", "too noisy", "disable design rules".
|
|
12
|
+
|
|
13
|
+
## Workflow
|
|
14
|
+
|
|
15
|
+
1. Identify the rule key from the diagnostic (e.g. `react-doctor/no-array-index-as-key`).
|
|
16
|
+
2. Explain it before changing anything:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx react-doctor@latest rules explain react-doctor/no-array-index-as-key
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
3. Pick the narrowest control that matches the user's intent (see decision guide).
|
|
23
|
+
4. Apply it with a `rules` subcommand (edits your `doctor.config.*` or `package.json#reactDoctor` in place, preserving other fields and formatting).
|
|
24
|
+
5. Validate the change did what they wanted:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx react-doctor@latest --verbose --diff
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx react-doctor@latest rules list # every rule + its effective severity
|
|
34
|
+
npx react-doctor@latest rules list --configured # only what your config changed
|
|
35
|
+
npx react-doctor@latest rules list --category Performance # filter by category
|
|
36
|
+
npx react-doctor@latest rules explain <rule> # why it matters + how to configure
|
|
37
|
+
npx react-doctor@latest rules disable <rule> # rule never runs
|
|
38
|
+
npx react-doctor@latest rules enable <rule> # turn back on at its recommended severity
|
|
39
|
+
npx react-doctor@latest rules set <rule> warn # off | warn | error
|
|
40
|
+
npx react-doctor@latest rules category "React Native" off # whole category
|
|
41
|
+
npx react-doctor@latest rules ignore-tag design # skip a rule family (design, test-noise, …)
|
|
42
|
+
npx react-doctor@latest rules unignore-tag design
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Rule references accept the full key (`react-doctor/no-danger`), the bare id (`no-danger`), or a legacy key (`react/no-danger`).
|
|
46
|
+
|
|
47
|
+
## Decision guide
|
|
48
|
+
|
|
49
|
+
Match the control to the intent — prefer the narrowest one:
|
|
50
|
+
|
|
51
|
+
- **User disagrees with one rule / it's a false positive for them** → `rules disable <rule>` (sets `rules.<key> = "off"`; the rule stops running everywhere). This is the default for "I don't want this rule".
|
|
52
|
+
- **Rule is fine but wrong severity** → `rules set <rule> warn` or `rules set <rule> error`.
|
|
53
|
+
- **A disabled-by-default rule they want on** → `rules enable <rule>`.
|
|
54
|
+
- **A whole area is unwanted** (e.g. all React Native rules) → `rules category "<Category>" off`.
|
|
55
|
+
- **A behavioral family is noisy** (`design`, `test-noise`, `migration-hint`) → `rules ignore-tag <tag>`.
|
|
56
|
+
- **Keep it locally but hide from PR comment / score / CI gate only** → do NOT disable. Edit `surfaces` in your config (`surfaces.prComment.excludeRules`, `surfaces.score.excludeTags`, `surfaces.ciFailure.excludeCategories`). The rule still shows in local `cli` output.
|
|
57
|
+
|
|
58
|
+
How the layers combine: `ignore.tags` disables every rule carrying that tag **before** linting, so a tagged rule stays off even if `rules`/`categories` set it to `warn`/`error` (a rule-level override cannot re-enable a tag-ignored rule). For rules that aren't tag-disabled, `rules` overrides `categories` overrides the rule's default. `surfaces` is visibility-only and never changes whether a rule runs.
|
|
59
|
+
|
|
60
|
+
## Config shape
|
|
61
|
+
|
|
62
|
+
Config lives in `doctor.config.ts` (or `.js`/`.mjs`/`.cjs`/`.json`/`.jsonc`), or the `reactDoctor` key in `package.json`. The `rules` commands edit whichever exists — TS/JS edits preserve formatting (via magicast) — and create `doctor.config.json` when none does, stamping `$schema`:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// doctor.config.ts
|
|
66
|
+
export default {
|
|
67
|
+
rules: { "react-doctor/no-array-index-as-key": "off" },
|
|
68
|
+
categories: { "React Native": "warn" },
|
|
69
|
+
ignore: { tags: ["design"] },
|
|
70
|
+
};
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Educating the user
|
|
74
|
+
|
|
75
|
+
When explaining a rule, lead with the "Why it matters" guidance from `rules explain` and, when they want depth, the per-rule recipe at `https://www.react.doctor/prompts/rules/<plugin>/<rule>.md`. Only after they understand it should you offer to disable it — many "bad" rules are catching real issues.
|
|
@@ -32,6 +32,10 @@ The playbook is the single source of truth — a scan → filter → triage →
|
|
|
32
32
|
|
|
33
33
|
Pair it with the matching per-rule prompts at `https://www.react.doctor/prompts/rules/<plugin>/<rule>.md` (fetched on demand inside the playbook) so each fix uses the canonical, reviewer-tested recipe.
|
|
34
34
|
|
|
35
|
+
## Configuring or explaining rules
|
|
36
|
+
|
|
37
|
+
When the user wants to understand a rule, disagrees with one, or wants to disable / tune which rules run (not fix code), use the `doctor-explain` skill (alias `/doctor-config`). Start with `npx react-doctor@latest rules explain <rule>`, then apply the narrowest control via `npx react-doctor@latest rules disable|set|category|ignore-tag …`, which edits your `doctor.config.*` (or `package.json#reactDoctor`).
|
|
38
|
+
|
|
35
39
|
## Command
|
|
36
40
|
|
|
37
41
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-doctor",
|
|
3
|
-
"version": "0.2.14-dev.
|
|
3
|
+
"version": "0.2.14-dev.9777f1a",
|
|
4
4
|
"description": "Diagnose and fix React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
@@ -54,13 +54,16 @@
|
|
|
54
54
|
"@sentry/node": "^10.54.0",
|
|
55
55
|
"agent-install": "0.0.5",
|
|
56
56
|
"conf": "^15.1.0",
|
|
57
|
+
"confbox": "^0.2.4",
|
|
57
58
|
"deslop-js": "^0.0.14",
|
|
58
59
|
"effect": "4.0.0-beta.70",
|
|
59
60
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
61
|
+
"jiti": "^2.7.0",
|
|
62
|
+
"magicast": "^0.5.3",
|
|
60
63
|
"oxlint": "^1.66.0",
|
|
61
64
|
"prompts": "^2.4.2",
|
|
62
65
|
"typescript": ">=5.0.4 <7",
|
|
63
|
-
"oxlint-plugin-react-doctor": "0.2.14-dev.
|
|
66
|
+
"oxlint-plugin-react-doctor": "0.2.14-dev.9777f1a"
|
|
64
67
|
},
|
|
65
68
|
"devDependencies": {
|
|
66
69
|
"@types/babel__code-frame": "^7.27.0",
|
|
@@ -68,8 +71,8 @@
|
|
|
68
71
|
"@xterm/headless": "^6.0.0",
|
|
69
72
|
"commander": "^14.0.3",
|
|
70
73
|
"ora": "^9.4.0",
|
|
71
|
-
"@react-doctor/
|
|
72
|
-
"@react-doctor/
|
|
74
|
+
"@react-doctor/api": "0.2.14",
|
|
75
|
+
"@react-doctor/core": "0.2.14"
|
|
73
76
|
},
|
|
74
77
|
"engines": {
|
|
75
78
|
"node": "^20.19.0 || >=22.12.0"
|