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.
Files changed (3) hide show
  1. package/README.md +26 -0
  2. package/dist/cli.js +51 -13
  3. 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.f036b0f";
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 && (flags.json || flags.score)) throw new Error("--annotations cannot be combined with --json or --score.");
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
- " branches: [main]",
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@v4",
8852
- " - uses: millionco/react-doctor@main",
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
- 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("--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", `
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.f036b0f",
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.f036b0f"
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/api": "0.2.11",
68
- "@react-doctor/core": "0.2.11"
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"