react-doctor 0.2.11-dev.f036b0f → 0.2.11-dev.f4035fc
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.js +51 -13
- 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
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { A as isReactDoctorError, C as filterSourceFiles, D as groupBy, E as get
|
|
|
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";
|
|
@@ -6666,7 +6666,7 @@ const resolveOxlintNodeEffect = (isLintEnabled, isQuiet) => Effect.gen(function*
|
|
|
6666
6666
|
const resolveOxlintNode = (isLintEnabled, isQuiet) => Effect.runPromise(resolveOxlintNodeEffect(isLintEnabled, isQuiet).pipe(Effect.provide(NodeResolver.layerNode)));
|
|
6667
6667
|
//#endregion
|
|
6668
6668
|
//#region src/cli/utils/version.ts
|
|
6669
|
-
const VERSION = "0.2.11-dev.
|
|
6669
|
+
const VERSION = "0.2.11-dev.f4035fc";
|
|
6670
6670
|
//#endregion
|
|
6671
6671
|
//#region src/inspect.ts
|
|
6672
6672
|
const silentConsole = makeNoopConsole();
|
|
@@ -7153,6 +7153,29 @@ const promptCopyIssues = async (input) => {
|
|
|
7153
7153
|
else cliLogger.log(issuesSummary);
|
|
7154
7154
|
};
|
|
7155
7155
|
//#endregion
|
|
7156
|
+
//#region src/cli/utils/path-format.ts
|
|
7157
|
+
const toForwardSlashes = (filePath) => filePath.replaceAll("\\", "/");
|
|
7158
|
+
//#endregion
|
|
7159
|
+
//#region src/cli/utils/read-changed-files-from.ts
|
|
7160
|
+
const isSafeRelativePath = (filePath) => {
|
|
7161
|
+
if (filePath.length === 0) return false;
|
|
7162
|
+
if (filePath.includes("\0")) return false;
|
|
7163
|
+
if (path.isAbsolute(filePath)) return false;
|
|
7164
|
+
const normalized = path.posix.normalize(filePath);
|
|
7165
|
+
if (normalized === "." || normalized.startsWith("../") || normalized === "..") return false;
|
|
7166
|
+
return normalized === filePath;
|
|
7167
|
+
};
|
|
7168
|
+
const readChangedFilesFrom = (filePath) => {
|
|
7169
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
7170
|
+
const uniqueFiles = /* @__PURE__ */ new Set();
|
|
7171
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
7172
|
+
const candidate = toForwardSlashes(line.trim());
|
|
7173
|
+
if (!isSafeRelativePath(candidate)) continue;
|
|
7174
|
+
uniqueFiles.add(candidate);
|
|
7175
|
+
}
|
|
7176
|
+
return [...uniqueFiles];
|
|
7177
|
+
};
|
|
7178
|
+
//#endregion
|
|
7156
7179
|
//#region src/cli/utils/render-multi-project-summary.ts
|
|
7157
7180
|
const SUMMARY_BAR_WIDTH_CHARS = 20;
|
|
7158
7181
|
const buildMiniBar = (score) => {
|
|
@@ -7611,7 +7634,6 @@ const resolveFailOnLevel = (flags, userConfig) => {
|
|
|
7611
7634
|
};
|
|
7612
7635
|
//#endregion
|
|
7613
7636
|
//#region src/cli/utils/resolve-project-diff-include-paths.ts
|
|
7614
|
-
const toForwardSlashes = (filePath) => filePath.replaceAll("\\", "/");
|
|
7615
7637
|
const resolveProjectDiffIncludePaths = (rootDirectory, projectDirectory, diffInfo) => {
|
|
7616
7638
|
const changedSourceFiles = filterSourceFiles(diffInfo.changedFiles);
|
|
7617
7639
|
const relativeProjectDirectory = toForwardSlashes(path.relative(rootDirectory, projectDirectory));
|
|
@@ -7810,7 +7832,7 @@ const validateModeFlags = (flags) => {
|
|
|
7810
7832
|
if (flags.yes && flags.full) throw new Error("Cannot combine --yes and --full; pick one.");
|
|
7811
7833
|
if (flags.score && flags.json) throw new Error("Cannot combine --score and --json; pick one output mode.");
|
|
7812
7834
|
if (flags.prComment && (flags.json || flags.score)) throw new Error("--pr-comment cannot be combined with --json or --score.");
|
|
7813
|
-
if (flags.annotations &&
|
|
7835
|
+
if (flags.annotations && flags.score) throw new Error("--annotations cannot be combined with --score.");
|
|
7814
7836
|
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
7837
|
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
7838
|
};
|
|
@@ -7836,6 +7858,12 @@ const finalizeScans = (input) => {
|
|
|
7836
7858
|
const ciFailureDiagnostics = filterDiagnosticsForSurface(input.diagnostics, "ciFailure", input.userConfig);
|
|
7837
7859
|
if (!input.isScoreOnly && shouldFailForDiagnostics(ciFailureDiagnostics, resolveFailOnLevel(input.flags, input.userConfig))) process.exitCode = 1;
|
|
7838
7860
|
};
|
|
7861
|
+
const buildChangedFilesDiffInfo = (changedFiles) => ({
|
|
7862
|
+
currentBranch: process.env.GITHUB_HEAD_REF?.trim() || null,
|
|
7863
|
+
baseBranch: process.env.GITHUB_BASE_REF?.trim() || "pull request target",
|
|
7864
|
+
changedFiles,
|
|
7865
|
+
isCurrentChanges: false
|
|
7866
|
+
});
|
|
7839
7867
|
const inspectAction = async (directory, flags) => {
|
|
7840
7868
|
const isScoreOnly = Boolean(flags.score);
|
|
7841
7869
|
const isJsonMode = Boolean(flags.json);
|
|
@@ -7938,9 +7966,10 @@ const inspectAction = async (directory, flags) => {
|
|
|
7938
7966
|
return;
|
|
7939
7967
|
}
|
|
7940
7968
|
const projectDirectories = await selectProjects(resolvedDirectory, flags.project, skipPrompts);
|
|
7969
|
+
const changedFilesDiffInfo = flags.changedFilesFrom && !flags.full ? buildChangedFilesDiffInfo(readChangedFilesFrom(path.resolve(flags.changedFilesFrom))) : null;
|
|
7941
7970
|
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);
|
|
7971
|
+
const diffInfo = changedFilesDiffInfo ?? (changedFilesDiffInfo === null && (effectiveDiff !== void 0 && effectiveDiff !== false || !skipPrompts && !isQuiet) ? await getDiffInfo(resolvedDirectory, typeof effectiveDiff === "string" ? effectiveDiff : void 0) : null);
|
|
7972
|
+
const isDiffMode = changedFilesDiffInfo !== null || await resolveDiffMode(diffInfo, effectiveDiff, skipPrompts, isQuiet);
|
|
7944
7973
|
setJsonReportMode(isDiffMode ? "diff" : "full");
|
|
7945
7974
|
if (isDiffMode && diffInfo && !isQuiet) {
|
|
7946
7975
|
if (diffInfo.isCurrentChanges) cliLogger.log("Scanning uncommitted changes");
|
|
@@ -8838,21 +8867,23 @@ const buildWorkflowContent = () => [
|
|
|
8838
8867
|
"",
|
|
8839
8868
|
"on:",
|
|
8840
8869
|
" pull_request:",
|
|
8841
|
-
"
|
|
8870
|
+
" types: [opened, synchronize, reopened, ready_for_review]",
|
|
8842
8871
|
"",
|
|
8843
8872
|
"permissions:",
|
|
8844
8873
|
" contents: read",
|
|
8845
8874
|
" pull-requests: write",
|
|
8875
|
+
" issues: write",
|
|
8876
|
+
"",
|
|
8877
|
+
"concurrency:",
|
|
8878
|
+
" group: react-doctor-${{ github.event.pull_request.number || github.ref }}",
|
|
8879
|
+
" cancel-in-progress: true",
|
|
8846
8880
|
"",
|
|
8847
8881
|
"jobs:",
|
|
8848
8882
|
" react-doctor:",
|
|
8849
8883
|
" runs-on: ubuntu-latest",
|
|
8850
8884
|
" steps:",
|
|
8851
|
-
" - uses: actions/checkout@
|
|
8852
|
-
" - uses: millionco/react-doctor@
|
|
8853
|
-
" with:",
|
|
8854
|
-
" github-token: ${{ secrets.GITHUB_TOKEN }}",
|
|
8855
|
-
" diff: main",
|
|
8885
|
+
" - uses: actions/checkout@v5",
|
|
8886
|
+
" - uses: millionco/react-doctor@v1",
|
|
8856
8887
|
""
|
|
8857
8888
|
].join("\n");
|
|
8858
8889
|
const runInstallReactDoctor = async (options = {}) => {
|
|
@@ -9050,6 +9081,7 @@ const ROOT_FLAG_SPEC = {
|
|
|
9050
9081
|
"--yes"
|
|
9051
9082
|
]),
|
|
9052
9083
|
longOptionsWithRequiredValues: new Set([
|
|
9084
|
+
"--changed-files-from",
|
|
9053
9085
|
"--explain",
|
|
9054
9086
|
"--fail-on",
|
|
9055
9087
|
"--project",
|
|
@@ -9143,10 +9175,16 @@ const stripUnknownCliFlags = (argv) => {
|
|
|
9143
9175
|
];
|
|
9144
9176
|
};
|
|
9145
9177
|
//#endregion
|
|
9178
|
+
//#region src/cli/utils/unref-stdin.ts
|
|
9179
|
+
const unrefStdin = () => {
|
|
9180
|
+
process.stdin.unref?.();
|
|
9181
|
+
};
|
|
9182
|
+
//#endregion
|
|
9146
9183
|
//#region src/cli/index.ts
|
|
9147
9184
|
process.on("SIGINT", exitGracefully);
|
|
9148
9185
|
process.on("SIGTERM", exitGracefully);
|
|
9149
|
-
|
|
9186
|
+
unrefStdin();
|
|
9187
|
+
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
9188
|
${highlighter.dim("Configuration:")}
|
|
9151
9189
|
Place a ${highlighter.info("react-doctor.config.json")} (or ${highlighter.info("\"reactDoctor\"")} key in your package.json) in the project root.
|
|
9152
9190
|
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.f4035fc",
|
|
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.f4035fc"
|
|
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/core": "0.2.11",
|
|
68
|
+
"@react-doctor/api": "0.2.11"
|
|
69
69
|
},
|
|
70
70
|
"engines": {
|
|
71
71
|
"node": "^20.19.0 || >=22.12.0"
|