react-doctor 0.2.11-dev.402c7ea → 0.2.11-dev.b5cf767
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 +26 -0
- package/dist/{cli-logger-Df45H6Lw.js → cli-logger-CmMJBgYF.js} +10 -2
- package/dist/cli.js +73 -24
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -43,6 +43,32 @@ Works with Claude Code, Cursor, Codex, OpenCode, and many more.
|
|
|
43
43
|
|
|
44
44
|
Add the reusable GitHub Action from Marketplace to scan every pull request, show inline annotations, and leave findings where reviewers already look.
|
|
45
45
|
|
|
46
|
+
```yaml
|
|
47
|
+
name: React Doctor
|
|
48
|
+
|
|
49
|
+
on:
|
|
50
|
+
pull_request:
|
|
51
|
+
types: [opened, synchronize, reopened, ready_for_review]
|
|
52
|
+
|
|
53
|
+
permissions:
|
|
54
|
+
contents: read
|
|
55
|
+
pull-requests: write
|
|
56
|
+
issues: write
|
|
57
|
+
|
|
58
|
+
concurrency:
|
|
59
|
+
group: react-doctor-${{ github.event.pull_request.number || github.ref }}
|
|
60
|
+
cancel-in-progress: true
|
|
61
|
+
|
|
62
|
+
jobs:
|
|
63
|
+
react-doctor:
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/checkout@v5
|
|
67
|
+
- uses: millionco/react-doctor@v1
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
React Doctor scans the files changed in the pull request, emits inline annotations, blocks on error-level findings, and updates one sticky PR comment with the score and issue summary. The built-in GitHub token is used automatically; no secret or PAT is required. On forked PRs where GitHub withholds write permissions, the scan and annotations still run, but the sticky comment may be skipped.
|
|
71
|
+
|
|
46
72
|
[Add GitHub Action →](https://github.com/marketplace/actions/react-doctor)
|
|
47
73
|
|
|
48
74
|
## Contributing
|
|
@@ -3050,6 +3050,7 @@ const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
|
3050
3050
|
const MILLISECONDS_PER_SECOND = 1e3;
|
|
3051
3051
|
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
3052
3052
|
const SHARE_BASE_URL = "https://www.react.doctor/share";
|
|
3053
|
+
const PROMPTS_RULES_BASE_URL = "https://www.react.doctor/prompts/rules";
|
|
3053
3054
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
3054
3055
|
const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
|
|
3055
3056
|
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
@@ -7360,6 +7361,13 @@ const highlighter = {
|
|
|
7360
7361
|
gray: import_picocolors.default.gray,
|
|
7361
7362
|
bold: import_picocolors.default.bold
|
|
7362
7363
|
};
|
|
7364
|
+
/**
|
|
7365
|
+
* Canonical URL for a rule's reviewer-tested fix recipe, served at
|
|
7366
|
+
* `https://www.react.doctor/prompts/rules/<plugin>/<rule>.md`. The
|
|
7367
|
+
* `/doctor` playbook fetches it on demand so each fix follows the
|
|
7368
|
+
* canonical recipe instead of being improvised per diagnostic.
|
|
7369
|
+
*/
|
|
7370
|
+
const buildRulePromptUrl = (plugin, rule) => `${PROMPTS_RULES_BASE_URL}/${plugin}/${rule}.md`;
|
|
7363
7371
|
const groupBy = (items, keyFn) => {
|
|
7364
7372
|
const groups = /* @__PURE__ */ new Map();
|
|
7365
7373
|
for (const item of items) {
|
|
@@ -7407,6 +7415,6 @@ const cliLogger = {
|
|
|
7407
7415
|
}
|
|
7408
7416
|
};
|
|
7409
7417
|
//#endregion
|
|
7410
|
-
export {
|
|
7418
|
+
export { isMonorepoRoot as A, filterDiagnosticsForSurface as C, getDiffInfo as D, formatReactDoctorError as E, restoreLegacyThrow as F, runInspect as I, toRelativePath as L, layerOtlp as M, listWorkspacePackages as N, groupBy as O, resolveScanTarget as P, discoverReactSubprojects as S, formatErrorChain as T, Score as _, DeadCode as a, buildJsonReportError as b, LintPartialFailures as c, OXLINT_NODE_REQUIREMENT as d, Progress as f, SKILL_NAME as g, SHARE_BASE_URL as h, Config as i, isReactDoctorError as j, highlighter as k, Linter as l, Reporter as m, cli_logger_exports as n, Files as o, Project as p, CANONICAL_GITHUB_URL as r, Git as s, cliLogger as t, NodeResolver as u, StagedFiles as v, filterSourceFiles as w, buildRulePromptUrl as x, buildJsonReport as y };
|
|
7411
7419
|
|
|
7412
|
-
//# sourceMappingURL=cli-logger-
|
|
7420
|
+
//# sourceMappingURL=cli-logger-CmMJBgYF.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { i as __toESM, n as __exportAll, r as __require, t as __commonJSMin } from "./rolldown-runtime-uZX_iqCz.js";
|
|
2
|
-
import { A as
|
|
2
|
+
import { A as isMonorepoRoot, C as filterDiagnosticsForSurface, D as getDiffInfo, E as formatReactDoctorError, F as restoreLegacyThrow, I as runInspect, L as toRelativePath, M as layerOtlp, N as listWorkspacePackages, O as groupBy, P as resolveScanTarget, S as discoverReactSubprojects, T as formatErrorChain, _ as Score, a as DeadCode, b as buildJsonReportError, c as LintPartialFailures, d as OXLINT_NODE_REQUIREMENT, f as Progress, g as SKILL_NAME, h as SHARE_BASE_URL, i as Config, j as isReactDoctorError, k as highlighter, l as Linter, m as Reporter, o as Files, p as Project, r as CANONICAL_GITHUB_URL, s as Git, t as cliLogger, u as NodeResolver, v as StagedFiles, w as filterSourceFiles, x as buildRulePromptUrl, y as buildJsonReport } from "./cli-logger-CmMJBgYF.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { execFileSync, execSync } from "node:child_process";
|
|
5
5
|
import path, { join } from "node:path";
|
|
6
|
-
import { accessSync, chmodSync, constants, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
+
import fs, { accessSync, chmodSync, constants, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
|
|
7
7
|
import process$1 from "node:process";
|
|
8
8
|
import * as Effect from "effect/Effect";
|
|
9
9
|
import * as Layer from "effect/Layer";
|
|
@@ -6220,6 +6220,8 @@ const padRuleNameToColumn = (ruleName, columnWidth) => {
|
|
|
6220
6220
|
return ruleName + " ".repeat(columnWidth - ruleName.length);
|
|
6221
6221
|
};
|
|
6222
6222
|
const grayLine = (text) => highlighter.gray(text);
|
|
6223
|
+
const FETCH_FIX_RECIPE_LABEL = "Fetch & follow the canonical fix recipe before fixing";
|
|
6224
|
+
const formatFixRecipeLine = (diagnostic) => `${FETCH_FIX_RECIPE_LABEL}: ${buildRulePromptUrl(diagnostic.plugin, diagnostic.rule)}`;
|
|
6223
6225
|
const buildCompactRuleGroupLine = (ruleKey, ruleDiagnostics, ruleNameColumnWidth) => {
|
|
6224
6226
|
const firstDiagnostic = ruleDiagnostics[0];
|
|
6225
6227
|
const icon = colorizeBySeverity(firstDiagnostic.severity === "error" ? "✗" : "⚠", firstDiagnostic.severity);
|
|
@@ -6255,6 +6257,7 @@ const buildVerboseRuleGroupLines = (ruleKey, ruleDiagnostics, ruleNameColumnWidt
|
|
|
6255
6257
|
const firstDiagnostic = ruleDiagnostics[0];
|
|
6256
6258
|
lines.push(grayLine(indentMultilineText(firstDiagnostic.message, " ")));
|
|
6257
6259
|
if (firstDiagnostic.help) lines.push(grayLine(indentMultilineText(`→ ${firstDiagnostic.help}`, " ")));
|
|
6260
|
+
lines.push(grayLine(` ${formatFixRecipeLine(firstDiagnostic)}`));
|
|
6258
6261
|
const fileSites = buildVerboseSiteMap(ruleDiagnostics);
|
|
6259
6262
|
for (const [filePath, sites] of fileSites) if (sites.length > 0) for (const site of sites) {
|
|
6260
6263
|
lines.push(grayLine(` ${filePath}:${site.line}`));
|
|
@@ -6299,6 +6302,7 @@ const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
|
|
|
6299
6302
|
];
|
|
6300
6303
|
if (firstDiagnostic.help) sections.push("", `Suggestion: ${firstDiagnostic.help}`);
|
|
6301
6304
|
if (firstDiagnostic.url) sections.push("", `Docs: ${firstDiagnostic.url}`);
|
|
6305
|
+
sections.push("", formatFixRecipeLine(firstDiagnostic));
|
|
6302
6306
|
sections.push("", "Files:");
|
|
6303
6307
|
const fileSites = buildVerboseSiteMap(ruleDiagnostics);
|
|
6304
6308
|
for (const [filePath, sites] of fileSites) if (sites.length > 0) for (const site of sites) {
|
|
@@ -6539,7 +6543,11 @@ const printCountsSummaryLine = (diagnostics, isVerbose) => Effect.gen(function*
|
|
|
6539
6543
|
const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
|
|
6540
6544
|
const issueText = (errorCount > 0 ? highlighter.error : warningCount > 0 ? highlighter.warn : highlighter.dim)(`${totalIssueCount} ${totalIssueCount === 1 ? "issue" : "issues"}`);
|
|
6541
6545
|
yield* Console.log(` ${issueText}`);
|
|
6542
|
-
if (!isVerbose && totalIssueCount > 0)
|
|
6546
|
+
if (!isVerbose && totalIssueCount > 0) {
|
|
6547
|
+
const exampleDiagnostic = diagnostics.find((diagnostic) => diagnostic.severity === "error") ?? diagnostics[0];
|
|
6548
|
+
yield* Console.log(highlighter.dim(` Run ${highlighter.info("npx react-doctor@latest --verbose")} to list every issue with its fix-recipe URL`));
|
|
6549
|
+
yield* Console.log(highlighter.dim(` Each rule links a canonical fix recipe to fetch & follow before fixing, e.g. ${highlighter.info(buildRulePromptUrl(exampleDiagnostic.plugin, exampleDiagnostic.rule))}`));
|
|
6550
|
+
}
|
|
6543
6551
|
});
|
|
6544
6552
|
const printSummary = (input) => Effect.gen(function* () {
|
|
6545
6553
|
if (input.scoreResult) yield* printScoreHeader(input.scoreResult, input.projectName);
|
|
@@ -6666,7 +6674,7 @@ const resolveOxlintNodeEffect = (isLintEnabled, isQuiet) => Effect.gen(function*
|
|
|
6666
6674
|
const resolveOxlintNode = (isLintEnabled, isQuiet) => Effect.runPromise(resolveOxlintNodeEffect(isLintEnabled, isQuiet).pipe(Effect.provide(NodeResolver.layerNode)));
|
|
6667
6675
|
//#endregion
|
|
6668
6676
|
//#region src/cli/utils/version.ts
|
|
6669
|
-
const VERSION = "0.2.11-dev.
|
|
6677
|
+
const VERSION = "0.2.11-dev.b5cf767";
|
|
6670
6678
|
//#endregion
|
|
6671
6679
|
//#region src/inspect.ts
|
|
6672
6680
|
const silentConsole = makeNoopConsole();
|
|
@@ -7066,15 +7074,16 @@ const buildIssuesSummary = (input) => {
|
|
|
7066
7074
|
if (input.score) lines.push(`Score: ${input.score.score}/100`);
|
|
7067
7075
|
lines.push(`${input.diagnostics.length} issues found`);
|
|
7068
7076
|
lines.push("");
|
|
7069
|
-
const sortedRules = [...groupBy([...input.diagnostics], (diagnostic) => diagnostic.rule).entries()].sort(([, diagnosticsA], [, diagnosticsB]) => diagnosticsB.length - diagnosticsA.length);
|
|
7077
|
+
const sortedRules = [...groupBy([...input.diagnostics], (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`).entries()].sort(([, diagnosticsA], [, diagnosticsB]) => diagnosticsB.length - diagnosticsA.length);
|
|
7070
7078
|
const visibleRules = sortedRules.slice(0, MAX_RULES_SHOWN);
|
|
7071
|
-
for (const [
|
|
7079
|
+
for (const [ruleKey, ruleDiagnostics] of visibleRules) {
|
|
7072
7080
|
const severity = ruleDiagnostics[0].severity;
|
|
7073
7081
|
const uniqueFiles = [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.filePath))];
|
|
7074
7082
|
const shownFiles = uniqueFiles.slice(0, MAX_FILES_PER_RULE);
|
|
7075
7083
|
const remainingFileCount = uniqueFiles.length - shownFiles.length;
|
|
7076
|
-
lines.push(`${severity === "error" ? "ERROR" : "WARN"} ${
|
|
7084
|
+
lines.push(`${severity === "error" ? "ERROR" : "WARN"} ${ruleKey} (×${ruleDiagnostics.length})`);
|
|
7077
7085
|
lines.push(` ${ruleDiagnostics[0].message}`);
|
|
7086
|
+
lines.push(` ${formatFixRecipeLine(ruleDiagnostics[0])}`);
|
|
7078
7087
|
for (const filePath of shownFiles) {
|
|
7079
7088
|
const firstSite = ruleDiagnostics.find((diagnostic) => diagnostic.filePath === filePath && diagnostic.line > 0);
|
|
7080
7089
|
lines.push(` - ${filePath}${firstSite ? `:${firstSite.line}` : ""}`);
|
|
@@ -7094,11 +7103,12 @@ const buildIssuesSummary = (input) => {
|
|
|
7094
7103
|
lines.push("");
|
|
7095
7104
|
lines.push("## How to fix");
|
|
7096
7105
|
lines.push("1. Run `npx react-doctor@latest --verbose` to see full details");
|
|
7097
|
-
lines.push("2.
|
|
7098
|
-
lines.push("3.
|
|
7099
|
-
lines.push("4.
|
|
7100
|
-
lines.push("5.
|
|
7101
|
-
lines.push("6.
|
|
7106
|
+
lines.push("2. For each rule above, fetch & follow its canonical fix recipe URL before fixing.");
|
|
7107
|
+
lines.push("3. Fix errors first, then warnings. Start with high-count rules.");
|
|
7108
|
+
lines.push("4. Read the code before acting. Treat findings as hypotheses, not commands.");
|
|
7109
|
+
lines.push("5. Fix root causes, not symptoms. Don't suppress rules without evidence.");
|
|
7110
|
+
lines.push("6. Run `npx react-doctor@latest --verbose --diff` after changes to verify.");
|
|
7111
|
+
lines.push("7. Split unrelated fixes into separate PRs.");
|
|
7102
7112
|
return lines.join("\n");
|
|
7103
7113
|
};
|
|
7104
7114
|
const copyToClipboard = (text) => {
|
|
@@ -7153,6 +7163,29 @@ const promptCopyIssues = async (input) => {
|
|
|
7153
7163
|
else cliLogger.log(issuesSummary);
|
|
7154
7164
|
};
|
|
7155
7165
|
//#endregion
|
|
7166
|
+
//#region src/cli/utils/path-format.ts
|
|
7167
|
+
const toForwardSlashes = (filePath) => filePath.replaceAll("\\", "/");
|
|
7168
|
+
//#endregion
|
|
7169
|
+
//#region src/cli/utils/read-changed-files-from.ts
|
|
7170
|
+
const isSafeRelativePath = (filePath) => {
|
|
7171
|
+
if (filePath.length === 0) return false;
|
|
7172
|
+
if (filePath.includes("\0")) return false;
|
|
7173
|
+
if (path.isAbsolute(filePath)) return false;
|
|
7174
|
+
const normalized = path.posix.normalize(filePath);
|
|
7175
|
+
if (normalized === "." || normalized.startsWith("../") || normalized === "..") return false;
|
|
7176
|
+
return normalized === filePath;
|
|
7177
|
+
};
|
|
7178
|
+
const readChangedFilesFrom = (filePath) => {
|
|
7179
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
7180
|
+
const uniqueFiles = /* @__PURE__ */ new Set();
|
|
7181
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
7182
|
+
const candidate = toForwardSlashes(line.trim());
|
|
7183
|
+
if (!isSafeRelativePath(candidate)) continue;
|
|
7184
|
+
uniqueFiles.add(candidate);
|
|
7185
|
+
}
|
|
7186
|
+
return [...uniqueFiles];
|
|
7187
|
+
};
|
|
7188
|
+
//#endregion
|
|
7156
7189
|
//#region src/cli/utils/render-multi-project-summary.ts
|
|
7157
7190
|
const SUMMARY_BAR_WIDTH_CHARS = 20;
|
|
7158
7191
|
const buildMiniBar = (score) => {
|
|
@@ -7215,6 +7248,7 @@ const printMultiProjectSummary = (input) => Effect.gen(function* () {
|
|
|
7215
7248
|
};
|
|
7216
7249
|
});
|
|
7217
7250
|
const longestProjectNameLength = Math.max(...entries.map((entry) => entry.projectName.length));
|
|
7251
|
+
yield* Console.log("");
|
|
7218
7252
|
for (const entry of entries) yield* Console.log(buildSummaryLine(entry, longestProjectNameLength));
|
|
7219
7253
|
yield* Console.log("");
|
|
7220
7254
|
});
|
|
@@ -7461,7 +7495,7 @@ const warnSetupPromptFailure = async (options, error) => {
|
|
|
7461
7495
|
return;
|
|
7462
7496
|
}
|
|
7463
7497
|
try {
|
|
7464
|
-
const { cliLogger } = await import("./cli-logger-
|
|
7498
|
+
const { cliLogger } = await import("./cli-logger-CmMJBgYF.js").then((n) => n.n);
|
|
7465
7499
|
cliLogger.warn(message);
|
|
7466
7500
|
} catch {}
|
|
7467
7501
|
};
|
|
@@ -7611,7 +7645,6 @@ const resolveFailOnLevel = (flags, userConfig) => {
|
|
|
7611
7645
|
};
|
|
7612
7646
|
//#endregion
|
|
7613
7647
|
//#region src/cli/utils/resolve-project-diff-include-paths.ts
|
|
7614
|
-
const toForwardSlashes = (filePath) => filePath.replaceAll("\\", "/");
|
|
7615
7648
|
const resolveProjectDiffIncludePaths = (rootDirectory, projectDirectory, diffInfo) => {
|
|
7616
7649
|
const changedSourceFiles = filterSourceFiles(diffInfo.changedFiles);
|
|
7617
7650
|
const relativeProjectDirectory = toForwardSlashes(path.relative(rootDirectory, projectDirectory));
|
|
@@ -7810,7 +7843,7 @@ const validateModeFlags = (flags) => {
|
|
|
7810
7843
|
if (flags.yes && flags.full) throw new Error("Cannot combine --yes and --full; pick one.");
|
|
7811
7844
|
if (flags.score && flags.json) throw new Error("Cannot combine --score and --json; pick one output mode.");
|
|
7812
7845
|
if (flags.prComment && (flags.json || flags.score)) throw new Error("--pr-comment cannot be combined with --json or --score.");
|
|
7813
|
-
if (flags.annotations &&
|
|
7846
|
+
if (flags.annotations && flags.score) throw new Error("--annotations cannot be combined with --score.");
|
|
7814
7847
|
if (flags.explain !== void 0 && flags.why !== void 0) throw new Error("Use --explain or --why, not both — they're aliases of the same flag.");
|
|
7815
7848
|
if ((flags.explain ?? flags.why) !== void 0 && (flags.json || flags.score || flags.annotations || flags.staged)) throw new Error("--explain cannot be combined with --json, --score, --annotations, or --staged.");
|
|
7816
7849
|
};
|
|
@@ -7836,6 +7869,12 @@ const finalizeScans = (input) => {
|
|
|
7836
7869
|
const ciFailureDiagnostics = filterDiagnosticsForSurface(input.diagnostics, "ciFailure", input.userConfig);
|
|
7837
7870
|
if (!input.isScoreOnly && shouldFailForDiagnostics(ciFailureDiagnostics, resolveFailOnLevel(input.flags, input.userConfig))) process.exitCode = 1;
|
|
7838
7871
|
};
|
|
7872
|
+
const buildChangedFilesDiffInfo = (changedFiles) => ({
|
|
7873
|
+
currentBranch: process.env.GITHUB_HEAD_REF?.trim() || null,
|
|
7874
|
+
baseBranch: process.env.GITHUB_BASE_REF?.trim() || "pull request target",
|
|
7875
|
+
changedFiles,
|
|
7876
|
+
isCurrentChanges: false
|
|
7877
|
+
});
|
|
7839
7878
|
const inspectAction = async (directory, flags) => {
|
|
7840
7879
|
const isScoreOnly = Boolean(flags.score);
|
|
7841
7880
|
const isJsonMode = Boolean(flags.json);
|
|
@@ -7938,9 +7977,10 @@ const inspectAction = async (directory, flags) => {
|
|
|
7938
7977
|
return;
|
|
7939
7978
|
}
|
|
7940
7979
|
const projectDirectories = await selectProjects(resolvedDirectory, flags.project, skipPrompts);
|
|
7980
|
+
const changedFilesDiffInfo = flags.changedFilesFrom && !flags.full ? buildChangedFilesDiffInfo(readChangedFilesFrom(path.resolve(flags.changedFilesFrom))) : null;
|
|
7941
7981
|
const effectiveDiff = resolveEffectiveDiff(flags, userConfig);
|
|
7942
|
-
const diffInfo = effectiveDiff !== void 0 && effectiveDiff !== false || !skipPrompts && !isQuiet ? await getDiffInfo(resolvedDirectory, typeof effectiveDiff === "string" ? effectiveDiff : void 0) : null;
|
|
7943
|
-
const isDiffMode = await resolveDiffMode(diffInfo, effectiveDiff, skipPrompts, isQuiet);
|
|
7982
|
+
const diffInfo = changedFilesDiffInfo ?? (changedFilesDiffInfo === null && (effectiveDiff !== void 0 && effectiveDiff !== false || !skipPrompts && !isQuiet) ? await getDiffInfo(resolvedDirectory, typeof effectiveDiff === "string" ? effectiveDiff : void 0) : null);
|
|
7983
|
+
const isDiffMode = changedFilesDiffInfo !== null || await resolveDiffMode(diffInfo, effectiveDiff, skipPrompts, isQuiet);
|
|
7944
7984
|
setJsonReportMode(isDiffMode ? "diff" : "full");
|
|
7945
7985
|
if (isDiffMode && diffInfo && !isQuiet) {
|
|
7946
7986
|
if (diffInfo.isCurrentChanges) cliLogger.log("Scanning uncommitted changes");
|
|
@@ -8838,21 +8878,23 @@ const buildWorkflowContent = () => [
|
|
|
8838
8878
|
"",
|
|
8839
8879
|
"on:",
|
|
8840
8880
|
" pull_request:",
|
|
8841
|
-
"
|
|
8881
|
+
" types: [opened, synchronize, reopened, ready_for_review]",
|
|
8842
8882
|
"",
|
|
8843
8883
|
"permissions:",
|
|
8844
8884
|
" contents: read",
|
|
8845
8885
|
" pull-requests: write",
|
|
8886
|
+
" issues: write",
|
|
8887
|
+
"",
|
|
8888
|
+
"concurrency:",
|
|
8889
|
+
" group: react-doctor-${{ github.event.pull_request.number || github.ref }}",
|
|
8890
|
+
" cancel-in-progress: true",
|
|
8846
8891
|
"",
|
|
8847
8892
|
"jobs:",
|
|
8848
8893
|
" react-doctor:",
|
|
8849
8894
|
" runs-on: ubuntu-latest",
|
|
8850
8895
|
" steps:",
|
|
8851
|
-
" - uses: actions/checkout@
|
|
8852
|
-
" - uses: millionco/react-doctor@
|
|
8853
|
-
" with:",
|
|
8854
|
-
" github-token: ${{ secrets.GITHUB_TOKEN }}",
|
|
8855
|
-
" diff: main",
|
|
8896
|
+
" - uses: actions/checkout@v5",
|
|
8897
|
+
" - uses: millionco/react-doctor@v1",
|
|
8856
8898
|
""
|
|
8857
8899
|
].join("\n");
|
|
8858
8900
|
const runInstallReactDoctor = async (options = {}) => {
|
|
@@ -9050,6 +9092,7 @@ const ROOT_FLAG_SPEC = {
|
|
|
9050
9092
|
"--yes"
|
|
9051
9093
|
]),
|
|
9052
9094
|
longOptionsWithRequiredValues: new Set([
|
|
9095
|
+
"--changed-files-from",
|
|
9053
9096
|
"--explain",
|
|
9054
9097
|
"--fail-on",
|
|
9055
9098
|
"--project",
|
|
@@ -9143,10 +9186,16 @@ const stripUnknownCliFlags = (argv) => {
|
|
|
9143
9186
|
];
|
|
9144
9187
|
};
|
|
9145
9188
|
//#endregion
|
|
9189
|
+
//#region src/cli/utils/unref-stdin.ts
|
|
9190
|
+
const unrefStdin = () => {
|
|
9191
|
+
process.stdin.unref?.();
|
|
9192
|
+
};
|
|
9193
|
+
//#endregion
|
|
9146
9194
|
//#region src/cli/index.ts
|
|
9147
9195
|
process.on("SIGINT", exitGracefully);
|
|
9148
9196
|
process.on("SIGTERM", exitGracefully);
|
|
9149
|
-
|
|
9197
|
+
unrefStdin();
|
|
9198
|
+
const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--full", "force a full scan (overrides any `diff` value in config or `--diff`)").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch (pass `false` to disable; overridden by --full)").option("--changed-files-from <file>", "internal: scan source files listed in a newline-delimited changed-files file").option("--no-score", "skip the score API and the share URL").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--fail-on <level>", "exit with error code on diagnostics: error, warning, none (default: none)").option("--annotations", "output diagnostics as GitHub Actions annotations").option("--pr-comment", "tune CLI output for sticky PR comments (drops weak-signal rule families like `design` from the printed list and the fail-on gate; configure via config.surfaces)").option("--explain <file:line>", "diagnose why a rule fired or why a suppression didn't apply at a specific location").option("--why <file:line>", "alias for --explain").option("--respect-inline-disables", "respect inline `// eslint-disable*` / `// oxlint-disable*` comments (default)").option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").addHelpText("after", `
|
|
9150
9199
|
${highlighter.dim("Configuration:")}
|
|
9151
9200
|
Place a ${highlighter.info("react-doctor.config.json")} (or ${highlighter.info("\"reactDoctor\"")} key in your package.json) in the project root.
|
|
9152
9201
|
CLI flags always override config values. See the README for the full schema.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-doctor",
|
|
3
|
-
"version": "0.2.11-dev.
|
|
3
|
+
"version": "0.2.11-dev.b5cf767",
|
|
4
4
|
"description": "Diagnose and fix React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
"oxlint": "^1.66.0",
|
|
59
59
|
"prompts": "^2.4.2",
|
|
60
60
|
"typescript": ">=5.0.4 <7",
|
|
61
|
-
"oxlint-plugin-react-doctor": "0.2.11-dev.
|
|
61
|
+
"oxlint-plugin-react-doctor": "0.2.11-dev.b5cf767"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@types/prompts": "^2.4.9",
|
|
65
65
|
"commander": "^14.0.3",
|
|
66
66
|
"ora": "^9.4.0",
|
|
67
|
-
"@react-doctor/
|
|
68
|
-
"@react-doctor/
|
|
67
|
+
"@react-doctor/api": "0.2.11",
|
|
68
|
+
"@react-doctor/core": "0.2.11"
|
|
69
69
|
},
|
|
70
70
|
"engines": {
|
|
71
71
|
"node": "^20.19.0 || >=22.12.0"
|