react-doctor 0.5.6-dev.81bbfcc → 0.5.6-dev.869f220
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 +758 -404
- package/dist/index.d.ts +19 -0
- package/dist/index.js +314 -194
- package/dist/lsp.js +332 -217
- package/package.json +4 -4
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]="4aa7cab0-c8a8-5513-bde6-3f7a0a1bdaa7")}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"));
|
|
@@ -39692,15 +39785,10 @@ const buildCapabilities = (project) => {
|
|
|
39692
39785
|
}
|
|
39693
39786
|
if (project.tailwindVersion !== null) {
|
|
39694
39787
|
capabilities.add("tailwind");
|
|
39695
|
-
|
|
39696
|
-
if (isTailwindAtLeast(tailwind, {
|
|
39788
|
+
if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
|
|
39697
39789
|
major: 3,
|
|
39698
39790
|
minor: 4
|
|
39699
39791
|
})) capabilities.add("tailwind:3.4");
|
|
39700
|
-
if (tailwind !== null && isTailwindAtLeast(tailwind, {
|
|
39701
|
-
major: 4,
|
|
39702
|
-
minor: 0
|
|
39703
|
-
})) capabilities.add("tailwind:4");
|
|
39704
39792
|
}
|
|
39705
39793
|
if (project.zodVersion !== null) {
|
|
39706
39794
|
capabilities.add("zod");
|
|
@@ -39908,7 +39996,7 @@ const readIgnoreFile = (filePath) => {
|
|
|
39908
39996
|
try {
|
|
39909
39997
|
content = NFS.readFileSync(filePath, "utf-8");
|
|
39910
39998
|
} catch (error) {
|
|
39911
|
-
const errnoCode = error
|
|
39999
|
+
const errnoCode = isErrnoException(error) ? error.code : void 0;
|
|
39912
40000
|
if (errnoCode && errnoCode !== "ENOENT") runSync(warn$1(`Could not read ignore file ${filePath}: ${errnoCode}`));
|
|
39913
40001
|
return [];
|
|
39914
40002
|
}
|
|
@@ -39946,8 +40034,8 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
39946
40034
|
cachedPatternsByRoot.set(rootDirectory, patterns);
|
|
39947
40035
|
return patterns;
|
|
39948
40036
|
};
|
|
40037
|
+
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39949
40038
|
const KNIP_JSON_FILENAME = "knip.json";
|
|
39950
|
-
const isRecord$1$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39951
40039
|
const readJsonFileSafe = (filePath) => {
|
|
39952
40040
|
let rawContents;
|
|
39953
40041
|
try {
|
|
@@ -39963,10 +40051,10 @@ const readJsonFileSafe = (filePath) => {
|
|
|
39963
40051
|
};
|
|
39964
40052
|
const readKnipConfig = (rootDirectory) => {
|
|
39965
40053
|
const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
|
|
39966
|
-
if (isRecord$
|
|
40054
|
+
if (isRecord$2(knipJson)) return knipJson;
|
|
39967
40055
|
const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
|
|
39968
|
-
const packageKnipConfig = isRecord$
|
|
39969
|
-
return isRecord$
|
|
40056
|
+
const packageKnipConfig = isRecord$2(packageJson) ? packageJson.knip : null;
|
|
40057
|
+
return isRecord$2(packageKnipConfig) ? packageKnipConfig : null;
|
|
39970
40058
|
};
|
|
39971
40059
|
const normalizePatternList = (value) => {
|
|
39972
40060
|
if (typeof value === "string" && value.length > 0) return [value];
|
|
@@ -39978,10 +40066,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
|
|
|
39978
40066
|
return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
|
|
39979
40067
|
};
|
|
39980
40068
|
const collectKnipWorkspacePatterns = (workspaces, settingName) => {
|
|
39981
|
-
if (!isRecord$
|
|
40069
|
+
if (!isRecord$2(workspaces)) return [];
|
|
39982
40070
|
const patterns = [];
|
|
39983
40071
|
for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
|
|
39984
|
-
if (!isRecord$
|
|
40072
|
+
if (!isRecord$2(workspaceConfig)) continue;
|
|
39985
40073
|
patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
|
|
39986
40074
|
}
|
|
39987
40075
|
return patterns;
|
|
@@ -40026,8 +40114,6 @@ const toCanonicalPath = (filePath) => {
|
|
|
40026
40114
|
};
|
|
40027
40115
|
const DEAD_CODE_PLUGIN = "deslop";
|
|
40028
40116
|
const DEAD_CODE_CATEGORY = "Maintainability";
|
|
40029
|
-
const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
|
|
40030
|
-
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
40031
40117
|
const DEAD_CODE_WORKER_SCRIPT = `
|
|
40032
40118
|
const inputChunks = [];
|
|
40033
40119
|
process.stdin.on("data", (chunk) => inputChunks.push(chunk));
|
|
@@ -40085,7 +40171,7 @@ process.stdin.on("end", () => {
|
|
|
40085
40171
|
});
|
|
40086
40172
|
`;
|
|
40087
40173
|
const resolveTsConfigPath = (rootDirectory) => {
|
|
40088
|
-
for (const filename of TSCONFIG_FILENAMES
|
|
40174
|
+
for (const filename of TSCONFIG_FILENAMES) {
|
|
40089
40175
|
const candidate = Path.join(rootDirectory, filename);
|
|
40090
40176
|
if (NFS.existsSync(candidate)) return candidate;
|
|
40091
40177
|
}
|
|
@@ -40466,15 +40552,13 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
|
|
|
40466
40552
|
})()) }));
|
|
40467
40553
|
static layerOf = (diagnostics) => succeed$3(DeadCode, DeadCode.of({ run: () => fromIterable$1(diagnostics) }));
|
|
40468
40554
|
};
|
|
40469
|
-
const createNodeReadFileLinesSync = (rootDirectory) => {
|
|
40470
|
-
|
|
40471
|
-
|
|
40472
|
-
|
|
40473
|
-
|
|
40474
|
-
|
|
40475
|
-
|
|
40476
|
-
}
|
|
40477
|
-
};
|
|
40555
|
+
const createNodeReadFileLinesSync = (rootDirectory) => (filePath) => {
|
|
40556
|
+
const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
|
|
40557
|
+
try {
|
|
40558
|
+
return NFS.readFileSync(absolutePath, "utf-8").split("\n");
|
|
40559
|
+
} catch {
|
|
40560
|
+
return null;
|
|
40561
|
+
}
|
|
40478
40562
|
};
|
|
40479
40563
|
var Files = class Files extends Service()("react-doctor/Files") {
|
|
40480
40564
|
static layerNode = succeed$3(Files, Files.of({
|
|
@@ -40685,7 +40769,10 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40685
40769
|
directory: input.directory,
|
|
40686
40770
|
cause
|
|
40687
40771
|
}) });
|
|
40688
|
-
})
|
|
40772
|
+
}), withSpan("git.exec", { attributes: {
|
|
40773
|
+
"git.command": input.command,
|
|
40774
|
+
"git.subcommand": input.args[0] ?? ""
|
|
40775
|
+
} }));
|
|
40689
40776
|
const runGit = (directory, args) => runCommand({
|
|
40690
40777
|
command: "git",
|
|
40691
40778
|
args,
|
|
@@ -40713,7 +40800,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40713
40800
|
]);
|
|
40714
40801
|
if (candidates.status !== 0) return null;
|
|
40715
40802
|
return trimOrNull(candidates.stdout.split("\n")[0] ?? "");
|
|
40716
|
-
});
|
|
40803
|
+
}).pipe(withSpan("Git.defaultBranch"));
|
|
40717
40804
|
const branchExists = (directory, branch) => runGit(directory, [
|
|
40718
40805
|
"rev-parse",
|
|
40719
40806
|
"--verify",
|
|
@@ -40760,7 +40847,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40760
40847
|
const result = resultOption.value;
|
|
40761
40848
|
if (result.status !== 0) return null;
|
|
40762
40849
|
return parseGithubViewerPermission(result.stdout);
|
|
40763
|
-
}).pipe(catch_$1(() => succeed$2(null)));
|
|
40850
|
+
}).pipe(catch_$1(() => succeed$2(null)), withSpan("Git.githubViewerPermission"));
|
|
40764
40851
|
/**
|
|
40765
40852
|
* Resolves a `--diff A..B` / `A...B` commit range into a changed-file
|
|
40766
40853
|
* selection. Each endpoint is validated with `isSafeGitRevision`
|
|
@@ -40874,7 +40961,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40874
40961
|
changedFiles: splitNullSeparated(diff.stdout),
|
|
40875
40962
|
isCurrentChanges: false
|
|
40876
40963
|
};
|
|
40877
|
-
}),
|
|
40964
|
+
}).pipe(withSpan("Git.diffSelection")),
|
|
40878
40965
|
stagedFilePaths: (directory) => runGit(directory, [
|
|
40879
40966
|
"diff",
|
|
40880
40967
|
"--cached",
|
|
@@ -40916,7 +41003,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40916
41003
|
status: result.status,
|
|
40917
41004
|
stdout: result.stdout
|
|
40918
41005
|
};
|
|
40919
|
-
}),
|
|
41006
|
+
}).pipe(withSpan("Git.grep")),
|
|
40920
41007
|
changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
|
|
40921
41008
|
if (files.length === 0) return [];
|
|
40922
41009
|
if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
|
|
@@ -40932,7 +41019,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40932
41019
|
]);
|
|
40933
41020
|
if (result.status !== 0) return null;
|
|
40934
41021
|
return parseChangedLineRanges(result.stdout);
|
|
40935
|
-
})
|
|
41022
|
+
}).pipe(withSpan("Git.changedLineRanges"))
|
|
40936
41023
|
});
|
|
40937
41024
|
})).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
|
|
40938
41025
|
/**
|
|
@@ -41147,7 +41234,7 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
|
|
|
41147
41234
|
for (const [absolutePath, originalContent] of originalContents) try {
|
|
41148
41235
|
NFS.writeFileSync(absolutePath, originalContent);
|
|
41149
41236
|
} catch (error) {
|
|
41150
|
-
process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${
|
|
41237
|
+
process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${messageFromUnknown(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
|
|
41151
41238
|
}
|
|
41152
41239
|
};
|
|
41153
41240
|
const onExit = () => restore();
|
|
@@ -41253,7 +41340,7 @@ const resolveUserPlugin = (spec, configSourceDirectory) => {
|
|
|
41253
41340
|
try {
|
|
41254
41341
|
resolvedSpecifier = isRelative ? Path.resolve(configSourceDirectory, spec) : candidateRequire.resolve(spec);
|
|
41255
41342
|
} catch (error) {
|
|
41256
|
-
warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${
|
|
41343
|
+
warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${messageFromUnknown(error)}`);
|
|
41257
41344
|
return null;
|
|
41258
41345
|
}
|
|
41259
41346
|
const { name, ruleNames } = readPluginShape(resolvedSpecifier, (target) => candidateRequire(target));
|
|
@@ -41325,8 +41412,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
41325
41412
|
}
|
|
41326
41413
|
return enabled;
|
|
41327
41414
|
};
|
|
41328
|
-
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
|
|
41329
|
-
const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
41415
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
|
|
41416
|
+
const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
41330
41417
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
41331
41418
|
const jsPlugins = [];
|
|
41332
41419
|
if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
|
|
@@ -41386,7 +41473,6 @@ const resolveOxlintBinary = () => {
|
|
|
41386
41473
|
return Path.join(oxlintPackageDirectory, "bin", "oxlint");
|
|
41387
41474
|
};
|
|
41388
41475
|
const resolvePluginPath = () => esmRequire.resolve("oxlint-plugin-react-doctor");
|
|
41389
|
-
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
41390
41476
|
const resolveTsConfigRelativePath = (rootDirectory) => {
|
|
41391
41477
|
for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
|
|
41392
41478
|
return null;
|
|
@@ -41758,7 +41844,7 @@ const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
|
|
|
41758
41844
|
const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
|
|
41759
41845
|
let currentNode = identifier.parent;
|
|
41760
41846
|
while (currentNode) {
|
|
41761
|
-
if (
|
|
41847
|
+
if (isScopeBoundary(currentNode)) {
|
|
41762
41848
|
if (scopeContainsNonImportBinding(currentNode, currentNode, identifier.text)) return true;
|
|
41763
41849
|
}
|
|
41764
41850
|
if (currentNode === sourceFile) return false;
|
|
@@ -41849,11 +41935,10 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
|
|
|
41849
41935
|
});
|
|
41850
41936
|
return resolution;
|
|
41851
41937
|
};
|
|
41852
|
-
const isScopeNode = isScopeBoundary;
|
|
41853
41938
|
const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, visitedDeclarations = /* @__PURE__ */ new Set()) => {
|
|
41854
41939
|
let currentNode = identifier.parent;
|
|
41855
41940
|
while (currentNode) {
|
|
41856
|
-
if (
|
|
41941
|
+
if (isScopeBoundary(currentNode)) {
|
|
41857
41942
|
const resolution = findResolutionInScope(currentNode, identifier.text, reactImportBindings, sourceFile, visitedDeclarations);
|
|
41858
41943
|
if (resolution) return resolution;
|
|
41859
41944
|
}
|
|
@@ -42023,9 +42108,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
42023
42108
|
try {
|
|
42024
42109
|
parsed = JSON.parse(sanitizedStdout);
|
|
42025
42110
|
} catch {
|
|
42026
|
-
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
42111
|
+
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
42027
42112
|
}
|
|
42028
|
-
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
42113
|
+
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
42029
42114
|
const minifiedFileCache = /* @__PURE__ */ new Map();
|
|
42030
42115
|
const isMinifiedDiagnosticFile = (filename) => {
|
|
42031
42116
|
const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
|
|
@@ -42101,7 +42186,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
42101
42186
|
child.kill("SIGKILL");
|
|
42102
42187
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
42103
42188
|
kind: "timeout",
|
|
42104
|
-
detail: `${spawnTimeoutMs /
|
|
42189
|
+
detail: `${spawnTimeoutMs / MILLISECONDS_PER_SECOND}s budget exceeded`
|
|
42105
42190
|
}) }));
|
|
42106
42191
|
}, spawnTimeoutMs);
|
|
42107
42192
|
timeoutHandle.unref?.();
|
|
@@ -42316,6 +42401,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
42316
42401
|
NFS.closeSync(fileHandle);
|
|
42317
42402
|
}
|
|
42318
42403
|
};
|
|
42404
|
+
const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
|
|
42405
|
+
/**
|
|
42406
|
+
* Detects an oxlint config-load crash caused by the optional
|
|
42407
|
+
* `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
|
|
42408
|
+
* builds the partial-failure note for it; returns `null` when the failure
|
|
42409
|
+
* was anything else.
|
|
42410
|
+
*
|
|
42411
|
+
* oxlint prints a framed error to stdout (not stderr) and exits non-zero
|
|
42412
|
+
* when a `jsPlugins` entry can't be imported; that non-JSON stdout
|
|
42413
|
+
* surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
|
|
42414
|
+
* config load on it, leaving the plugin in would drop every curated
|
|
42415
|
+
* react-doctor diagnostic too — so the caller retries with the plugin
|
|
42416
|
+
* stripped (issue #833). Both markers sit at the start of oxlint's
|
|
42417
|
+
* message, so they survive the `preview` slice even for deep pnpm paths.
|
|
42418
|
+
*/
|
|
42419
|
+
const reactHooksJsPluginDropNote = (error) => {
|
|
42420
|
+
if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
|
|
42421
|
+
const { preview } = error.reason;
|
|
42422
|
+
if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
|
|
42423
|
+
const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
|
|
42424
|
+
return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
|
|
42425
|
+
};
|
|
42319
42426
|
/**
|
|
42320
42427
|
* The oxlint runner. Composed of three pieces in `runners/oxlint/`:
|
|
42321
42428
|
*
|
|
@@ -42343,15 +42450,16 @@ const runOxlint = async (options) => {
|
|
|
42343
42450
|
const pluginPath = resolvePluginPath();
|
|
42344
42451
|
const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
|
|
42345
42452
|
const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
|
|
42346
|
-
const buildConfig = (
|
|
42453
|
+
const buildConfig = (overrides) => createOxlintConfig({
|
|
42347
42454
|
pluginPath,
|
|
42348
42455
|
project,
|
|
42349
42456
|
customRulesOnly,
|
|
42350
|
-
extendsPaths:
|
|
42457
|
+
extendsPaths: overrides.extendsPaths,
|
|
42351
42458
|
ignoredTags,
|
|
42352
42459
|
serverAuthFunctionNames,
|
|
42353
42460
|
severityControls,
|
|
42354
|
-
userPlugins
|
|
42461
|
+
userPlugins,
|
|
42462
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
42355
42463
|
});
|
|
42356
42464
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
42357
42465
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
@@ -42387,12 +42495,22 @@ const runOxlint = async (options) => {
|
|
|
42387
42495
|
outputMaxBytes,
|
|
42388
42496
|
concurrency: options.concurrency
|
|
42389
42497
|
});
|
|
42390
|
-
writeOxlintConfig(configPath, buildConfig(extendsPaths));
|
|
42498
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
|
|
42391
42499
|
try {
|
|
42392
42500
|
return await runBatches();
|
|
42393
42501
|
} catch (error) {
|
|
42502
|
+
const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
|
|
42503
|
+
if (reactHooksJsDropNote !== null) {
|
|
42504
|
+
writeOxlintConfig(configPath, buildConfig({
|
|
42505
|
+
extendsPaths,
|
|
42506
|
+
disableReactHooksJsPlugin: true
|
|
42507
|
+
}));
|
|
42508
|
+
const diagnostics = await runBatches();
|
|
42509
|
+
onPartialFailure?.(reactHooksJsDropNote);
|
|
42510
|
+
return diagnostics;
|
|
42511
|
+
}
|
|
42394
42512
|
if (extendsPaths.length === 0) throw error;
|
|
42395
|
-
writeOxlintConfig(configPath, buildConfig([]));
|
|
42513
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
|
|
42396
42514
|
return await runBatches();
|
|
42397
42515
|
}
|
|
42398
42516
|
} finally {
|
|
@@ -43190,17 +43308,17 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43190
43308
|
}))))))));
|
|
43191
43309
|
const deadCodeFailureState = yield* get$2(deadCodeFailure);
|
|
43192
43310
|
const scanElapsedMilliseconds = Date.now() - scanStartTime;
|
|
43193
|
-
const scanElapsedSeconds = (scanElapsedMilliseconds /
|
|
43311
|
+
const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
|
|
43194
43312
|
if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
|
|
43195
43313
|
else if (input.suppressScanSummary) yield* scanProgress.stop();
|
|
43196
43314
|
else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
|
|
43197
43315
|
yield* reporterService.finalize;
|
|
43198
|
-
const finalDiagnostics = [
|
|
43316
|
+
const finalDiagnostics = assignFixGroups([
|
|
43199
43317
|
...envCollected,
|
|
43200
43318
|
...supplyChainCollected,
|
|
43201
43319
|
...lintCollected,
|
|
43202
43320
|
...deadCodeCollected
|
|
43203
|
-
];
|
|
43321
|
+
]);
|
|
43204
43322
|
const githubViewerPermission = yield* join(githubViewerPermissionFiber);
|
|
43205
43323
|
const scoreMetadata = {
|
|
43206
43324
|
...repo !== null ? { repo } : {},
|
|
@@ -43427,7 +43545,7 @@ var StagedFiles = class StagedFiles extends Service()("react-doctor/StagedFiles"
|
|
|
43427
43545
|
static layerNode = effect(StagedFiles, gen(function* () {
|
|
43428
43546
|
const git = yield* Git;
|
|
43429
43547
|
return StagedFiles.of({
|
|
43430
|
-
discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile))),
|
|
43548
|
+
discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile)), withSpan("StagedFiles.discoverSourceFiles")),
|
|
43431
43549
|
materialize: ({ directory, stagedFiles, tempDirectory }) => materializeSourceTree({
|
|
43432
43550
|
directory,
|
|
43433
43551
|
files: stagedFiles,
|
|
@@ -43437,7 +43555,7 @@ var StagedFiles = class StagedFiles extends Service()("react-doctor/StagedFiles"
|
|
|
43437
43555
|
tempDirectory: tree.tempDirectory,
|
|
43438
43556
|
stagedFiles: tree.materializedFiles,
|
|
43439
43557
|
cleanup: tree.cleanup
|
|
43440
|
-
})))
|
|
43558
|
+
})), withSpan("StagedFiles.materialize"))
|
|
43441
43559
|
});
|
|
43442
43560
|
}));
|
|
43443
43561
|
/**
|
|
@@ -43570,6 +43688,7 @@ const buildJsonReport = (input) => {
|
|
|
43570
43688
|
score: result.score,
|
|
43571
43689
|
skippedChecks: result.skippedChecks,
|
|
43572
43690
|
...result.skippedCheckReasons ? { skippedCheckReasons: result.skippedCheckReasons } : {},
|
|
43691
|
+
...typeof result.scannedFileCount === "number" ? { scannedFileCount: result.scannedFileCount } : {},
|
|
43573
43692
|
elapsedMilliseconds: result.elapsedMilliseconds
|
|
43574
43693
|
}));
|
|
43575
43694
|
const flattenedDiagnostics = projects.flatMap((entry) => entry.diagnostics);
|
|
@@ -43844,7 +43963,7 @@ const FALSY_CI_FLAG_VALUES = new Set([
|
|
|
43844
43963
|
"false"
|
|
43845
43964
|
]);
|
|
43846
43965
|
const isCiFlagSet = (value) => value !== void 0 && !FALSY_CI_FLAG_VALUES.has(value.toLowerCase());
|
|
43847
|
-
const isCiEnvironment = () => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(
|
|
43966
|
+
const isCiEnvironment = (env = process.env) => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(env[environmentVariable])) || isCiFlagSet(env.CI);
|
|
43848
43967
|
const detectCiProvider = () => {
|
|
43849
43968
|
for (const [environmentVariable, provider] of CI_PROVIDER_BY_ENVIRONMENT_VARIABLE) if (process.env[environmentVariable]) return provider;
|
|
43850
43969
|
return isCiFlagSet(process.env.CI) ? "unknown" : null;
|
|
@@ -43869,6 +43988,53 @@ const detectCodingAgent = () => {
|
|
|
43869
43988
|
const isCodingAgentEnvironment = () => detectCodingAgent() !== null;
|
|
43870
43989
|
const isCiOrCodingAgentEnvironment = () => isCiEnvironment() || isCodingAgentEnvironment();
|
|
43871
43990
|
//#endregion
|
|
43991
|
+
//#region src/cli/utils/detect-terminal-kind.ts
|
|
43992
|
+
const TERMINAL_BY_TERM_PROGRAM = [
|
|
43993
|
+
["vscode", "vscode"],
|
|
43994
|
+
["iTerm.app", "iterm"],
|
|
43995
|
+
["Apple_Terminal", "apple-terminal"],
|
|
43996
|
+
["WezTerm", "wezterm"],
|
|
43997
|
+
["ghostty", "ghostty"],
|
|
43998
|
+
["Hyper", "hyper"],
|
|
43999
|
+
["Tabby", "tabby"],
|
|
44000
|
+
["rio", "rio"]
|
|
44001
|
+
];
|
|
44002
|
+
/**
|
|
44003
|
+
* Best-effort label for the terminal emulator / editor hosting the CLI,
|
|
44004
|
+
* derived from terminal-identity env vars. Recorded as the `terminalKind` run
|
|
44005
|
+
* tag so we can see where React Doctor is actually run (nvim, VS Code, iTerm,
|
|
44006
|
+
* …) — the split Sentry can't otherwise see. Low-cardinality and free of any
|
|
44007
|
+
* username/path/secret, so it's safe as a tag. Editor terminals (nvim/vim)
|
|
44008
|
+
* win over the outer emulator because that's the surface a user is reading in;
|
|
44009
|
+
* "ci" marks a run with no interactive terminal; "unknown" when nothing matches.
|
|
44010
|
+
*/
|
|
44011
|
+
const detectTerminalKind = (env = process.env) => {
|
|
44012
|
+
if (env.NVIM) return "neovim";
|
|
44013
|
+
if (env.VIM_TERMINAL) return "vim";
|
|
44014
|
+
const termProgram = env.TERM_PROGRAM;
|
|
44015
|
+
if (termProgram) {
|
|
44016
|
+
for (const [marker, label] of TERMINAL_BY_TERM_PROGRAM) if (termProgram === marker) return label;
|
|
44017
|
+
}
|
|
44018
|
+
if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return "kitty";
|
|
44019
|
+
if (env.WT_SESSION) return "windows-terminal";
|
|
44020
|
+
if (env.ALACRITTY_WINDOW_ID || env.TERM === "alacritty") return "alacritty";
|
|
44021
|
+
if (env.VTE_VERSION) return "vte";
|
|
44022
|
+
if (env.TMUX) return "tmux";
|
|
44023
|
+
if (isCiEnvironment(env)) return "ci";
|
|
44024
|
+
return "unknown";
|
|
44025
|
+
};
|
|
44026
|
+
//#endregion
|
|
44027
|
+
//#region src/cli/utils/is-debug-flag.ts
|
|
44028
|
+
/**
|
|
44029
|
+
* Whether the user passed `--debug` (surface the run's Sentry trace id, and
|
|
44030
|
+
* force performance tracing on so there's a trace to surface). Read straight
|
|
44031
|
+
* from argv rather than Commander's parsed flags because `initializeSentry()`
|
|
44032
|
+
* runs before Commander parses — the same reason `shouldEnableSentry()` reads
|
|
44033
|
+
* `--no-score` from argv. Sharing this one reader keeps the init-time sampling
|
|
44034
|
+
* override and the end-of-run print in agreement.
|
|
44035
|
+
*/
|
|
44036
|
+
const isDebugFlagEnabled = (argv = process.argv) => argv.includes("--debug");
|
|
44037
|
+
//#endregion
|
|
43872
44038
|
//#region src/cli/utils/is-git-hook-environment.ts
|
|
43873
44039
|
const isGitHookEnvironment = () => Boolean(process.env.GIT_DIR);
|
|
43874
44040
|
//#endregion
|
|
@@ -43891,6 +44057,7 @@ const NON_INTERACTIVE_ENVIRONMENT_VARIABLES = [
|
|
|
43891
44057
|
const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || isCodingAgentEnvironment();
|
|
43892
44058
|
//#endregion
|
|
43893
44059
|
//#region src/cli/utils/constants.ts
|
|
44060
|
+
const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
|
|
43894
44061
|
const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
|
|
43895
44062
|
const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
|
|
43896
44063
|
const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
|
|
@@ -43975,7 +44142,7 @@ const makeNoopConsole = () => ({
|
|
|
43975
44142
|
});
|
|
43976
44143
|
//#endregion
|
|
43977
44144
|
//#region src/cli/utils/version.ts
|
|
43978
|
-
const VERSION = "0.5.6-dev.
|
|
44145
|
+
const VERSION = "0.5.6-dev.869f220";
|
|
43979
44146
|
//#endregion
|
|
43980
44147
|
//#region src/cli/utils/json-mode.ts
|
|
43981
44148
|
let context = null;
|
|
@@ -44125,7 +44292,9 @@ const buildRunContext = () => {
|
|
|
44125
44292
|
viaAction: isOfficialGithubAction(),
|
|
44126
44293
|
codingAgent: detectCodingAgent(),
|
|
44127
44294
|
interactive: !isNonInteractiveEnvironment(),
|
|
44295
|
+
terminalKind: detectTerminalKind(),
|
|
44128
44296
|
jsonMode: isJsonModeActive(),
|
|
44297
|
+
debug: isDebugFlagEnabled(),
|
|
44129
44298
|
invokedVia: detectInvokedVia()
|
|
44130
44299
|
};
|
|
44131
44300
|
};
|
|
@@ -44195,7 +44364,9 @@ const buildSentryScope = (runContext = buildRunContext()) => {
|
|
|
44195
44364
|
viaAction: runContext.viaAction,
|
|
44196
44365
|
codingAgent: runContext.codingAgent,
|
|
44197
44366
|
interactive: runContext.interactive,
|
|
44367
|
+
terminalKind: runContext.terminalKind,
|
|
44198
44368
|
jsonMode: runContext.jsonMode,
|
|
44369
|
+
debug: runContext.debug,
|
|
44199
44370
|
invokedVia: runContext.invokedVia,
|
|
44200
44371
|
nodeMajor: runContext.nodeMajor
|
|
44201
44372
|
};
|
|
@@ -44333,13 +44504,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
44333
44504
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
44334
44505
|
* standard `SENTRY_RELEASE` override.
|
|
44335
44506
|
*/
|
|
44336
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.
|
|
44507
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.869f220`;
|
|
44337
44508
|
/**
|
|
44338
44509
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
44339
44510
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
44340
44511
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
44341
44512
|
*/
|
|
44342
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.
|
|
44513
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.869f220") ? "development" : "production");
|
|
44343
44514
|
/**
|
|
44344
44515
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
44345
44516
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -44403,7 +44574,7 @@ const flushSentry = async () => {
|
|
|
44403
44574
|
const initializeSentry = () => {
|
|
44404
44575
|
if (isInitialized || !shouldEnableSentry()) return;
|
|
44405
44576
|
isInitialized = true;
|
|
44406
|
-
resolvedTracesSampleRate = resolveTracesSampleRate();
|
|
44577
|
+
resolvedTracesSampleRate = isDebugFlagEnabled() ? 1 : resolveTracesSampleRate();
|
|
44407
44578
|
const { tags, contexts } = buildSentryScope();
|
|
44408
44579
|
Sentry.init({
|
|
44409
44580
|
dsn: process.env.SENTRY_DSN || "https://f253d570240a59b8dbd77b7a548ef133@o4510226365743104.ingest.us.sentry.io/4511487817809920",
|
|
@@ -47619,6 +47790,11 @@ const setActiveRunTrace = (trace) => {
|
|
|
47619
47790
|
activeRunTrace = trace;
|
|
47620
47791
|
};
|
|
47621
47792
|
const getActiveRunTrace = () => activeRunTrace;
|
|
47793
|
+
let lastRunTraceId = null;
|
|
47794
|
+
const recordRunTraceId = (traceId) => {
|
|
47795
|
+
lastRunTraceId = traceId;
|
|
47796
|
+
};
|
|
47797
|
+
const getLastRunTraceId = () => lastRunTraceId;
|
|
47622
47798
|
//#endregion
|
|
47623
47799
|
//#region src/cli/utils/to-span-attributes.ts
|
|
47624
47800
|
/**
|
|
@@ -47681,14 +47857,13 @@ const withSentryRunSpan = (run, options = {}) => {
|
|
|
47681
47857
|
op: "cli.inspect",
|
|
47682
47858
|
attributes: toSpanAttributes(tags)
|
|
47683
47859
|
}, (rootSpan) => {
|
|
47684
|
-
|
|
47685
|
-
|
|
47686
|
-
|
|
47687
|
-
|
|
47688
|
-
|
|
47689
|
-
|
|
47690
|
-
|
|
47691
|
-
}
|
|
47860
|
+
const spanContext = rootSpan.spanContext();
|
|
47861
|
+
recordRunTraceId(spanContext.traceId);
|
|
47862
|
+
if (options.concurrentScan !== true) setActiveRunTrace({
|
|
47863
|
+
traceId: spanContext.traceId,
|
|
47864
|
+
spanId: spanContext.spanId,
|
|
47865
|
+
sampled: (spanContext.traceFlags & 1) === 1
|
|
47866
|
+
});
|
|
47692
47867
|
return run(rootSpan);
|
|
47693
47868
|
});
|
|
47694
47869
|
};
|
|
@@ -47828,6 +48003,42 @@ const recordScanMetrics = (input) => {
|
|
|
47828
48003
|
});
|
|
47829
48004
|
};
|
|
47830
48005
|
//#endregion
|
|
48006
|
+
//#region src/cli/utils/diagnostic-grouping.ts
|
|
48007
|
+
const buildRulePriorityMap = (scores) => {
|
|
48008
|
+
const rulePriority = /* @__PURE__ */ new Map();
|
|
48009
|
+
for (const score of scores) {
|
|
48010
|
+
if (!score?.rules) continue;
|
|
48011
|
+
for (const [ruleKey, info] of Object.entries(score.rules)) if (typeof info.priority === "number") rulePriority.set(ruleKey, info.priority);
|
|
48012
|
+
}
|
|
48013
|
+
return rulePriority;
|
|
48014
|
+
};
|
|
48015
|
+
const compareByRulePriority = (ruleKeyA, ruleKeyB, rulePriority) => {
|
|
48016
|
+
const priorityA = rulePriority?.get(ruleKeyA);
|
|
48017
|
+
const priorityB = rulePriority?.get(ruleKeyB);
|
|
48018
|
+
if (priorityA === void 0 && priorityB === void 0) return 0;
|
|
48019
|
+
if (priorityA === void 0) return 1;
|
|
48020
|
+
if (priorityB === void 0) return -1;
|
|
48021
|
+
return priorityB - priorityA;
|
|
48022
|
+
};
|
|
48023
|
+
const sortRuleGroupsByImportance = (diagnosticGroups, rulePriority) => diagnosticGroups.toSorted(([ruleKeyA], [ruleKeyB]) => compareByRulePriority(ruleKeyA, ruleKeyB, rulePriority));
|
|
48024
|
+
const buildSortedRuleGroups = (diagnostics, rulePriority) => sortRuleGroupsByImportance([...groupBy([...diagnostics], (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`)], rulePriority);
|
|
48025
|
+
const getSharedFixSiteCount = (diagnostics) => {
|
|
48026
|
+
if (diagnostics.length < 2) return 0;
|
|
48027
|
+
const firstFixGroupId = diagnostics[0]?.fixGroupId;
|
|
48028
|
+
if (!firstFixGroupId) return 0;
|
|
48029
|
+
return diagnostics.every((diagnostic) => diagnostic.fixGroupId === firstFixGroupId) ? diagnostics.length : 0;
|
|
48030
|
+
};
|
|
48031
|
+
const FIX_RECIPE_DIRECTIVE_LABEL = "Curl with no cache & follow the canonical fix and false positive check recipe before fixing";
|
|
48032
|
+
const formatFixRecipeLine = (diagnostic) => hasPublishedFixRecipe(diagnostic) ? `${FIX_RECIPE_DIRECTIVE_LABEL}: ${buildRuleDocsUrl(diagnostic.plugin, diagnostic.rule)}` : null;
|
|
48033
|
+
const formatLearnMoreLine = (diagnostic) => hasPublishedFixRecipe(diagnostic) ? `Learn more: ${buildRuleDocsUrl(diagnostic.plugin, diagnostic.rule)}` : null;
|
|
48034
|
+
const buildRuleBlastRadii = (diagnostics) => buildSortedRuleGroups(diagnostics).map(([ruleKey, ruleDiagnostics]) => ({
|
|
48035
|
+
ruleKey,
|
|
48036
|
+
title: ruleDiagnostics[0].title ?? ruleKey,
|
|
48037
|
+
siteCount: ruleDiagnostics.length,
|
|
48038
|
+
fileCount: new Set(ruleDiagnostics.map((diagnostic) => diagnostic.filePath)).size
|
|
48039
|
+
})).toSorted((left, right) => right.fileCount - left.fileCount);
|
|
48040
|
+
const findMigrationScaleBuckets = (diagnostics) => buildRuleBlastRadii(diagnostics).filter((bucket) => bucket.fileCount >= 40);
|
|
48041
|
+
//#endregion
|
|
47831
48042
|
//#region src/cli/utils/cli-logger.ts
|
|
47832
48043
|
/**
|
|
47833
48044
|
* Thin synchronous façade over Effect's `Console` module. Used by
|
|
@@ -47944,12 +48155,17 @@ const buildOutcomeAttributes = (input) => {
|
|
|
47944
48155
|
topRule = rule;
|
|
47945
48156
|
topRuleCount = count;
|
|
47946
48157
|
}
|
|
48158
|
+
const largestRuleBucket = buildRuleBlastRadii(result.diagnostics)[0] ?? null;
|
|
47947
48159
|
let diagnosticsInTestFiles = 0;
|
|
47948
48160
|
let diagnosticsInStoryFiles = 0;
|
|
48161
|
+
const findingsPerFixGroup = /* @__PURE__ */ new Map();
|
|
47949
48162
|
for (const diagnostic of result.diagnostics) {
|
|
47950
48163
|
if (diagnostic.fileContext === "test") diagnosticsInTestFiles += 1;
|
|
47951
48164
|
if (diagnostic.fileContext === "story") diagnosticsInStoryFiles += 1;
|
|
48165
|
+
if (diagnostic.fixGroupId) findingsPerFixGroup.set(diagnostic.fixGroupId, (findingsPerFixGroup.get(diagnostic.fixGroupId) ?? 0) + 1);
|
|
47952
48166
|
}
|
|
48167
|
+
let fixGroupedFindings = 0;
|
|
48168
|
+
for (const count of findingsPerFixGroup.values()) fixGroupedFindings += count;
|
|
47953
48169
|
const attributes = {
|
|
47954
48170
|
outcome,
|
|
47955
48171
|
exitCode: wouldBlock ? 1 : 0,
|
|
@@ -47963,7 +48179,12 @@ const buildOutcomeAttributes = (input) => {
|
|
|
47963
48179
|
diagnosticsInTestFiles,
|
|
47964
48180
|
diagnosticsInStoryFiles,
|
|
47965
48181
|
distinctRulesFired: countByRule.size,
|
|
48182
|
+
"diag.fixGroups": findingsPerFixGroup.size,
|
|
48183
|
+
"diag.fixGroupedFindings": fixGroupedFindings,
|
|
47966
48184
|
topRule,
|
|
48185
|
+
"migration.largestRuleBucketFiles": largestRuleBucket ? largestRuleBucket.fileCount : null,
|
|
48186
|
+
"migration.largestRuleBucketSites": largestRuleBucket ? largestRuleBucket.siteCount : null,
|
|
48187
|
+
"migration.largestRuleBucketRule": largestRuleBucket ? largestRuleBucket.ruleKey : null,
|
|
47967
48188
|
scannedFileCount: result.scannedFileCount ?? null,
|
|
47968
48189
|
elapsedMs: result.elapsedMilliseconds,
|
|
47969
48190
|
scanPhaseMs: result.scanElapsedMilliseconds ?? null,
|
|
@@ -48124,6 +48345,7 @@ const AGENT_GUIDANCE_LINES = [
|
|
|
48124
48345
|
"Run `npx react-doctor@latest --verbose --scope changed` before and after changes, plus relevant tests after each focused batch.",
|
|
48125
48346
|
"When available, spawn subagents or isolated worktrees for independent rule families, then review and merge only the best safe fixes.",
|
|
48126
48347
|
"Split unrelated, broad, or behavior-changing work into separate PRs/branches instead of one large cleanup.",
|
|
48348
|
+
"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.",
|
|
48127
48349
|
"For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.",
|
|
48128
48350
|
"If a fix needs an API, UX, or architecture decision, stop and ask before editing."
|
|
48129
48351
|
];
|
|
@@ -48133,29 +48355,6 @@ const printAgentGuidance = () => gen(function* () {
|
|
|
48133
48355
|
yield* log("");
|
|
48134
48356
|
});
|
|
48135
48357
|
//#endregion
|
|
48136
|
-
//#region src/cli/utils/diagnostic-grouping.ts
|
|
48137
|
-
const buildRulePriorityMap = (scores) => {
|
|
48138
|
-
const rulePriority = /* @__PURE__ */ new Map();
|
|
48139
|
-
for (const score of scores) {
|
|
48140
|
-
if (!score?.rules) continue;
|
|
48141
|
-
for (const [ruleKey, info] of Object.entries(score.rules)) if (typeof info.priority === "number") rulePriority.set(ruleKey, info.priority);
|
|
48142
|
-
}
|
|
48143
|
-
return rulePriority;
|
|
48144
|
-
};
|
|
48145
|
-
const compareByRulePriority = (ruleKeyA, ruleKeyB, rulePriority) => {
|
|
48146
|
-
const priorityA = rulePriority?.get(ruleKeyA);
|
|
48147
|
-
const priorityB = rulePriority?.get(ruleKeyB);
|
|
48148
|
-
if (priorityA === void 0 && priorityB === void 0) return 0;
|
|
48149
|
-
if (priorityA === void 0) return 1;
|
|
48150
|
-
if (priorityB === void 0) return -1;
|
|
48151
|
-
return priorityB - priorityA;
|
|
48152
|
-
};
|
|
48153
|
-
const sortRuleGroupsByImportance = (diagnosticGroups, rulePriority) => diagnosticGroups.toSorted(([ruleKeyA], [ruleKeyB]) => compareByRulePriority(ruleKeyA, ruleKeyB, rulePriority));
|
|
48154
|
-
const buildSortedRuleGroups = (diagnostics, rulePriority) => sortRuleGroupsByImportance([...groupBy([...diagnostics], (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`)], rulePriority);
|
|
48155
|
-
const FIX_RECIPE_DIRECTIVE_LABEL = "Curl with no cache & follow the canonical fix and false positive check recipe before fixing";
|
|
48156
|
-
const formatFixRecipeLine = (diagnostic) => hasPublishedFixRecipe(diagnostic) ? `${FIX_RECIPE_DIRECTIVE_LABEL}: ${buildRuleDocsUrl(diagnostic.plugin, diagnostic.rule)}` : null;
|
|
48157
|
-
const formatLearnMoreLine = (diagnostic) => hasPublishedFixRecipe(diagnostic) ? `Learn more: ${buildRuleDocsUrl(diagnostic.plugin, diagnostic.rule)}` : null;
|
|
48158
|
-
//#endregion
|
|
48159
48358
|
//#region src/cli/utils/box-text.ts
|
|
48160
48359
|
const ESCAPE = String.fromCharCode(27);
|
|
48161
48360
|
const ANSI_ESCAPE_PATTERN = new RegExp(`${ESCAPE}\\[[0-9;]*m`, "g");
|
|
@@ -48196,6 +48395,15 @@ const boxText = (content, innerWidth) => {
|
|
|
48196
48395
|
].join("\n");
|
|
48197
48396
|
};
|
|
48198
48397
|
//#endregion
|
|
48398
|
+
//#region src/cli/utils/resolve-absolute-path.ts
|
|
48399
|
+
/**
|
|
48400
|
+
* Resolves a diagnostic's `filePath` (relative to its project root, or
|
|
48401
|
+
* already absolute) to an absolute path. Shared by the code-frame reader and
|
|
48402
|
+
* the terminal hyperlink builder so both turn a relative path into the same
|
|
48403
|
+
* on-disk location.
|
|
48404
|
+
*/
|
|
48405
|
+
const resolveAbsolutePath = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
|
|
48406
|
+
//#endregion
|
|
48199
48407
|
//#region src/cli/utils/build-code-frame.ts
|
|
48200
48408
|
/**
|
|
48201
48409
|
* Renders a syntax-highlighted source excerpt around a diagnostic site
|
|
@@ -48206,7 +48414,7 @@ const boxText = (content, innerWidth) => {
|
|
|
48206
48414
|
*/
|
|
48207
48415
|
const buildCodeFrame = (input) => {
|
|
48208
48416
|
if (input.line <= 0) return null;
|
|
48209
|
-
const absolutePath =
|
|
48417
|
+
const absolutePath = resolveAbsolutePath(input.filePath, input.rootDirectory);
|
|
48210
48418
|
let source;
|
|
48211
48419
|
try {
|
|
48212
48420
|
source = NFS.readFileSync(absolutePath, "utf8");
|
|
@@ -48246,6 +48454,16 @@ const resolveMeasureWidth = (reservedColumns = 0) => resolveClampedWidth({
|
|
|
48246
48454
|
const DIVIDER_INDENT = " ";
|
|
48247
48455
|
const buildSectionDivider = () => highlighter.dim(`${DIVIDER_INDENT}${"─".repeat(resolveMeasureWidth(2))}`);
|
|
48248
48456
|
//#endregion
|
|
48457
|
+
//#region src/cli/utils/format-hyperlink.ts
|
|
48458
|
+
const OSC = "\x1B]";
|
|
48459
|
+
const ST = "\x1B\\";
|
|
48460
|
+
/**
|
|
48461
|
+
* Wraps `text` in an OSC 8 hyperlink pointing at `uri`. The visible characters
|
|
48462
|
+
* are exactly `text`; the link is carried in escape sequences a capable
|
|
48463
|
+
* terminal turns into a click target.
|
|
48464
|
+
*/
|
|
48465
|
+
const formatHyperlink = (text, uri) => `${OSC}8;;${uri}${ST}${text}${OSC}8;;${ST}`;
|
|
48466
|
+
//#endregion
|
|
48249
48467
|
//#region src/cli/utils/indent-multiline-text.ts
|
|
48250
48468
|
const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
|
|
48251
48469
|
//#endregion
|
|
@@ -48399,17 +48617,23 @@ const clusterNearbyDiagnostics = (diagnostics) => {
|
|
|
48399
48617
|
}
|
|
48400
48618
|
return clusters;
|
|
48401
48619
|
};
|
|
48402
|
-
const
|
|
48620
|
+
const formatClusterLocationText = (cluster) => {
|
|
48621
|
+
const { filePath } = cluster.diagnostics[0];
|
|
48622
|
+
if (cluster.startLine <= 0) return filePath;
|
|
48623
|
+
if (cluster.endLine > cluster.startLine) return `${filePath}:${cluster.startLine}-${cluster.endLine}`;
|
|
48624
|
+
return `${filePath}:${cluster.startLine}`;
|
|
48625
|
+
};
|
|
48626
|
+
const formatClusterLocation = (cluster, resolveSourceRoot, hyperlinks) => {
|
|
48403
48627
|
const lead = cluster.diagnostics[0];
|
|
48404
48628
|
const contextTag = formatFileContextTag(lead);
|
|
48405
|
-
|
|
48406
|
-
if (
|
|
48407
|
-
return `${lead.filePath
|
|
48629
|
+
const location = formatClusterLocationText(cluster);
|
|
48630
|
+
if (!hyperlinks) return `${location}${contextTag}`;
|
|
48631
|
+
return `${formatHyperlink(location, pathToFileURL(resolveAbsolutePath(lead.filePath, resolveSourceRoot(lead))).href)}${contextTag}`;
|
|
48408
48632
|
};
|
|
48409
|
-
const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame) => {
|
|
48633
|
+
const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame, hyperlinks) => {
|
|
48410
48634
|
const lead = cluster.diagnostics[0];
|
|
48411
48635
|
const isMultiSite = cluster.diagnostics.length > 1;
|
|
48412
|
-
const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster)}`)];
|
|
48636
|
+
const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster, resolveSourceRoot, hyperlinks)}`)];
|
|
48413
48637
|
const codeFrame = renderCodeFrame ? buildCodeFrame({
|
|
48414
48638
|
filePath: lead.filePath,
|
|
48415
48639
|
line: cluster.startLine,
|
|
@@ -48428,7 +48652,7 @@ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame
|
|
|
48428
48652
|
}
|
|
48429
48653
|
return lines;
|
|
48430
48654
|
};
|
|
48431
|
-
const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment) => {
|
|
48655
|
+
const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment, hyperlinks) => {
|
|
48432
48656
|
const representative = pickRepresentativeDiagnostic(ruleDiagnostics);
|
|
48433
48657
|
const { severity } = representative;
|
|
48434
48658
|
const trailingBadge = formatTrailingSiteBadge(ruleDiagnostics.length);
|
|
@@ -48442,13 +48666,15 @@ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, rende
|
|
|
48442
48666
|
const impactMessages = isCollapsedWarningGroup ? [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.message))] : [representative.message];
|
|
48443
48667
|
for (const impactMessage of impactMessages) for (const explanationLine of wrapTextToWidth(impactMessage, resolveMeasureWidth(4), { breakLongWords: false })) lines.push(`${TOP_ERROR_DETAIL_INDENT}${explanationLine}`);
|
|
48444
48668
|
if (representative.help) for (const fixLine of wrapTextToWidth(`→ ${representative.help}`, resolveMeasureWidth(4), { breakLongWords: false })) lines.push(highlighter.dim(`${TOP_ERROR_DETAIL_INDENT}${fixLine}`));
|
|
48669
|
+
const sharedFixSiteCount = getSharedFixSiteCount(ruleDiagnostics);
|
|
48670
|
+
if (sharedFixSiteCount > 0) lines.push(highlighter.dim(`${TOP_ERROR_DETAIL_INDENT}↳ One fix clears all ${sharedFixSiteCount} findings.`));
|
|
48445
48671
|
if (renderEverySite && isAgentEnvironment) {
|
|
48446
48672
|
const fixRecipeLine = formatFixRecipeLine(representative);
|
|
48447
48673
|
if (fixRecipeLine) lines.push(highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${fixRecipeLine}`));
|
|
48448
48674
|
}
|
|
48449
48675
|
const renderCodeFrame = severity === "error";
|
|
48450
48676
|
const sites = renderEverySite ? ruleDiagnostics : [representative];
|
|
48451
|
-
if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame));
|
|
48677
|
+
if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame, hyperlinks));
|
|
48452
48678
|
return lines;
|
|
48453
48679
|
};
|
|
48454
48680
|
const selectErrorRuleGroups = (diagnostics, rulePriority) => buildSortedRuleGroups(diagnostics.filter((diagnostic) => diagnostic.severity === "error"), rulePriority);
|
|
@@ -48460,8 +48686,21 @@ const buildOverflowSummaryLine = (diagnostics, rulePriority) => {
|
|
|
48460
48686
|
const command = highlighter.bold(highlighter.info("npx react-doctor@latest --verbose"));
|
|
48461
48687
|
return ` ${highlighter.dim("Run")} ${command} ${highlighter.dim("to list every error and warning")}`;
|
|
48462
48688
|
};
|
|
48689
|
+
const formatMigrationBucketLine = (bucket) => `${TOP_ERROR_DETAIL_INDENT}${bucket.title} ${highlighter.gray(`×${bucket.siteCount} across ${bucket.fileCount} files`)}`;
|
|
48690
|
+
const buildMigrationScaleAdvisoryLines = (diagnostics) => {
|
|
48691
|
+
const buckets = findMigrationScaleBuckets(diagnostics);
|
|
48692
|
+
if (buckets.length === 0) return [];
|
|
48693
|
+
const shownBuckets = buckets.slice(0, 3);
|
|
48694
|
+
const lines = [` ${highlighter.warn("⚠")} ${highlighter.bold("Migration-scale change")}${highlighter.dim(": sample before you sweep")}`, ...shownBuckets.map(formatMigrationBucketLine)];
|
|
48695
|
+
const remainingBuckets = buckets.length - shownBuckets.length;
|
|
48696
|
+
if (remainingBuckets > 0) lines.push(highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}+${remainingBuckets} more ${remainingBuckets === 1 ? "rule" : "rules"} at this scale`));
|
|
48697
|
+
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}`));
|
|
48698
|
+
const command = highlighter.info("npx react-doctor@latest <path>");
|
|
48699
|
+
lines.push(`${TOP_ERROR_DETAIL_INDENT}${highlighter.dim("Scope it down one area at a time:")} ${command}`);
|
|
48700
|
+
return lines;
|
|
48701
|
+
};
|
|
48463
48702
|
const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(selectTopErrorRuleGroups(diagnostics, limit, rulePriority).map(([ruleKey]) => ruleKey));
|
|
48464
|
-
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) => {
|
|
48703
|
+
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, hyperlinks, rulePriority) => {
|
|
48465
48704
|
const topRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority).slice(0, 3);
|
|
48466
48705
|
if (topRuleGroups.length === 0) return {
|
|
48467
48706
|
lines: [],
|
|
@@ -48471,7 +48710,7 @@ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) =>
|
|
|
48471
48710
|
const blockOffsets = [];
|
|
48472
48711
|
for (const [ruleKey, ruleDiagnostics] of topRuleGroups) {
|
|
48473
48712
|
blockOffsets.push(lines.length);
|
|
48474
|
-
lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false));
|
|
48713
|
+
lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false, hyperlinks));
|
|
48475
48714
|
lines.push("");
|
|
48476
48715
|
}
|
|
48477
48716
|
return {
|
|
@@ -48509,24 +48748,24 @@ const buildOverviewHeaderLines = (diagnostics) => {
|
|
|
48509
48748
|
* single Effect.forEach over Console.log so failures or fiber
|
|
48510
48749
|
* interruption produce predictable partial output.
|
|
48511
48750
|
*/
|
|
48512
|
-
const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}) => gen(function* () {
|
|
48751
|
+
const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}, hyperlinks = false) => gen(function* () {
|
|
48513
48752
|
const sectionPause = onboarding.sectionPause ?? void_;
|
|
48514
48753
|
const animateCountUp = onboarding.animateCountUp ?? false;
|
|
48515
48754
|
const resolveSourceRoot = typeof sourceRoot === "function" ? sourceRoot : () => sourceRoot;
|
|
48516
48755
|
let detailLines;
|
|
48517
48756
|
let topErrorBlockOffsets = [];
|
|
48518
48757
|
if (!isVerbose) {
|
|
48519
|
-
const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, rulePriority);
|
|
48758
|
+
const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, hyperlinks, rulePriority);
|
|
48520
48759
|
detailLines = topErrors.lines;
|
|
48521
48760
|
topErrorBlockOffsets = topErrors.blockOffsets;
|
|
48522
48761
|
} else detailLines = buildSortedRuleGroups(diagnostics, rulePriority).flatMap(([ruleKey, ruleDiagnostics]) => {
|
|
48523
|
-
return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment), ""];
|
|
48762
|
+
return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment, hyperlinks), ""];
|
|
48524
48763
|
});
|
|
48525
48764
|
const overflowLine = isVerbose ? void 0 : buildOverflowSummaryLine(diagnostics, rulePriority);
|
|
48526
48765
|
const categoryTallies = buildCategoryDiagnosticGroups(diagnostics, rulePriority).map(buildCategoryTally);
|
|
48527
48766
|
const categoryLines = buildCategoryTallyLines(categoryTallies);
|
|
48528
48767
|
const overviewDividerLines = detailLines.length > 0 && categoryLines.length > 0 ? [buildSectionDivider()] : [];
|
|
48529
|
-
const { lines, sectionStarts } = joinSections(detailLines, overviewDividerLines, buildOverviewHeaderLines(diagnostics), categoryLines, overflowLine ? [overflowLine] : []);
|
|
48768
|
+
const { lines, sectionStarts } = joinSections(detailLines, overviewDividerLines, buildOverviewHeaderLines(diagnostics), categoryLines, overflowLine ? [overflowLine] : [], buildMigrationScaleAdvisoryLines(diagnostics));
|
|
48530
48769
|
const [detailStart, , , categoryStart] = sectionStarts;
|
|
48531
48770
|
const pauseBeforeLineIndices = detailStart == null ? /* @__PURE__ */ new Set() : new Set(topErrorBlockOffsets.map((offset) => detailStart + offset));
|
|
48532
48771
|
let lineIndex = 0;
|
|
@@ -48581,6 +48820,48 @@ const computeProjectedScore = async (topErrorSource, rescoreSource, currentScore
|
|
|
48581
48820
|
//#endregion
|
|
48582
48821
|
//#region src/cli/utils/filter-diagnostics-by-categories.ts
|
|
48583
48822
|
const filterDiagnosticsByCategories = (diagnostics, categories) => categories.size === 0 ? [...diagnostics] : diagnostics.filter((diagnostic) => categories.has(diagnostic.category));
|
|
48823
|
+
//#endregion
|
|
48824
|
+
//#region src/cli/utils/supports-hyperlinks.ts
|
|
48825
|
+
const HYPERLINK_CAPABLE_TERM_PROGRAMS = new Set([
|
|
48826
|
+
"iTerm.app",
|
|
48827
|
+
"WezTerm",
|
|
48828
|
+
"vscode",
|
|
48829
|
+
"Hyper",
|
|
48830
|
+
"ghostty",
|
|
48831
|
+
"Tabby",
|
|
48832
|
+
"rio"
|
|
48833
|
+
]);
|
|
48834
|
+
const parseVteVersion = (raw) => {
|
|
48835
|
+
const parsed = Number.parseInt(raw ?? "", 10);
|
|
48836
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
48837
|
+
};
|
|
48838
|
+
/**
|
|
48839
|
+
* Whether `stream` is a terminal that renders OSC 8 hyperlinks. Auto-detected
|
|
48840
|
+
* from terminal-identity env vars; the de-facto `FORCE_HYPERLINK` env var
|
|
48841
|
+
* overrides detection (`FORCE_HYPERLINK=0`/`false` forces off, any other value
|
|
48842
|
+
* forces on), mirroring how the ecosystem's terminal libraries gate the same
|
|
48843
|
+
* feature. Off for non-TTYs, `TERM=dumb`, and CI (whose log viewers render the
|
|
48844
|
+
* raw escape rather than a link). Unknown terminals default to off.
|
|
48845
|
+
*/
|
|
48846
|
+
const supportsHyperlinks = (stream = process.stdout, env = process.env) => {
|
|
48847
|
+
const forced = env.FORCE_HYPERLINK;
|
|
48848
|
+
if (forced !== void 0 && forced !== "") return forced !== "0" && forced.toLowerCase() !== "false";
|
|
48849
|
+
if (stream.isTTY !== true) return false;
|
|
48850
|
+
if (env.TERM === "dumb") return false;
|
|
48851
|
+
if (isCiEnvironment(env)) return false;
|
|
48852
|
+
if (env.WT_SESSION) return true;
|
|
48853
|
+
if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return true;
|
|
48854
|
+
if (parseVteVersion(env.VTE_VERSION) >= 5e3) return true;
|
|
48855
|
+
return Boolean(env.TERM_PROGRAM && HYPERLINK_CAPABLE_TERM_PROGRAMS.has(env.TERM_PROGRAM));
|
|
48856
|
+
};
|
|
48857
|
+
//#endregion
|
|
48858
|
+
//#region src/cli/utils/should-render-hyperlinks.ts
|
|
48859
|
+
/**
|
|
48860
|
+
* Whether to emit OSC 8 clickable `file:line` locations for this run: a
|
|
48861
|
+
* hyperlink-capable terminal AND not a coding agent (whose output parsers
|
|
48862
|
+
* would choke on the escape sequences).
|
|
48863
|
+
*/
|
|
48864
|
+
const shouldRenderHyperlinks = (stream = process.stdout) => supportsHyperlinks(stream) && !isCodingAgentEnvironment();
|
|
48584
48865
|
const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
|
|
48585
48866
|
const FALSY_FLAG_VALUES = new Set([
|
|
48586
48867
|
"",
|
|
@@ -48600,10 +48881,9 @@ const canAnimateOnboarding = (stream = process.stdout) => {
|
|
|
48600
48881
|
};
|
|
48601
48882
|
//#endregion
|
|
48602
48883
|
//#region src/cli/utils/onboarding-state.ts
|
|
48603
|
-
const GLOBAL_CONFIG_PROJECT_NAME$2 = "react-doctor";
|
|
48604
48884
|
const ONBOARDED_AT_KEY = "onboardedAt";
|
|
48605
48885
|
const getOnboardingStore = (options = {}) => new Conf({
|
|
48606
|
-
projectName:
|
|
48886
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
48607
48887
|
cwd: options.cwd
|
|
48608
48888
|
});
|
|
48609
48889
|
const hasCompletedOnboarding = (options = {}) => {
|
|
@@ -49059,6 +49339,78 @@ const resolveCliCategories = (categoryFlag) => {
|
|
|
49059
49339
|
return resolvedCategories.length > 0 ? resolvedCategories : void 0;
|
|
49060
49340
|
};
|
|
49061
49341
|
//#endregion
|
|
49342
|
+
//#region src/cli/utils/git-hook-shared.ts
|
|
49343
|
+
const HOOK_FILE_NAME = "pre-commit";
|
|
49344
|
+
const HOOK_RELATIVE_PATH = "hooks/pre-commit";
|
|
49345
|
+
const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
|
|
49346
|
+
const HUSKY_HOOKS_PATH = ".husky";
|
|
49347
|
+
const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
|
|
49348
|
+
const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
|
|
49349
|
+
const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
|
|
49350
|
+
const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
|
|
49351
|
+
const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
|
|
49352
|
+
const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
|
|
49353
|
+
"react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
|
|
49354
|
+
`if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
|
|
49355
|
+
"rm -f \"$react_doctor_output\";",
|
|
49356
|
+
"else",
|
|
49357
|
+
"rm -f \"$react_doctor_output\";",
|
|
49358
|
+
`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;`,
|
|
49359
|
+
"fi"
|
|
49360
|
+
].join(" ");
|
|
49361
|
+
const PACKAGE_JSON_FILE_NAME = "package.json";
|
|
49362
|
+
const runGit = (projectRoot, args) => {
|
|
49363
|
+
try {
|
|
49364
|
+
return execFileSync("git", [...args], {
|
|
49365
|
+
cwd: projectRoot,
|
|
49366
|
+
encoding: "utf8",
|
|
49367
|
+
stdio: [
|
|
49368
|
+
"ignore",
|
|
49369
|
+
"pipe",
|
|
49370
|
+
"ignore"
|
|
49371
|
+
]
|
|
49372
|
+
}).trim();
|
|
49373
|
+
} catch {
|
|
49374
|
+
return null;
|
|
49375
|
+
}
|
|
49376
|
+
};
|
|
49377
|
+
const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
|
|
49378
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49379
|
+
const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
|
|
49380
|
+
const readPackageJson = (projectRoot) => {
|
|
49381
|
+
try {
|
|
49382
|
+
return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
|
|
49383
|
+
} catch {
|
|
49384
|
+
return null;
|
|
49385
|
+
}
|
|
49386
|
+
};
|
|
49387
|
+
const writeJsonFile$1 = (filePath, value) => {
|
|
49388
|
+
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
49389
|
+
};
|
|
49390
|
+
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
49391
|
+
const packageJson = readPackageJson(projectRoot);
|
|
49392
|
+
if (!isRecord$1(packageJson)) return false;
|
|
49393
|
+
return [
|
|
49394
|
+
"dependencies",
|
|
49395
|
+
"devDependencies",
|
|
49396
|
+
"optionalDependencies"
|
|
49397
|
+
].some((fieldName) => {
|
|
49398
|
+
const dependencies = packageJson[fieldName];
|
|
49399
|
+
return isRecord$1(dependencies) && typeof dependencies[dependencyName] === "string";
|
|
49400
|
+
});
|
|
49401
|
+
};
|
|
49402
|
+
const packageHasRecordKey = (projectRoot, key) => {
|
|
49403
|
+
const packageJson = readPackageJson(projectRoot);
|
|
49404
|
+
return isRecord$1(packageJson) && isRecord$1(packageJson[key]);
|
|
49405
|
+
};
|
|
49406
|
+
const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
|
|
49407
|
+
const packageJson = readPackageJson(projectRoot);
|
|
49408
|
+
if (!isRecord$1(packageJson)) return false;
|
|
49409
|
+
const value = packageJson[key];
|
|
49410
|
+
return isRecord$1(value) && isRecord$1(value[nestedKey]);
|
|
49411
|
+
};
|
|
49412
|
+
const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
|
|
49413
|
+
//#endregion
|
|
49062
49414
|
//#region src/cli/utils/scan-result-cache.ts
|
|
49063
49415
|
const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
|
|
49064
49416
|
const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
@@ -49069,7 +49421,7 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
|
49069
49421
|
"eslint-plugin-react-hooks/package.json"
|
|
49070
49422
|
];
|
|
49071
49423
|
const bundledRequire = createRequire(import.meta.url);
|
|
49072
|
-
const isRecord
|
|
49424
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49073
49425
|
const normalizeForStableJson = (value) => {
|
|
49074
49426
|
if (value === null) return null;
|
|
49075
49427
|
if (value === void 0) return void 0;
|
|
@@ -49098,24 +49450,9 @@ const stringifyStableJson = (value) => {
|
|
|
49098
49450
|
}
|
|
49099
49451
|
};
|
|
49100
49452
|
const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
|
|
49101
|
-
const
|
|
49102
|
-
try {
|
|
49103
|
-
return execFileSync("git", [...args], {
|
|
49104
|
-
cwd: directory,
|
|
49105
|
-
encoding: "utf8",
|
|
49106
|
-
stdio: [
|
|
49107
|
-
"ignore",
|
|
49108
|
-
"pipe",
|
|
49109
|
-
"ignore"
|
|
49110
|
-
]
|
|
49111
|
-
}).trim();
|
|
49112
|
-
} catch {
|
|
49113
|
-
return null;
|
|
49114
|
-
}
|
|
49115
|
-
};
|
|
49116
|
-
const readHeadSha = (projectDirectory) => runGit$1(projectDirectory, ["rev-parse", "HEAD"]);
|
|
49453
|
+
const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
|
|
49117
49454
|
const isWorktreeClean = (projectDirectory) => {
|
|
49118
|
-
const status = runGit
|
|
49455
|
+
const status = runGit(projectDirectory, [
|
|
49119
49456
|
"status",
|
|
49120
49457
|
"--porcelain=v1",
|
|
49121
49458
|
"--untracked-files=normal"
|
|
@@ -49123,7 +49460,7 @@ const isWorktreeClean = (projectDirectory) => {
|
|
|
49123
49460
|
return status !== null && status.length === 0;
|
|
49124
49461
|
};
|
|
49125
49462
|
const hasHiddenTrackedFileState = (projectDirectory) => {
|
|
49126
|
-
const output = runGit
|
|
49463
|
+
const output = runGit(projectDirectory, ["ls-files", "-v"]);
|
|
49127
49464
|
if (output === null) return true;
|
|
49128
49465
|
return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
|
|
49129
49466
|
};
|
|
@@ -49136,7 +49473,7 @@ const resolveCacheFilePath = (projectDirectory) => {
|
|
|
49136
49473
|
const readPersistedCache = (cacheFilePath) => {
|
|
49137
49474
|
try {
|
|
49138
49475
|
const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
|
|
49139
|
-
if (!isRecord
|
|
49476
|
+
if (!isRecord(parsed) || parsed.version !== 1) return {
|
|
49140
49477
|
version: 1,
|
|
49141
49478
|
entries: []
|
|
49142
49479
|
};
|
|
@@ -49146,8 +49483,8 @@ const readPersistedCache = (cacheFilePath) => {
|
|
|
49146
49483
|
};
|
|
49147
49484
|
const entries = [];
|
|
49148
49485
|
for (const entry of parsed.entries) {
|
|
49149
|
-
if (!isRecord
|
|
49150
|
-
if (!isRecord
|
|
49486
|
+
if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
|
|
49487
|
+
if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
|
|
49151
49488
|
entries.push(entry);
|
|
49152
49489
|
}
|
|
49153
49490
|
return {
|
|
@@ -49679,6 +50016,7 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49679
50016
|
}
|
|
49680
50017
|
const animateRender = !options.silent && !options.verbose && canAnimateOnboarding(process.stdout);
|
|
49681
50018
|
const pause = onboardingSectionPause(animateRender);
|
|
50019
|
+
const useHyperlinks = shouldRenderHyperlinks(process.stdout);
|
|
49682
50020
|
const demotedDiagnosticCount = diagnostics.length - surfaceDiagnostics.length;
|
|
49683
50021
|
const lintSourceFileCount = options.includePaths.length > 0 ? options.includePaths.length : project.sourceFileCount;
|
|
49684
50022
|
if (printedDiagnostics.length === 0) {
|
|
@@ -49704,7 +50042,7 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49704
50042
|
yield* printDiagnostics([...printedDiagnostics], options.verbose, directory, buildRulePriorityMap([score]), isCodingAgentEnvironment(), {
|
|
49705
50043
|
sectionPause: pause,
|
|
49706
50044
|
animateCountUp: animateRender
|
|
49707
|
-
});
|
|
50045
|
+
}, useHyperlinks);
|
|
49708
50046
|
if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
|
|
49709
50047
|
if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
|
|
49710
50048
|
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.`));
|
|
@@ -49893,24 +50231,18 @@ const isExpectedUserError = (error) => error instanceof CliInputError || isProje
|
|
|
49893
50231
|
//#region src/cli/utils/build-handoff-payload.ts
|
|
49894
50232
|
const buildHandoffPayload = (input) => {
|
|
49895
50233
|
const topGroups = buildSortedRuleGroups(input.diagnostics).slice(0, 3);
|
|
50234
|
+
const migrationScaleBuckets = new Map(findMigrationScaleBuckets(input.diagnostics).map((bucket) => [bucket.ruleKey, bucket]));
|
|
49896
50235
|
let outputDirectory = null;
|
|
49897
50236
|
try {
|
|
49898
50237
|
outputDirectory = writeDiagnosticsDirectory([...input.diagnostics], input.outputDirectory);
|
|
49899
50238
|
} catch {}
|
|
49900
|
-
const lines = [
|
|
49901
|
-
"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:",
|
|
49902
|
-
`- It's used by teams at ${CI_TRUST_COMPANIES}.`,
|
|
49903
|
-
"- I don't have to fix everything at once; CI keeps new PRs clean while I incrementally pay down the existing issues.",
|
|
49904
|
-
`- Setup guide: ${CI_URL}`,
|
|
49905
|
-
"If I say yes, run `npx react-doctor install` (or add the GitHub Actions workflow). Either way, then continue with the fixes below.",
|
|
49906
|
-
"",
|
|
49907
|
-
`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.`,
|
|
49908
|
-
""
|
|
49909
|
-
];
|
|
50239
|
+
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.`, ""];
|
|
49910
50240
|
topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
|
|
49911
50241
|
const representative = ruleDiagnostics[0];
|
|
49912
50242
|
const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
|
|
49913
|
-
|
|
50243
|
+
const sharedFixSiteCount = getSharedFixSiteCount(ruleDiagnostics);
|
|
50244
|
+
const countBadge = sharedFixSiteCount > 0 ? `one fix · ${sharedFixSiteCount} sites` : `×${ruleDiagnostics.length}`;
|
|
50245
|
+
lines.push(`${index + 1}. ${severityLabel} ${representative.category}: ${representative.title ?? ruleKey} (${countBadge})`, ` ${representative.message}`);
|
|
49914
50246
|
const fixRecipeLine = formatFixRecipeLine(representative);
|
|
49915
50247
|
if (fixRecipeLine) lines.push(` ${fixRecipeLine}`);
|
|
49916
50248
|
const uniqueFiles = [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.filePath))];
|
|
@@ -49920,10 +50252,19 @@ const buildHandoffPayload = (input) => {
|
|
|
49920
50252
|
}
|
|
49921
50253
|
const remainingFiles = uniqueFiles.length - 3;
|
|
49922
50254
|
if (remainingFiles > 0) lines.push(` - +${remainingFiles} more files`);
|
|
50255
|
+
const migrationBucket = migrationScaleBuckets.get(ruleKey);
|
|
50256
|
+
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.`);
|
|
49923
50257
|
});
|
|
49924
50258
|
lines.push("");
|
|
49925
50259
|
if (outputDirectory) lines.push(`Full results for all ${input.diagnostics.length} issues (diagnostics.json + a .txt per rule): ${outputDirectory}`, "");
|
|
49926
|
-
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.", ""
|
|
50260
|
+
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.", "");
|
|
50261
|
+
const shownRuleKeys = new Set(topGroups.map(([ruleKey]) => ruleKey));
|
|
50262
|
+
const deferredMigrationBuckets = [...migrationScaleBuckets.values()].filter((bucket) => !shownRuleKeys.has(bucket.ruleKey));
|
|
50263
|
+
if (deferredMigrationBuckets.length > 0) {
|
|
50264
|
+
const ruleSummaries = deferredMigrationBuckets.map((bucket) => `${bucket.title} (${bucket.fileCount} files)`).join(", ");
|
|
50265
|
+
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.`, "");
|
|
50266
|
+
}
|
|
50267
|
+
lines.push("Then work through the rest from the full results above.");
|
|
49927
50268
|
return lines.join("\n");
|
|
49928
50269
|
};
|
|
49929
50270
|
//#endregion
|
|
@@ -49967,78 +50308,6 @@ const detectAvailableAgents = async () => {
|
|
|
49967
50308
|
return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
|
|
49968
50309
|
};
|
|
49969
50310
|
//#endregion
|
|
49970
|
-
//#region src/cli/utils/git-hook-shared.ts
|
|
49971
|
-
const HOOK_FILE_NAME = "pre-commit";
|
|
49972
|
-
const HOOK_RELATIVE_PATH = "hooks/pre-commit";
|
|
49973
|
-
const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
|
|
49974
|
-
const HUSKY_HOOKS_PATH = ".husky";
|
|
49975
|
-
const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
|
|
49976
|
-
const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
|
|
49977
|
-
const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
|
|
49978
|
-
const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
|
|
49979
|
-
const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
|
|
49980
|
-
const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
|
|
49981
|
-
"react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
|
|
49982
|
-
`if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
|
|
49983
|
-
"rm -f \"$react_doctor_output\";",
|
|
49984
|
-
"else",
|
|
49985
|
-
"rm -f \"$react_doctor_output\";",
|
|
49986
|
-
`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;`,
|
|
49987
|
-
"fi"
|
|
49988
|
-
].join(" ");
|
|
49989
|
-
const PACKAGE_JSON_FILE_NAME = "package.json";
|
|
49990
|
-
const runGit = (projectRoot, args) => {
|
|
49991
|
-
try {
|
|
49992
|
-
return execFileSync("git", [...args], {
|
|
49993
|
-
cwd: projectRoot,
|
|
49994
|
-
encoding: "utf8",
|
|
49995
|
-
stdio: [
|
|
49996
|
-
"ignore",
|
|
49997
|
-
"pipe",
|
|
49998
|
-
"ignore"
|
|
49999
|
-
]
|
|
50000
|
-
}).trim();
|
|
50001
|
-
} catch {
|
|
50002
|
-
return null;
|
|
50003
|
-
}
|
|
50004
|
-
};
|
|
50005
|
-
const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
|
|
50006
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
50007
|
-
const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
|
|
50008
|
-
const readPackageJson = (projectRoot) => {
|
|
50009
|
-
try {
|
|
50010
|
-
return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
|
|
50011
|
-
} catch {
|
|
50012
|
-
return null;
|
|
50013
|
-
}
|
|
50014
|
-
};
|
|
50015
|
-
const writeJsonFile$1 = (filePath, value) => {
|
|
50016
|
-
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
50017
|
-
};
|
|
50018
|
-
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
50019
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50020
|
-
if (!isRecord(packageJson)) return false;
|
|
50021
|
-
return [
|
|
50022
|
-
"dependencies",
|
|
50023
|
-
"devDependencies",
|
|
50024
|
-
"optionalDependencies"
|
|
50025
|
-
].some((fieldName) => {
|
|
50026
|
-
const dependencies = packageJson[fieldName];
|
|
50027
|
-
return isRecord(dependencies) && typeof dependencies[dependencyName] === "string";
|
|
50028
|
-
});
|
|
50029
|
-
};
|
|
50030
|
-
const packageHasRecordKey = (projectRoot, key) => {
|
|
50031
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50032
|
-
return isRecord(packageJson) && isRecord(packageJson[key]);
|
|
50033
|
-
};
|
|
50034
|
-
const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
|
|
50035
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50036
|
-
if (!isRecord(packageJson)) return false;
|
|
50037
|
-
const value = packageJson[key];
|
|
50038
|
-
return isRecord(value) && isRecord(value[nestedKey]);
|
|
50039
|
-
};
|
|
50040
|
-
const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
|
|
50041
|
-
//#endregion
|
|
50042
50311
|
//#region src/cli/utils/install-doctor-script.ts
|
|
50043
50312
|
const DOCTOR_SCRIPT_NAME = "doctor";
|
|
50044
50313
|
const FALLBACK_DOCTOR_SCRIPT_NAME = "react-doctor";
|
|
@@ -50064,31 +50333,31 @@ const findNearestPackageDirectory = (startDirectory, stopDirectory) => {
|
|
|
50064
50333
|
};
|
|
50065
50334
|
const hasDoctorScript = (projectRoot) => {
|
|
50066
50335
|
const packageJson = readPackageJson(findNearestPackageDirectory(projectRoot) ?? projectRoot);
|
|
50067
|
-
if (!isRecord(packageJson)) return false;
|
|
50336
|
+
if (!isRecord$1(packageJson)) return false;
|
|
50068
50337
|
const scripts = packageJson.scripts;
|
|
50069
|
-
if (!isRecord(scripts)) return false;
|
|
50338
|
+
if (!isRecord$1(scripts)) return false;
|
|
50070
50339
|
return isReactDoctorScriptCommand(scripts[DOCTOR_SCRIPT_NAME]) || isReactDoctorScriptCommand(scripts[FALLBACK_DOCTOR_SCRIPT_NAME]);
|
|
50071
50340
|
};
|
|
50072
50341
|
const hasDoctorDependency = (packageJson) => DEPENDENCY_FIELD_NAMES.some((fieldName) => {
|
|
50073
50342
|
const dependencies = packageJson[fieldName];
|
|
50074
|
-
return isRecord(dependencies) && Object.hasOwn(dependencies, "react-doctor");
|
|
50343
|
+
return isRecord$1(dependencies) && Object.hasOwn(dependencies, "react-doctor");
|
|
50075
50344
|
});
|
|
50076
50345
|
const installDoctorScript = (options) => {
|
|
50077
50346
|
const packageDirectory = findNearestPackageDirectory(options.projectRoot) ?? options.projectRoot;
|
|
50078
50347
|
const packageJsonPath = getPackageJsonPath(packageDirectory);
|
|
50079
50348
|
const packageJson = readPackageJson(packageDirectory);
|
|
50080
|
-
if (!isRecord(packageJson)) return {
|
|
50349
|
+
if (!isRecord$1(packageJson)) return {
|
|
50081
50350
|
packageJsonPath,
|
|
50082
50351
|
scriptStatus: "skipped",
|
|
50083
50352
|
scriptReason: "missing-or-invalid-package-json"
|
|
50084
50353
|
};
|
|
50085
50354
|
const scripts = packageJson.scripts;
|
|
50086
50355
|
const scriptTarget = (() => {
|
|
50087
|
-
if (scripts !== void 0 && !isRecord(scripts)) return {
|
|
50356
|
+
if (scripts !== void 0 && !isRecord$1(scripts)) return {
|
|
50088
50357
|
status: "skipped",
|
|
50089
50358
|
reason: "invalid-scripts"
|
|
50090
50359
|
};
|
|
50091
|
-
const scriptRecord = isRecord(scripts) ? scripts : {};
|
|
50360
|
+
const scriptRecord = isRecord$1(scripts) ? scripts : {};
|
|
50092
50361
|
if (isReactDoctorScriptCommand(scriptRecord[DOCTOR_SCRIPT_NAME])) return {
|
|
50093
50362
|
scriptName: DOCTOR_SCRIPT_NAME,
|
|
50094
50363
|
status: "existing"
|
|
@@ -50122,7 +50391,7 @@ const installDoctorScript = (options) => {
|
|
|
50122
50391
|
if (scriptStatus === "created") writeJsonFile$1(packageJsonPath, {
|
|
50123
50392
|
...packageJson,
|
|
50124
50393
|
scripts: {
|
|
50125
|
-
...isRecord(scripts) ? scripts : {},
|
|
50394
|
+
...isRecord$1(scripts) ? scripts : {},
|
|
50126
50395
|
[scriptTarget.scriptName ?? DOCTOR_SCRIPT_NAME]: DOCTOR_SCRIPT_COMMAND
|
|
50127
50396
|
}
|
|
50128
50397
|
});
|
|
@@ -50276,38 +50545,52 @@ const upgradeReactDoctorWorkflowInPlace = (projectRoot) => {
|
|
|
50276
50545
|
//#region src/cli/utils/hash-project-root.ts
|
|
50277
50546
|
const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
|
|
50278
50547
|
//#endregion
|
|
50279
|
-
//#region src/cli/utils/
|
|
50280
|
-
const
|
|
50281
|
-
const
|
|
50282
|
-
|
|
50283
|
-
|
|
50284
|
-
});
|
|
50285
|
-
|
|
50286
|
-
|
|
50287
|
-
|
|
50288
|
-
|
|
50289
|
-
|
|
50290
|
-
|
|
50291
|
-
|
|
50292
|
-
};
|
|
50293
|
-
const recordActionUpgradeDecision = (projectRoot, outcome, storeOptions = {}) => {
|
|
50294
|
-
try {
|
|
50295
|
-
const store = getActionUpgradeStore(storeOptions);
|
|
50296
|
-
const upgrades = store.get("actionUpgrades", {});
|
|
50297
|
-
store.set("actionUpgrades", {
|
|
50298
|
-
...upgrades,
|
|
50299
|
-
[hashProjectRoot(projectRoot)]: {
|
|
50300
|
-
rootDirectory: Path.resolve(projectRoot),
|
|
50301
|
-
outcome,
|
|
50302
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
50548
|
+
//#region src/cli/utils/project-decision-store.ts
|
|
50549
|
+
const createProjectDecisionStore = (storeKey) => {
|
|
50550
|
+
const getStore = (options = {}) => new Conf({
|
|
50551
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
50552
|
+
cwd: options.cwd
|
|
50553
|
+
});
|
|
50554
|
+
return {
|
|
50555
|
+
getConfigPath: (options = {}) => getStore(options).path,
|
|
50556
|
+
hasHandled: (projectRoot, options = {}) => {
|
|
50557
|
+
try {
|
|
50558
|
+
return Boolean(getStore(options).get(storeKey, {})[hashProjectRoot(projectRoot)]);
|
|
50559
|
+
} catch {
|
|
50560
|
+
return true;
|
|
50303
50561
|
}
|
|
50304
|
-
}
|
|
50305
|
-
|
|
50306
|
-
|
|
50307
|
-
|
|
50308
|
-
|
|
50562
|
+
},
|
|
50563
|
+
record: (projectRoot, outcome, options = {}) => {
|
|
50564
|
+
try {
|
|
50565
|
+
const store = getStore(options);
|
|
50566
|
+
store.set(storeKey, {
|
|
50567
|
+
...store.get(storeKey, {}),
|
|
50568
|
+
[hashProjectRoot(projectRoot)]: {
|
|
50569
|
+
rootDirectory: Path.resolve(projectRoot),
|
|
50570
|
+
outcome,
|
|
50571
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
50572
|
+
}
|
|
50573
|
+
});
|
|
50574
|
+
return true;
|
|
50575
|
+
} catch {
|
|
50576
|
+
return false;
|
|
50577
|
+
}
|
|
50578
|
+
}
|
|
50579
|
+
};
|
|
50309
50580
|
};
|
|
50310
50581
|
//#endregion
|
|
50582
|
+
//#region src/cli/utils/action-upgrade-prompt.ts
|
|
50583
|
+
const store$1 = createProjectDecisionStore("actionUpgrades");
|
|
50584
|
+
store$1.getConfigPath;
|
|
50585
|
+
const hasHandledActionUpgrade = store$1.hasHandled;
|
|
50586
|
+
const recordActionUpgradeDecision = store$1.record;
|
|
50587
|
+
//#endregion
|
|
50588
|
+
//#region src/cli/utils/ci-prompt-decision.ts
|
|
50589
|
+
const store = createProjectDecisionStore("ciPrompts");
|
|
50590
|
+
store.getConfigPath;
|
|
50591
|
+
const hasHandledCiPrompt = store.hasHandled;
|
|
50592
|
+
const recordCiPromptDecision = store.record;
|
|
50593
|
+
//#endregion
|
|
50311
50594
|
//#region src/cli/utils/open-url.ts
|
|
50312
50595
|
const resolveOpenCommand = (url) => {
|
|
50313
50596
|
if (process$1.platform === "darwin") return {
|
|
@@ -50936,13 +51219,13 @@ const installPackageJsonHook = (options, strategy) => {
|
|
|
50936
51219
|
const packageJsonPath = getPackageJsonPath(options.projectRoot);
|
|
50937
51220
|
const didHookExist = NFS.existsSync(packageJsonPath);
|
|
50938
51221
|
const packageJson = readPackageJson(options.projectRoot);
|
|
50939
|
-
const nextPackageJson = isRecord(packageJson) ? { ...packageJson } : {};
|
|
51222
|
+
const nextPackageJson = isRecord$1(packageJson) ? { ...packageJson } : {};
|
|
50940
51223
|
const parentKeys = strategy.path.slice(0, -1);
|
|
50941
51224
|
const leafKey = strategy.path[strategy.path.length - 1];
|
|
50942
51225
|
let parent = nextPackageJson;
|
|
50943
51226
|
for (const key of parentKeys) {
|
|
50944
51227
|
const existing = parent[key];
|
|
50945
|
-
const cloned = isRecord(existing) ? { ...existing } : {};
|
|
51228
|
+
const cloned = isRecord$1(existing) ? { ...existing } : {};
|
|
50946
51229
|
parent[key] = cloned;
|
|
50947
51230
|
parent = cloned;
|
|
50948
51231
|
}
|
|
@@ -51113,7 +51396,7 @@ const isHuskyProject = (projectRoot) => NFS.existsSync(Path.join(projectRoot, ".
|
|
|
51113
51396
|
const isVitePlusProject = (projectRoot) => packageHasDependency(projectRoot, "vite-plus");
|
|
51114
51397
|
const isSimpleGitHooksProject = (projectRoot) => {
|
|
51115
51398
|
const packageJson = readPackageJson(projectRoot);
|
|
51116
|
-
return isRecord(packageJson) && isRecord(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
|
|
51399
|
+
return isRecord$1(packageJson) && isRecord$1(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
|
|
51117
51400
|
};
|
|
51118
51401
|
const getLefthookConfigPath = (projectRoot) => {
|
|
51119
51402
|
for (const fileName of LEFTHOOK_CONFIG_FILES) {
|
|
@@ -51279,7 +51562,7 @@ const detectPackageManager = (projectRoot) => {
|
|
|
51279
51562
|
let currentDirectory = Path.resolve(projectRoot);
|
|
51280
51563
|
while (true) {
|
|
51281
51564
|
const packageJson = readPackageJson(currentDirectory);
|
|
51282
|
-
if (isRecord(packageJson) && typeof packageJson.packageManager === "string") {
|
|
51565
|
+
if (isRecord$1(packageJson) && typeof packageJson.packageManager === "string") {
|
|
51283
51566
|
const packageManagerName = packageJson.packageManager.split("@")[0];
|
|
51284
51567
|
if (packageManagerName === "pnpm" || packageManagerName === "yarn" || packageManagerName === "bun" || packageManagerName === "npm") return packageManagerName;
|
|
51285
51568
|
}
|
|
@@ -51355,12 +51638,12 @@ const isSupplyChainTrustError = (error) => {
|
|
|
51355
51638
|
const formatInstallCommand = (input) => [input.command, ...input.args].join(" ");
|
|
51356
51639
|
const installReactDoctorDependency = async (options) => {
|
|
51357
51640
|
const packageJson = readPackageJson(options.projectRoot);
|
|
51358
|
-
if (!isRecord(packageJson)) return {
|
|
51641
|
+
if (!isRecord$1(packageJson)) return {
|
|
51359
51642
|
dependencyStatus: "skipped",
|
|
51360
51643
|
dependencyReason: "missing-or-invalid-package-json"
|
|
51361
51644
|
};
|
|
51362
51645
|
if (hasDoctorDependency(packageJson)) return { dependencyStatus: "existing" };
|
|
51363
|
-
if (packageJson.devDependencies !== void 0 && !isRecord(packageJson.devDependencies)) return {
|
|
51646
|
+
if (packageJson.devDependencies !== void 0 && !isRecord$1(packageJson.devDependencies)) return {
|
|
51364
51647
|
dependencyStatus: "skipped",
|
|
51365
51648
|
dependencyReason: "invalid-dev-dependencies"
|
|
51366
51649
|
};
|
|
@@ -51524,10 +51807,12 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
51524
51807
|
const existingWorkflow = readReactDoctorWorkflow(projectRoot);
|
|
51525
51808
|
const canInstallWorkflow = !NFS.existsSync(workflowTargetPath);
|
|
51526
51809
|
const canUpgradeWorkflow = existingWorkflow !== null && workflowUsesV1Action(existingWorkflow.content) && !hasHandledActionUpgrade(projectRoot);
|
|
51527
|
-
const
|
|
51810
|
+
const ciPromptOutcome = canInstallWorkflow && !options.yes && !skipPrompts && !hasHandledCiPrompt(projectRoot) ? await askAddToGitHubActions(prompt) : null;
|
|
51811
|
+
const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || ciPromptOutcome === "yes");
|
|
51528
51812
|
const upgradePromptOutcome = canUpgradeWorkflow && !options.yes && !skipPrompts ? await askUpgradeActionVersion(prompt) : null;
|
|
51529
51813
|
const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
|
|
51530
51814
|
if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
|
|
51815
|
+
if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
|
|
51531
51816
|
const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
|
|
51532
51817
|
type: "multiselect",
|
|
51533
51818
|
name: "agents",
|
|
@@ -51774,18 +52059,24 @@ const handoffToAgent = async (input) => {
|
|
|
51774
52059
|
if (!input.interactive || input.diagnostics.length === 0) return;
|
|
51775
52060
|
cliLogger.break();
|
|
51776
52061
|
const projectRootForCi = findNearestPackageDirectory(input.rootDirectory) ?? input.rootDirectory;
|
|
51777
|
-
|
|
52062
|
+
const isGitHubActionsConfigured = isReactDoctorWorkflowInstalled(projectRootForCi);
|
|
52063
|
+
if (!isGitHubActionsConfigured && !hasHandledCiPrompt(projectRootForCi)) {
|
|
51778
52064
|
const ciOutcome = await askAddToGitHubActions();
|
|
51779
52065
|
recordCount(METRIC.agentHandoff, 1, {
|
|
51780
52066
|
outcome: `ci-${ciOutcome}`,
|
|
51781
52067
|
diagnosticsCount: input.diagnostics.length
|
|
51782
52068
|
});
|
|
51783
52069
|
if (ciOutcome === "cancel") return;
|
|
52070
|
+
recordCiPromptDecision(projectRootForCi, ciOutcome === "yes" ? "accepted" : "declined");
|
|
51784
52071
|
if (ciOutcome === "yes") {
|
|
51785
52072
|
await setUpGitHubActions({ rootDirectory: input.rootDirectory });
|
|
51786
52073
|
cliLogger.break();
|
|
51787
52074
|
}
|
|
51788
|
-
} else await maybeOfferActionUpgrade(projectRootForCi);
|
|
52075
|
+
} else if (isGitHubActionsConfigured) await maybeOfferActionUpgrade(projectRootForCi);
|
|
52076
|
+
else recordCount(METRIC.agentHandoff, 1, {
|
|
52077
|
+
outcome: "ci-suppressed",
|
|
52078
|
+
diagnosticsCount: input.diagnostics.length
|
|
52079
|
+
});
|
|
51789
52080
|
const { handoffTarget } = await prompts({
|
|
51790
52081
|
type: "select",
|
|
51791
52082
|
name: "handoffTarget",
|
|
@@ -52008,6 +52299,7 @@ const reportErrorToSentry = async (error) => {
|
|
|
52008
52299
|
sampled: runTrace.sampled,
|
|
52009
52300
|
sampleRand: Math.random()
|
|
52010
52301
|
});
|
|
52302
|
+
recordRunTraceId(scope.getPropagationContext().traceId);
|
|
52011
52303
|
return Sentry.captureException(error);
|
|
52012
52304
|
});
|
|
52013
52305
|
await Sentry.flush(SENTRY_FLUSH_TIMEOUT_MS);
|
|
@@ -52091,7 +52383,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52091
52383
|
yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
|
|
52092
52384
|
if (displayDiagnostics.length > 0) {
|
|
52093
52385
|
yield* log("");
|
|
52094
|
-
yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender });
|
|
52386
|
+
yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender }, shouldRenderHyperlinks(process.stdout));
|
|
52095
52387
|
}
|
|
52096
52388
|
const lowestScoredScan = findLowestScoredScan(completedScans);
|
|
52097
52389
|
const aggregateScore = lowestScoredScan?.result.score ?? null;
|
|
@@ -52129,9 +52421,8 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52129
52421
|
});
|
|
52130
52422
|
//#endregion
|
|
52131
52423
|
//#region src/cli/utils/prompt-install-setup.ts
|
|
52132
|
-
const GLOBAL_CONFIG_PROJECT_NAME = "react-doctor";
|
|
52133
52424
|
const getSetupPromptStore = (options = {}) => new Conf({
|
|
52134
|
-
projectName:
|
|
52425
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
52135
52426
|
cwd: options.cwd
|
|
52136
52427
|
});
|
|
52137
52428
|
const getSetupPromptProjectKey = (projectRoot) => hashProjectRoot(projectRoot);
|
|
@@ -52142,6 +52433,24 @@ const hasDisabledSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
|
52142
52433
|
return false;
|
|
52143
52434
|
}
|
|
52144
52435
|
};
|
|
52436
|
+
const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
52437
|
+
try {
|
|
52438
|
+
const store = getSetupPromptStore(storeOptions);
|
|
52439
|
+
const projects = store.get("projects", {});
|
|
52440
|
+
const projectKey = getSetupPromptProjectKey(projectRoot);
|
|
52441
|
+
store.set("projects", {
|
|
52442
|
+
...projects,
|
|
52443
|
+
[projectKey]: {
|
|
52444
|
+
...projects[projectKey] ?? {},
|
|
52445
|
+
rootDirectory: Path.resolve(projectRoot),
|
|
52446
|
+
setupPrompt: false
|
|
52447
|
+
}
|
|
52448
|
+
});
|
|
52449
|
+
return true;
|
|
52450
|
+
} catch {
|
|
52451
|
+
return false;
|
|
52452
|
+
}
|
|
52453
|
+
};
|
|
52145
52454
|
const resolveInstallSetupProjectRoot = (options) => {
|
|
52146
52455
|
if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
|
|
52147
52456
|
const packageDirectories = /* @__PURE__ */ new Set();
|
|
@@ -52548,6 +52857,14 @@ const runExplain = async (fileLineArgument, context) => {
|
|
|
52548
52857
|
const colorizedRule = colorizeRuleByDiagnostic(ruleIdentifier, diagnostic.severity);
|
|
52549
52858
|
const severityLabel = colorizeRuleByDiagnostic(diagnostic.severity, diagnostic.severity);
|
|
52550
52859
|
cliLogger.log(`${severitySymbol} ${colorizedRule} ${highlighter.dim(`(${severityLabel})`)} — ${diagnostic.message}`);
|
|
52860
|
+
const codeFrame = buildCodeFrame({
|
|
52861
|
+
filePath: diagnostic.filePath,
|
|
52862
|
+
line: diagnostic.line,
|
|
52863
|
+
column: diagnostic.column,
|
|
52864
|
+
endLine: diagnostic.endLine,
|
|
52865
|
+
rootDirectory: targetDirectory
|
|
52866
|
+
});
|
|
52867
|
+
if (codeFrame) cliLogger.log(indentMultilineText(codeFrame, " "));
|
|
52551
52868
|
if (diagnostic.category) cliLogger.dim(` Category: ${diagnostic.category}`);
|
|
52552
52869
|
if (diagnostic.help) cliLogger.dim(` ${diagnostic.help}`);
|
|
52553
52870
|
cliLogger.dim(` If this needs follow-up or looks like a false positive, open: ${buildDiagnosticIssueUrl({
|
|
@@ -52597,6 +52914,10 @@ const validateModeFlags = (flags) => {
|
|
|
52597
52914
|
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.`);
|
|
52598
52915
|
if (flags.score && flags.json) throw new CliInputError("Cannot combine --score and --json; pick one output mode.");
|
|
52599
52916
|
if (flags.score && flags.telemetry === false) throw new CliInputError("Cannot combine --score with --no-telemetry; --score prints the score that --no-telemetry disables.");
|
|
52917
|
+
if (flags.debug && (flags.score === false || flags.telemetry === false)) {
|
|
52918
|
+
const disablingFlag = flags.score === false ? "--no-score" : "--no-telemetry";
|
|
52919
|
+
throw new CliInputError(`Cannot combine --debug with ${disablingFlag}; ${disablingFlag} disables the Sentry reporting --debug needs to capture a trace.`);
|
|
52920
|
+
}
|
|
52600
52921
|
};
|
|
52601
52922
|
//#endregion
|
|
52602
52923
|
//#region src/cli/commands/inspect.ts
|
|
@@ -52944,11 +53265,13 @@ const inspectAction = async (directory, flags) => {
|
|
|
52944
53265
|
})) {
|
|
52945
53266
|
printAgentInstallHint();
|
|
52946
53267
|
recordCount(METRIC.agentInstallHintShown, 1);
|
|
53268
|
+
disableSetupPrompt(setupProjectRoot);
|
|
52947
53269
|
}
|
|
52948
53270
|
}
|
|
52949
53271
|
} catch (error) {
|
|
52950
53272
|
const isUserError = isExpectedUserError(error);
|
|
52951
53273
|
const sentryEventId = isUserError ? void 0 : await reportErrorToSentry(error);
|
|
53274
|
+
if (isDebugFlagEnabled()) await flushSentry();
|
|
52952
53275
|
if (isJsonMode) {
|
|
52953
53276
|
writeJsonErrorReport(error, sentryEventId);
|
|
52954
53277
|
process.exitCode = 1;
|
|
@@ -53671,6 +53994,33 @@ const normalizeHelpInvocation = (argv, knownCommands) => {
|
|
|
53671
53994
|
return [...nodeArguments, "--help"];
|
|
53672
53995
|
};
|
|
53673
53996
|
//#endregion
|
|
53997
|
+
//#region src/cli/utils/print-debug-trace.ts
|
|
53998
|
+
/**
|
|
53999
|
+
* The `--debug` end-of-run line, pure so it's testable without the Sentry SDK.
|
|
54000
|
+
* Mirrors the crash-reference phrasing in `handle-error.ts` ("mention this when
|
|
54001
|
+
* reporting") so users learn one habit for both paths. A `null` trace says why,
|
|
54002
|
+
* so `--debug` never silently does nothing.
|
|
54003
|
+
*/
|
|
54004
|
+
const buildDebugTraceMessage = (traceId) => traceId === null ? "Sentry trace unavailable for this run (no trace was recorded)." : `Sentry trace (mention this when reporting): ${traceId}`;
|
|
54005
|
+
/**
|
|
54006
|
+
* Prints the run's Sentry trace id to stderr at the end of a `--debug` run, so
|
|
54007
|
+
* maintainers can pull the full trace from a pasted id. Runs from the process
|
|
54008
|
+
* `exit` handler, so it's the last line on both the success path and the error
|
|
54009
|
+
* funnels (which `process.exit()` before the promise chain could resume).
|
|
54010
|
+
*
|
|
54011
|
+
* Writes straight to `process.stderr` (not `Console`) for three reasons: the
|
|
54012
|
+
* exit handler is synchronous, JSON mode patches the global console to no-ops —
|
|
54013
|
+
* a diagnostic the user explicitly asked for must survive that — and stderr
|
|
54014
|
+
* keeps `--json` / `--score` stdout machine-clean. The write is wrapped because
|
|
54015
|
+
* a diagnostic must never throw out of an exit handler.
|
|
54016
|
+
*/
|
|
54017
|
+
const printDebugTrace = () => {
|
|
54018
|
+
if (!Sentry.isInitialized()) return;
|
|
54019
|
+
try {
|
|
54020
|
+
process.stderr.write(`${highlighter.dim(buildDebugTraceMessage(getLastRunTraceId()))}\n`);
|
|
54021
|
+
} catch {}
|
|
54022
|
+
};
|
|
54023
|
+
//#endregion
|
|
53674
54024
|
//#region src/cli/utils/removed-cli-flags.ts
|
|
53675
54025
|
const REMOVED_FLAGS = new Map([
|
|
53676
54026
|
["--full", "use `--diff false` to force a full scan"],
|
|
@@ -53697,6 +54047,7 @@ const ROOT_FLAG_SPEC = {
|
|
|
53697
54047
|
longOptionsWithoutValues: new Set([
|
|
53698
54048
|
"--color",
|
|
53699
54049
|
"--dead-code",
|
|
54050
|
+
"--debug",
|
|
53700
54051
|
"--help",
|
|
53701
54052
|
"--json",
|
|
53702
54053
|
"--json-compact",
|
|
@@ -53864,6 +54215,9 @@ const stripUnknownCliFlags = (argv) => {
|
|
|
53864
54215
|
initializeSentry();
|
|
53865
54216
|
process.on("SIGINT", exitGracefully);
|
|
53866
54217
|
process.on("SIGTERM", exitGracefully);
|
|
54218
|
+
process.on("exit", () => {
|
|
54219
|
+
if (isDebugFlagEnabled()) printDebugTrace();
|
|
54220
|
+
});
|
|
53867
54221
|
unrefStdin();
|
|
53868
54222
|
guardStdin();
|
|
53869
54223
|
const formatExampleLines = (examples) => {
|
|
@@ -53908,7 +54262,7 @@ ${highlighter.dim("Learn more:")}
|
|
|
53908
54262
|
${highlighter.info(CANONICAL_GITHUB_URL)}
|
|
53909
54263
|
`;
|
|
53910
54264
|
const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
|
|
53911
|
-
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);
|
|
54265
|
+
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);
|
|
53912
54266
|
program.action(inspectAction);
|
|
53913
54267
|
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));
|
|
53914
54268
|
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);
|
|
@@ -53951,4 +54305,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
53951
54305
|
export {};
|
|
53952
54306
|
|
|
53953
54307
|
//# sourceMappingURL=cli.js.map
|
|
53954
|
-
//# debugId=
|
|
54308
|
+
//# debugId=4aa7cab0-c8a8-5513-bde6-3f7a0a1bdaa7
|