react-doctor 0.2.11 → 0.2.12-dev.269ca17
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-pbFEieEc.js → cli-logger-CSZagq1E.js} +263 -60
- package/dist/cli.js +89 -33
- package/dist/index.d.ts +13 -8
- package/dist/index.js +258 -62
- package/package.json +4 -4
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-CSZagq1E.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.
|
|
6677
|
+
const VERSION = "0.2.12-dev.269ca17";
|
|
6670
6678
|
//#endregion
|
|
6671
6679
|
//#region src/inspect.ts
|
|
6672
6680
|
const silentConsole = makeNoopConsole();
|
|
@@ -6764,9 +6772,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
6764
6772
|
const output = await Effect.runPromise(restoreLegacyThrow(programWithLayers));
|
|
6765
6773
|
const didLintFail = lintBindingMissing || output.didLintFail;
|
|
6766
6774
|
const lintFailureReason = lintBindingMissing ? `oxlint native binding not found for Node ${process.version}; expected one matching ${OXLINT_NODE_REQUIREMENT}` : output.lintFailureReason;
|
|
6767
|
-
|
|
6768
|
-
const isNativeBindingFailure = lintFailureReasonTag === "OxlintUnavailable" || lintFailureReasonTag === "OxlintSpawnFailed";
|
|
6769
|
-
if (!options.scoreOnly && !lintBindingMissing && output.didLintFail && lintFailureReason !== null) if (isNativeBindingFailure && /native binding/.test(lintFailureReason)) runConsole(Console.log(highlighter.gray(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`)));
|
|
6775
|
+
if (!options.scoreOnly && !lintBindingMissing && output.didLintFail && lintFailureReason !== null) if (output.lintFailureReasonKind === "native-binding-missing") runConsole(Console.log(highlighter.gray(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`)));
|
|
6770
6776
|
else runConsole(Console.error(highlighter.error(lintFailureReason)));
|
|
6771
6777
|
const inspectDiagnostics = output.diagnostics;
|
|
6772
6778
|
const score = didLintFail ? null : output.score;
|
|
@@ -7068,15 +7074,16 @@ const buildIssuesSummary = (input) => {
|
|
|
7068
7074
|
if (input.score) lines.push(`Score: ${input.score.score}/100`);
|
|
7069
7075
|
lines.push(`${input.diagnostics.length} issues found`);
|
|
7070
7076
|
lines.push("");
|
|
7071
|
-
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);
|
|
7072
7078
|
const visibleRules = sortedRules.slice(0, MAX_RULES_SHOWN);
|
|
7073
|
-
for (const [
|
|
7079
|
+
for (const [ruleKey, ruleDiagnostics] of visibleRules) {
|
|
7074
7080
|
const severity = ruleDiagnostics[0].severity;
|
|
7075
7081
|
const uniqueFiles = [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.filePath))];
|
|
7076
7082
|
const shownFiles = uniqueFiles.slice(0, MAX_FILES_PER_RULE);
|
|
7077
7083
|
const remainingFileCount = uniqueFiles.length - shownFiles.length;
|
|
7078
|
-
lines.push(`${severity === "error" ? "ERROR" : "WARN"} ${
|
|
7084
|
+
lines.push(`${severity === "error" ? "ERROR" : "WARN"} ${ruleKey} (×${ruleDiagnostics.length})`);
|
|
7079
7085
|
lines.push(` ${ruleDiagnostics[0].message}`);
|
|
7086
|
+
lines.push(` ${formatFixRecipeLine(ruleDiagnostics[0])}`);
|
|
7080
7087
|
for (const filePath of shownFiles) {
|
|
7081
7088
|
const firstSite = ruleDiagnostics.find((diagnostic) => diagnostic.filePath === filePath && diagnostic.line > 0);
|
|
7082
7089
|
lines.push(` - ${filePath}${firstSite ? `:${firstSite.line}` : ""}`);
|
|
@@ -7096,11 +7103,12 @@ const buildIssuesSummary = (input) => {
|
|
|
7096
7103
|
lines.push("");
|
|
7097
7104
|
lines.push("## How to fix");
|
|
7098
7105
|
lines.push("1. Run `npx react-doctor@latest --verbose` to see full details");
|
|
7099
|
-
lines.push("2.
|
|
7100
|
-
lines.push("3.
|
|
7101
|
-
lines.push("4.
|
|
7102
|
-
lines.push("5.
|
|
7103
|
-
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.");
|
|
7104
7112
|
return lines.join("\n");
|
|
7105
7113
|
};
|
|
7106
7114
|
const copyToClipboard = (text) => {
|
|
@@ -7155,6 +7163,29 @@ const promptCopyIssues = async (input) => {
|
|
|
7155
7163
|
else cliLogger.log(issuesSummary);
|
|
7156
7164
|
};
|
|
7157
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
|
|
7158
7189
|
//#region src/cli/utils/render-multi-project-summary.ts
|
|
7159
7190
|
const SUMMARY_BAR_WIDTH_CHARS = 20;
|
|
7160
7191
|
const buildMiniBar = (score) => {
|
|
@@ -7217,6 +7248,7 @@ const printMultiProjectSummary = (input) => Effect.gen(function* () {
|
|
|
7217
7248
|
};
|
|
7218
7249
|
});
|
|
7219
7250
|
const longestProjectNameLength = Math.max(...entries.map((entry) => entry.projectName.length));
|
|
7251
|
+
yield* Console.log("");
|
|
7220
7252
|
for (const entry of entries) yield* Console.log(buildSummaryLine(entry, longestProjectNameLength));
|
|
7221
7253
|
yield* Console.log("");
|
|
7222
7254
|
});
|
|
@@ -7463,7 +7495,7 @@ const warnSetupPromptFailure = async (options, error) => {
|
|
|
7463
7495
|
return;
|
|
7464
7496
|
}
|
|
7465
7497
|
try {
|
|
7466
|
-
const { cliLogger } = await import("./cli-logger-
|
|
7498
|
+
const { cliLogger } = await import("./cli-logger-CSZagq1E.js").then((n) => n.n);
|
|
7467
7499
|
cliLogger.warn(message);
|
|
7468
7500
|
} catch {}
|
|
7469
7501
|
};
|
|
@@ -7566,12 +7598,14 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isQui
|
|
|
7566
7598
|
message: "Choose what to scan",
|
|
7567
7599
|
choices: [{
|
|
7568
7600
|
title: "Full codebase",
|
|
7601
|
+
description: "Scan every source file",
|
|
7569
7602
|
value: "full"
|
|
7570
7603
|
}, {
|
|
7571
|
-
title: `Changed files (${changedSourceFiles.length})`,
|
|
7604
|
+
title: diffInfo.isCurrentChanges ? `Uncommitted changes (${changedSourceFiles.length})` : `Changed files on ${diffInfo.currentBranch ?? "this branch"} (${changedSourceFiles.length})`,
|
|
7605
|
+
description: diffInfo.isCurrentChanges ? "Compare working tree changes against HEAD" : `Compare against ${diffInfo.baseBranch} from the branch merge-base`,
|
|
7572
7606
|
value: "branch"
|
|
7573
7607
|
}],
|
|
7574
|
-
initial: 0
|
|
7608
|
+
initial: diffInfo.isCurrentChanges ? 0 : 1
|
|
7575
7609
|
});
|
|
7576
7610
|
return scanScope === "branch";
|
|
7577
7611
|
};
|
|
@@ -7601,17 +7635,16 @@ const VALID_FAIL_ON_LEVELS = new Set([
|
|
|
7601
7635
|
"warning",
|
|
7602
7636
|
"none"
|
|
7603
7637
|
]);
|
|
7604
|
-
const DEFAULT_FAIL_ON_LEVEL = "
|
|
7638
|
+
const DEFAULT_FAIL_ON_LEVEL = "none";
|
|
7605
7639
|
const isValidFailOnLevel = (level) => VALID_FAIL_ON_LEVELS.has(level);
|
|
7606
7640
|
const resolveFailOnLevel = (flags, userConfig) => {
|
|
7607
7641
|
const sourceValue = flags.failOn ?? userConfig?.failOn ?? DEFAULT_FAIL_ON_LEVEL;
|
|
7608
7642
|
if (isValidFailOnLevel(sourceValue)) return sourceValue;
|
|
7609
|
-
cliLogger.warn(`Invalid failOn level "${sourceValue}". Expected one of: error, warning, none. Falling back to "
|
|
7610
|
-
return
|
|
7643
|
+
cliLogger.warn(`Invalid failOn level "${sourceValue}". Expected one of: error, warning, none. Falling back to "${DEFAULT_FAIL_ON_LEVEL}".`);
|
|
7644
|
+
return DEFAULT_FAIL_ON_LEVEL;
|
|
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);
|
|
@@ -7892,7 +7931,14 @@ const inspectAction = async (directory, flags) => {
|
|
|
7892
7931
|
cliLogger.log(`Scanning ${highlighter.info(`${stagedFiles.length}`)} staged files...`);
|
|
7893
7932
|
cliLogger.break();
|
|
7894
7933
|
}
|
|
7895
|
-
const
|
|
7934
|
+
const tempDirectory = mkdtempSync(path.join(tmpdir(), STAGED_FILES_TEMP_DIR_PREFIX));
|
|
7935
|
+
const snapshot = await materializeStagedFiles(resolvedDirectory, stagedFiles, tempDirectory).catch((error) => {
|
|
7936
|
+
rmSync(tempDirectory, {
|
|
7937
|
+
recursive: true,
|
|
7938
|
+
force: true
|
|
7939
|
+
});
|
|
7940
|
+
throw error;
|
|
7941
|
+
});
|
|
7896
7942
|
try {
|
|
7897
7943
|
const scanResult = await inspect(snapshot.tempDirectory, {
|
|
7898
7944
|
...scanOptions,
|
|
@@ -7901,7 +7947,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
7901
7947
|
});
|
|
7902
7948
|
const remappedDiagnostics = scanResult.diagnostics.map((diagnostic) => ({
|
|
7903
7949
|
...diagnostic,
|
|
7904
|
-
filePath: path.isAbsolute(diagnostic.filePath) ? diagnostic.filePath.replaceAll(snapshot.tempDirectory, resolvedDirectory) : diagnostic.filePath
|
|
7950
|
+
filePath: path.isAbsolute(diagnostic.filePath) ? diagnostic.filePath.replaceAll(snapshot.tempDirectory, () => resolvedDirectory) : diagnostic.filePath
|
|
7905
7951
|
}));
|
|
7906
7952
|
finalizeScans({
|
|
7907
7953
|
diagnostics: remappedDiagnostics,
|
|
@@ -7931,9 +7977,10 @@ const inspectAction = async (directory, flags) => {
|
|
|
7931
7977
|
return;
|
|
7932
7978
|
}
|
|
7933
7979
|
const projectDirectories = await selectProjects(resolvedDirectory, flags.project, skipPrompts);
|
|
7980
|
+
const changedFilesDiffInfo = flags.changedFilesFrom && !flags.full ? buildChangedFilesDiffInfo(readChangedFilesFrom(path.resolve(flags.changedFilesFrom))) : null;
|
|
7934
7981
|
const effectiveDiff = resolveEffectiveDiff(flags, userConfig);
|
|
7935
|
-
const diffInfo = effectiveDiff !== void 0 && effectiveDiff !== false || !skipPrompts && !isQuiet ? await getDiffInfo(resolvedDirectory, typeof effectiveDiff === "string" ? effectiveDiff : void 0) : null;
|
|
7936
|
-
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);
|
|
7937
7984
|
setJsonReportMode(isDiffMode ? "diff" : "full");
|
|
7938
7985
|
if (isDiffMode && diffInfo && !isQuiet) {
|
|
7939
7986
|
if (diffInfo.isCurrentChanges) cliLogger.log("Scanning uncommitted changes");
|
|
@@ -8831,21 +8878,23 @@ const buildWorkflowContent = () => [
|
|
|
8831
8878
|
"",
|
|
8832
8879
|
"on:",
|
|
8833
8880
|
" pull_request:",
|
|
8834
|
-
"
|
|
8881
|
+
" types: [opened, synchronize, reopened, ready_for_review]",
|
|
8835
8882
|
"",
|
|
8836
8883
|
"permissions:",
|
|
8837
8884
|
" contents: read",
|
|
8838
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",
|
|
8839
8891
|
"",
|
|
8840
8892
|
"jobs:",
|
|
8841
8893
|
" react-doctor:",
|
|
8842
8894
|
" runs-on: ubuntu-latest",
|
|
8843
8895
|
" steps:",
|
|
8844
|
-
" - uses: actions/checkout@
|
|
8896
|
+
" - uses: actions/checkout@v5",
|
|
8845
8897
|
" - uses: millionco/react-doctor@main",
|
|
8846
|
-
" with:",
|
|
8847
|
-
" github-token: ${{ secrets.GITHUB_TOKEN }}",
|
|
8848
|
-
" diff: main",
|
|
8849
8898
|
""
|
|
8850
8899
|
].join("\n");
|
|
8851
8900
|
const runInstallReactDoctor = async (options = {}) => {
|
|
@@ -9043,6 +9092,7 @@ const ROOT_FLAG_SPEC = {
|
|
|
9043
9092
|
"--yes"
|
|
9044
9093
|
]),
|
|
9045
9094
|
longOptionsWithRequiredValues: new Set([
|
|
9095
|
+
"--changed-files-from",
|
|
9046
9096
|
"--explain",
|
|
9047
9097
|
"--fail-on",
|
|
9048
9098
|
"--project",
|
|
@@ -9136,10 +9186,16 @@ const stripUnknownCliFlags = (argv) => {
|
|
|
9136
9186
|
];
|
|
9137
9187
|
};
|
|
9138
9188
|
//#endregion
|
|
9189
|
+
//#region src/cli/utils/unref-stdin.ts
|
|
9190
|
+
const unrefStdin = () => {
|
|
9191
|
+
process.stdin.unref?.();
|
|
9192
|
+
};
|
|
9193
|
+
//#endregion
|
|
9139
9194
|
//#region src/cli/index.ts
|
|
9140
9195
|
process.on("SIGINT", exitGracefully);
|
|
9141
9196
|
process.on("SIGTERM", exitGracefully);
|
|
9142
|
-
|
|
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", `
|
|
9143
9199
|
${highlighter.dim("Configuration:")}
|
|
9144
9200
|
Place a ${highlighter.info("react-doctor.config.json")} (or ${highlighter.info("\"reactDoctor\"")} key in your package.json) in the project root.
|
|
9145
9201
|
CLI flags always override config values. See the README for the full schema.
|
package/dist/index.d.ts
CHANGED
|
@@ -305,20 +305,25 @@ interface ProjectInfo {
|
|
|
305
305
|
reactVersion: string | null;
|
|
306
306
|
reactMajorVersion: number | null;
|
|
307
307
|
tailwindVersion: string | null;
|
|
308
|
+
zodVersion: string | null;
|
|
309
|
+
/** Parsed major from `zodVersion`, or `null` when absent/unparseable. Mirrors `reactMajorVersion`. */
|
|
310
|
+
zodMajorVersion: number | null;
|
|
308
311
|
framework: Framework;
|
|
309
312
|
hasTypeScript: boolean;
|
|
310
313
|
hasReactCompiler: boolean;
|
|
311
314
|
hasTanStackQuery: boolean;
|
|
312
315
|
/**
|
|
313
|
-
*
|
|
314
|
-
* dependency
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
316
|
+
* The declared `preact` version spec, or `null` when Preact isn't a
|
|
317
|
+
* dependency. Parallels `reactVersion` so a React-compatible runtime is
|
|
318
|
+
* modeled the same way React is. Drives the `preact` capability in
|
|
319
|
+
* `buildCapabilities` (which gates every `preact-*` rule) — keyed off
|
|
320
|
+
* this rather than `framework` because the dominant Preact setup
|
|
321
|
+
* (Preact-on-Vite) classifies as `framework: "vite"` but still needs
|
|
322
|
+
* Preact rules to fire.
|
|
320
323
|
*/
|
|
321
|
-
|
|
324
|
+
preactVersion: string | null;
|
|
325
|
+
/** Parsed major from `preactVersion`, or `null` when absent/unparseable. Mirrors `reactMajorVersion`. */
|
|
326
|
+
preactMajorVersion: number | null;
|
|
322
327
|
/**
|
|
323
328
|
* `true` when the project (or any of its workspace packages) declares
|
|
324
329
|
* React Native or Expo as a dependency. Enables the `react-native`
|