react-doctor 0.5.6 → 0.5.7-dev.350a6ed
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.js +774 -412
- package/dist/index.d.ts +23 -3
- package/dist/index.js +324 -196
- package/dist/lsp.js +343 -220
- package/package.json +5 -5
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="87ed2e7a-69a3-5a4d-a104-8ba680ac1aa2")}catch(e){}}();
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import * as NodeChildProcess from "node:child_process";
|
|
5
5
|
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
@@ -14,7 +14,7 @@ import * as OS from "node:os";
|
|
|
14
14
|
import os, { tmpdir } from "node:os";
|
|
15
15
|
import { parseJSON5 } from "confbox";
|
|
16
16
|
import * as NodeUrl from "node:url";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
18
|
import { createJiti } from "jiti";
|
|
19
19
|
import * as Crypto from "node:crypto";
|
|
20
20
|
import crypto, { createHash, randomUUID } from "node:crypto";
|
|
@@ -22358,7 +22358,8 @@ var Diagnostic = class extends Class("Diagnostic")({
|
|
|
22358
22358
|
category: String$1,
|
|
22359
22359
|
fileContext: optional(Literals(["test", "story"])),
|
|
22360
22360
|
suppressionHint: optional(String$1),
|
|
22361
|
-
relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation))
|
|
22361
|
+
relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation)),
|
|
22362
|
+
fixGroupId: optional(String$1)
|
|
22362
22363
|
}) {};
|
|
22363
22364
|
const JsonReportMode = Literals([
|
|
22364
22365
|
"full",
|
|
@@ -22400,6 +22401,7 @@ var JsonReportProjectEntry = class extends Class("JsonReportProjectEntry")({
|
|
|
22400
22401
|
score: Unknown,
|
|
22401
22402
|
skippedChecks: ArraySchema(String$1),
|
|
22402
22403
|
skippedCheckReasons: optional(Record$1(String$1, String$1)),
|
|
22404
|
+
scannedFileCount: optional(Number$1),
|
|
22403
22405
|
elapsedMilliseconds: Number$1
|
|
22404
22406
|
}) {};
|
|
22405
22407
|
/**
|
|
@@ -35893,6 +35895,7 @@ const isLargeMinifiedFile = (absolutePath) => {
|
|
|
35893
35895
|
if (sizeBytes < 2e4) return false;
|
|
35894
35896
|
return isMinifiedSource(absolutePath);
|
|
35895
35897
|
};
|
|
35898
|
+
const isErrnoException = (error) => error instanceof Error && "code" in error;
|
|
35896
35899
|
const IGNORABLE_READDIR_ERROR_CODES = new Set([
|
|
35897
35900
|
"EACCES",
|
|
35898
35901
|
"EPERM",
|
|
@@ -35902,11 +35905,7 @@ const IGNORABLE_READDIR_ERROR_CODES = new Set([
|
|
|
35902
35905
|
"ELOOP",
|
|
35903
35906
|
"ENAMETOOLONG"
|
|
35904
35907
|
]);
|
|
35905
|
-
const isIgnorableReaddirError = (error) =>
|
|
35906
|
-
if (typeof error !== "object" || error === null) return false;
|
|
35907
|
-
const errorCode = error.code;
|
|
35908
|
-
return typeof errorCode === "string" && IGNORABLE_READDIR_ERROR_CODES.has(errorCode);
|
|
35909
|
-
};
|
|
35908
|
+
const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
|
|
35910
35909
|
const readDirectoryEntries = (directoryPath) => {
|
|
35911
35910
|
try {
|
|
35912
35911
|
return NFS.readdirSync(directoryPath, { withFileTypes: true });
|
|
@@ -35953,7 +35952,7 @@ const readPackageJsonUncached = (packageJsonPath) => {
|
|
|
35953
35952
|
return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
|
|
35954
35953
|
} catch (error) {
|
|
35955
35954
|
if (error instanceof SyntaxError) return {};
|
|
35956
|
-
if (error
|
|
35955
|
+
if (isErrnoException(error)) {
|
|
35957
35956
|
const { code } = error;
|
|
35958
35957
|
if (code === "EISDIR" || code === "EACCES" || code === "EPERM" || code === "ENOENT") return {};
|
|
35959
35958
|
}
|
|
@@ -36678,17 +36677,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
|
|
|
36678
36677
|
return false;
|
|
36679
36678
|
};
|
|
36680
36679
|
const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
|
|
36681
|
-
const
|
|
36682
|
-
const spec = packageJson.dependencies?.
|
|
36680
|
+
const getDependencySpec = (packageJson, packageName) => {
|
|
36681
|
+
const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
|
|
36683
36682
|
return typeof spec === "string" ? spec : null;
|
|
36684
36683
|
};
|
|
36685
|
-
const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson,
|
|
36684
|
+
const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
|
|
36686
36685
|
const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
|
|
36687
|
-
const
|
|
36688
|
-
const spec = packageJson.dependencies?.["@shopify/flash-list"] ?? packageJson.devDependencies?.["@shopify/flash-list"] ?? packageJson.peerDependencies?.["@shopify/flash-list"] ?? packageJson.optionalDependencies?.["@shopify/flash-list"];
|
|
36689
|
-
return typeof spec === "string" ? spec : null;
|
|
36690
|
-
};
|
|
36691
|
-
const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getShopifyFlashListDependencySpec);
|
|
36686
|
+
const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
|
|
36692
36687
|
const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
|
|
36693
36688
|
if (version === null || !isCatalogReference(version)) return version;
|
|
36694
36689
|
const catalogName = extractCatalogName(version);
|
|
@@ -36700,11 +36695,7 @@ const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson,
|
|
|
36700
36695
|
if (!isFile(monorepoPackageJsonPath)) return version;
|
|
36701
36696
|
return resolveCatalogVersion(readPackageJson$1(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
|
|
36702
36697
|
};
|
|
36703
|
-
const
|
|
36704
|
-
const spec = packageJson.dependencies?.next ?? packageJson.devDependencies?.next ?? packageJson.peerDependencies?.next ?? packageJson.optionalDependencies?.next;
|
|
36705
|
-
return typeof spec === "string" ? spec : null;
|
|
36706
|
-
};
|
|
36707
|
-
const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getNextjsDependencySpec);
|
|
36698
|
+
const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
|
|
36708
36699
|
const getPreactVersion = (packageJson) => {
|
|
36709
36700
|
return {
|
|
36710
36701
|
...packageJson.peerDependencies,
|
|
@@ -36793,6 +36784,11 @@ const ES_TARGET_YEAR_BY_NAME = {
|
|
|
36793
36784
|
esnext: 9999
|
|
36794
36785
|
};
|
|
36795
36786
|
/**
|
|
36787
|
+
* tsconfig filenames probed when resolving a project's TypeScript
|
|
36788
|
+
* compiler options — the root config first, then a monorepo base config.
|
|
36789
|
+
*/
|
|
36790
|
+
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
36791
|
+
/**
|
|
36796
36792
|
* Project-config files that `StagedFiles.materialize` copies into
|
|
36797
36793
|
* the temp directory alongside staged sources so oxlint resolves
|
|
36798
36794
|
* `tsconfig` / `package.json` / lint configs the same way it would
|
|
@@ -36865,6 +36861,13 @@ const APP_ONLY_RULE_KEYS = new Set([
|
|
|
36865
36861
|
]);
|
|
36866
36862
|
const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
|
|
36867
36863
|
const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
|
|
36864
|
+
const ROOT_CAUSE_GROUPABLE_RULE_KEYS = new Set([
|
|
36865
|
+
"react-doctor/no-derived-state",
|
|
36866
|
+
"react-doctor/no-derived-state-effect",
|
|
36867
|
+
"react-doctor/no-derived-useState",
|
|
36868
|
+
"react-doctor/no-adjust-state-on-prop-change",
|
|
36869
|
+
"react-doctor/no-reset-all-state-on-prop-change"
|
|
36870
|
+
]);
|
|
36868
36871
|
const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
|
|
36869
36872
|
const CONFIG_CACHE_TTL_MS = 300 * 1e3;
|
|
36870
36873
|
const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
|
|
@@ -37314,6 +37317,7 @@ const isTailwindAtLeast = (detected, required) => {
|
|
|
37314
37317
|
if (detected.major !== required.major) return detected.major > required.major;
|
|
37315
37318
|
return detected.minor >= required.minor;
|
|
37316
37319
|
};
|
|
37320
|
+
const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
|
|
37317
37321
|
var InvalidGlobPatternError = class extends Error {
|
|
37318
37322
|
pattern;
|
|
37319
37323
|
reason;
|
|
@@ -37342,7 +37346,7 @@ const compileGlobPattern = (rawPattern) => {
|
|
|
37342
37346
|
try {
|
|
37343
37347
|
return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
|
|
37344
37348
|
} catch (caughtError) {
|
|
37345
|
-
throw new InvalidGlobPatternError(rawPattern,
|
|
37349
|
+
throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
|
|
37346
37350
|
}
|
|
37347
37351
|
};
|
|
37348
37352
|
const compileGlobPatternsLenient = (patterns, onInvalid) => {
|
|
@@ -37438,115 +37442,6 @@ const buildRuleSeverityControls = (config) => {
|
|
|
37438
37442
|
...config.buckets !== void 0 ? { buckets: config.buckets } : {}
|
|
37439
37443
|
};
|
|
37440
37444
|
};
|
|
37441
|
-
const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
|
|
37442
|
-
const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
|
|
37443
|
-
const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
|
|
37444
|
-
let stringDelimiter = null;
|
|
37445
|
-
for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
|
|
37446
|
-
const character = line[charIndex];
|
|
37447
|
-
if (stringDelimiter !== null) {
|
|
37448
|
-
if (character === "\\") {
|
|
37449
|
-
charIndex++;
|
|
37450
|
-
continue;
|
|
37451
|
-
}
|
|
37452
|
-
if (character === stringDelimiter) stringDelimiter = null;
|
|
37453
|
-
continue;
|
|
37454
|
-
}
|
|
37455
|
-
if (character === "\"" || character === "'" || character === "`") {
|
|
37456
|
-
stringDelimiter = character;
|
|
37457
|
-
continue;
|
|
37458
|
-
}
|
|
37459
|
-
if (character === "/" && line[charIndex + 1] === "/") return true;
|
|
37460
|
-
}
|
|
37461
|
-
return false;
|
|
37462
|
-
};
|
|
37463
|
-
const findOpenerTagOnLine = (line) => {
|
|
37464
|
-
for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
|
|
37465
|
-
if (match.index === void 0) continue;
|
|
37466
|
-
if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
|
|
37467
|
-
}
|
|
37468
|
-
return null;
|
|
37469
|
-
};
|
|
37470
|
-
const findJsxOpenerSpan = (lines, openerLineIndex) => {
|
|
37471
|
-
const openerLine = lines[openerLineIndex];
|
|
37472
|
-
if (openerLine === void 0) return null;
|
|
37473
|
-
const opener = findOpenerTagOnLine(openerLine);
|
|
37474
|
-
if (!opener) return null;
|
|
37475
|
-
const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
|
|
37476
|
-
let braceDepth = 0;
|
|
37477
|
-
let innerAngleDepth = 0;
|
|
37478
|
-
let stringDelimiter = null;
|
|
37479
|
-
for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
|
|
37480
|
-
const currentLine = lines[lineIndex];
|
|
37481
|
-
const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
|
|
37482
|
-
for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
|
|
37483
|
-
const character = currentLine[charIndex];
|
|
37484
|
-
if (stringDelimiter !== null) {
|
|
37485
|
-
if (character === "\\") {
|
|
37486
|
-
charIndex++;
|
|
37487
|
-
continue;
|
|
37488
|
-
}
|
|
37489
|
-
if (character === stringDelimiter) stringDelimiter = null;
|
|
37490
|
-
continue;
|
|
37491
|
-
}
|
|
37492
|
-
if (character === "\"" || character === "'" || character === "`") {
|
|
37493
|
-
stringDelimiter = character;
|
|
37494
|
-
continue;
|
|
37495
|
-
}
|
|
37496
|
-
if (character === "{") {
|
|
37497
|
-
braceDepth++;
|
|
37498
|
-
continue;
|
|
37499
|
-
}
|
|
37500
|
-
if (character === "}") {
|
|
37501
|
-
braceDepth--;
|
|
37502
|
-
continue;
|
|
37503
|
-
}
|
|
37504
|
-
if (braceDepth !== 0) continue;
|
|
37505
|
-
if (character === "<") {
|
|
37506
|
-
const followCharacter = currentLine[charIndex + 1];
|
|
37507
|
-
if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
|
|
37508
|
-
continue;
|
|
37509
|
-
}
|
|
37510
|
-
if (character !== ">") continue;
|
|
37511
|
-
const previousCharacter = currentLine[charIndex - 1];
|
|
37512
|
-
const nextCharacter = currentLine[charIndex + 1];
|
|
37513
|
-
if (previousCharacter === "=" || nextCharacter === "=") continue;
|
|
37514
|
-
if (innerAngleDepth > 0) {
|
|
37515
|
-
innerAngleDepth--;
|
|
37516
|
-
continue;
|
|
37517
|
-
}
|
|
37518
|
-
return lineIndex;
|
|
37519
|
-
}
|
|
37520
|
-
}
|
|
37521
|
-
return null;
|
|
37522
|
-
};
|
|
37523
|
-
const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
|
|
37524
|
-
for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
|
|
37525
|
-
const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
|
|
37526
|
-
if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
|
|
37527
|
-
}
|
|
37528
|
-
return null;
|
|
37529
|
-
};
|
|
37530
|
-
const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
|
|
37531
|
-
const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
|
|
37532
|
-
const collected = [];
|
|
37533
|
-
let isStillInChain = true;
|
|
37534
|
-
for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
|
|
37535
|
-
const candidateLine = lines[candidateIndex];
|
|
37536
|
-
if (candidateLine === void 0) break;
|
|
37537
|
-
const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
|
|
37538
|
-
if (match) {
|
|
37539
|
-
collected.push({
|
|
37540
|
-
commentLineIndex: candidateIndex,
|
|
37541
|
-
ruleList: match[1],
|
|
37542
|
-
isInChain: isStillInChain
|
|
37543
|
-
});
|
|
37544
|
-
continue;
|
|
37545
|
-
}
|
|
37546
|
-
isStillInChain = false;
|
|
37547
|
-
}
|
|
37548
|
-
return collected;
|
|
37549
|
-
};
|
|
37550
37445
|
const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
|
|
37551
37446
|
"effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
|
|
37552
37447
|
"effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
|
|
@@ -37671,7 +37566,13 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
|
|
|
37671
37566
|
}
|
|
37672
37567
|
const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
|
|
37673
37568
|
const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
|
|
37674
|
-
const
|
|
37569
|
+
const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
|
|
37570
|
+
const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
|
|
37571
|
+
const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
|
|
37572
|
+
const canonicalTarget = canonicalizeRuleKey(targetRuleKey);
|
|
37573
|
+
if (canonicalCandidate === canonicalTarget) return true;
|
|
37574
|
+
return isReactDoctorShortIdOf(canonicalCandidate, canonicalTarget) || isReactDoctorShortIdOf(canonicalTarget, canonicalCandidate);
|
|
37575
|
+
};
|
|
37675
37576
|
const getEquivalentRuleKeys = (ruleKey) => {
|
|
37676
37577
|
const nativeRuleKey = canonicalizeRuleKey(ruleKey);
|
|
37677
37578
|
return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
|
|
@@ -37681,12 +37582,182 @@ const stripDescriptionTail = (ruleList) => {
|
|
|
37681
37582
|
if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
|
|
37682
37583
|
return ruleList.slice(0, descriptionMatch.index);
|
|
37683
37584
|
};
|
|
37684
|
-
const
|
|
37585
|
+
const tokenizeRuleList = (ruleList) => {
|
|
37685
37586
|
const trimmed = ruleList?.trim();
|
|
37686
|
-
if (!trimmed) return
|
|
37587
|
+
if (!trimmed) return [];
|
|
37687
37588
|
const ruleSection = stripDescriptionTail(trimmed).trim();
|
|
37688
|
-
if (!ruleSection) return
|
|
37689
|
-
return ruleSection.split(/[,\s]+/).
|
|
37589
|
+
if (!ruleSection) return [];
|
|
37590
|
+
return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
|
|
37591
|
+
};
|
|
37592
|
+
const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
|
|
37593
|
+
const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
|
|
37594
|
+
const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
|
|
37595
|
+
const buildHint = (tool, token, ruleId) => `oxlint matches plugin rules only by their full name, so \`${token}\` in your ${tool}-disable comment does not silence \`${ruleId}\` — change it to \`${ruleId}\`.`;
|
|
37596
|
+
const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
|
|
37597
|
+
const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
|
|
37598
|
+
const candidates = [{
|
|
37599
|
+
line: lines[diagnosticLineIndex],
|
|
37600
|
+
requiredScope: "line"
|
|
37601
|
+
}, {
|
|
37602
|
+
line: lines[diagnosticLineIndex - 1],
|
|
37603
|
+
requiredScope: "next-line"
|
|
37604
|
+
}];
|
|
37605
|
+
for (const { line, requiredScope } of candidates) {
|
|
37606
|
+
const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
|
|
37607
|
+
if (!match) continue;
|
|
37608
|
+
const [, tool, scope, ruleList] = match;
|
|
37609
|
+
if (scope !== requiredScope) continue;
|
|
37610
|
+
const tokens = tokenizeRuleList(ruleList);
|
|
37611
|
+
if (tokens.includes(ruleId)) continue;
|
|
37612
|
+
for (const token of tokens) if (tokenMisnamesRule(token, ruleId)) return buildHint(tool, token, ruleId);
|
|
37613
|
+
}
|
|
37614
|
+
return null;
|
|
37615
|
+
};
|
|
37616
|
+
const detectBlockNearMiss = (lines, diagnosticLineIndex, ruleId) => {
|
|
37617
|
+
let openMisname = null;
|
|
37618
|
+
const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
|
|
37619
|
+
for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
|
|
37620
|
+
const line = lines[lineIndex];
|
|
37621
|
+
if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
|
|
37622
|
+
const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
|
|
37623
|
+
if (disableMatch) {
|
|
37624
|
+
const [, tool, ruleList] = disableMatch;
|
|
37625
|
+
const tokens = tokenizeRuleList(ruleList);
|
|
37626
|
+
if (tokens.includes(ruleId)) openMisname = null;
|
|
37627
|
+
else {
|
|
37628
|
+
const misnamed = tokens.find((token) => tokenMisnamesRule(token, ruleId));
|
|
37629
|
+
if (misnamed) openMisname = {
|
|
37630
|
+
tool,
|
|
37631
|
+
token: misnamed
|
|
37632
|
+
};
|
|
37633
|
+
}
|
|
37634
|
+
continue;
|
|
37635
|
+
}
|
|
37636
|
+
const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
|
|
37637
|
+
if (enableMatch) {
|
|
37638
|
+
const enabledRules = tokenizeRuleList(enableMatch[1]);
|
|
37639
|
+
if (enabledRules.length === 0 || enabledRules.some((rule) => isSameRuleKey(rule, ruleId))) openMisname = null;
|
|
37640
|
+
}
|
|
37641
|
+
}
|
|
37642
|
+
return openMisname ? buildHint(openMisname.tool, openMisname.token, ruleId) : null;
|
|
37643
|
+
};
|
|
37644
|
+
const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
|
|
37645
|
+
if (!ruleId.startsWith("react-doctor/")) return null;
|
|
37646
|
+
return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
|
|
37647
|
+
};
|
|
37648
|
+
const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
|
|
37649
|
+
const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
|
|
37650
|
+
const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
|
|
37651
|
+
let stringDelimiter = null;
|
|
37652
|
+
for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
|
|
37653
|
+
const character = line[charIndex];
|
|
37654
|
+
if (stringDelimiter !== null) {
|
|
37655
|
+
if (character === "\\") {
|
|
37656
|
+
charIndex++;
|
|
37657
|
+
continue;
|
|
37658
|
+
}
|
|
37659
|
+
if (character === stringDelimiter) stringDelimiter = null;
|
|
37660
|
+
continue;
|
|
37661
|
+
}
|
|
37662
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
37663
|
+
stringDelimiter = character;
|
|
37664
|
+
continue;
|
|
37665
|
+
}
|
|
37666
|
+
if (character === "/" && line[charIndex + 1] === "/") return true;
|
|
37667
|
+
}
|
|
37668
|
+
return false;
|
|
37669
|
+
};
|
|
37670
|
+
const findOpenerTagOnLine = (line) => {
|
|
37671
|
+
for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
|
|
37672
|
+
if (match.index === void 0) continue;
|
|
37673
|
+
if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
|
|
37674
|
+
}
|
|
37675
|
+
return null;
|
|
37676
|
+
};
|
|
37677
|
+
const findJsxOpenerSpan = (lines, openerLineIndex) => {
|
|
37678
|
+
const openerLine = lines[openerLineIndex];
|
|
37679
|
+
if (openerLine === void 0) return null;
|
|
37680
|
+
const opener = findOpenerTagOnLine(openerLine);
|
|
37681
|
+
if (!opener) return null;
|
|
37682
|
+
const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
|
|
37683
|
+
let braceDepth = 0;
|
|
37684
|
+
let innerAngleDepth = 0;
|
|
37685
|
+
let stringDelimiter = null;
|
|
37686
|
+
for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
|
|
37687
|
+
const currentLine = lines[lineIndex];
|
|
37688
|
+
const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
|
|
37689
|
+
for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
|
|
37690
|
+
const character = currentLine[charIndex];
|
|
37691
|
+
if (stringDelimiter !== null) {
|
|
37692
|
+
if (character === "\\") {
|
|
37693
|
+
charIndex++;
|
|
37694
|
+
continue;
|
|
37695
|
+
}
|
|
37696
|
+
if (character === stringDelimiter) stringDelimiter = null;
|
|
37697
|
+
continue;
|
|
37698
|
+
}
|
|
37699
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
37700
|
+
stringDelimiter = character;
|
|
37701
|
+
continue;
|
|
37702
|
+
}
|
|
37703
|
+
if (character === "{") {
|
|
37704
|
+
braceDepth++;
|
|
37705
|
+
continue;
|
|
37706
|
+
}
|
|
37707
|
+
if (character === "}") {
|
|
37708
|
+
braceDepth--;
|
|
37709
|
+
continue;
|
|
37710
|
+
}
|
|
37711
|
+
if (braceDepth !== 0) continue;
|
|
37712
|
+
if (character === "<") {
|
|
37713
|
+
const followCharacter = currentLine[charIndex + 1];
|
|
37714
|
+
if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
|
|
37715
|
+
continue;
|
|
37716
|
+
}
|
|
37717
|
+
if (character !== ">") continue;
|
|
37718
|
+
const previousCharacter = currentLine[charIndex - 1];
|
|
37719
|
+
const nextCharacter = currentLine[charIndex + 1];
|
|
37720
|
+
if (previousCharacter === "=" || nextCharacter === "=") continue;
|
|
37721
|
+
if (innerAngleDepth > 0) {
|
|
37722
|
+
innerAngleDepth--;
|
|
37723
|
+
continue;
|
|
37724
|
+
}
|
|
37725
|
+
return lineIndex;
|
|
37726
|
+
}
|
|
37727
|
+
}
|
|
37728
|
+
return null;
|
|
37729
|
+
};
|
|
37730
|
+
const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
|
|
37731
|
+
for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
|
|
37732
|
+
const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
|
|
37733
|
+
if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
|
|
37734
|
+
}
|
|
37735
|
+
return null;
|
|
37736
|
+
};
|
|
37737
|
+
const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
|
|
37738
|
+
const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
|
|
37739
|
+
const collected = [];
|
|
37740
|
+
let isStillInChain = true;
|
|
37741
|
+
for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
|
|
37742
|
+
const candidateLine = lines[candidateIndex];
|
|
37743
|
+
if (candidateLine === void 0) break;
|
|
37744
|
+
const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
|
|
37745
|
+
if (match) {
|
|
37746
|
+
collected.push({
|
|
37747
|
+
commentLineIndex: candidateIndex,
|
|
37748
|
+
ruleList: match[1],
|
|
37749
|
+
isInChain: isStillInChain
|
|
37750
|
+
});
|
|
37751
|
+
continue;
|
|
37752
|
+
}
|
|
37753
|
+
isStillInChain = false;
|
|
37754
|
+
}
|
|
37755
|
+
return collected;
|
|
37756
|
+
};
|
|
37757
|
+
const isRuleListedInComment = (ruleList, ruleId) => {
|
|
37758
|
+
const tokens = tokenizeRuleList(ruleList);
|
|
37759
|
+
if (tokens.length === 0) return true;
|
|
37760
|
+
return tokens.some((token) => isSameRuleKey(token, ruleId));
|
|
37690
37761
|
};
|
|
37691
37762
|
const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
|
|
37692
37763
|
const formatLineGap = (gapLineCount) => `${gapLineCount} line${gapLineCount === 1 ? "" : "s"}`;
|
|
@@ -37730,7 +37801,7 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
|
|
|
37730
37801
|
};
|
|
37731
37802
|
return {
|
|
37732
37803
|
isSuppressed: false,
|
|
37733
|
-
nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId)
|
|
37804
|
+
nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId) ?? detectForeignDisableNearMiss(lines, diagnosticLineIndex, ruleId)
|
|
37734
37805
|
};
|
|
37735
37806
|
};
|
|
37736
37807
|
/**
|
|
@@ -38520,7 +38591,6 @@ const PACKAGE_JSON_FILENAME = "package.json";
|
|
|
38520
38591
|
const PACKAGE_JSON_CONFIG_KEY$1 = "reactDoctor";
|
|
38521
38592
|
const LEGACY_CONFIG_FILENAME = "react-doctor.config.json";
|
|
38522
38593
|
const jiti = createJiti(import.meta.url);
|
|
38523
|
-
const formatError = (error) => error instanceof Error ? error.message : String(error);
|
|
38524
38594
|
const importDefaultExport = async (jitiInstance, filePath) => {
|
|
38525
38595
|
const imported = await jitiInstance.import(filePath);
|
|
38526
38596
|
return imported?.default ?? imported;
|
|
@@ -38552,7 +38622,7 @@ const loadModuleConfig = async (filePath) => {
|
|
|
38552
38622
|
try {
|
|
38553
38623
|
return await importDefaultExport(aliasJiti, filePath);
|
|
38554
38624
|
} catch (retryError) {
|
|
38555
|
-
throw new Error(`${
|
|
38625
|
+
throw new Error(`${messageFromUnknown(error)} (retry with ${SELF_PACKAGE_IMPORT_SPECIFIER} aliased to the running react-doctor package also failed: ${messageFromUnknown(retryError)})`, { cause: retryError });
|
|
38556
38626
|
}
|
|
38557
38627
|
}
|
|
38558
38628
|
};
|
|
@@ -38601,7 +38671,7 @@ const loadLegacyConfig = (directory) => {
|
|
|
38601
38671
|
}
|
|
38602
38672
|
warn(`${LEGACY_CONFIG_FILENAME} must contain an object, ignoring.`);
|
|
38603
38673
|
} catch (error) {
|
|
38604
|
-
warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${
|
|
38674
|
+
warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${messageFromUnknown(error)}`);
|
|
38605
38675
|
}
|
|
38606
38676
|
return {
|
|
38607
38677
|
status: "invalid",
|
|
@@ -38628,7 +38698,7 @@ const loadConfigFromDirectory = async (directory) => {
|
|
|
38628
38698
|
warn(`${CONFIG_BASENAME}.${extension} must export an object, ignoring.`);
|
|
38629
38699
|
sawBrokenConfigFile = true;
|
|
38630
38700
|
} catch (error) {
|
|
38631
|
-
warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${
|
|
38701
|
+
warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${messageFromUnknown(error)}`);
|
|
38632
38702
|
sawBrokenConfigFile = true;
|
|
38633
38703
|
}
|
|
38634
38704
|
}
|
|
@@ -38758,6 +38828,29 @@ const resolveScanTarget = async (requestedDirectory, options = {}) => {
|
|
|
38758
38828
|
didRedirectViaRootDir: redirectedDirectory !== null
|
|
38759
38829
|
};
|
|
38760
38830
|
};
|
|
38831
|
+
const buildFixGroupId = (diagnostic) => createHash("sha1").update(JSON.stringify([
|
|
38832
|
+
diagnostic.filePath,
|
|
38833
|
+
`${diagnostic.plugin}/${diagnostic.rule}`,
|
|
38834
|
+
diagnostic.message
|
|
38835
|
+
])).digest("hex").slice(0, 16);
|
|
38836
|
+
const isGroupableRule = (diagnostic) => ROOT_CAUSE_GROUPABLE_RULE_KEYS.has(`${diagnostic.plugin}/${diagnostic.rule}`);
|
|
38837
|
+
const assignFixGroups = (diagnostics) => {
|
|
38838
|
+
const siteCountByGroupId = /* @__PURE__ */ new Map();
|
|
38839
|
+
for (const diagnostic of diagnostics) {
|
|
38840
|
+
if (!isGroupableRule(diagnostic)) continue;
|
|
38841
|
+
const groupId = buildFixGroupId(diagnostic);
|
|
38842
|
+
siteCountByGroupId.set(groupId, (siteCountByGroupId.get(groupId) ?? 0) + 1);
|
|
38843
|
+
}
|
|
38844
|
+
return diagnostics.map((diagnostic) => {
|
|
38845
|
+
if (!isGroupableRule(diagnostic)) return diagnostic;
|
|
38846
|
+
const groupId = buildFixGroupId(diagnostic);
|
|
38847
|
+
if ((siteCountByGroupId.get(groupId) ?? 0) < 2) return diagnostic;
|
|
38848
|
+
return {
|
|
38849
|
+
...diagnostic,
|
|
38850
|
+
fixGroupId: groupId
|
|
38851
|
+
};
|
|
38852
|
+
});
|
|
38853
|
+
};
|
|
38761
38854
|
const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
|
|
38762
38855
|
const buildExpoCheckContext = (rootDirectory, expoVersion) => {
|
|
38763
38856
|
const packageJson = readPackageJson$1(Path.join(rootDirectory, "package.json"));
|
|
@@ -39264,10 +39357,15 @@ const buildHardeningDiagnostic = (input) => ({
|
|
|
39264
39357
|
column: input.column ?? 0,
|
|
39265
39358
|
category: "Security"
|
|
39266
39359
|
});
|
|
39267
|
-
const checkPnpmHardening = (
|
|
39268
|
-
if (!isPnpmManagedProject(
|
|
39269
|
-
const workspacePath = Path.join(
|
|
39270
|
-
const
|
|
39360
|
+
const checkPnpmHardening = (scanDirectory) => {
|
|
39361
|
+
if (!isPnpmManagedProject(scanDirectory)) return [];
|
|
39362
|
+
const workspacePath = Path.join(scanDirectory, PNPM_WORKSPACE_FILE);
|
|
39363
|
+
const hasWorkspaceFile = isFile(workspacePath);
|
|
39364
|
+
if (!hasWorkspaceFile) {
|
|
39365
|
+
const monorepoRoot = findMonorepoRoot(scanDirectory);
|
|
39366
|
+
if (monorepoRoot !== null && isFile(Path.join(monorepoRoot, PNPM_WORKSPACE_FILE))) return [];
|
|
39367
|
+
}
|
|
39368
|
+
const settings = parseHardeningSettings(hasWorkspaceFile ? NFS.readFileSync(workspacePath, "utf-8") : "");
|
|
39271
39369
|
const diagnostics = [];
|
|
39272
39370
|
if (settings.minimumReleaseAge === null) diagnostics.push(buildHardeningDiagnostic({
|
|
39273
39371
|
message: "pnpm-workspace.yaml is missing `minimumReleaseAge` — newly published versions can ship malware that gets caught and unpublished within hours",
|
|
@@ -39903,7 +40001,7 @@ const readIgnoreFile = (filePath) => {
|
|
|
39903
40001
|
try {
|
|
39904
40002
|
content = NFS.readFileSync(filePath, "utf-8");
|
|
39905
40003
|
} catch (error) {
|
|
39906
|
-
const errnoCode = error
|
|
40004
|
+
const errnoCode = isErrnoException(error) ? error.code : void 0;
|
|
39907
40005
|
if (errnoCode && errnoCode !== "ENOENT") runSync(warn$1(`Could not read ignore file ${filePath}: ${errnoCode}`));
|
|
39908
40006
|
return [];
|
|
39909
40007
|
}
|
|
@@ -39941,8 +40039,8 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
39941
40039
|
cachedPatternsByRoot.set(rootDirectory, patterns);
|
|
39942
40040
|
return patterns;
|
|
39943
40041
|
};
|
|
40042
|
+
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39944
40043
|
const KNIP_JSON_FILENAME = "knip.json";
|
|
39945
|
-
const isRecord$1$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39946
40044
|
const readJsonFileSafe = (filePath) => {
|
|
39947
40045
|
let rawContents;
|
|
39948
40046
|
try {
|
|
@@ -39958,10 +40056,10 @@ const readJsonFileSafe = (filePath) => {
|
|
|
39958
40056
|
};
|
|
39959
40057
|
const readKnipConfig = (rootDirectory) => {
|
|
39960
40058
|
const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
|
|
39961
|
-
if (isRecord$
|
|
40059
|
+
if (isRecord$2(knipJson)) return knipJson;
|
|
39962
40060
|
const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
|
|
39963
|
-
const packageKnipConfig = isRecord$
|
|
39964
|
-
return isRecord$
|
|
40061
|
+
const packageKnipConfig = isRecord$2(packageJson) ? packageJson.knip : null;
|
|
40062
|
+
return isRecord$2(packageKnipConfig) ? packageKnipConfig : null;
|
|
39965
40063
|
};
|
|
39966
40064
|
const normalizePatternList = (value) => {
|
|
39967
40065
|
if (typeof value === "string" && value.length > 0) return [value];
|
|
@@ -39973,10 +40071,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
|
|
|
39973
40071
|
return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
|
|
39974
40072
|
};
|
|
39975
40073
|
const collectKnipWorkspacePatterns = (workspaces, settingName) => {
|
|
39976
|
-
if (!isRecord$
|
|
40074
|
+
if (!isRecord$2(workspaces)) return [];
|
|
39977
40075
|
const patterns = [];
|
|
39978
40076
|
for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
|
|
39979
|
-
if (!isRecord$
|
|
40077
|
+
if (!isRecord$2(workspaceConfig)) continue;
|
|
39980
40078
|
patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
|
|
39981
40079
|
}
|
|
39982
40080
|
return patterns;
|
|
@@ -39986,12 +40084,11 @@ const collectKnipPatterns = (rootDirectory, settingName) => {
|
|
|
39986
40084
|
if (!config) return [];
|
|
39987
40085
|
return [...normalizePatternList(config[settingName]), ...collectKnipWorkspacePatterns(config.workspaces, settingName)];
|
|
39988
40086
|
};
|
|
39989
|
-
const collectDeadCodeIgnorePatterns = (rootDirectory
|
|
40087
|
+
const collectDeadCodeIgnorePatterns = (rootDirectory) => {
|
|
39990
40088
|
const seen = /* @__PURE__ */ new Set();
|
|
39991
40089
|
const sources = [
|
|
39992
40090
|
readIgnoreFile(path.join(rootDirectory, ".gitignore")),
|
|
39993
40091
|
collectIgnorePatterns(rootDirectory),
|
|
39994
|
-
userConfig?.ignore?.files ?? [],
|
|
39995
40092
|
collectKnipPatterns(rootDirectory, "ignore")
|
|
39996
40093
|
];
|
|
39997
40094
|
for (const source of sources) for (const pattern of source) seen.add(pattern);
|
|
@@ -40022,8 +40119,6 @@ const toCanonicalPath = (filePath) => {
|
|
|
40022
40119
|
};
|
|
40023
40120
|
const DEAD_CODE_PLUGIN = "deslop";
|
|
40024
40121
|
const DEAD_CODE_CATEGORY = "Maintainability";
|
|
40025
|
-
const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
|
|
40026
|
-
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
40027
40122
|
const DEAD_CODE_WORKER_SCRIPT = `
|
|
40028
40123
|
const inputChunks = [];
|
|
40029
40124
|
process.stdin.on("data", (chunk) => inputChunks.push(chunk));
|
|
@@ -40081,7 +40176,7 @@ process.stdin.on("end", () => {
|
|
|
40081
40176
|
});
|
|
40082
40177
|
`;
|
|
40083
40178
|
const resolveTsConfigPath = (rootDirectory) => {
|
|
40084
|
-
for (const filename of TSCONFIG_FILENAMES
|
|
40179
|
+
for (const filename of TSCONFIG_FILENAMES) {
|
|
40085
40180
|
const candidate = Path.join(rootDirectory, filename);
|
|
40086
40181
|
if (NFS.existsSync(candidate)) return candidate;
|
|
40087
40182
|
}
|
|
@@ -40269,11 +40364,10 @@ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve
|
|
|
40269
40364
|
});
|
|
40270
40365
|
});
|
|
40271
40366
|
const checkDeadCode = async (options) => {
|
|
40272
|
-
const { userConfig } = options;
|
|
40273
40367
|
const rootDirectory = toCanonicalPath(options.rootDirectory);
|
|
40274
40368
|
if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
|
|
40275
40369
|
const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
|
|
40276
|
-
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory
|
|
40370
|
+
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
40277
40371
|
const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
40278
40372
|
rootDirectory,
|
|
40279
40373
|
entryPatterns,
|
|
@@ -40463,15 +40557,13 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
|
|
|
40463
40557
|
})()) }));
|
|
40464
40558
|
static layerOf = (diagnostics) => succeed$3(DeadCode, DeadCode.of({ run: () => fromIterable$1(diagnostics) }));
|
|
40465
40559
|
};
|
|
40466
|
-
const createNodeReadFileLinesSync = (rootDirectory) => {
|
|
40467
|
-
|
|
40468
|
-
|
|
40469
|
-
|
|
40470
|
-
|
|
40471
|
-
|
|
40472
|
-
|
|
40473
|
-
}
|
|
40474
|
-
};
|
|
40560
|
+
const createNodeReadFileLinesSync = (rootDirectory) => (filePath) => {
|
|
40561
|
+
const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
|
|
40562
|
+
try {
|
|
40563
|
+
return NFS.readFileSync(absolutePath, "utf-8").split("\n");
|
|
40564
|
+
} catch {
|
|
40565
|
+
return null;
|
|
40566
|
+
}
|
|
40475
40567
|
};
|
|
40476
40568
|
var Files = class Files extends Service()("react-doctor/Files") {
|
|
40477
40569
|
static layerNode = succeed$3(Files, Files.of({
|
|
@@ -40682,7 +40774,10 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40682
40774
|
directory: input.directory,
|
|
40683
40775
|
cause
|
|
40684
40776
|
}) });
|
|
40685
|
-
})
|
|
40777
|
+
}), withSpan("git.exec", { attributes: {
|
|
40778
|
+
"git.command": input.command,
|
|
40779
|
+
"git.subcommand": input.args[0] ?? ""
|
|
40780
|
+
} }));
|
|
40686
40781
|
const runGit = (directory, args) => runCommand({
|
|
40687
40782
|
command: "git",
|
|
40688
40783
|
args,
|
|
@@ -40710,7 +40805,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40710
40805
|
]);
|
|
40711
40806
|
if (candidates.status !== 0) return null;
|
|
40712
40807
|
return trimOrNull(candidates.stdout.split("\n")[0] ?? "");
|
|
40713
|
-
});
|
|
40808
|
+
}).pipe(withSpan("Git.defaultBranch"));
|
|
40714
40809
|
const branchExists = (directory, branch) => runGit(directory, [
|
|
40715
40810
|
"rev-parse",
|
|
40716
40811
|
"--verify",
|
|
@@ -40757,7 +40852,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40757
40852
|
const result = resultOption.value;
|
|
40758
40853
|
if (result.status !== 0) return null;
|
|
40759
40854
|
return parseGithubViewerPermission(result.stdout);
|
|
40760
|
-
}).pipe(catch_$1(() => succeed$2(null)));
|
|
40855
|
+
}).pipe(catch_$1(() => succeed$2(null)), withSpan("Git.githubViewerPermission"));
|
|
40761
40856
|
/**
|
|
40762
40857
|
* Resolves a `--diff A..B` / `A...B` commit range into a changed-file
|
|
40763
40858
|
* selection. Each endpoint is validated with `isSafeGitRevision`
|
|
@@ -40871,7 +40966,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40871
40966
|
changedFiles: splitNullSeparated(diff.stdout),
|
|
40872
40967
|
isCurrentChanges: false
|
|
40873
40968
|
};
|
|
40874
|
-
}),
|
|
40969
|
+
}).pipe(withSpan("Git.diffSelection")),
|
|
40875
40970
|
stagedFilePaths: (directory) => runGit(directory, [
|
|
40876
40971
|
"diff",
|
|
40877
40972
|
"--cached",
|
|
@@ -40913,7 +41008,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40913
41008
|
status: result.status,
|
|
40914
41009
|
stdout: result.stdout
|
|
40915
41010
|
};
|
|
40916
|
-
}),
|
|
41011
|
+
}).pipe(withSpan("Git.grep")),
|
|
40917
41012
|
changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
|
|
40918
41013
|
if (files.length === 0) return [];
|
|
40919
41014
|
if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
|
|
@@ -40929,7 +41024,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40929
41024
|
]);
|
|
40930
41025
|
if (result.status !== 0) return null;
|
|
40931
41026
|
return parseChangedLineRanges(result.stdout);
|
|
40932
|
-
})
|
|
41027
|
+
}).pipe(withSpan("Git.changedLineRanges"))
|
|
40933
41028
|
});
|
|
40934
41029
|
})).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
|
|
40935
41030
|
/**
|
|
@@ -41144,7 +41239,7 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
|
|
|
41144
41239
|
for (const [absolutePath, originalContent] of originalContents) try {
|
|
41145
41240
|
NFS.writeFileSync(absolutePath, originalContent);
|
|
41146
41241
|
} catch (error) {
|
|
41147
|
-
process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${
|
|
41242
|
+
process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${messageFromUnknown(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
|
|
41148
41243
|
}
|
|
41149
41244
|
};
|
|
41150
41245
|
const onExit = () => restore();
|
|
@@ -41250,7 +41345,7 @@ const resolveUserPlugin = (spec, configSourceDirectory) => {
|
|
|
41250
41345
|
try {
|
|
41251
41346
|
resolvedSpecifier = isRelative ? Path.resolve(configSourceDirectory, spec) : candidateRequire.resolve(spec);
|
|
41252
41347
|
} catch (error) {
|
|
41253
|
-
warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${
|
|
41348
|
+
warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${messageFromUnknown(error)}`);
|
|
41254
41349
|
return null;
|
|
41255
41350
|
}
|
|
41256
41351
|
const { name, ruleNames } = readPluginShape(resolvedSpecifier, (target) => candidateRequire(target));
|
|
@@ -41322,8 +41417,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
41322
41417
|
}
|
|
41323
41418
|
return enabled;
|
|
41324
41419
|
};
|
|
41325
|
-
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
|
|
41326
|
-
const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
41420
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
|
|
41421
|
+
const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
41327
41422
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
41328
41423
|
const jsPlugins = [];
|
|
41329
41424
|
if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
|
|
@@ -41383,7 +41478,6 @@ const resolveOxlintBinary = () => {
|
|
|
41383
41478
|
return Path.join(oxlintPackageDirectory, "bin", "oxlint");
|
|
41384
41479
|
};
|
|
41385
41480
|
const resolvePluginPath = () => esmRequire.resolve("oxlint-plugin-react-doctor");
|
|
41386
|
-
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
41387
41481
|
const resolveTsConfigRelativePath = (rootDirectory) => {
|
|
41388
41482
|
for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
|
|
41389
41483
|
return null;
|
|
@@ -41755,7 +41849,7 @@ const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
|
|
|
41755
41849
|
const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
|
|
41756
41850
|
let currentNode = identifier.parent;
|
|
41757
41851
|
while (currentNode) {
|
|
41758
|
-
if (
|
|
41852
|
+
if (isScopeBoundary(currentNode)) {
|
|
41759
41853
|
if (scopeContainsNonImportBinding(currentNode, currentNode, identifier.text)) return true;
|
|
41760
41854
|
}
|
|
41761
41855
|
if (currentNode === sourceFile) return false;
|
|
@@ -41846,11 +41940,10 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
|
|
|
41846
41940
|
});
|
|
41847
41941
|
return resolution;
|
|
41848
41942
|
};
|
|
41849
|
-
const isScopeNode = isScopeBoundary;
|
|
41850
41943
|
const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, visitedDeclarations = /* @__PURE__ */ new Set()) => {
|
|
41851
41944
|
let currentNode = identifier.parent;
|
|
41852
41945
|
while (currentNode) {
|
|
41853
|
-
if (
|
|
41946
|
+
if (isScopeBoundary(currentNode)) {
|
|
41854
41947
|
const resolution = findResolutionInScope(currentNode, identifier.text, reactImportBindings, sourceFile, visitedDeclarations);
|
|
41855
41948
|
if (resolution) return resolution;
|
|
41856
41949
|
}
|
|
@@ -42020,9 +42113,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
42020
42113
|
try {
|
|
42021
42114
|
parsed = JSON.parse(sanitizedStdout);
|
|
42022
42115
|
} catch {
|
|
42023
|
-
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
42116
|
+
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
42024
42117
|
}
|
|
42025
|
-
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
42118
|
+
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
42026
42119
|
const minifiedFileCache = /* @__PURE__ */ new Map();
|
|
42027
42120
|
const isMinifiedDiagnosticFile = (filename) => {
|
|
42028
42121
|
const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
|
|
@@ -42098,7 +42191,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
42098
42191
|
child.kill("SIGKILL");
|
|
42099
42192
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
42100
42193
|
kind: "timeout",
|
|
42101
|
-
detail: `${spawnTimeoutMs /
|
|
42194
|
+
detail: `${spawnTimeoutMs / MILLISECONDS_PER_SECOND}s budget exceeded`
|
|
42102
42195
|
}) }));
|
|
42103
42196
|
}, spawnTimeoutMs);
|
|
42104
42197
|
timeoutHandle.unref?.();
|
|
@@ -42313,6 +42406,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
42313
42406
|
NFS.closeSync(fileHandle);
|
|
42314
42407
|
}
|
|
42315
42408
|
};
|
|
42409
|
+
const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
|
|
42410
|
+
/**
|
|
42411
|
+
* Detects an oxlint config-load crash caused by the optional
|
|
42412
|
+
* `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
|
|
42413
|
+
* builds the partial-failure note for it; returns `null` when the failure
|
|
42414
|
+
* was anything else.
|
|
42415
|
+
*
|
|
42416
|
+
* oxlint prints a framed error to stdout (not stderr) and exits non-zero
|
|
42417
|
+
* when a `jsPlugins` entry can't be imported; that non-JSON stdout
|
|
42418
|
+
* surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
|
|
42419
|
+
* config load on it, leaving the plugin in would drop every curated
|
|
42420
|
+
* react-doctor diagnostic too — so the caller retries with the plugin
|
|
42421
|
+
* stripped (issue #833). Both markers sit at the start of oxlint's
|
|
42422
|
+
* message, so they survive the `preview` slice even for deep pnpm paths.
|
|
42423
|
+
*/
|
|
42424
|
+
const reactHooksJsPluginDropNote = (error) => {
|
|
42425
|
+
if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
|
|
42426
|
+
const { preview } = error.reason;
|
|
42427
|
+
if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
|
|
42428
|
+
const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
|
|
42429
|
+
return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
|
|
42430
|
+
};
|
|
42316
42431
|
/**
|
|
42317
42432
|
* The oxlint runner. Composed of three pieces in `runners/oxlint/`:
|
|
42318
42433
|
*
|
|
@@ -42340,15 +42455,16 @@ const runOxlint = async (options) => {
|
|
|
42340
42455
|
const pluginPath = resolvePluginPath();
|
|
42341
42456
|
const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
|
|
42342
42457
|
const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
|
|
42343
|
-
const buildConfig = (
|
|
42458
|
+
const buildConfig = (overrides) => createOxlintConfig({
|
|
42344
42459
|
pluginPath,
|
|
42345
42460
|
project,
|
|
42346
42461
|
customRulesOnly,
|
|
42347
|
-
extendsPaths:
|
|
42462
|
+
extendsPaths: overrides.extendsPaths,
|
|
42348
42463
|
ignoredTags,
|
|
42349
42464
|
serverAuthFunctionNames,
|
|
42350
42465
|
severityControls,
|
|
42351
|
-
userPlugins
|
|
42466
|
+
userPlugins,
|
|
42467
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
42352
42468
|
});
|
|
42353
42469
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
42354
42470
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
@@ -42384,12 +42500,22 @@ const runOxlint = async (options) => {
|
|
|
42384
42500
|
outputMaxBytes,
|
|
42385
42501
|
concurrency: options.concurrency
|
|
42386
42502
|
});
|
|
42387
|
-
writeOxlintConfig(configPath, buildConfig(extendsPaths));
|
|
42503
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
|
|
42388
42504
|
try {
|
|
42389
42505
|
return await runBatches();
|
|
42390
42506
|
} catch (error) {
|
|
42507
|
+
const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
|
|
42508
|
+
if (reactHooksJsDropNote !== null) {
|
|
42509
|
+
writeOxlintConfig(configPath, buildConfig({
|
|
42510
|
+
extendsPaths,
|
|
42511
|
+
disableReactHooksJsPlugin: true
|
|
42512
|
+
}));
|
|
42513
|
+
const diagnostics = await runBatches();
|
|
42514
|
+
onPartialFailure?.(reactHooksJsDropNote);
|
|
42515
|
+
return diagnostics;
|
|
42516
|
+
}
|
|
42391
42517
|
if (extendsPaths.length === 0) throw error;
|
|
42392
|
-
writeOxlintConfig(configPath, buildConfig([]));
|
|
42518
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
|
|
42393
42519
|
return await runBatches();
|
|
42394
42520
|
}
|
|
42395
42521
|
} finally {
|
|
@@ -43187,17 +43313,17 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43187
43313
|
}))))))));
|
|
43188
43314
|
const deadCodeFailureState = yield* get$2(deadCodeFailure);
|
|
43189
43315
|
const scanElapsedMilliseconds = Date.now() - scanStartTime;
|
|
43190
|
-
const scanElapsedSeconds = (scanElapsedMilliseconds /
|
|
43316
|
+
const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
|
|
43191
43317
|
if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
|
|
43192
43318
|
else if (input.suppressScanSummary) yield* scanProgress.stop();
|
|
43193
43319
|
else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
|
|
43194
43320
|
yield* reporterService.finalize;
|
|
43195
|
-
const finalDiagnostics = [
|
|
43321
|
+
const finalDiagnostics = assignFixGroups([
|
|
43196
43322
|
...envCollected,
|
|
43197
43323
|
...supplyChainCollected,
|
|
43198
43324
|
...lintCollected,
|
|
43199
43325
|
...deadCodeCollected
|
|
43200
|
-
];
|
|
43326
|
+
]);
|
|
43201
43327
|
const githubViewerPermission = yield* join(githubViewerPermissionFiber);
|
|
43202
43328
|
const scoreMetadata = {
|
|
43203
43329
|
...repo !== null ? { repo } : {},
|
|
@@ -43424,7 +43550,7 @@ var StagedFiles = class StagedFiles extends Service()("react-doctor/StagedFiles"
|
|
|
43424
43550
|
static layerNode = effect(StagedFiles, gen(function* () {
|
|
43425
43551
|
const git = yield* Git;
|
|
43426
43552
|
return StagedFiles.of({
|
|
43427
|
-
discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile))),
|
|
43553
|
+
discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile)), withSpan("StagedFiles.discoverSourceFiles")),
|
|
43428
43554
|
materialize: ({ directory, stagedFiles, tempDirectory }) => materializeSourceTree({
|
|
43429
43555
|
directory,
|
|
43430
43556
|
files: stagedFiles,
|
|
@@ -43434,7 +43560,7 @@ var StagedFiles = class StagedFiles extends Service()("react-doctor/StagedFiles"
|
|
|
43434
43560
|
tempDirectory: tree.tempDirectory,
|
|
43435
43561
|
stagedFiles: tree.materializedFiles,
|
|
43436
43562
|
cleanup: tree.cleanup
|
|
43437
|
-
})))
|
|
43563
|
+
})), withSpan("StagedFiles.materialize"))
|
|
43438
43564
|
});
|
|
43439
43565
|
}));
|
|
43440
43566
|
/**
|
|
@@ -43567,6 +43693,7 @@ const buildJsonReport = (input) => {
|
|
|
43567
43693
|
score: result.score,
|
|
43568
43694
|
skippedChecks: result.skippedChecks,
|
|
43569
43695
|
...result.skippedCheckReasons ? { skippedCheckReasons: result.skippedCheckReasons } : {},
|
|
43696
|
+
...typeof result.scannedFileCount === "number" ? { scannedFileCount: result.scannedFileCount } : {},
|
|
43570
43697
|
elapsedMilliseconds: result.elapsedMilliseconds
|
|
43571
43698
|
}));
|
|
43572
43699
|
const flattenedDiagnostics = projects.flatMap((entry) => entry.diagnostics);
|
|
@@ -43841,7 +43968,7 @@ const FALSY_CI_FLAG_VALUES = new Set([
|
|
|
43841
43968
|
"false"
|
|
43842
43969
|
]);
|
|
43843
43970
|
const isCiFlagSet = (value) => value !== void 0 && !FALSY_CI_FLAG_VALUES.has(value.toLowerCase());
|
|
43844
|
-
const isCiEnvironment = () => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(
|
|
43971
|
+
const isCiEnvironment = (env = process.env) => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(env[environmentVariable])) || isCiFlagSet(env.CI);
|
|
43845
43972
|
const detectCiProvider = () => {
|
|
43846
43973
|
for (const [environmentVariable, provider] of CI_PROVIDER_BY_ENVIRONMENT_VARIABLE) if (process.env[environmentVariable]) return provider;
|
|
43847
43974
|
return isCiFlagSet(process.env.CI) ? "unknown" : null;
|
|
@@ -43866,6 +43993,53 @@ const detectCodingAgent = () => {
|
|
|
43866
43993
|
const isCodingAgentEnvironment = () => detectCodingAgent() !== null;
|
|
43867
43994
|
const isCiOrCodingAgentEnvironment = () => isCiEnvironment() || isCodingAgentEnvironment();
|
|
43868
43995
|
//#endregion
|
|
43996
|
+
//#region src/cli/utils/detect-terminal-kind.ts
|
|
43997
|
+
const TERMINAL_BY_TERM_PROGRAM = [
|
|
43998
|
+
["vscode", "vscode"],
|
|
43999
|
+
["iTerm.app", "iterm"],
|
|
44000
|
+
["Apple_Terminal", "apple-terminal"],
|
|
44001
|
+
["WezTerm", "wezterm"],
|
|
44002
|
+
["ghostty", "ghostty"],
|
|
44003
|
+
["Hyper", "hyper"],
|
|
44004
|
+
["Tabby", "tabby"],
|
|
44005
|
+
["rio", "rio"]
|
|
44006
|
+
];
|
|
44007
|
+
/**
|
|
44008
|
+
* Best-effort label for the terminal emulator / editor hosting the CLI,
|
|
44009
|
+
* derived from terminal-identity env vars. Recorded as the `terminalKind` run
|
|
44010
|
+
* tag so we can see where React Doctor is actually run (nvim, VS Code, iTerm,
|
|
44011
|
+
* …) — the split Sentry can't otherwise see. Low-cardinality and free of any
|
|
44012
|
+
* username/path/secret, so it's safe as a tag. Editor terminals (nvim/vim)
|
|
44013
|
+
* win over the outer emulator because that's the surface a user is reading in;
|
|
44014
|
+
* "ci" marks a run with no interactive terminal; "unknown" when nothing matches.
|
|
44015
|
+
*/
|
|
44016
|
+
const detectTerminalKind = (env = process.env) => {
|
|
44017
|
+
if (env.NVIM) return "neovim";
|
|
44018
|
+
if (env.VIM_TERMINAL) return "vim";
|
|
44019
|
+
const termProgram = env.TERM_PROGRAM;
|
|
44020
|
+
if (termProgram) {
|
|
44021
|
+
for (const [marker, label] of TERMINAL_BY_TERM_PROGRAM) if (termProgram === marker) return label;
|
|
44022
|
+
}
|
|
44023
|
+
if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return "kitty";
|
|
44024
|
+
if (env.WT_SESSION) return "windows-terminal";
|
|
44025
|
+
if (env.ALACRITTY_WINDOW_ID || env.TERM === "alacritty") return "alacritty";
|
|
44026
|
+
if (env.VTE_VERSION) return "vte";
|
|
44027
|
+
if (env.TMUX) return "tmux";
|
|
44028
|
+
if (isCiEnvironment(env)) return "ci";
|
|
44029
|
+
return "unknown";
|
|
44030
|
+
};
|
|
44031
|
+
//#endregion
|
|
44032
|
+
//#region src/cli/utils/is-debug-flag.ts
|
|
44033
|
+
/**
|
|
44034
|
+
* Whether the user passed `--debug` (surface the run's Sentry trace id, and
|
|
44035
|
+
* force performance tracing on so there's a trace to surface). Read straight
|
|
44036
|
+
* from argv rather than Commander's parsed flags because `initializeSentry()`
|
|
44037
|
+
* runs before Commander parses — the same reason `shouldEnableSentry()` reads
|
|
44038
|
+
* `--no-score` from argv. Sharing this one reader keeps the init-time sampling
|
|
44039
|
+
* override and the end-of-run print in agreement.
|
|
44040
|
+
*/
|
|
44041
|
+
const isDebugFlagEnabled = (argv = process.argv) => argv.includes("--debug");
|
|
44042
|
+
//#endregion
|
|
43869
44043
|
//#region src/cli/utils/is-git-hook-environment.ts
|
|
43870
44044
|
const isGitHookEnvironment = () => Boolean(process.env.GIT_DIR);
|
|
43871
44045
|
//#endregion
|
|
@@ -43888,6 +44062,7 @@ const NON_INTERACTIVE_ENVIRONMENT_VARIABLES = [
|
|
|
43888
44062
|
const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || isCodingAgentEnvironment();
|
|
43889
44063
|
//#endregion
|
|
43890
44064
|
//#region src/cli/utils/constants.ts
|
|
44065
|
+
const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
|
|
43891
44066
|
const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
|
|
43892
44067
|
const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
|
|
43893
44068
|
const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
|
|
@@ -43972,7 +44147,7 @@ const makeNoopConsole = () => ({
|
|
|
43972
44147
|
});
|
|
43973
44148
|
//#endregion
|
|
43974
44149
|
//#region src/cli/utils/version.ts
|
|
43975
|
-
const VERSION = "0.5.
|
|
44150
|
+
const VERSION = "0.5.7-dev.350a6ed";
|
|
43976
44151
|
//#endregion
|
|
43977
44152
|
//#region src/cli/utils/json-mode.ts
|
|
43978
44153
|
let context = null;
|
|
@@ -44122,7 +44297,9 @@ const buildRunContext = () => {
|
|
|
44122
44297
|
viaAction: isOfficialGithubAction(),
|
|
44123
44298
|
codingAgent: detectCodingAgent(),
|
|
44124
44299
|
interactive: !isNonInteractiveEnvironment(),
|
|
44300
|
+
terminalKind: detectTerminalKind(),
|
|
44125
44301
|
jsonMode: isJsonModeActive(),
|
|
44302
|
+
debug: isDebugFlagEnabled(),
|
|
44126
44303
|
invokedVia: detectInvokedVia()
|
|
44127
44304
|
};
|
|
44128
44305
|
};
|
|
@@ -44192,7 +44369,9 @@ const buildSentryScope = (runContext = buildRunContext()) => {
|
|
|
44192
44369
|
viaAction: runContext.viaAction,
|
|
44193
44370
|
codingAgent: runContext.codingAgent,
|
|
44194
44371
|
interactive: runContext.interactive,
|
|
44372
|
+
terminalKind: runContext.terminalKind,
|
|
44195
44373
|
jsonMode: runContext.jsonMode,
|
|
44374
|
+
debug: runContext.debug,
|
|
44196
44375
|
invokedVia: runContext.invokedVia,
|
|
44197
44376
|
nodeMajor: runContext.nodeMajor
|
|
44198
44377
|
};
|
|
@@ -44330,13 +44509,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
44330
44509
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
44331
44510
|
* standard `SENTRY_RELEASE` override.
|
|
44332
44511
|
*/
|
|
44333
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.
|
|
44512
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.7-dev.350a6ed`;
|
|
44334
44513
|
/**
|
|
44335
44514
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
44336
44515
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
44337
44516
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
44338
44517
|
*/
|
|
44339
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.
|
|
44518
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.7-dev.350a6ed") ? "development" : "production");
|
|
44340
44519
|
/**
|
|
44341
44520
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
44342
44521
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -44400,7 +44579,7 @@ const flushSentry = async () => {
|
|
|
44400
44579
|
const initializeSentry = () => {
|
|
44401
44580
|
if (isInitialized || !shouldEnableSentry()) return;
|
|
44402
44581
|
isInitialized = true;
|
|
44403
|
-
resolvedTracesSampleRate = resolveTracesSampleRate();
|
|
44582
|
+
resolvedTracesSampleRate = isDebugFlagEnabled() ? 1 : resolveTracesSampleRate();
|
|
44404
44583
|
const { tags, contexts } = buildSentryScope();
|
|
44405
44584
|
Sentry.init({
|
|
44406
44585
|
dsn: process.env.SENTRY_DSN || "https://f253d570240a59b8dbd77b7a548ef133@o4510226365743104.ingest.us.sentry.io/4511487817809920",
|
|
@@ -47616,6 +47795,11 @@ const setActiveRunTrace = (trace) => {
|
|
|
47616
47795
|
activeRunTrace = trace;
|
|
47617
47796
|
};
|
|
47618
47797
|
const getActiveRunTrace = () => activeRunTrace;
|
|
47798
|
+
let lastRunTraceId = null;
|
|
47799
|
+
const recordRunTraceId = (traceId) => {
|
|
47800
|
+
lastRunTraceId = traceId;
|
|
47801
|
+
};
|
|
47802
|
+
const getLastRunTraceId = () => lastRunTraceId;
|
|
47619
47803
|
//#endregion
|
|
47620
47804
|
//#region src/cli/utils/to-span-attributes.ts
|
|
47621
47805
|
/**
|
|
@@ -47678,14 +47862,13 @@ const withSentryRunSpan = (run, options = {}) => {
|
|
|
47678
47862
|
op: "cli.inspect",
|
|
47679
47863
|
attributes: toSpanAttributes(tags)
|
|
47680
47864
|
}, (rootSpan) => {
|
|
47681
|
-
|
|
47682
|
-
|
|
47683
|
-
|
|
47684
|
-
|
|
47685
|
-
|
|
47686
|
-
|
|
47687
|
-
|
|
47688
|
-
}
|
|
47865
|
+
const spanContext = rootSpan.spanContext();
|
|
47866
|
+
recordRunTraceId(spanContext.traceId);
|
|
47867
|
+
if (options.concurrentScan !== true) setActiveRunTrace({
|
|
47868
|
+
traceId: spanContext.traceId,
|
|
47869
|
+
spanId: spanContext.spanId,
|
|
47870
|
+
sampled: (spanContext.traceFlags & 1) === 1
|
|
47871
|
+
});
|
|
47689
47872
|
return run(rootSpan);
|
|
47690
47873
|
});
|
|
47691
47874
|
};
|
|
@@ -47825,6 +48008,42 @@ const recordScanMetrics = (input) => {
|
|
|
47825
48008
|
});
|
|
47826
48009
|
};
|
|
47827
48010
|
//#endregion
|
|
48011
|
+
//#region src/cli/utils/diagnostic-grouping.ts
|
|
48012
|
+
const buildRulePriorityMap = (scores) => {
|
|
48013
|
+
const rulePriority = /* @__PURE__ */ new Map();
|
|
48014
|
+
for (const score of scores) {
|
|
48015
|
+
if (!score?.rules) continue;
|
|
48016
|
+
for (const [ruleKey, info] of Object.entries(score.rules)) if (typeof info.priority === "number") rulePriority.set(ruleKey, info.priority);
|
|
48017
|
+
}
|
|
48018
|
+
return rulePriority;
|
|
48019
|
+
};
|
|
48020
|
+
const compareByRulePriority = (ruleKeyA, ruleKeyB, rulePriority) => {
|
|
48021
|
+
const priorityA = rulePriority?.get(ruleKeyA);
|
|
48022
|
+
const priorityB = rulePriority?.get(ruleKeyB);
|
|
48023
|
+
if (priorityA === void 0 && priorityB === void 0) return 0;
|
|
48024
|
+
if (priorityA === void 0) return 1;
|
|
48025
|
+
if (priorityB === void 0) return -1;
|
|
48026
|
+
return priorityB - priorityA;
|
|
48027
|
+
};
|
|
48028
|
+
const sortRuleGroupsByImportance = (diagnosticGroups, rulePriority) => diagnosticGroups.toSorted(([ruleKeyA], [ruleKeyB]) => compareByRulePriority(ruleKeyA, ruleKeyB, rulePriority));
|
|
48029
|
+
const buildSortedRuleGroups = (diagnostics, rulePriority) => sortRuleGroupsByImportance([...groupBy([...diagnostics], (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`)], rulePriority);
|
|
48030
|
+
const getSharedFixSiteCount = (diagnostics) => {
|
|
48031
|
+
if (diagnostics.length < 2) return 0;
|
|
48032
|
+
const firstFixGroupId = diagnostics[0]?.fixGroupId;
|
|
48033
|
+
if (!firstFixGroupId) return 0;
|
|
48034
|
+
return diagnostics.every((diagnostic) => diagnostic.fixGroupId === firstFixGroupId) ? diagnostics.length : 0;
|
|
48035
|
+
};
|
|
48036
|
+
const FIX_RECIPE_DIRECTIVE_LABEL = "Curl with no cache & follow the canonical fix and false positive check recipe before fixing";
|
|
48037
|
+
const formatFixRecipeLine = (diagnostic) => hasPublishedFixRecipe(diagnostic) ? `${FIX_RECIPE_DIRECTIVE_LABEL}: ${buildRuleDocsUrl(diagnostic.plugin, diagnostic.rule)}` : null;
|
|
48038
|
+
const formatLearnMoreLine = (diagnostic) => hasPublishedFixRecipe(diagnostic) ? `Learn more: ${buildRuleDocsUrl(diagnostic.plugin, diagnostic.rule)}` : null;
|
|
48039
|
+
const buildRuleBlastRadii = (diagnostics) => buildSortedRuleGroups(diagnostics).map(([ruleKey, ruleDiagnostics]) => ({
|
|
48040
|
+
ruleKey,
|
|
48041
|
+
title: ruleDiagnostics[0].title ?? ruleKey,
|
|
48042
|
+
siteCount: ruleDiagnostics.length,
|
|
48043
|
+
fileCount: new Set(ruleDiagnostics.map((diagnostic) => diagnostic.filePath)).size
|
|
48044
|
+
})).toSorted((left, right) => right.fileCount - left.fileCount);
|
|
48045
|
+
const findMigrationScaleBuckets = (diagnostics) => buildRuleBlastRadii(diagnostics).filter((bucket) => bucket.fileCount >= 40);
|
|
48046
|
+
//#endregion
|
|
47828
48047
|
//#region src/cli/utils/cli-logger.ts
|
|
47829
48048
|
/**
|
|
47830
48049
|
* Thin synchronous façade over Effect's `Console` module. Used by
|
|
@@ -47941,12 +48160,17 @@ const buildOutcomeAttributes = (input) => {
|
|
|
47941
48160
|
topRule = rule;
|
|
47942
48161
|
topRuleCount = count;
|
|
47943
48162
|
}
|
|
48163
|
+
const largestRuleBucket = buildRuleBlastRadii(result.diagnostics)[0] ?? null;
|
|
47944
48164
|
let diagnosticsInTestFiles = 0;
|
|
47945
48165
|
let diagnosticsInStoryFiles = 0;
|
|
48166
|
+
const findingsPerFixGroup = /* @__PURE__ */ new Map();
|
|
47946
48167
|
for (const diagnostic of result.diagnostics) {
|
|
47947
48168
|
if (diagnostic.fileContext === "test") diagnosticsInTestFiles += 1;
|
|
47948
48169
|
if (diagnostic.fileContext === "story") diagnosticsInStoryFiles += 1;
|
|
48170
|
+
if (diagnostic.fixGroupId) findingsPerFixGroup.set(diagnostic.fixGroupId, (findingsPerFixGroup.get(diagnostic.fixGroupId) ?? 0) + 1);
|
|
47949
48171
|
}
|
|
48172
|
+
let fixGroupedFindings = 0;
|
|
48173
|
+
for (const count of findingsPerFixGroup.values()) fixGroupedFindings += count;
|
|
47950
48174
|
const attributes = {
|
|
47951
48175
|
outcome,
|
|
47952
48176
|
exitCode: wouldBlock ? 1 : 0,
|
|
@@ -47960,7 +48184,12 @@ const buildOutcomeAttributes = (input) => {
|
|
|
47960
48184
|
diagnosticsInTestFiles,
|
|
47961
48185
|
diagnosticsInStoryFiles,
|
|
47962
48186
|
distinctRulesFired: countByRule.size,
|
|
48187
|
+
"diag.fixGroups": findingsPerFixGroup.size,
|
|
48188
|
+
"diag.fixGroupedFindings": fixGroupedFindings,
|
|
47963
48189
|
topRule,
|
|
48190
|
+
"migration.largestRuleBucketFiles": largestRuleBucket ? largestRuleBucket.fileCount : null,
|
|
48191
|
+
"migration.largestRuleBucketSites": largestRuleBucket ? largestRuleBucket.siteCount : null,
|
|
48192
|
+
"migration.largestRuleBucketRule": largestRuleBucket ? largestRuleBucket.ruleKey : null,
|
|
47964
48193
|
scannedFileCount: result.scannedFileCount ?? null,
|
|
47965
48194
|
elapsedMs: result.elapsedMilliseconds,
|
|
47966
48195
|
scanPhaseMs: result.scanElapsedMilliseconds ?? null,
|
|
@@ -48118,9 +48347,10 @@ const AGENT_GUIDANCE_LINES = [
|
|
|
48118
48347
|
"Investigate deeply where relevant: race conditions, security-sensitive flows, state propagation, multi-file refactors, and downstream dependency chains.",
|
|
48119
48348
|
"Ignore pure style preferences, theoretical issues without real impact, missing features, and unrelated pre-existing code.",
|
|
48120
48349
|
"Start with high-confidence fixes that preserve behavior. Leave low-confidence or product-dependent changes as notes.",
|
|
48121
|
-
"Run `npx react-doctor@latest --verbose --
|
|
48350
|
+
"Run `npx react-doctor@latest --verbose --scope changed` before and after changes, plus relevant tests after each focused batch.",
|
|
48122
48351
|
"When available, spawn subagents or isolated worktrees for independent rule families, then review and merge only the best safe fixes.",
|
|
48123
48352
|
"Split unrelated, broad, or behavior-changing work into separate PRs/branches instead of one large cleanup.",
|
|
48353
|
+
"When one rule spans dozens of files (a migration-scale change), fix a representative sample first, confirm the recipe holds, and get the code owner's sign-off before changing the rest. Don't mass-fix a broad pattern in one unreviewed pass.",
|
|
48124
48354
|
"For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.",
|
|
48125
48355
|
"If a fix needs an API, UX, or architecture decision, stop and ask before editing."
|
|
48126
48356
|
];
|
|
@@ -48130,29 +48360,6 @@ const printAgentGuidance = () => gen(function* () {
|
|
|
48130
48360
|
yield* log("");
|
|
48131
48361
|
});
|
|
48132
48362
|
//#endregion
|
|
48133
|
-
//#region src/cli/utils/diagnostic-grouping.ts
|
|
48134
|
-
const buildRulePriorityMap = (scores) => {
|
|
48135
|
-
const rulePriority = /* @__PURE__ */ new Map();
|
|
48136
|
-
for (const score of scores) {
|
|
48137
|
-
if (!score?.rules) continue;
|
|
48138
|
-
for (const [ruleKey, info] of Object.entries(score.rules)) if (typeof info.priority === "number") rulePriority.set(ruleKey, info.priority);
|
|
48139
|
-
}
|
|
48140
|
-
return rulePriority;
|
|
48141
|
-
};
|
|
48142
|
-
const compareByRulePriority = (ruleKeyA, ruleKeyB, rulePriority) => {
|
|
48143
|
-
const priorityA = rulePriority?.get(ruleKeyA);
|
|
48144
|
-
const priorityB = rulePriority?.get(ruleKeyB);
|
|
48145
|
-
if (priorityA === void 0 && priorityB === void 0) return 0;
|
|
48146
|
-
if (priorityA === void 0) return 1;
|
|
48147
|
-
if (priorityB === void 0) return -1;
|
|
48148
|
-
return priorityB - priorityA;
|
|
48149
|
-
};
|
|
48150
|
-
const sortRuleGroupsByImportance = (diagnosticGroups, rulePriority) => diagnosticGroups.toSorted(([ruleKeyA], [ruleKeyB]) => compareByRulePriority(ruleKeyA, ruleKeyB, rulePriority));
|
|
48151
|
-
const buildSortedRuleGroups = (diagnostics, rulePriority) => sortRuleGroupsByImportance([...groupBy([...diagnostics], (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`)], rulePriority);
|
|
48152
|
-
const FIX_RECIPE_DIRECTIVE_LABEL = "Curl with no cache & follow the canonical fix and false positive check recipe before fixing";
|
|
48153
|
-
const formatFixRecipeLine = (diagnostic) => hasPublishedFixRecipe(diagnostic) ? `${FIX_RECIPE_DIRECTIVE_LABEL}: ${buildRuleDocsUrl(diagnostic.plugin, diagnostic.rule)}` : null;
|
|
48154
|
-
const formatLearnMoreLine = (diagnostic) => hasPublishedFixRecipe(diagnostic) ? `Learn more: ${buildRuleDocsUrl(diagnostic.plugin, diagnostic.rule)}` : null;
|
|
48155
|
-
//#endregion
|
|
48156
48363
|
//#region src/cli/utils/box-text.ts
|
|
48157
48364
|
const ESCAPE = String.fromCharCode(27);
|
|
48158
48365
|
const ANSI_ESCAPE_PATTERN = new RegExp(`${ESCAPE}\\[[0-9;]*m`, "g");
|
|
@@ -48193,6 +48400,15 @@ const boxText = (content, innerWidth) => {
|
|
|
48193
48400
|
].join("\n");
|
|
48194
48401
|
};
|
|
48195
48402
|
//#endregion
|
|
48403
|
+
//#region src/cli/utils/resolve-absolute-path.ts
|
|
48404
|
+
/**
|
|
48405
|
+
* Resolves a diagnostic's `filePath` (relative to its project root, or
|
|
48406
|
+
* already absolute) to an absolute path. Shared by the code-frame reader and
|
|
48407
|
+
* the terminal hyperlink builder so both turn a relative path into the same
|
|
48408
|
+
* on-disk location.
|
|
48409
|
+
*/
|
|
48410
|
+
const resolveAbsolutePath = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
|
|
48411
|
+
//#endregion
|
|
48196
48412
|
//#region src/cli/utils/build-code-frame.ts
|
|
48197
48413
|
/**
|
|
48198
48414
|
* Renders a syntax-highlighted source excerpt around a diagnostic site
|
|
@@ -48203,7 +48419,7 @@ const boxText = (content, innerWidth) => {
|
|
|
48203
48419
|
*/
|
|
48204
48420
|
const buildCodeFrame = (input) => {
|
|
48205
48421
|
if (input.line <= 0) return null;
|
|
48206
|
-
const absolutePath =
|
|
48422
|
+
const absolutePath = resolveAbsolutePath(input.filePath, input.rootDirectory);
|
|
48207
48423
|
let source;
|
|
48208
48424
|
try {
|
|
48209
48425
|
source = NFS.readFileSync(absolutePath, "utf8");
|
|
@@ -48243,6 +48459,16 @@ const resolveMeasureWidth = (reservedColumns = 0) => resolveClampedWidth({
|
|
|
48243
48459
|
const DIVIDER_INDENT = " ";
|
|
48244
48460
|
const buildSectionDivider = () => highlighter.dim(`${DIVIDER_INDENT}${"─".repeat(resolveMeasureWidth(2))}`);
|
|
48245
48461
|
//#endregion
|
|
48462
|
+
//#region src/cli/utils/format-hyperlink.ts
|
|
48463
|
+
const OSC = "\x1B]";
|
|
48464
|
+
const ST = "\x1B\\";
|
|
48465
|
+
/**
|
|
48466
|
+
* Wraps `text` in an OSC 8 hyperlink pointing at `uri`. The visible characters
|
|
48467
|
+
* are exactly `text`; the link is carried in escape sequences a capable
|
|
48468
|
+
* terminal turns into a click target.
|
|
48469
|
+
*/
|
|
48470
|
+
const formatHyperlink = (text, uri) => `${OSC}8;;${uri}${ST}${text}${OSC}8;;${ST}`;
|
|
48471
|
+
//#endregion
|
|
48246
48472
|
//#region src/cli/utils/indent-multiline-text.ts
|
|
48247
48473
|
const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
|
|
48248
48474
|
//#endregion
|
|
@@ -48396,17 +48622,23 @@ const clusterNearbyDiagnostics = (diagnostics) => {
|
|
|
48396
48622
|
}
|
|
48397
48623
|
return clusters;
|
|
48398
48624
|
};
|
|
48399
|
-
const
|
|
48625
|
+
const formatClusterLocationText = (cluster) => {
|
|
48626
|
+
const { filePath } = cluster.diagnostics[0];
|
|
48627
|
+
if (cluster.startLine <= 0) return filePath;
|
|
48628
|
+
if (cluster.endLine > cluster.startLine) return `${filePath}:${cluster.startLine}-${cluster.endLine}`;
|
|
48629
|
+
return `${filePath}:${cluster.startLine}`;
|
|
48630
|
+
};
|
|
48631
|
+
const formatClusterLocation = (cluster, resolveSourceRoot, hyperlinks) => {
|
|
48400
48632
|
const lead = cluster.diagnostics[0];
|
|
48401
48633
|
const contextTag = formatFileContextTag(lead);
|
|
48402
|
-
|
|
48403
|
-
if (
|
|
48404
|
-
return `${lead.filePath
|
|
48634
|
+
const location = formatClusterLocationText(cluster);
|
|
48635
|
+
if (!hyperlinks) return `${location}${contextTag}`;
|
|
48636
|
+
return `${formatHyperlink(location, pathToFileURL(resolveAbsolutePath(lead.filePath, resolveSourceRoot(lead))).href)}${contextTag}`;
|
|
48405
48637
|
};
|
|
48406
|
-
const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame) => {
|
|
48638
|
+
const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame, hyperlinks) => {
|
|
48407
48639
|
const lead = cluster.diagnostics[0];
|
|
48408
48640
|
const isMultiSite = cluster.diagnostics.length > 1;
|
|
48409
|
-
const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster)}`)];
|
|
48641
|
+
const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster, resolveSourceRoot, hyperlinks)}`)];
|
|
48410
48642
|
const codeFrame = renderCodeFrame ? buildCodeFrame({
|
|
48411
48643
|
filePath: lead.filePath,
|
|
48412
48644
|
line: cluster.startLine,
|
|
@@ -48425,7 +48657,7 @@ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame
|
|
|
48425
48657
|
}
|
|
48426
48658
|
return lines;
|
|
48427
48659
|
};
|
|
48428
|
-
const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment) => {
|
|
48660
|
+
const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment, hyperlinks) => {
|
|
48429
48661
|
const representative = pickRepresentativeDiagnostic(ruleDiagnostics);
|
|
48430
48662
|
const { severity } = representative;
|
|
48431
48663
|
const trailingBadge = formatTrailingSiteBadge(ruleDiagnostics.length);
|
|
@@ -48439,13 +48671,15 @@ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, rende
|
|
|
48439
48671
|
const impactMessages = isCollapsedWarningGroup ? [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.message))] : [representative.message];
|
|
48440
48672
|
for (const impactMessage of impactMessages) for (const explanationLine of wrapTextToWidth(impactMessage, resolveMeasureWidth(4), { breakLongWords: false })) lines.push(`${TOP_ERROR_DETAIL_INDENT}${explanationLine}`);
|
|
48441
48673
|
if (representative.help) for (const fixLine of wrapTextToWidth(`→ ${representative.help}`, resolveMeasureWidth(4), { breakLongWords: false })) lines.push(highlighter.dim(`${TOP_ERROR_DETAIL_INDENT}${fixLine}`));
|
|
48674
|
+
const sharedFixSiteCount = getSharedFixSiteCount(ruleDiagnostics);
|
|
48675
|
+
if (sharedFixSiteCount > 0) lines.push(highlighter.dim(`${TOP_ERROR_DETAIL_INDENT}↳ One fix clears all ${sharedFixSiteCount} findings.`));
|
|
48442
48676
|
if (renderEverySite && isAgentEnvironment) {
|
|
48443
48677
|
const fixRecipeLine = formatFixRecipeLine(representative);
|
|
48444
48678
|
if (fixRecipeLine) lines.push(highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${fixRecipeLine}`));
|
|
48445
48679
|
}
|
|
48446
48680
|
const renderCodeFrame = severity === "error";
|
|
48447
48681
|
const sites = renderEverySite ? ruleDiagnostics : [representative];
|
|
48448
|
-
if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame));
|
|
48682
|
+
if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame, hyperlinks));
|
|
48449
48683
|
return lines;
|
|
48450
48684
|
};
|
|
48451
48685
|
const selectErrorRuleGroups = (diagnostics, rulePriority) => buildSortedRuleGroups(diagnostics.filter((diagnostic) => diagnostic.severity === "error"), rulePriority);
|
|
@@ -48457,8 +48691,21 @@ const buildOverflowSummaryLine = (diagnostics, rulePriority) => {
|
|
|
48457
48691
|
const command = highlighter.bold(highlighter.info("npx react-doctor@latest --verbose"));
|
|
48458
48692
|
return ` ${highlighter.dim("Run")} ${command} ${highlighter.dim("to list every error and warning")}`;
|
|
48459
48693
|
};
|
|
48694
|
+
const formatMigrationBucketLine = (bucket) => `${TOP_ERROR_DETAIL_INDENT}${bucket.title} ${highlighter.gray(`×${bucket.siteCount} across ${bucket.fileCount} files`)}`;
|
|
48695
|
+
const buildMigrationScaleAdvisoryLines = (diagnostics) => {
|
|
48696
|
+
const buckets = findMigrationScaleBuckets(diagnostics);
|
|
48697
|
+
if (buckets.length === 0) return [];
|
|
48698
|
+
const shownBuckets = buckets.slice(0, 3);
|
|
48699
|
+
const lines = [` ${highlighter.warn("⚠")} ${highlighter.bold("Migration-scale change")}${highlighter.dim(": sample before you sweep")}`, ...shownBuckets.map(formatMigrationBucketLine)];
|
|
48700
|
+
const remainingBuckets = buckets.length - shownBuckets.length;
|
|
48701
|
+
if (remainingBuckets > 0) lines.push(highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}+${remainingBuckets} more ${remainingBuckets === 1 ? "rule" : "rules"} at this scale`));
|
|
48702
|
+
for (const guidanceLine of wrapTextToWidth("Fixing all of them at once is hard to review and prone to subtle mistakes across the whole repo. Fix a representative few first and confirm the recipe holds. Then get the code owner's sign-off before changing the rest.", resolveMeasureWidth(4), { breakLongWords: false })) lines.push(highlighter.dim(`${TOP_ERROR_DETAIL_INDENT}${guidanceLine}`));
|
|
48703
|
+
const command = highlighter.info("npx react-doctor@latest <path>");
|
|
48704
|
+
lines.push(`${TOP_ERROR_DETAIL_INDENT}${highlighter.dim("Scope it down one area at a time:")} ${command}`);
|
|
48705
|
+
return lines;
|
|
48706
|
+
};
|
|
48460
48707
|
const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(selectTopErrorRuleGroups(diagnostics, limit, rulePriority).map(([ruleKey]) => ruleKey));
|
|
48461
|
-
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) => {
|
|
48708
|
+
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, hyperlinks, rulePriority) => {
|
|
48462
48709
|
const topRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority).slice(0, 3);
|
|
48463
48710
|
if (topRuleGroups.length === 0) return {
|
|
48464
48711
|
lines: [],
|
|
@@ -48468,7 +48715,7 @@ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) =>
|
|
|
48468
48715
|
const blockOffsets = [];
|
|
48469
48716
|
for (const [ruleKey, ruleDiagnostics] of topRuleGroups) {
|
|
48470
48717
|
blockOffsets.push(lines.length);
|
|
48471
|
-
lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false));
|
|
48718
|
+
lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false, hyperlinks));
|
|
48472
48719
|
lines.push("");
|
|
48473
48720
|
}
|
|
48474
48721
|
return {
|
|
@@ -48506,24 +48753,24 @@ const buildOverviewHeaderLines = (diagnostics) => {
|
|
|
48506
48753
|
* single Effect.forEach over Console.log so failures or fiber
|
|
48507
48754
|
* interruption produce predictable partial output.
|
|
48508
48755
|
*/
|
|
48509
|
-
const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}) => gen(function* () {
|
|
48756
|
+
const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}, hyperlinks = false) => gen(function* () {
|
|
48510
48757
|
const sectionPause = onboarding.sectionPause ?? void_;
|
|
48511
48758
|
const animateCountUp = onboarding.animateCountUp ?? false;
|
|
48512
48759
|
const resolveSourceRoot = typeof sourceRoot === "function" ? sourceRoot : () => sourceRoot;
|
|
48513
48760
|
let detailLines;
|
|
48514
48761
|
let topErrorBlockOffsets = [];
|
|
48515
48762
|
if (!isVerbose) {
|
|
48516
|
-
const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, rulePriority);
|
|
48763
|
+
const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, hyperlinks, rulePriority);
|
|
48517
48764
|
detailLines = topErrors.lines;
|
|
48518
48765
|
topErrorBlockOffsets = topErrors.blockOffsets;
|
|
48519
48766
|
} else detailLines = buildSortedRuleGroups(diagnostics, rulePriority).flatMap(([ruleKey, ruleDiagnostics]) => {
|
|
48520
|
-
return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment), ""];
|
|
48767
|
+
return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment, hyperlinks), ""];
|
|
48521
48768
|
});
|
|
48522
48769
|
const overflowLine = isVerbose ? void 0 : buildOverflowSummaryLine(diagnostics, rulePriority);
|
|
48523
48770
|
const categoryTallies = buildCategoryDiagnosticGroups(diagnostics, rulePriority).map(buildCategoryTally);
|
|
48524
48771
|
const categoryLines = buildCategoryTallyLines(categoryTallies);
|
|
48525
48772
|
const overviewDividerLines = detailLines.length > 0 && categoryLines.length > 0 ? [buildSectionDivider()] : [];
|
|
48526
|
-
const { lines, sectionStarts } = joinSections(detailLines, overviewDividerLines, buildOverviewHeaderLines(diagnostics), categoryLines, overflowLine ? [overflowLine] : []);
|
|
48773
|
+
const { lines, sectionStarts } = joinSections(detailLines, overviewDividerLines, buildOverviewHeaderLines(diagnostics), categoryLines, overflowLine ? [overflowLine] : [], buildMigrationScaleAdvisoryLines(diagnostics));
|
|
48527
48774
|
const [detailStart, , , categoryStart] = sectionStarts;
|
|
48528
48775
|
const pauseBeforeLineIndices = detailStart == null ? /* @__PURE__ */ new Set() : new Set(topErrorBlockOffsets.map((offset) => detailStart + offset));
|
|
48529
48776
|
let lineIndex = 0;
|
|
@@ -48578,6 +48825,48 @@ const computeProjectedScore = async (topErrorSource, rescoreSource, currentScore
|
|
|
48578
48825
|
//#endregion
|
|
48579
48826
|
//#region src/cli/utils/filter-diagnostics-by-categories.ts
|
|
48580
48827
|
const filterDiagnosticsByCategories = (diagnostics, categories) => categories.size === 0 ? [...diagnostics] : diagnostics.filter((diagnostic) => categories.has(diagnostic.category));
|
|
48828
|
+
//#endregion
|
|
48829
|
+
//#region src/cli/utils/supports-hyperlinks.ts
|
|
48830
|
+
const HYPERLINK_CAPABLE_TERM_PROGRAMS = new Set([
|
|
48831
|
+
"iTerm.app",
|
|
48832
|
+
"WezTerm",
|
|
48833
|
+
"vscode",
|
|
48834
|
+
"Hyper",
|
|
48835
|
+
"ghostty",
|
|
48836
|
+
"Tabby",
|
|
48837
|
+
"rio"
|
|
48838
|
+
]);
|
|
48839
|
+
const parseVteVersion = (raw) => {
|
|
48840
|
+
const parsed = Number.parseInt(raw ?? "", 10);
|
|
48841
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
48842
|
+
};
|
|
48843
|
+
/**
|
|
48844
|
+
* Whether `stream` is a terminal that renders OSC 8 hyperlinks. Auto-detected
|
|
48845
|
+
* from terminal-identity env vars; the de-facto `FORCE_HYPERLINK` env var
|
|
48846
|
+
* overrides detection (`FORCE_HYPERLINK=0`/`false` forces off, any other value
|
|
48847
|
+
* forces on), mirroring how the ecosystem's terminal libraries gate the same
|
|
48848
|
+
* feature. Off for non-TTYs, `TERM=dumb`, and CI (whose log viewers render the
|
|
48849
|
+
* raw escape rather than a link). Unknown terminals default to off.
|
|
48850
|
+
*/
|
|
48851
|
+
const supportsHyperlinks = (stream = process.stdout, env = process.env) => {
|
|
48852
|
+
const forced = env.FORCE_HYPERLINK;
|
|
48853
|
+
if (forced !== void 0 && forced !== "") return forced !== "0" && forced.toLowerCase() !== "false";
|
|
48854
|
+
if (stream.isTTY !== true) return false;
|
|
48855
|
+
if (env.TERM === "dumb") return false;
|
|
48856
|
+
if (isCiEnvironment(env)) return false;
|
|
48857
|
+
if (env.WT_SESSION) return true;
|
|
48858
|
+
if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return true;
|
|
48859
|
+
if (parseVteVersion(env.VTE_VERSION) >= 5e3) return true;
|
|
48860
|
+
return Boolean(env.TERM_PROGRAM && HYPERLINK_CAPABLE_TERM_PROGRAMS.has(env.TERM_PROGRAM));
|
|
48861
|
+
};
|
|
48862
|
+
//#endregion
|
|
48863
|
+
//#region src/cli/utils/should-render-hyperlinks.ts
|
|
48864
|
+
/**
|
|
48865
|
+
* Whether to emit OSC 8 clickable `file:line` locations for this run: a
|
|
48866
|
+
* hyperlink-capable terminal AND not a coding agent (whose output parsers
|
|
48867
|
+
* would choke on the escape sequences).
|
|
48868
|
+
*/
|
|
48869
|
+
const shouldRenderHyperlinks = (stream = process.stdout) => supportsHyperlinks(stream) && !isCodingAgentEnvironment();
|
|
48581
48870
|
const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
|
|
48582
48871
|
const FALSY_FLAG_VALUES = new Set([
|
|
48583
48872
|
"",
|
|
@@ -48597,10 +48886,9 @@ const canAnimateOnboarding = (stream = process.stdout) => {
|
|
|
48597
48886
|
};
|
|
48598
48887
|
//#endregion
|
|
48599
48888
|
//#region src/cli/utils/onboarding-state.ts
|
|
48600
|
-
const GLOBAL_CONFIG_PROJECT_NAME$2 = "react-doctor";
|
|
48601
48889
|
const ONBOARDED_AT_KEY = "onboardedAt";
|
|
48602
48890
|
const getOnboardingStore = (options = {}) => new Conf({
|
|
48603
|
-
projectName:
|
|
48891
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
48604
48892
|
cwd: options.cwd
|
|
48605
48893
|
});
|
|
48606
48894
|
const hasCompletedOnboarding = (options = {}) => {
|
|
@@ -49056,6 +49344,78 @@ const resolveCliCategories = (categoryFlag) => {
|
|
|
49056
49344
|
return resolvedCategories.length > 0 ? resolvedCategories : void 0;
|
|
49057
49345
|
};
|
|
49058
49346
|
//#endregion
|
|
49347
|
+
//#region src/cli/utils/git-hook-shared.ts
|
|
49348
|
+
const HOOK_FILE_NAME = "pre-commit";
|
|
49349
|
+
const HOOK_RELATIVE_PATH = "hooks/pre-commit";
|
|
49350
|
+
const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
|
|
49351
|
+
const HUSKY_HOOKS_PATH = ".husky";
|
|
49352
|
+
const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
|
|
49353
|
+
const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
|
|
49354
|
+
const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
|
|
49355
|
+
const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
|
|
49356
|
+
const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
|
|
49357
|
+
const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
|
|
49358
|
+
"react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
|
|
49359
|
+
`if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
|
|
49360
|
+
"rm -f \"$react_doctor_output\";",
|
|
49361
|
+
"else",
|
|
49362
|
+
"rm -f \"$react_doctor_output\";",
|
|
49363
|
+
`printf "%s\\n" "React Doctor found staged regressions." "Run ${REACT_DOCTOR_COMMAND} to inspect." "Want them fixed? Ask your agent to run that command and resolve the findings." >&2;`,
|
|
49364
|
+
"fi"
|
|
49365
|
+
].join(" ");
|
|
49366
|
+
const PACKAGE_JSON_FILE_NAME = "package.json";
|
|
49367
|
+
const runGit = (projectRoot, args) => {
|
|
49368
|
+
try {
|
|
49369
|
+
return execFileSync("git", [...args], {
|
|
49370
|
+
cwd: projectRoot,
|
|
49371
|
+
encoding: "utf8",
|
|
49372
|
+
stdio: [
|
|
49373
|
+
"ignore",
|
|
49374
|
+
"pipe",
|
|
49375
|
+
"ignore"
|
|
49376
|
+
]
|
|
49377
|
+
}).trim();
|
|
49378
|
+
} catch {
|
|
49379
|
+
return null;
|
|
49380
|
+
}
|
|
49381
|
+
};
|
|
49382
|
+
const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
|
|
49383
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49384
|
+
const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
|
|
49385
|
+
const readPackageJson = (projectRoot) => {
|
|
49386
|
+
try {
|
|
49387
|
+
return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
|
|
49388
|
+
} catch {
|
|
49389
|
+
return null;
|
|
49390
|
+
}
|
|
49391
|
+
};
|
|
49392
|
+
const writeJsonFile$1 = (filePath, value) => {
|
|
49393
|
+
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
49394
|
+
};
|
|
49395
|
+
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
49396
|
+
const packageJson = readPackageJson(projectRoot);
|
|
49397
|
+
if (!isRecord$1(packageJson)) return false;
|
|
49398
|
+
return [
|
|
49399
|
+
"dependencies",
|
|
49400
|
+
"devDependencies",
|
|
49401
|
+
"optionalDependencies"
|
|
49402
|
+
].some((fieldName) => {
|
|
49403
|
+
const dependencies = packageJson[fieldName];
|
|
49404
|
+
return isRecord$1(dependencies) && typeof dependencies[dependencyName] === "string";
|
|
49405
|
+
});
|
|
49406
|
+
};
|
|
49407
|
+
const packageHasRecordKey = (projectRoot, key) => {
|
|
49408
|
+
const packageJson = readPackageJson(projectRoot);
|
|
49409
|
+
return isRecord$1(packageJson) && isRecord$1(packageJson[key]);
|
|
49410
|
+
};
|
|
49411
|
+
const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
|
|
49412
|
+
const packageJson = readPackageJson(projectRoot);
|
|
49413
|
+
if (!isRecord$1(packageJson)) return false;
|
|
49414
|
+
const value = packageJson[key];
|
|
49415
|
+
return isRecord$1(value) && isRecord$1(value[nestedKey]);
|
|
49416
|
+
};
|
|
49417
|
+
const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
|
|
49418
|
+
//#endregion
|
|
49059
49419
|
//#region src/cli/utils/scan-result-cache.ts
|
|
49060
49420
|
const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
|
|
49061
49421
|
const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
@@ -49066,7 +49426,7 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
|
49066
49426
|
"eslint-plugin-react-hooks/package.json"
|
|
49067
49427
|
];
|
|
49068
49428
|
const bundledRequire = createRequire(import.meta.url);
|
|
49069
|
-
const isRecord
|
|
49429
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49070
49430
|
const normalizeForStableJson = (value) => {
|
|
49071
49431
|
if (value === null) return null;
|
|
49072
49432
|
if (value === void 0) return void 0;
|
|
@@ -49095,24 +49455,9 @@ const stringifyStableJson = (value) => {
|
|
|
49095
49455
|
}
|
|
49096
49456
|
};
|
|
49097
49457
|
const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
|
|
49098
|
-
const
|
|
49099
|
-
try {
|
|
49100
|
-
return execFileSync("git", [...args], {
|
|
49101
|
-
cwd: directory,
|
|
49102
|
-
encoding: "utf8",
|
|
49103
|
-
stdio: [
|
|
49104
|
-
"ignore",
|
|
49105
|
-
"pipe",
|
|
49106
|
-
"ignore"
|
|
49107
|
-
]
|
|
49108
|
-
}).trim();
|
|
49109
|
-
} catch {
|
|
49110
|
-
return null;
|
|
49111
|
-
}
|
|
49112
|
-
};
|
|
49113
|
-
const readHeadSha = (projectDirectory) => runGit$1(projectDirectory, ["rev-parse", "HEAD"]);
|
|
49458
|
+
const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
|
|
49114
49459
|
const isWorktreeClean = (projectDirectory) => {
|
|
49115
|
-
const status = runGit
|
|
49460
|
+
const status = runGit(projectDirectory, [
|
|
49116
49461
|
"status",
|
|
49117
49462
|
"--porcelain=v1",
|
|
49118
49463
|
"--untracked-files=normal"
|
|
@@ -49120,7 +49465,7 @@ const isWorktreeClean = (projectDirectory) => {
|
|
|
49120
49465
|
return status !== null && status.length === 0;
|
|
49121
49466
|
};
|
|
49122
49467
|
const hasHiddenTrackedFileState = (projectDirectory) => {
|
|
49123
|
-
const output = runGit
|
|
49468
|
+
const output = runGit(projectDirectory, ["ls-files", "-v"]);
|
|
49124
49469
|
if (output === null) return true;
|
|
49125
49470
|
return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
|
|
49126
49471
|
};
|
|
@@ -49133,7 +49478,7 @@ const resolveCacheFilePath = (projectDirectory) => {
|
|
|
49133
49478
|
const readPersistedCache = (cacheFilePath) => {
|
|
49134
49479
|
try {
|
|
49135
49480
|
const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
|
|
49136
|
-
if (!isRecord
|
|
49481
|
+
if (!isRecord(parsed) || parsed.version !== 1) return {
|
|
49137
49482
|
version: 1,
|
|
49138
49483
|
entries: []
|
|
49139
49484
|
};
|
|
@@ -49143,8 +49488,8 @@ const readPersistedCache = (cacheFilePath) => {
|
|
|
49143
49488
|
};
|
|
49144
49489
|
const entries = [];
|
|
49145
49490
|
for (const entry of parsed.entries) {
|
|
49146
|
-
if (!isRecord
|
|
49147
|
-
if (!isRecord
|
|
49491
|
+
if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
|
|
49492
|
+
if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
|
|
49148
49493
|
entries.push(entry);
|
|
49149
49494
|
}
|
|
49150
49495
|
return {
|
|
@@ -49676,6 +50021,7 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49676
50021
|
}
|
|
49677
50022
|
const animateRender = !options.silent && !options.verbose && canAnimateOnboarding(process.stdout);
|
|
49678
50023
|
const pause = onboardingSectionPause(animateRender);
|
|
50024
|
+
const useHyperlinks = shouldRenderHyperlinks(process.stdout);
|
|
49679
50025
|
const demotedDiagnosticCount = diagnostics.length - surfaceDiagnostics.length;
|
|
49680
50026
|
const lintSourceFileCount = options.includePaths.length > 0 ? options.includePaths.length : project.sourceFileCount;
|
|
49681
50027
|
if (printedDiagnostics.length === 0) {
|
|
@@ -49701,7 +50047,7 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49701
50047
|
yield* printDiagnostics([...printedDiagnostics], options.verbose, directory, buildRulePriorityMap([score]), isCodingAgentEnvironment(), {
|
|
49702
50048
|
sectionPause: pause,
|
|
49703
50049
|
animateCountUp: animateRender
|
|
49704
|
-
});
|
|
50050
|
+
}, useHyperlinks);
|
|
49705
50051
|
if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
|
|
49706
50052
|
if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
|
|
49707
50053
|
yield* log(highlighter.gray(` ${demotedDiagnosticCount} demoted from the ${options.outputSurface} surface (e.g. design cleanup) — run \`npx react-doctor@latest .\` locally for the full list.`));
|
|
@@ -49890,24 +50236,18 @@ const isExpectedUserError = (error) => error instanceof CliInputError || isProje
|
|
|
49890
50236
|
//#region src/cli/utils/build-handoff-payload.ts
|
|
49891
50237
|
const buildHandoffPayload = (input) => {
|
|
49892
50238
|
const topGroups = buildSortedRuleGroups(input.diagnostics).slice(0, 3);
|
|
50239
|
+
const migrationScaleBuckets = new Map(findMigrationScaleBuckets(input.diagnostics).map((bucket) => [bucket.ruleKey, bucket]));
|
|
49893
50240
|
let outputDirectory = null;
|
|
49894
50241
|
try {
|
|
49895
50242
|
outputDirectory = writeDiagnosticsDirectory([...input.diagnostics], input.outputDirectory);
|
|
49896
50243
|
} catch {}
|
|
49897
|
-
const lines = [
|
|
49898
|
-
"Before fixing anything, first ask me whether I'd like to add React Doctor to CI so every pull request is scanned automatically. Explain that:",
|
|
49899
|
-
`- It's used by teams at ${CI_TRUST_COMPANIES}.`,
|
|
49900
|
-
"- I don't have to fix everything at once; CI keeps new PRs clean while I incrementally pay down the existing issues.",
|
|
49901
|
-
`- Setup guide: ${CI_URL}`,
|
|
49902
|
-
"If I say yes, run `npx react-doctor install` (or add the GitHub Actions workflow). Either way, then continue with the fixes below.",
|
|
49903
|
-
"",
|
|
49904
|
-
`Fix the top ${topGroups.length} React Doctor ${topGroups.length === 1 ? "issue" : "issues"} in ${input.projectName} on this pass — leave the rest for a follow-up.`,
|
|
49905
|
-
""
|
|
49906
|
-
];
|
|
50244
|
+
const lines = [`Fix the top ${topGroups.length} React Doctor ${topGroups.length === 1 ? "issue" : "issues"} in ${input.projectName} on this pass — leave the rest for a follow-up.`, ""];
|
|
49907
50245
|
topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
|
|
49908
50246
|
const representative = ruleDiagnostics[0];
|
|
49909
50247
|
const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
|
|
49910
|
-
|
|
50248
|
+
const sharedFixSiteCount = getSharedFixSiteCount(ruleDiagnostics);
|
|
50249
|
+
const countBadge = sharedFixSiteCount > 0 ? `one fix · ${sharedFixSiteCount} sites` : `×${ruleDiagnostics.length}`;
|
|
50250
|
+
lines.push(`${index + 1}. ${severityLabel} ${representative.category}: ${representative.title ?? ruleKey} (${countBadge})`, ` ${representative.message}`);
|
|
49911
50251
|
const fixRecipeLine = formatFixRecipeLine(representative);
|
|
49912
50252
|
if (fixRecipeLine) lines.push(` ${fixRecipeLine}`);
|
|
49913
50253
|
const uniqueFiles = [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.filePath))];
|
|
@@ -49917,10 +50257,19 @@ const buildHandoffPayload = (input) => {
|
|
|
49917
50257
|
}
|
|
49918
50258
|
const remainingFiles = uniqueFiles.length - 3;
|
|
49919
50259
|
if (remainingFiles > 0) lines.push(` - +${remainingFiles} more files`);
|
|
50260
|
+
const migrationBucket = migrationScaleBuckets.get(ruleKey);
|
|
50261
|
+
if (migrationBucket) lines.push(` Migration-scale (${migrationBucket.fileCount} files): fix a representative sample, confirm the recipe holds, and get the code owner's sign-off before changing the rest in one pass.`);
|
|
49920
50262
|
});
|
|
49921
50263
|
lines.push("");
|
|
49922
50264
|
if (outputDirectory) lines.push(`Full results for all ${input.diagnostics.length} issues (diagnostics.json + a .txt per rule): ${outputDirectory}`, "");
|
|
49923
|
-
lines.push("Read each file and fix the root cause — don't suppress or silence the rule.", "", "Verify against the real thing, don't assume: confirm each change matches the canonical fix recipe you fetched for that rule, then re-run `npx react-doctor@latest --verbose` and check the issue is actually gone against the real tool before moving on.", "", "Teach me as you go: for every issue you touch, explain it in plain language (no jargon) — what the problem is, why it's a problem, and how serious it is in human terms. Describe the real-world impact and severity concretely (e.g. \"this crashes the page for users on Safari\" vs. \"this is a minor cleanup with no user impact\") so I understand why it matters, not just what changed.", ""
|
|
50265
|
+
lines.push("Read each file and fix the root cause — don't suppress or silence the rule.", "", "Findings that share a `fixGroupId` (in diagnostics.json) are one root cause — a single fix clears all of them, so treat each `fixGroupId` as ONE task, not one per site.", "", "Verify against the real thing, don't assume: confirm each change matches the canonical fix recipe you fetched for that rule, then re-run `npx react-doctor@latest --verbose` and check the issue is actually gone against the real tool before moving on.", "", "Teach me as you go: for every issue you touch, explain it in plain language (no jargon) — what the problem is, why it's a problem, and how serious it is in human terms. Describe the real-world impact and severity concretely (e.g. \"this crashes the page for users on Safari\" vs. \"this is a minor cleanup with no user impact\") so I understand why it matters, not just what changed.", "");
|
|
50266
|
+
const shownRuleKeys = new Set(topGroups.map(([ruleKey]) => ruleKey));
|
|
50267
|
+
const deferredMigrationBuckets = [...migrationScaleBuckets.values()].filter((bucket) => !shownRuleKeys.has(bucket.ruleKey));
|
|
50268
|
+
if (deferredMigrationBuckets.length > 0) {
|
|
50269
|
+
const ruleSummaries = deferredMigrationBuckets.map((bucket) => `${bucket.title} (${bucket.fileCount} files)`).join(", ");
|
|
50270
|
+
lines.push(`Some of the rest are migration-scale (span dozens of files): ${ruleSummaries}. For each, fix a representative sample, confirm the recipe holds, and get the code owner's sign-off before changing the rest in one pass.`, "");
|
|
50271
|
+
}
|
|
50272
|
+
lines.push("Then work through the rest from the full results above.");
|
|
49924
50273
|
return lines.join("\n");
|
|
49925
50274
|
};
|
|
49926
50275
|
//#endregion
|
|
@@ -49964,78 +50313,6 @@ const detectAvailableAgents = async () => {
|
|
|
49964
50313
|
return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
|
|
49965
50314
|
};
|
|
49966
50315
|
//#endregion
|
|
49967
|
-
//#region src/cli/utils/git-hook-shared.ts
|
|
49968
|
-
const HOOK_FILE_NAME = "pre-commit";
|
|
49969
|
-
const HOOK_RELATIVE_PATH = "hooks/pre-commit";
|
|
49970
|
-
const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
|
|
49971
|
-
const HUSKY_HOOKS_PATH = ".husky";
|
|
49972
|
-
const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
|
|
49973
|
-
const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
|
|
49974
|
-
const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
|
|
49975
|
-
const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
|
|
49976
|
-
const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
|
|
49977
|
-
const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
|
|
49978
|
-
"react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
|
|
49979
|
-
`if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
|
|
49980
|
-
"rm -f \"$react_doctor_output\";",
|
|
49981
|
-
"else",
|
|
49982
|
-
"rm -f \"$react_doctor_output\";",
|
|
49983
|
-
`printf "%s\\n" "React Doctor found staged regressions." "Run ${REACT_DOCTOR_COMMAND} to inspect." "Want them fixed? Ask your agent to run that command and resolve the findings." >&2;`,
|
|
49984
|
-
"fi"
|
|
49985
|
-
].join(" ");
|
|
49986
|
-
const PACKAGE_JSON_FILE_NAME = "package.json";
|
|
49987
|
-
const runGit = (projectRoot, args) => {
|
|
49988
|
-
try {
|
|
49989
|
-
return execFileSync("git", [...args], {
|
|
49990
|
-
cwd: projectRoot,
|
|
49991
|
-
encoding: "utf8",
|
|
49992
|
-
stdio: [
|
|
49993
|
-
"ignore",
|
|
49994
|
-
"pipe",
|
|
49995
|
-
"ignore"
|
|
49996
|
-
]
|
|
49997
|
-
}).trim();
|
|
49998
|
-
} catch {
|
|
49999
|
-
return null;
|
|
50000
|
-
}
|
|
50001
|
-
};
|
|
50002
|
-
const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
|
|
50003
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
50004
|
-
const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
|
|
50005
|
-
const readPackageJson = (projectRoot) => {
|
|
50006
|
-
try {
|
|
50007
|
-
return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
|
|
50008
|
-
} catch {
|
|
50009
|
-
return null;
|
|
50010
|
-
}
|
|
50011
|
-
};
|
|
50012
|
-
const writeJsonFile$1 = (filePath, value) => {
|
|
50013
|
-
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
50014
|
-
};
|
|
50015
|
-
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
50016
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50017
|
-
if (!isRecord(packageJson)) return false;
|
|
50018
|
-
return [
|
|
50019
|
-
"dependencies",
|
|
50020
|
-
"devDependencies",
|
|
50021
|
-
"optionalDependencies"
|
|
50022
|
-
].some((fieldName) => {
|
|
50023
|
-
const dependencies = packageJson[fieldName];
|
|
50024
|
-
return isRecord(dependencies) && typeof dependencies[dependencyName] === "string";
|
|
50025
|
-
});
|
|
50026
|
-
};
|
|
50027
|
-
const packageHasRecordKey = (projectRoot, key) => {
|
|
50028
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50029
|
-
return isRecord(packageJson) && isRecord(packageJson[key]);
|
|
50030
|
-
};
|
|
50031
|
-
const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
|
|
50032
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50033
|
-
if (!isRecord(packageJson)) return false;
|
|
50034
|
-
const value = packageJson[key];
|
|
50035
|
-
return isRecord(value) && isRecord(value[nestedKey]);
|
|
50036
|
-
};
|
|
50037
|
-
const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
|
|
50038
|
-
//#endregion
|
|
50039
50316
|
//#region src/cli/utils/install-doctor-script.ts
|
|
50040
50317
|
const DOCTOR_SCRIPT_NAME = "doctor";
|
|
50041
50318
|
const FALLBACK_DOCTOR_SCRIPT_NAME = "react-doctor";
|
|
@@ -50061,31 +50338,31 @@ const findNearestPackageDirectory = (startDirectory, stopDirectory) => {
|
|
|
50061
50338
|
};
|
|
50062
50339
|
const hasDoctorScript = (projectRoot) => {
|
|
50063
50340
|
const packageJson = readPackageJson(findNearestPackageDirectory(projectRoot) ?? projectRoot);
|
|
50064
|
-
if (!isRecord(packageJson)) return false;
|
|
50341
|
+
if (!isRecord$1(packageJson)) return false;
|
|
50065
50342
|
const scripts = packageJson.scripts;
|
|
50066
|
-
if (!isRecord(scripts)) return false;
|
|
50343
|
+
if (!isRecord$1(scripts)) return false;
|
|
50067
50344
|
return isReactDoctorScriptCommand(scripts[DOCTOR_SCRIPT_NAME]) || isReactDoctorScriptCommand(scripts[FALLBACK_DOCTOR_SCRIPT_NAME]);
|
|
50068
50345
|
};
|
|
50069
50346
|
const hasDoctorDependency = (packageJson) => DEPENDENCY_FIELD_NAMES.some((fieldName) => {
|
|
50070
50347
|
const dependencies = packageJson[fieldName];
|
|
50071
|
-
return isRecord(dependencies) && Object.hasOwn(dependencies, "react-doctor");
|
|
50348
|
+
return isRecord$1(dependencies) && Object.hasOwn(dependencies, "react-doctor");
|
|
50072
50349
|
});
|
|
50073
50350
|
const installDoctorScript = (options) => {
|
|
50074
50351
|
const packageDirectory = findNearestPackageDirectory(options.projectRoot) ?? options.projectRoot;
|
|
50075
50352
|
const packageJsonPath = getPackageJsonPath(packageDirectory);
|
|
50076
50353
|
const packageJson = readPackageJson(packageDirectory);
|
|
50077
|
-
if (!isRecord(packageJson)) return {
|
|
50354
|
+
if (!isRecord$1(packageJson)) return {
|
|
50078
50355
|
packageJsonPath,
|
|
50079
50356
|
scriptStatus: "skipped",
|
|
50080
50357
|
scriptReason: "missing-or-invalid-package-json"
|
|
50081
50358
|
};
|
|
50082
50359
|
const scripts = packageJson.scripts;
|
|
50083
50360
|
const scriptTarget = (() => {
|
|
50084
|
-
if (scripts !== void 0 && !isRecord(scripts)) return {
|
|
50361
|
+
if (scripts !== void 0 && !isRecord$1(scripts)) return {
|
|
50085
50362
|
status: "skipped",
|
|
50086
50363
|
reason: "invalid-scripts"
|
|
50087
50364
|
};
|
|
50088
|
-
const scriptRecord = isRecord(scripts) ? scripts : {};
|
|
50365
|
+
const scriptRecord = isRecord$1(scripts) ? scripts : {};
|
|
50089
50366
|
if (isReactDoctorScriptCommand(scriptRecord[DOCTOR_SCRIPT_NAME])) return {
|
|
50090
50367
|
scriptName: DOCTOR_SCRIPT_NAME,
|
|
50091
50368
|
status: "existing"
|
|
@@ -50119,7 +50396,7 @@ const installDoctorScript = (options) => {
|
|
|
50119
50396
|
if (scriptStatus === "created") writeJsonFile$1(packageJsonPath, {
|
|
50120
50397
|
...packageJson,
|
|
50121
50398
|
scripts: {
|
|
50122
|
-
...isRecord(scripts) ? scripts : {},
|
|
50399
|
+
...isRecord$1(scripts) ? scripts : {},
|
|
50123
50400
|
[scriptTarget.scriptName ?? DOCTOR_SCRIPT_NAME]: DOCTOR_SCRIPT_COMMAND
|
|
50124
50401
|
}
|
|
50125
50402
|
});
|
|
@@ -50273,38 +50550,52 @@ const upgradeReactDoctorWorkflowInPlace = (projectRoot) => {
|
|
|
50273
50550
|
//#region src/cli/utils/hash-project-root.ts
|
|
50274
50551
|
const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
|
|
50275
50552
|
//#endregion
|
|
50276
|
-
//#region src/cli/utils/
|
|
50277
|
-
const
|
|
50278
|
-
const
|
|
50279
|
-
|
|
50280
|
-
|
|
50281
|
-
});
|
|
50282
|
-
|
|
50283
|
-
|
|
50284
|
-
|
|
50285
|
-
|
|
50286
|
-
|
|
50287
|
-
|
|
50288
|
-
|
|
50289
|
-
};
|
|
50290
|
-
const recordActionUpgradeDecision = (projectRoot, outcome, storeOptions = {}) => {
|
|
50291
|
-
try {
|
|
50292
|
-
const store = getActionUpgradeStore(storeOptions);
|
|
50293
|
-
const upgrades = store.get("actionUpgrades", {});
|
|
50294
|
-
store.set("actionUpgrades", {
|
|
50295
|
-
...upgrades,
|
|
50296
|
-
[hashProjectRoot(projectRoot)]: {
|
|
50297
|
-
rootDirectory: Path.resolve(projectRoot),
|
|
50298
|
-
outcome,
|
|
50299
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
50553
|
+
//#region src/cli/utils/project-decision-store.ts
|
|
50554
|
+
const createProjectDecisionStore = (storeKey) => {
|
|
50555
|
+
const getStore = (options = {}) => new Conf({
|
|
50556
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
50557
|
+
cwd: options.cwd
|
|
50558
|
+
});
|
|
50559
|
+
return {
|
|
50560
|
+
getConfigPath: (options = {}) => getStore(options).path,
|
|
50561
|
+
hasHandled: (projectRoot, options = {}) => {
|
|
50562
|
+
try {
|
|
50563
|
+
return Boolean(getStore(options).get(storeKey, {})[hashProjectRoot(projectRoot)]);
|
|
50564
|
+
} catch {
|
|
50565
|
+
return true;
|
|
50300
50566
|
}
|
|
50301
|
-
}
|
|
50302
|
-
|
|
50303
|
-
|
|
50304
|
-
|
|
50305
|
-
|
|
50567
|
+
},
|
|
50568
|
+
record: (projectRoot, outcome, options = {}) => {
|
|
50569
|
+
try {
|
|
50570
|
+
const store = getStore(options);
|
|
50571
|
+
store.set(storeKey, {
|
|
50572
|
+
...store.get(storeKey, {}),
|
|
50573
|
+
[hashProjectRoot(projectRoot)]: {
|
|
50574
|
+
rootDirectory: Path.resolve(projectRoot),
|
|
50575
|
+
outcome,
|
|
50576
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
50577
|
+
}
|
|
50578
|
+
});
|
|
50579
|
+
return true;
|
|
50580
|
+
} catch {
|
|
50581
|
+
return false;
|
|
50582
|
+
}
|
|
50583
|
+
}
|
|
50584
|
+
};
|
|
50306
50585
|
};
|
|
50307
50586
|
//#endregion
|
|
50587
|
+
//#region src/cli/utils/action-upgrade-prompt.ts
|
|
50588
|
+
const store$1 = createProjectDecisionStore("actionUpgrades");
|
|
50589
|
+
store$1.getConfigPath;
|
|
50590
|
+
const hasHandledActionUpgrade = store$1.hasHandled;
|
|
50591
|
+
const recordActionUpgradeDecision = store$1.record;
|
|
50592
|
+
//#endregion
|
|
50593
|
+
//#region src/cli/utils/ci-prompt-decision.ts
|
|
50594
|
+
const store = createProjectDecisionStore("ciPrompts");
|
|
50595
|
+
store.getConfigPath;
|
|
50596
|
+
const hasHandledCiPrompt = store.hasHandled;
|
|
50597
|
+
const recordCiPromptDecision = store.record;
|
|
50598
|
+
//#endregion
|
|
50308
50599
|
//#region src/cli/utils/open-url.ts
|
|
50309
50600
|
const resolveOpenCommand = (url) => {
|
|
50310
50601
|
if (process$1.platform === "darwin") return {
|
|
@@ -50760,22 +51051,22 @@ const buildAgentHookScript = () => [
|
|
|
50760
51051
|
"",
|
|
50761
51052
|
"run_react_doctor() {",
|
|
50762
51053
|
" if [ -x ./node_modules/.bin/react-doctor ]; then",
|
|
50763
|
-
" ./node_modules/.bin/react-doctor --verbose --
|
|
51054
|
+
" ./node_modules/.bin/react-doctor --verbose --scope changed --blocking warning --no-score",
|
|
50764
51055
|
" return",
|
|
50765
51056
|
" fi",
|
|
50766
51057
|
"",
|
|
50767
51058
|
" if command -v react-doctor >/dev/null 2>&1; then",
|
|
50768
|
-
" react-doctor --verbose --
|
|
51059
|
+
" react-doctor --verbose --scope changed --blocking warning --no-score",
|
|
50769
51060
|
" return",
|
|
50770
51061
|
" fi",
|
|
50771
51062
|
"",
|
|
50772
51063
|
" if command -v pnpm >/dev/null 2>&1; then",
|
|
50773
|
-
" pnpm dlx react-doctor@latest --verbose --
|
|
51064
|
+
" pnpm dlx react-doctor@latest --verbose --scope changed --blocking warning --no-score",
|
|
50774
51065
|
" return",
|
|
50775
51066
|
" fi",
|
|
50776
51067
|
"",
|
|
50777
51068
|
" if command -v npx >/dev/null 2>&1; then",
|
|
50778
|
-
" npx --yes react-doctor@latest --verbose --
|
|
51069
|
+
" npx --yes react-doctor@latest --verbose --scope changed --blocking warning --no-score",
|
|
50779
51070
|
" return",
|
|
50780
51071
|
" fi",
|
|
50781
51072
|
"",
|
|
@@ -50933,13 +51224,13 @@ const installPackageJsonHook = (options, strategy) => {
|
|
|
50933
51224
|
const packageJsonPath = getPackageJsonPath(options.projectRoot);
|
|
50934
51225
|
const didHookExist = NFS.existsSync(packageJsonPath);
|
|
50935
51226
|
const packageJson = readPackageJson(options.projectRoot);
|
|
50936
|
-
const nextPackageJson = isRecord(packageJson) ? { ...packageJson } : {};
|
|
51227
|
+
const nextPackageJson = isRecord$1(packageJson) ? { ...packageJson } : {};
|
|
50937
51228
|
const parentKeys = strategy.path.slice(0, -1);
|
|
50938
51229
|
const leafKey = strategy.path[strategy.path.length - 1];
|
|
50939
51230
|
let parent = nextPackageJson;
|
|
50940
51231
|
for (const key of parentKeys) {
|
|
50941
51232
|
const existing = parent[key];
|
|
50942
|
-
const cloned = isRecord(existing) ? { ...existing } : {};
|
|
51233
|
+
const cloned = isRecord$1(existing) ? { ...existing } : {};
|
|
50943
51234
|
parent[key] = cloned;
|
|
50944
51235
|
parent = cloned;
|
|
50945
51236
|
}
|
|
@@ -51110,7 +51401,7 @@ const isHuskyProject = (projectRoot) => NFS.existsSync(Path.join(projectRoot, ".
|
|
|
51110
51401
|
const isVitePlusProject = (projectRoot) => packageHasDependency(projectRoot, "vite-plus");
|
|
51111
51402
|
const isSimpleGitHooksProject = (projectRoot) => {
|
|
51112
51403
|
const packageJson = readPackageJson(projectRoot);
|
|
51113
|
-
return isRecord(packageJson) && isRecord(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
|
|
51404
|
+
return isRecord$1(packageJson) && isRecord$1(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
|
|
51114
51405
|
};
|
|
51115
51406
|
const getLefthookConfigPath = (projectRoot) => {
|
|
51116
51407
|
for (const fileName of LEFTHOOK_CONFIG_FILES) {
|
|
@@ -51276,7 +51567,7 @@ const detectPackageManager = (projectRoot) => {
|
|
|
51276
51567
|
let currentDirectory = Path.resolve(projectRoot);
|
|
51277
51568
|
while (true) {
|
|
51278
51569
|
const packageJson = readPackageJson(currentDirectory);
|
|
51279
|
-
if (isRecord(packageJson) && typeof packageJson.packageManager === "string") {
|
|
51570
|
+
if (isRecord$1(packageJson) && typeof packageJson.packageManager === "string") {
|
|
51280
51571
|
const packageManagerName = packageJson.packageManager.split("@")[0];
|
|
51281
51572
|
if (packageManagerName === "pnpm" || packageManagerName === "yarn" || packageManagerName === "bun" || packageManagerName === "npm") return packageManagerName;
|
|
51282
51573
|
}
|
|
@@ -51352,12 +51643,12 @@ const isSupplyChainTrustError = (error) => {
|
|
|
51352
51643
|
const formatInstallCommand = (input) => [input.command, ...input.args].join(" ");
|
|
51353
51644
|
const installReactDoctorDependency = async (options) => {
|
|
51354
51645
|
const packageJson = readPackageJson(options.projectRoot);
|
|
51355
|
-
if (!isRecord(packageJson)) return {
|
|
51646
|
+
if (!isRecord$1(packageJson)) return {
|
|
51356
51647
|
dependencyStatus: "skipped",
|
|
51357
51648
|
dependencyReason: "missing-or-invalid-package-json"
|
|
51358
51649
|
};
|
|
51359
51650
|
if (hasDoctorDependency(packageJson)) return { dependencyStatus: "existing" };
|
|
51360
|
-
if (packageJson.devDependencies !== void 0 && !isRecord(packageJson.devDependencies)) return {
|
|
51651
|
+
if (packageJson.devDependencies !== void 0 && !isRecord$1(packageJson.devDependencies)) return {
|
|
51361
51652
|
dependencyStatus: "skipped",
|
|
51362
51653
|
dependencyReason: "invalid-dev-dependencies"
|
|
51363
51654
|
};
|
|
@@ -51521,10 +51812,12 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
51521
51812
|
const existingWorkflow = readReactDoctorWorkflow(projectRoot);
|
|
51522
51813
|
const canInstallWorkflow = !NFS.existsSync(workflowTargetPath);
|
|
51523
51814
|
const canUpgradeWorkflow = existingWorkflow !== null && workflowUsesV1Action(existingWorkflow.content) && !hasHandledActionUpgrade(projectRoot);
|
|
51524
|
-
const
|
|
51815
|
+
const ciPromptOutcome = canInstallWorkflow && !options.yes && !skipPrompts && !hasHandledCiPrompt(projectRoot) ? await askAddToGitHubActions(prompt) : null;
|
|
51816
|
+
const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || ciPromptOutcome === "yes");
|
|
51525
51817
|
const upgradePromptOutcome = canUpgradeWorkflow && !options.yes && !skipPrompts ? await askUpgradeActionVersion(prompt) : null;
|
|
51526
51818
|
const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
|
|
51527
51819
|
if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
|
|
51820
|
+
if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
|
|
51528
51821
|
const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
|
|
51529
51822
|
type: "multiselect",
|
|
51530
51823
|
name: "agents",
|
|
@@ -51771,18 +52064,24 @@ const handoffToAgent = async (input) => {
|
|
|
51771
52064
|
if (!input.interactive || input.diagnostics.length === 0) return;
|
|
51772
52065
|
cliLogger.break();
|
|
51773
52066
|
const projectRootForCi = findNearestPackageDirectory(input.rootDirectory) ?? input.rootDirectory;
|
|
51774
|
-
|
|
52067
|
+
const isGitHubActionsConfigured = isReactDoctorWorkflowInstalled(projectRootForCi);
|
|
52068
|
+
if (!isGitHubActionsConfigured && !hasHandledCiPrompt(projectRootForCi)) {
|
|
51775
52069
|
const ciOutcome = await askAddToGitHubActions();
|
|
51776
52070
|
recordCount(METRIC.agentHandoff, 1, {
|
|
51777
52071
|
outcome: `ci-${ciOutcome}`,
|
|
51778
52072
|
diagnosticsCount: input.diagnostics.length
|
|
51779
52073
|
});
|
|
51780
52074
|
if (ciOutcome === "cancel") return;
|
|
52075
|
+
recordCiPromptDecision(projectRootForCi, ciOutcome === "yes" ? "accepted" : "declined");
|
|
51781
52076
|
if (ciOutcome === "yes") {
|
|
51782
52077
|
await setUpGitHubActions({ rootDirectory: input.rootDirectory });
|
|
51783
52078
|
cliLogger.break();
|
|
51784
52079
|
}
|
|
51785
|
-
} else await maybeOfferActionUpgrade(projectRootForCi);
|
|
52080
|
+
} else if (isGitHubActionsConfigured) await maybeOfferActionUpgrade(projectRootForCi);
|
|
52081
|
+
else recordCount(METRIC.agentHandoff, 1, {
|
|
52082
|
+
outcome: "ci-suppressed",
|
|
52083
|
+
diagnosticsCount: input.diagnostics.length
|
|
52084
|
+
});
|
|
51786
52085
|
const { handoffTarget } = await prompts({
|
|
51787
52086
|
type: "select",
|
|
51788
52087
|
name: "handoffTarget",
|
|
@@ -52005,6 +52304,7 @@ const reportErrorToSentry = async (error) => {
|
|
|
52005
52304
|
sampled: runTrace.sampled,
|
|
52006
52305
|
sampleRand: Math.random()
|
|
52007
52306
|
});
|
|
52307
|
+
recordRunTraceId(scope.getPropagationContext().traceId);
|
|
52008
52308
|
return Sentry.captureException(error);
|
|
52009
52309
|
});
|
|
52010
52310
|
await Sentry.flush(SENTRY_FLUSH_TIMEOUT_MS);
|
|
@@ -52088,7 +52388,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52088
52388
|
yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
|
|
52089
52389
|
if (displayDiagnostics.length > 0) {
|
|
52090
52390
|
yield* log("");
|
|
52091
|
-
yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender });
|
|
52391
|
+
yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender }, shouldRenderHyperlinks(process.stdout));
|
|
52092
52392
|
}
|
|
52093
52393
|
const lowestScoredScan = findLowestScoredScan(completedScans);
|
|
52094
52394
|
const aggregateScore = lowestScoredScan?.result.score ?? null;
|
|
@@ -52126,9 +52426,8 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52126
52426
|
});
|
|
52127
52427
|
//#endregion
|
|
52128
52428
|
//#region src/cli/utils/prompt-install-setup.ts
|
|
52129
|
-
const GLOBAL_CONFIG_PROJECT_NAME = "react-doctor";
|
|
52130
52429
|
const getSetupPromptStore = (options = {}) => new Conf({
|
|
52131
|
-
projectName:
|
|
52430
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
52132
52431
|
cwd: options.cwd
|
|
52133
52432
|
});
|
|
52134
52433
|
const getSetupPromptProjectKey = (projectRoot) => hashProjectRoot(projectRoot);
|
|
@@ -52139,6 +52438,24 @@ const hasDisabledSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
|
52139
52438
|
return false;
|
|
52140
52439
|
}
|
|
52141
52440
|
};
|
|
52441
|
+
const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
52442
|
+
try {
|
|
52443
|
+
const store = getSetupPromptStore(storeOptions);
|
|
52444
|
+
const projects = store.get("projects", {});
|
|
52445
|
+
const projectKey = getSetupPromptProjectKey(projectRoot);
|
|
52446
|
+
store.set("projects", {
|
|
52447
|
+
...projects,
|
|
52448
|
+
[projectKey]: {
|
|
52449
|
+
...projects[projectKey] ?? {},
|
|
52450
|
+
rootDirectory: Path.resolve(projectRoot),
|
|
52451
|
+
setupPrompt: false
|
|
52452
|
+
}
|
|
52453
|
+
});
|
|
52454
|
+
return true;
|
|
52455
|
+
} catch {
|
|
52456
|
+
return false;
|
|
52457
|
+
}
|
|
52458
|
+
};
|
|
52142
52459
|
const resolveInstallSetupProjectRoot = (options) => {
|
|
52143
52460
|
if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
|
|
52144
52461
|
const packageDirectories = /* @__PURE__ */ new Set();
|
|
@@ -52545,6 +52862,14 @@ const runExplain = async (fileLineArgument, context) => {
|
|
|
52545
52862
|
const colorizedRule = colorizeRuleByDiagnostic(ruleIdentifier, diagnostic.severity);
|
|
52546
52863
|
const severityLabel = colorizeRuleByDiagnostic(diagnostic.severity, diagnostic.severity);
|
|
52547
52864
|
cliLogger.log(`${severitySymbol} ${colorizedRule} ${highlighter.dim(`(${severityLabel})`)} — ${diagnostic.message}`);
|
|
52865
|
+
const codeFrame = buildCodeFrame({
|
|
52866
|
+
filePath: diagnostic.filePath,
|
|
52867
|
+
line: diagnostic.line,
|
|
52868
|
+
column: diagnostic.column,
|
|
52869
|
+
endLine: diagnostic.endLine,
|
|
52870
|
+
rootDirectory: targetDirectory
|
|
52871
|
+
});
|
|
52872
|
+
if (codeFrame) cliLogger.log(indentMultilineText(codeFrame, " "));
|
|
52548
52873
|
if (diagnostic.category) cliLogger.dim(` Category: ${diagnostic.category}`);
|
|
52549
52874
|
if (diagnostic.help) cliLogger.dim(` ${diagnostic.help}`);
|
|
52550
52875
|
cliLogger.dim(` If this needs follow-up or looks like a false positive, open: ${buildDiagnosticIssueUrl({
|
|
@@ -52594,6 +52919,10 @@ const validateModeFlags = (flags) => {
|
|
|
52594
52919
|
if (flags.staged && (flags.scope === "full" || flags.scope === "changed")) throw new CliInputError(`Cannot combine --staged with --scope ${flags.scope}; use --scope files or --scope lines, or drop --scope.`);
|
|
52595
52920
|
if (flags.score && flags.json) throw new CliInputError("Cannot combine --score and --json; pick one output mode.");
|
|
52596
52921
|
if (flags.score && flags.telemetry === false) throw new CliInputError("Cannot combine --score with --no-telemetry; --score prints the score that --no-telemetry disables.");
|
|
52922
|
+
if (flags.debug && (flags.score === false || flags.telemetry === false)) {
|
|
52923
|
+
const disablingFlag = flags.score === false ? "--no-score" : "--no-telemetry";
|
|
52924
|
+
throw new CliInputError(`Cannot combine --debug with ${disablingFlag}; ${disablingFlag} disables the Sentry reporting --debug needs to capture a trace.`);
|
|
52925
|
+
}
|
|
52597
52926
|
};
|
|
52598
52927
|
//#endregion
|
|
52599
52928
|
//#region src/cli/commands/inspect.ts
|
|
@@ -52941,11 +53270,13 @@ const inspectAction = async (directory, flags) => {
|
|
|
52941
53270
|
})) {
|
|
52942
53271
|
printAgentInstallHint();
|
|
52943
53272
|
recordCount(METRIC.agentInstallHintShown, 1);
|
|
53273
|
+
disableSetupPrompt(setupProjectRoot);
|
|
52944
53274
|
}
|
|
52945
53275
|
}
|
|
52946
53276
|
} catch (error) {
|
|
52947
53277
|
const isUserError = isExpectedUserError(error);
|
|
52948
53278
|
const sentryEventId = isUserError ? void 0 : await reportErrorToSentry(error);
|
|
53279
|
+
if (isDebugFlagEnabled()) await flushSentry();
|
|
52949
53280
|
if (isJsonMode) {
|
|
52950
53281
|
writeJsonErrorReport(error, sentryEventId);
|
|
52951
53282
|
process.exitCode = 1;
|
|
@@ -53668,6 +53999,33 @@ const normalizeHelpInvocation = (argv, knownCommands) => {
|
|
|
53668
53999
|
return [...nodeArguments, "--help"];
|
|
53669
54000
|
};
|
|
53670
54001
|
//#endregion
|
|
54002
|
+
//#region src/cli/utils/print-debug-trace.ts
|
|
54003
|
+
/**
|
|
54004
|
+
* The `--debug` end-of-run line, pure so it's testable without the Sentry SDK.
|
|
54005
|
+
* Mirrors the crash-reference phrasing in `handle-error.ts` ("mention this when
|
|
54006
|
+
* reporting") so users learn one habit for both paths. A `null` trace says why,
|
|
54007
|
+
* so `--debug` never silently does nothing.
|
|
54008
|
+
*/
|
|
54009
|
+
const buildDebugTraceMessage = (traceId) => traceId === null ? "Sentry trace unavailable for this run (no trace was recorded)." : `Sentry trace (mention this when reporting): ${traceId}`;
|
|
54010
|
+
/**
|
|
54011
|
+
* Prints the run's Sentry trace id to stderr at the end of a `--debug` run, so
|
|
54012
|
+
* maintainers can pull the full trace from a pasted id. Runs from the process
|
|
54013
|
+
* `exit` handler, so it's the last line on both the success path and the error
|
|
54014
|
+
* funnels (which `process.exit()` before the promise chain could resume).
|
|
54015
|
+
*
|
|
54016
|
+
* Writes straight to `process.stderr` (not `Console`) for three reasons: the
|
|
54017
|
+
* exit handler is synchronous, JSON mode patches the global console to no-ops —
|
|
54018
|
+
* a diagnostic the user explicitly asked for must survive that — and stderr
|
|
54019
|
+
* keeps `--json` / `--score` stdout machine-clean. The write is wrapped because
|
|
54020
|
+
* a diagnostic must never throw out of an exit handler.
|
|
54021
|
+
*/
|
|
54022
|
+
const printDebugTrace = () => {
|
|
54023
|
+
if (!Sentry.isInitialized()) return;
|
|
54024
|
+
try {
|
|
54025
|
+
process.stderr.write(`${highlighter.dim(buildDebugTraceMessage(getLastRunTraceId()))}\n`);
|
|
54026
|
+
} catch {}
|
|
54027
|
+
};
|
|
54028
|
+
//#endregion
|
|
53671
54029
|
//#region src/cli/utils/removed-cli-flags.ts
|
|
53672
54030
|
const REMOVED_FLAGS = new Map([
|
|
53673
54031
|
["--full", "use `--diff false` to force a full scan"],
|
|
@@ -53694,6 +54052,7 @@ const ROOT_FLAG_SPEC = {
|
|
|
53694
54052
|
longOptionsWithoutValues: new Set([
|
|
53695
54053
|
"--color",
|
|
53696
54054
|
"--dead-code",
|
|
54055
|
+
"--debug",
|
|
53697
54056
|
"--help",
|
|
53698
54057
|
"--json",
|
|
53699
54058
|
"--json-compact",
|
|
@@ -53861,6 +54220,9 @@ const stripUnknownCliFlags = (argv) => {
|
|
|
53861
54220
|
initializeSentry();
|
|
53862
54221
|
process.on("SIGINT", exitGracefully);
|
|
53863
54222
|
process.on("SIGTERM", exitGracefully);
|
|
54223
|
+
process.on("exit", () => {
|
|
54224
|
+
if (isDebugFlagEnabled()) printDebugTrace();
|
|
54225
|
+
});
|
|
53864
54226
|
unrefStdin();
|
|
53865
54227
|
guardStdin();
|
|
53866
54228
|
const formatExampleLines = (examples) => {
|
|
@@ -53872,7 +54234,7 @@ ${highlighter.dim("Examples:")}
|
|
|
53872
54234
|
${formatExampleLines([
|
|
53873
54235
|
["react-doctor", "scan the current project"],
|
|
53874
54236
|
["react-doctor ./apps/web", "scan a specific directory"],
|
|
53875
|
-
["react-doctor --
|
|
54237
|
+
["react-doctor --scope changed --base main", "scan only new issues vs. main"],
|
|
53876
54238
|
["react-doctor --project modules/a,modules/b", "score each module separately (names or paths)"],
|
|
53877
54239
|
["react-doctor --staged", "scan staged files (pre-commit hook)"],
|
|
53878
54240
|
["react-doctor --category Security", "show only one diagnostic category"],
|
|
@@ -53905,7 +54267,7 @@ ${highlighter.dim("Learn more:")}
|
|
|
53905
54267
|
${highlighter.info(CANONICAL_GITHUB_URL)}
|
|
53906
54268
|
`;
|
|
53907
54269
|
const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
|
|
53908
|
-
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("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").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("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
|
|
54270
|
+
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("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").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("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
|
|
53909
54271
|
program.action(inspectAction);
|
|
53910
54272
|
program.command("why <location>").description("Explain why a rule fired (or why a suppression didn't apply) at a file:line").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple)").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").action((location, options) => whyAction(location, options));
|
|
53911
54273
|
program.command("install").alias("setup").description("Install the react-doctor skill into your coding agents and optional git hook").option("-y, --yes", "skip prompts, install for all detected agents").option("--dry-run", "show what would be installed without writing files").option("--agent-hooks", "install native non-blocking agent hooks for Claude Code and Cursor").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderInstallHelpEpilog).action(installAction);
|
|
@@ -53948,4 +54310,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
53948
54310
|
export {};
|
|
53949
54311
|
|
|
53950
54312
|
//# sourceMappingURL=cli.js.map
|
|
53951
|
-
//# debugId=
|
|
54313
|
+
//# debugId=87ed2e7a-69a3-5a4d-a104-8ba680ac1aa2
|