react-doctor 0.2.9 → 0.2.11-dev.d917f62
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/dist/{cli-logger-BliQX9s8.js → cli-logger-Df45H6Lw.js} +430 -77
- package/dist/cli.js +131 -96
- package/dist/index.d.ts +23 -2
- package/dist/index.js +433 -79
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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 isReactDoctorError, C as filterSourceFiles, D as groupBy, E as getDiffInfo, F as runInspect, I as toRelativePath, M as listWorkspacePackages, N as resolveScanTarget, O as highlighter, P as restoreLegacyThrow, S as filterDiagnosticsForSurface, T as formatReactDoctorError, _ 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 layerOtlp, k as isMonorepoRoot, 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 formatErrorChain, x as discoverReactSubprojects, y as buildJsonReport } from "./cli-logger-
|
|
2
|
+
import { A as isReactDoctorError, C as filterSourceFiles, D as groupBy, E as getDiffInfo, F as runInspect, I as toRelativePath, M as listWorkspacePackages, N as resolveScanTarget, O as highlighter, P as restoreLegacyThrow, S as filterDiagnosticsForSurface, T as formatReactDoctorError, _ 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 layerOtlp, k as isMonorepoRoot, 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 formatErrorChain, x as discoverReactSubprojects, y as buildJsonReport } from "./cli-logger-Df45H6Lw.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";
|
|
@@ -6320,11 +6320,11 @@ const colorizeByScore = (text, score) => {
|
|
|
6320
6320
|
return highlighter.error(text);
|
|
6321
6321
|
};
|
|
6322
6322
|
//#endregion
|
|
6323
|
+
//#region src/cli/utils/constants.ts
|
|
6324
|
+
const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
|
|
6325
|
+
const INTERNAL_ERROR_JSON_FALLBACK = "{\"schemaVersion\":1,\"ok\":false,\"error\":{\"message\":\"Internal error\",\"name\":\"Error\",\"chain\":[]}}\n";
|
|
6326
|
+
//#endregion
|
|
6323
6327
|
//#region src/cli/utils/render-score-header.ts
|
|
6324
|
-
const SCORE_BAR_ANIMATION_FRAME_COUNT = 40;
|
|
6325
|
-
const SCORE_BAR_ANIMATION_FRAME_DELAY_MS = 50;
|
|
6326
|
-
const PERFECT_SCORE_RAINBOW_FRAME_COUNT = 16;
|
|
6327
|
-
const PERFECT_SCORE_RAINBOW_FRAME_DELAY_MS = 50;
|
|
6328
6328
|
const RAINBOW_HUE_SHIFT_PER_FRAME = 9;
|
|
6329
6329
|
const RAINBOW_GRADIENT_WIDTH = 80;
|
|
6330
6330
|
const RAINBOW_OKLCH_LIGHTNESS = .638;
|
|
@@ -6433,8 +6433,8 @@ const buildInitialScoreHeaderLine = ({ isPerfectScore, shouldAnimate, lineIndex,
|
|
|
6433
6433
|
};
|
|
6434
6434
|
const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectName) => Effect.gen(function* () {
|
|
6435
6435
|
const isPerfectScore = score === 100;
|
|
6436
|
-
for (let frame = 0; frame <=
|
|
6437
|
-
const progress = easeOutCubic(frame /
|
|
6436
|
+
for (let frame = 0; frame <= 40; frame += 1) {
|
|
6437
|
+
const progress = easeOutCubic(frame / 40);
|
|
6438
6438
|
const animatedScore = Math.round(score * progress);
|
|
6439
6439
|
if (isPerfectScore) {
|
|
6440
6440
|
yield* writeScoreHeaderLine(`${frame === 0 ? "" : "\x1B[4A"}\r${buildRainbowScoreHeaderFrame({
|
|
@@ -6444,16 +6444,16 @@ const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectNam
|
|
|
6444
6444
|
frame,
|
|
6445
6445
|
projectName
|
|
6446
6446
|
})}`);
|
|
6447
|
-
if (frame <
|
|
6447
|
+
if (frame < 40) yield* sleep(50);
|
|
6448
6448
|
continue;
|
|
6449
6449
|
}
|
|
6450
6450
|
const animatedScoreLine = buildScoreLine(animatedScore, score, label, projectName);
|
|
6451
6451
|
const animatedBarLine = buildScoreBar(animatedScore, score);
|
|
6452
6452
|
yield* writeScoreHeaderLine(`${frame === 0 ? "" : "\x1B[2A"}\r${buildScoreHeaderLine(scoreFaceLine, animatedScoreLine)}\n\r${buildScoreHeaderLine(barFaceLine, animatedBarLine)}\n`);
|
|
6453
|
-
if (frame <
|
|
6453
|
+
if (frame < 40) yield* sleep(50);
|
|
6454
6454
|
}
|
|
6455
6455
|
if (!isPerfectScore) return;
|
|
6456
|
-
for (let frame = 0; frame <
|
|
6456
|
+
for (let frame = 0; frame < 16; frame += 1) {
|
|
6457
6457
|
yield* writeScoreHeaderLine(`\x1b[4A\r${buildRainbowScoreHeaderFrame({
|
|
6458
6458
|
score,
|
|
6459
6459
|
displayScore: score,
|
|
@@ -6461,9 +6461,9 @@ const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectNam
|
|
|
6461
6461
|
frame,
|
|
6462
6462
|
projectName
|
|
6463
6463
|
})}`);
|
|
6464
|
-
yield* sleep(
|
|
6464
|
+
yield* sleep(50);
|
|
6465
6465
|
}
|
|
6466
|
-
yield* writeScoreHeaderLine(`\x1b[4A\r${buildFinalPerfectScoreHeaderFrame(score, label,
|
|
6466
|
+
yield* writeScoreHeaderLine(`\x1b[4A\r${buildFinalPerfectScoreHeaderFrame(score, label, 16, projectName)}\x1b[2A`);
|
|
6467
6467
|
});
|
|
6468
6468
|
const printScoreHeader = (scoreResult, projectName) => Effect.gen(function* () {
|
|
6469
6469
|
const isPerfectScore = scoreResult.score === 100;
|
|
@@ -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.
|
|
6669
|
+
const VERSION = "0.2.11-dev.d917f62";
|
|
6670
6670
|
//#endregion
|
|
6671
6671
|
//#region src/inspect.ts
|
|
6672
6672
|
const silentConsole = makeNoopConsole();
|
|
@@ -6764,9 +6764,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
6764
6764
|
const output = await Effect.runPromise(restoreLegacyThrow(programWithLayers));
|
|
6765
6765
|
const didLintFail = lintBindingMissing || output.didLintFail;
|
|
6766
6766
|
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`)));
|
|
6767
|
+
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
6768
|
else runConsole(Console.error(highlighter.error(lintFailureReason)));
|
|
6771
6769
|
const inspectDiagnostics = output.diagnostics;
|
|
6772
6770
|
const score = didLintFail ? null : output.score;
|
|
@@ -6854,10 +6852,6 @@ const finalizeAndRender = (input) => Effect.gen(function* () {
|
|
|
6854
6852
|
return buildResult();
|
|
6855
6853
|
});
|
|
6856
6854
|
//#endregion
|
|
6857
|
-
//#region src/cli/utils/constants.ts
|
|
6858
|
-
const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
|
|
6859
|
-
const INTERNAL_ERROR_JSON_FALLBACK = "{\"schemaVersion\":1,\"ok\":false,\"error\":{\"message\":\"Internal error\",\"name\":\"Error\",\"chain\":[]}}\n";
|
|
6860
|
-
//#endregion
|
|
6861
6855
|
//#region src/cli/utils/get-staged-files.ts
|
|
6862
6856
|
const stagedFilesLayer = StagedFiles.layerNode.pipe(Layer.provide(Git.layerNode));
|
|
6863
6857
|
const getStagedSourceFiles = async (directory) => {
|
|
@@ -7416,7 +7410,7 @@ const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
|
7416
7410
|
return true;
|
|
7417
7411
|
};
|
|
7418
7412
|
const shouldPromptInstallSetup = (options) => {
|
|
7419
|
-
if (!options.hasScoredScan) return false;
|
|
7413
|
+
if (!(options.hasCompletedScan ?? options.hasScoredScan ?? false)) return false;
|
|
7420
7414
|
if (options.isJsonMode) return false;
|
|
7421
7415
|
if (options.isScoreOnly) return false;
|
|
7422
7416
|
if (options.isStaged) return false;
|
|
@@ -7426,13 +7420,13 @@ const shouldPromptInstallSetup = (options) => {
|
|
|
7426
7420
|
return !hasDoctorScript(options.projectRoot);
|
|
7427
7421
|
};
|
|
7428
7422
|
const resolveInstallSetupProjectRoot = (options) => {
|
|
7429
|
-
if (options.
|
|
7423
|
+
if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
|
|
7430
7424
|
const packageDirectories = /* @__PURE__ */ new Set();
|
|
7431
|
-
for (const scanDirectory of options.
|
|
7425
|
+
for (const scanDirectory of options.scanDirectories) {
|
|
7432
7426
|
const packageDirectory = findNearestPackageDirectory(scanDirectory, options.scanRoot) ?? findNearestPackageDirectory(scanDirectory) ?? scanDirectory;
|
|
7433
7427
|
packageDirectories.add(packageDirectory);
|
|
7434
7428
|
}
|
|
7435
|
-
if (packageDirectories.size !== 1) return
|
|
7429
|
+
if (packageDirectories.size !== 1) return findNearestPackageDirectory(options.scanRoot, options.scanRoot);
|
|
7436
7430
|
return [...packageDirectories][0] ?? null;
|
|
7437
7431
|
};
|
|
7438
7432
|
const defaultWait = (milliseconds) => new Promise((resolve) => {
|
|
@@ -7467,7 +7461,7 @@ const warnSetupPromptFailure = async (options, error) => {
|
|
|
7467
7461
|
return;
|
|
7468
7462
|
}
|
|
7469
7463
|
try {
|
|
7470
|
-
const { cliLogger } = await import("./cli-logger-
|
|
7464
|
+
const { cliLogger } = await import("./cli-logger-Df45H6Lw.js").then((n) => n.n);
|
|
7471
7465
|
cliLogger.warn(message);
|
|
7472
7466
|
} catch {}
|
|
7473
7467
|
};
|
|
@@ -7483,7 +7477,7 @@ const promptInstallSetup = async (options) => {
|
|
|
7483
7477
|
writeLine("You can always run `npx react-doctor@latest install` to set it up later.");
|
|
7484
7478
|
return;
|
|
7485
7479
|
}
|
|
7486
|
-
const install = options.install ?? (await Promise.resolve().then(() =>
|
|
7480
|
+
const install = options.install ?? (await Promise.resolve().then(() => install_react_doctor_exports)).runInstallReactDoctor;
|
|
7487
7481
|
const previousExitCode = process.exitCode;
|
|
7488
7482
|
let setupExitCode;
|
|
7489
7483
|
try {
|
|
@@ -7502,7 +7496,7 @@ const promptInstallSetup = async (options) => {
|
|
|
7502
7496
|
}
|
|
7503
7497
|
};
|
|
7504
7498
|
const shouldShowAgentInstallHint = (options) => {
|
|
7505
|
-
if (!options.hasScoredScan) return false;
|
|
7499
|
+
if (!(options.hasCompletedScan ?? options.hasScoredScan ?? false)) return false;
|
|
7506
7500
|
if (options.isJsonMode) return false;
|
|
7507
7501
|
if (options.isScoreOnly) return false;
|
|
7508
7502
|
if (options.isStaged) return false;
|
|
@@ -7567,15 +7561,17 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isQui
|
|
|
7567
7561
|
const { scanScope } = await prompts({
|
|
7568
7562
|
type: "select",
|
|
7569
7563
|
name: "scanScope",
|
|
7570
|
-
message: "
|
|
7564
|
+
message: "Choose what to scan",
|
|
7571
7565
|
choices: [{
|
|
7572
7566
|
title: "Full codebase",
|
|
7567
|
+
description: "Scan every source file",
|
|
7573
7568
|
value: "full"
|
|
7574
7569
|
}, {
|
|
7575
|
-
title: `Changed files (${changedSourceFiles.length})`,
|
|
7570
|
+
title: diffInfo.isCurrentChanges ? `Uncommitted changes (${changedSourceFiles.length})` : `Changed files on ${diffInfo.currentBranch ?? "this branch"} (${changedSourceFiles.length})`,
|
|
7571
|
+
description: diffInfo.isCurrentChanges ? "Compare working tree changes against HEAD" : `Compare against ${diffInfo.baseBranch} from the branch merge-base`,
|
|
7576
7572
|
value: "branch"
|
|
7577
7573
|
}],
|
|
7578
|
-
initial: 0
|
|
7574
|
+
initial: diffInfo.isCurrentChanges ? 0 : 1
|
|
7579
7575
|
});
|
|
7580
7576
|
return scanScope === "branch";
|
|
7581
7577
|
};
|
|
@@ -7605,13 +7601,13 @@ const VALID_FAIL_ON_LEVELS = new Set([
|
|
|
7605
7601
|
"warning",
|
|
7606
7602
|
"none"
|
|
7607
7603
|
]);
|
|
7608
|
-
const DEFAULT_FAIL_ON_LEVEL = "
|
|
7604
|
+
const DEFAULT_FAIL_ON_LEVEL = "none";
|
|
7609
7605
|
const isValidFailOnLevel = (level) => VALID_FAIL_ON_LEVELS.has(level);
|
|
7610
7606
|
const resolveFailOnLevel = (flags, userConfig) => {
|
|
7611
7607
|
const sourceValue = flags.failOn ?? userConfig?.failOn ?? DEFAULT_FAIL_ON_LEVEL;
|
|
7612
7608
|
if (isValidFailOnLevel(sourceValue)) return sourceValue;
|
|
7613
|
-
cliLogger.warn(`Invalid failOn level "${sourceValue}". Expected one of: error, warning, none. Falling back to "
|
|
7614
|
-
return
|
|
7609
|
+
cliLogger.warn(`Invalid failOn level "${sourceValue}". Expected one of: error, warning, none. Falling back to "${DEFAULT_FAIL_ON_LEVEL}".`);
|
|
7610
|
+
return DEFAULT_FAIL_ON_LEVEL;
|
|
7615
7611
|
};
|
|
7616
7612
|
//#endregion
|
|
7617
7613
|
//#region src/cli/utils/resolve-project-diff-include-paths.ts
|
|
@@ -7709,7 +7705,7 @@ const selectProjects = async (rootDirectory, projectFlag, skipPrompts) => {
|
|
|
7709
7705
|
}
|
|
7710
7706
|
if (packages.length === 0) return [rootDirectory];
|
|
7711
7707
|
if (packages.length === 1) {
|
|
7712
|
-
cliLogger.log(`${highlighter.success("✔")} Select projects
|
|
7708
|
+
cliLogger.log(`${highlighter.success("✔")} Select projects ${highlighter.dim("›")} ${packages[0].name}`);
|
|
7713
7709
|
return [packages[0].directory];
|
|
7714
7710
|
}
|
|
7715
7711
|
if (projectFlag) return resolveProjectFlag(projectFlag, packages);
|
|
@@ -7733,13 +7729,13 @@ const resolveProjectFlag = (projectFlag, workspacePackages) => {
|
|
|
7733
7729
|
return resolvedDirectories;
|
|
7734
7730
|
};
|
|
7735
7731
|
const printDiscoveredProjects = (packages) => {
|
|
7736
|
-
cliLogger.log(`${highlighter.success("✔")} Select projects
|
|
7732
|
+
cliLogger.log(`${highlighter.success("✔")} Select projects ${highlighter.dim("›")} ${packages.map((workspacePackage) => workspacePackage.name).join(", ")}`);
|
|
7737
7733
|
};
|
|
7738
7734
|
const promptProjectSelection = async (workspacePackages, rootDirectory) => {
|
|
7739
7735
|
const { selectedDirectories } = await prompts({
|
|
7740
7736
|
type: "multiselect",
|
|
7741
7737
|
name: "selectedDirectories",
|
|
7742
|
-
message: "Select projects
|
|
7738
|
+
message: "Select projects",
|
|
7743
7739
|
choices: workspacePackages.map((workspacePackage) => ({
|
|
7744
7740
|
title: workspacePackage.name,
|
|
7745
7741
|
description: path.relative(rootDirectory, workspacePackage.directory),
|
|
@@ -7896,7 +7892,14 @@ const inspectAction = async (directory, flags) => {
|
|
|
7896
7892
|
cliLogger.log(`Scanning ${highlighter.info(`${stagedFiles.length}`)} staged files...`);
|
|
7897
7893
|
cliLogger.break();
|
|
7898
7894
|
}
|
|
7899
|
-
const
|
|
7895
|
+
const tempDirectory = mkdtempSync(path.join(tmpdir(), STAGED_FILES_TEMP_DIR_PREFIX));
|
|
7896
|
+
const snapshot = await materializeStagedFiles(resolvedDirectory, stagedFiles, tempDirectory).catch((error) => {
|
|
7897
|
+
rmSync(tempDirectory, {
|
|
7898
|
+
recursive: true,
|
|
7899
|
+
force: true
|
|
7900
|
+
});
|
|
7901
|
+
throw error;
|
|
7902
|
+
});
|
|
7900
7903
|
try {
|
|
7901
7904
|
const scanResult = await inspect(snapshot.tempDirectory, {
|
|
7902
7905
|
...scanOptions,
|
|
@@ -7905,7 +7908,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
7905
7908
|
});
|
|
7906
7909
|
const remappedDiagnostics = scanResult.diagnostics.map((diagnostic) => ({
|
|
7907
7910
|
...diagnostic,
|
|
7908
|
-
filePath: path.isAbsolute(diagnostic.filePath) ? diagnostic.filePath.replaceAll(snapshot.tempDirectory, resolvedDirectory) : diagnostic.filePath
|
|
7911
|
+
filePath: path.isAbsolute(diagnostic.filePath) ? diagnostic.filePath.replaceAll(snapshot.tempDirectory, () => resolvedDirectory) : diagnostic.filePath
|
|
7909
7912
|
}));
|
|
7910
7913
|
finalizeScans({
|
|
7911
7914
|
diagnostics: remappedDiagnostics,
|
|
@@ -7996,13 +7999,13 @@ const inspectAction = async (directory, flags) => {
|
|
|
7996
7999
|
});
|
|
7997
8000
|
const setupProjectRoot = resolveInstallSetupProjectRoot({
|
|
7998
8001
|
scanRoot: resolvedDirectory,
|
|
7999
|
-
|
|
8002
|
+
scanDirectories: projectDirectories
|
|
8000
8003
|
});
|
|
8001
8004
|
if (setupProjectRoot !== null) {
|
|
8002
|
-
const
|
|
8005
|
+
const hasCompletedScan = completedScans.length > 0;
|
|
8003
8006
|
await promptInstallSetup({
|
|
8004
8007
|
projectRoot: setupProjectRoot,
|
|
8005
|
-
|
|
8008
|
+
hasCompletedScan,
|
|
8006
8009
|
issueCount: filterDiagnosticsForSurface(allDiagnostics, scanOptions.outputSurface ?? "cli", userConfig).length,
|
|
8007
8010
|
isJsonMode,
|
|
8008
8011
|
isScoreOnly,
|
|
@@ -8011,7 +8014,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
8011
8014
|
});
|
|
8012
8015
|
if (shouldShowAgentInstallHint({
|
|
8013
8016
|
projectRoot: setupProjectRoot,
|
|
8014
|
-
|
|
8017
|
+
hasCompletedScan,
|
|
8015
8018
|
isJsonMode,
|
|
8016
8019
|
isScoreOnly,
|
|
8017
8020
|
isStaged: Boolean(flags.staged)
|
|
@@ -8643,8 +8646,12 @@ const installReactDoctorGitHook = (options) => {
|
|
|
8643
8646
|
return installDirectGitHook(options);
|
|
8644
8647
|
};
|
|
8645
8648
|
//#endregion
|
|
8646
|
-
//#region src/cli/utils/install-
|
|
8647
|
-
var
|
|
8649
|
+
//#region src/cli/utils/install-react-doctor.ts
|
|
8650
|
+
var install_react_doctor_exports = /* @__PURE__ */ __exportAll({ runInstallReactDoctor: () => runInstallReactDoctor });
|
|
8651
|
+
const SETUP_OPTION_GIT_HOOK = "git-hook";
|
|
8652
|
+
const SETUP_OPTION_AGENT_HOOKS = "agent-hooks";
|
|
8653
|
+
const SETUP_OPTION_WORKFLOW = "workflow";
|
|
8654
|
+
const SETUP_OPTION_SKIP = "skip";
|
|
8648
8655
|
const CONFIG_ONLY_GIT_HOOK_KINDS = new Set([
|
|
8649
8656
|
"ghooks",
|
|
8650
8657
|
"git-hooks-js",
|
|
@@ -8825,7 +8832,30 @@ const getSkillSourceDirectory = () => {
|
|
|
8825
8832
|
const distDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
8826
8833
|
return path.join(distDirectory, "skills", SKILL_NAME);
|
|
8827
8834
|
};
|
|
8828
|
-
const
|
|
8835
|
+
const canInstallNativeAgentHooks = (agents) => agents.some((agent) => agent === "claude-code" || agent === "cursor");
|
|
8836
|
+
const buildWorkflowContent = () => [
|
|
8837
|
+
"name: React Doctor",
|
|
8838
|
+
"",
|
|
8839
|
+
"on:",
|
|
8840
|
+
" pull_request:",
|
|
8841
|
+
" branches: [main]",
|
|
8842
|
+
"",
|
|
8843
|
+
"permissions:",
|
|
8844
|
+
" contents: read",
|
|
8845
|
+
" pull-requests: write",
|
|
8846
|
+
"",
|
|
8847
|
+
"jobs:",
|
|
8848
|
+
" react-doctor:",
|
|
8849
|
+
" runs-on: ubuntu-latest",
|
|
8850
|
+
" steps:",
|
|
8851
|
+
" - uses: actions/checkout@v4",
|
|
8852
|
+
" - uses: millionco/react-doctor@main",
|
|
8853
|
+
" with:",
|
|
8854
|
+
" github-token: ${{ secrets.GITHUB_TOKEN }}",
|
|
8855
|
+
" diff: main",
|
|
8856
|
+
""
|
|
8857
|
+
].join("\n");
|
|
8858
|
+
const runInstallReactDoctor = async (options = {}) => {
|
|
8829
8859
|
const requestedProjectRoot = options.projectRoot ?? process.cwd();
|
|
8830
8860
|
const projectRoot = findNearestPackageDirectory(requestedProjectRoot) ?? requestedProjectRoot;
|
|
8831
8861
|
const sourceDir = options.sourceDir ?? getSkillSourceDirectory();
|
|
@@ -8846,7 +8876,8 @@ const runInstallSkill = async (options = {}) => {
|
|
|
8846
8876
|
const gitHookTarget = options.gitHookPath === void 0 ? detectGitHookTarget(projectRoot) : options.gitHookPath === null ? null : buildManualGitHookTarget(options.gitHookPath, projectRoot);
|
|
8847
8877
|
const gitHookPath = gitHookTarget?.hookPath;
|
|
8848
8878
|
const promptOptions = options.onPromptCancel === void 0 ? {} : { onCancel: options.onPromptCancel };
|
|
8849
|
-
const
|
|
8879
|
+
const prompt = options.prompt ?? prompts;
|
|
8880
|
+
const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
|
|
8850
8881
|
type: "multiselect",
|
|
8851
8882
|
name: "agents",
|
|
8852
8883
|
message: `Install the ${highlighter.info(`/react-doctor`)} skill for:`,
|
|
@@ -8859,13 +8890,48 @@ const runInstallSkill = async (options = {}) => {
|
|
|
8859
8890
|
min: 1
|
|
8860
8891
|
}, promptOptions)).agents ?? [];
|
|
8861
8892
|
if (selectedAgents.length === 0) return;
|
|
8862
|
-
const
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8893
|
+
const workflowsDirectory = path.join(projectRoot, ".github", "workflows");
|
|
8894
|
+
const workflowTargetPath = path.join(workflowsDirectory, "react-doctor.yml");
|
|
8895
|
+
const hasExistingWorkflows = existsSync(workflowsDirectory);
|
|
8896
|
+
const canInstallWorkflow = !existsSync(workflowTargetPath);
|
|
8897
|
+
const setupActionChoices = [
|
|
8898
|
+
...gitHookPath === null || gitHookPath === void 0 ? [] : [{
|
|
8899
|
+
title: "Pre-commit hook",
|
|
8900
|
+
description: "Check staged changes before each commit",
|
|
8901
|
+
value: SETUP_OPTION_GIT_HOOK,
|
|
8902
|
+
selected: true
|
|
8903
|
+
}],
|
|
8904
|
+
...canInstallNativeAgentHooks(selectedAgents) ? [{
|
|
8905
|
+
title: "Agent hooks",
|
|
8906
|
+
description: "Ask Claude Code or Cursor to scan after code edits",
|
|
8907
|
+
value: SETUP_OPTION_AGENT_HOOKS,
|
|
8908
|
+
selected: Boolean(options.agentHooks)
|
|
8909
|
+
}] : [],
|
|
8910
|
+
...canInstallWorkflow ? [{
|
|
8911
|
+
title: "GitHub Actions workflow",
|
|
8912
|
+
description: "Scan pull requests in CI",
|
|
8913
|
+
value: SETUP_OPTION_WORKFLOW,
|
|
8914
|
+
selected: hasExistingWorkflows
|
|
8915
|
+
}] : []
|
|
8916
|
+
];
|
|
8917
|
+
const setupChoices = setupActionChoices.length === 0 ? [] : [{
|
|
8918
|
+
title: "Skip optional setup",
|
|
8919
|
+
description: "Install only the agent skill and package setup",
|
|
8920
|
+
value: SETUP_OPTION_SKIP,
|
|
8921
|
+
selected: false
|
|
8922
|
+
}, ...setupActionChoices];
|
|
8923
|
+
const selectedSetupOptions = skipPrompts || setupChoices.length === 0 ? [] : (await prompt({
|
|
8924
|
+
type: "multiselect",
|
|
8925
|
+
name: "setupOptions",
|
|
8926
|
+
message: "Select additional React Doctor setup:",
|
|
8927
|
+
choices: setupChoices,
|
|
8928
|
+
instructions: false
|
|
8929
|
+
}, promptOptions)).setupOptions ?? [];
|
|
8930
|
+
const selectedSetupActions = selectedSetupOptions.filter((setupOption) => setupOption !== SETUP_OPTION_SKIP);
|
|
8931
|
+
const didSkipOptionalSetup = selectedSetupActions.length === 0 && selectedSetupOptions.includes(SETUP_OPTION_SKIP);
|
|
8932
|
+
const shouldInstallGitHook = gitHookPath !== null && gitHookPath !== void 0 && (Boolean(options.yes) || !didSkipOptionalSetup && selectedSetupActions.includes(SETUP_OPTION_GIT_HOOK));
|
|
8933
|
+
const shouldInstallAgentHooks = Boolean(options.agentHooks) || !didSkipOptionalSetup && selectedSetupActions.includes(SETUP_OPTION_AGENT_HOOKS);
|
|
8934
|
+
const shouldInstallWorkflow = !skipPrompts && !didSkipOptionalSetup && canInstallWorkflow && selectedSetupActions.includes(SETUP_OPTION_WORKFLOW);
|
|
8869
8935
|
if (options.dryRun) {
|
|
8870
8936
|
cliLogger.log(`Dry run — would install ${SKILL_NAME} skill for:`);
|
|
8871
8937
|
for (const agent of selectedAgents) cliLogger.dim(` - ${getSkillAgentConfig(agent).displayName}`);
|
|
@@ -8874,6 +8940,7 @@ const runInstallSkill = async (options = {}) => {
|
|
|
8874
8940
|
cliLogger.dim(" Dev dependency: react-doctor");
|
|
8875
8941
|
if (shouldInstallGitHook) cliLogger.dim(` Git hook: ${gitHookPath}`);
|
|
8876
8942
|
if (shouldInstallAgentHooks) cliLogger.dim(" Agent hooks: Claude Code / Cursor when selected");
|
|
8943
|
+
if (shouldInstallWorkflow) cliLogger.dim(` GitHub Actions workflow: ${path.relative(projectRoot, workflowTargetPath)}`);
|
|
8877
8944
|
return;
|
|
8878
8945
|
}
|
|
8879
8946
|
const installSpinner = spinner(`Installing ${SKILL_NAME} skill...`).start();
|
|
@@ -8921,47 +8988,15 @@ const runInstallSkill = async (options = {}) => {
|
|
|
8921
8988
|
throw error;
|
|
8922
8989
|
}
|
|
8923
8990
|
}
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
}, promptOptions);
|
|
8934
|
-
if (shouldInstallWorkflow) {
|
|
8935
|
-
if (!hasExistingWorkflows) mkdirSync(workflowsDirectory, { recursive: true });
|
|
8936
|
-
const workflowSpinner = spinner("Adding GitHub Actions workflow...").start();
|
|
8937
|
-
try {
|
|
8938
|
-
writeFileSync(workflowTargetPath, [
|
|
8939
|
-
"name: React Doctor",
|
|
8940
|
-
"",
|
|
8941
|
-
"on:",
|
|
8942
|
-
" pull_request:",
|
|
8943
|
-
" branches: [main]",
|
|
8944
|
-
"",
|
|
8945
|
-
"permissions:",
|
|
8946
|
-
" contents: read",
|
|
8947
|
-
" pull-requests: write",
|
|
8948
|
-
"",
|
|
8949
|
-
"jobs:",
|
|
8950
|
-
" react-doctor:",
|
|
8951
|
-
" runs-on: ubuntu-latest",
|
|
8952
|
-
" steps:",
|
|
8953
|
-
" - uses: actions/checkout@v4",
|
|
8954
|
-
" - uses: millionco/react-doctor@main",
|
|
8955
|
-
" with:",
|
|
8956
|
-
" github-token: ${{ secrets.GITHUB_TOKEN }}",
|
|
8957
|
-
" diff: main",
|
|
8958
|
-
""
|
|
8959
|
-
].join("\n"));
|
|
8960
|
-
workflowSpinner.succeed(`GitHub Actions workflow added at ${path.relative(projectRoot, workflowTargetPath)}.`);
|
|
8961
|
-
} catch (error) {
|
|
8962
|
-
workflowSpinner.fail("Failed to add GitHub Actions workflow.");
|
|
8963
|
-
throw error;
|
|
8964
|
-
}
|
|
8991
|
+
if (shouldInstallWorkflow) {
|
|
8992
|
+
if (!hasExistingWorkflows) mkdirSync(workflowsDirectory, { recursive: true });
|
|
8993
|
+
const workflowSpinner = spinner("Adding GitHub Actions workflow...").start();
|
|
8994
|
+
try {
|
|
8995
|
+
writeFileSync(workflowTargetPath, buildWorkflowContent());
|
|
8996
|
+
workflowSpinner.succeed(`GitHub Actions workflow added at ${path.relative(projectRoot, workflowTargetPath)}.`);
|
|
8997
|
+
} catch (error) {
|
|
8998
|
+
workflowSpinner.fail("Failed to add GitHub Actions workflow.");
|
|
8999
|
+
throw error;
|
|
8965
9000
|
}
|
|
8966
9001
|
}
|
|
8967
9002
|
};
|
|
@@ -8971,7 +9006,7 @@ const installAction = async (options, command) => {
|
|
|
8971
9006
|
Effect.runSync(printBrandedHeader);
|
|
8972
9007
|
try {
|
|
8973
9008
|
const parentOptions = command?.parent?.opts?.();
|
|
8974
|
-
await
|
|
9009
|
+
await runInstallReactDoctor({
|
|
8975
9010
|
yes: options.yes ?? parentOptions?.yes,
|
|
8976
9011
|
dryRun: options.dryRun,
|
|
8977
9012
|
agentHooks: options.agentHooks,
|
|
@@ -9111,7 +9146,7 @@ const stripUnknownCliFlags = (argv) => {
|
|
|
9111
9146
|
//#region src/cli/index.ts
|
|
9112
9147
|
process.on("SIGINT", exitGracefully);
|
|
9113
9148
|
process.on("SIGTERM", exitGracefully);
|
|
9114
|
-
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:
|
|
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", `
|
|
9115
9150
|
${highlighter.dim("Configuration:")}
|
|
9116
9151
|
Place a ${highlighter.info("react-doctor.config.json")} (or ${highlighter.info("\"reactDoctor\"")} key in your package.json) in the project root.
|
|
9117
9152
|
CLI flags always override config values. See the README for the full schema.
|
package/dist/index.d.ts
CHANGED
|
@@ -232,7 +232,9 @@ interface ReactDoctorConfig {
|
|
|
232
232
|
* `categories` field, but keyed by React Doctor's display
|
|
233
233
|
* categories (`"Server"`, `"React Native"`, `"Architecture"`,
|
|
234
234
|
* `"Bundle Size"`, `"State & Effects"`, `"Security"`,
|
|
235
|
-
* `"Accessibility"`, `"Performance"`, `"Correctness"`,
|
|
235
|
+
* `"Accessibility"`, `"Performance"`, `"Correctness"`,
|
|
236
|
+
* `"Next.js"`, `"Preact"`, `"TanStack Query"`,
|
|
237
|
+
* `"TanStack Start"`, …).
|
|
236
238
|
*
|
|
237
239
|
* ```json
|
|
238
240
|
* { "categories": { "React Native": "warn", "Server": "off" } }
|
|
@@ -296,7 +298,7 @@ interface Diagnostic {
|
|
|
296
298
|
}
|
|
297
299
|
//#endregion
|
|
298
300
|
//#region src/types/project-info.d.ts
|
|
299
|
-
type Framework = "nextjs" | "vite" | "cra" | "remix" | "gatsby" | "expo" | "react-native" | "tanstack-start" | "unknown";
|
|
301
|
+
type Framework = "nextjs" | "vite" | "cra" | "remix" | "gatsby" | "expo" | "react-native" | "tanstack-start" | "preact" | "unknown";
|
|
300
302
|
interface ProjectInfo {
|
|
301
303
|
rootDirectory: string;
|
|
302
304
|
projectName: string;
|
|
@@ -307,6 +309,18 @@ interface ProjectInfo {
|
|
|
307
309
|
hasTypeScript: boolean;
|
|
308
310
|
hasReactCompiler: boolean;
|
|
309
311
|
hasTanStackQuery: boolean;
|
|
312
|
+
/**
|
|
313
|
+
* The declared `preact` version spec, or `null` when Preact isn't a
|
|
314
|
+
* dependency. Parallels `reactVersion` so a React-compatible runtime is
|
|
315
|
+
* modeled the same way React is. Drives the `preact` capability in
|
|
316
|
+
* `buildCapabilities` (which gates every `preact-*` rule) — keyed off
|
|
317
|
+
* this rather than `framework` because the dominant Preact setup
|
|
318
|
+
* (Preact-on-Vite) classifies as `framework: "vite"` but still needs
|
|
319
|
+
* Preact rules to fire.
|
|
320
|
+
*/
|
|
321
|
+
preactVersion: string | null;
|
|
322
|
+
/** Parsed major from `preactVersion`, or `null` when absent/unparseable. Mirrors `reactMajorVersion`. */
|
|
323
|
+
preactMajorVersion: number | null;
|
|
310
324
|
/**
|
|
311
325
|
* `true` when the project (or any of its workspace packages) declares
|
|
312
326
|
* React Native or Expo as a dependency. Enables the `react-native`
|
|
@@ -321,6 +335,13 @@ interface ProjectInfo {
|
|
|
321
335
|
* — no `rn-*` rules load for the project at all.
|
|
322
336
|
*/
|
|
323
337
|
hasReactNativeWorkspace: boolean;
|
|
338
|
+
/**
|
|
339
|
+
* `true` when the project (or any of its workspace packages) declares
|
|
340
|
+
* `react-native-reanimated`. Lets diagnostics surface reanimated's
|
|
341
|
+
* Compiler-compatible `.get()` / `.set()` accessors only where they
|
|
342
|
+
* apply, instead of on every React Native project.
|
|
343
|
+
*/
|
|
344
|
+
hasReanimated: boolean;
|
|
324
345
|
sourceFileCount: number;
|
|
325
346
|
}
|
|
326
347
|
//#endregion
|