react-doctor 0.5.6-dev.431e515 → 0.5.6-dev.44db3e0
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 +605 -366
- package/dist/index.d.ts +10 -0
- package/dist/index.js +237 -182
- package/dist/lsp.js +255 -204
- package/package.json +2 -2
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]="e076a347-15ab-55bb-b11a-a813bb818760")}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";
|
|
@@ -22400,6 +22400,7 @@ var JsonReportProjectEntry = class extends Class("JsonReportProjectEntry")({
|
|
|
22400
22400
|
score: Unknown,
|
|
22401
22401
|
skippedChecks: ArraySchema(String$1),
|
|
22402
22402
|
skippedCheckReasons: optional(Record$1(String$1, String$1)),
|
|
22403
|
+
scannedFileCount: optional(Number$1),
|
|
22403
22404
|
elapsedMilliseconds: Number$1
|
|
22404
22405
|
}) {};
|
|
22405
22406
|
/**
|
|
@@ -35893,6 +35894,7 @@ const isLargeMinifiedFile = (absolutePath) => {
|
|
|
35893
35894
|
if (sizeBytes < 2e4) return false;
|
|
35894
35895
|
return isMinifiedSource(absolutePath);
|
|
35895
35896
|
};
|
|
35897
|
+
const isErrnoException = (error) => error instanceof Error && "code" in error;
|
|
35896
35898
|
const IGNORABLE_READDIR_ERROR_CODES = new Set([
|
|
35897
35899
|
"EACCES",
|
|
35898
35900
|
"EPERM",
|
|
@@ -35902,11 +35904,7 @@ const IGNORABLE_READDIR_ERROR_CODES = new Set([
|
|
|
35902
35904
|
"ELOOP",
|
|
35903
35905
|
"ENAMETOOLONG"
|
|
35904
35906
|
]);
|
|
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
|
-
};
|
|
35907
|
+
const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
|
|
35910
35908
|
const readDirectoryEntries = (directoryPath) => {
|
|
35911
35909
|
try {
|
|
35912
35910
|
return NFS.readdirSync(directoryPath, { withFileTypes: true });
|
|
@@ -35953,7 +35951,7 @@ const readPackageJsonUncached = (packageJsonPath) => {
|
|
|
35953
35951
|
return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
|
|
35954
35952
|
} catch (error) {
|
|
35955
35953
|
if (error instanceof SyntaxError) return {};
|
|
35956
|
-
if (error
|
|
35954
|
+
if (isErrnoException(error)) {
|
|
35957
35955
|
const { code } = error;
|
|
35958
35956
|
if (code === "EISDIR" || code === "EACCES" || code === "EPERM" || code === "ENOENT") return {};
|
|
35959
35957
|
}
|
|
@@ -36678,17 +36676,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
|
|
|
36678
36676
|
return false;
|
|
36679
36677
|
};
|
|
36680
36678
|
const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
|
|
36681
|
-
const
|
|
36682
|
-
const spec = packageJson.dependencies?.
|
|
36679
|
+
const getDependencySpec = (packageJson, packageName) => {
|
|
36680
|
+
const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
|
|
36683
36681
|
return typeof spec === "string" ? spec : null;
|
|
36684
36682
|
};
|
|
36685
|
-
const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson,
|
|
36683
|
+
const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
|
|
36686
36684
|
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);
|
|
36685
|
+
const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
|
|
36692
36686
|
const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
|
|
36693
36687
|
if (version === null || !isCatalogReference(version)) return version;
|
|
36694
36688
|
const catalogName = extractCatalogName(version);
|
|
@@ -36700,11 +36694,7 @@ const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson,
|
|
|
36700
36694
|
if (!isFile(monorepoPackageJsonPath)) return version;
|
|
36701
36695
|
return resolveCatalogVersion(readPackageJson$1(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
|
|
36702
36696
|
};
|
|
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);
|
|
36697
|
+
const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
|
|
36708
36698
|
const getPreactVersion = (packageJson) => {
|
|
36709
36699
|
return {
|
|
36710
36700
|
...packageJson.peerDependencies,
|
|
@@ -36793,6 +36783,11 @@ const ES_TARGET_YEAR_BY_NAME = {
|
|
|
36793
36783
|
esnext: 9999
|
|
36794
36784
|
};
|
|
36795
36785
|
/**
|
|
36786
|
+
* tsconfig filenames probed when resolving a project's TypeScript
|
|
36787
|
+
* compiler options — the root config first, then a monorepo base config.
|
|
36788
|
+
*/
|
|
36789
|
+
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
36790
|
+
/**
|
|
36796
36791
|
* Project-config files that `StagedFiles.materialize` copies into
|
|
36797
36792
|
* the temp directory alongside staged sources so oxlint resolves
|
|
36798
36793
|
* `tsconfig` / `package.json` / lint configs the same way it would
|
|
@@ -37314,6 +37309,7 @@ const isTailwindAtLeast = (detected, required) => {
|
|
|
37314
37309
|
if (detected.major !== required.major) return detected.major > required.major;
|
|
37315
37310
|
return detected.minor >= required.minor;
|
|
37316
37311
|
};
|
|
37312
|
+
const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
|
|
37317
37313
|
var InvalidGlobPatternError = class extends Error {
|
|
37318
37314
|
pattern;
|
|
37319
37315
|
reason;
|
|
@@ -37342,7 +37338,7 @@ const compileGlobPattern = (rawPattern) => {
|
|
|
37342
37338
|
try {
|
|
37343
37339
|
return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
|
|
37344
37340
|
} catch (caughtError) {
|
|
37345
|
-
throw new InvalidGlobPatternError(rawPattern,
|
|
37341
|
+
throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
|
|
37346
37342
|
}
|
|
37347
37343
|
};
|
|
37348
37344
|
const compileGlobPatternsLenient = (patterns, onInvalid) => {
|
|
@@ -37438,115 +37434,6 @@ const buildRuleSeverityControls = (config) => {
|
|
|
37438
37434
|
...config.buckets !== void 0 ? { buckets: config.buckets } : {}
|
|
37439
37435
|
};
|
|
37440
37436
|
};
|
|
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
37437
|
const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
|
|
37551
37438
|
"effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
|
|
37552
37439
|
"effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
|
|
@@ -37671,7 +37558,13 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
|
|
|
37671
37558
|
}
|
|
37672
37559
|
const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
|
|
37673
37560
|
const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
|
|
37674
|
-
const
|
|
37561
|
+
const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
|
|
37562
|
+
const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
|
|
37563
|
+
const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
|
|
37564
|
+
const canonicalTarget = canonicalizeRuleKey(targetRuleKey);
|
|
37565
|
+
if (canonicalCandidate === canonicalTarget) return true;
|
|
37566
|
+
return isReactDoctorShortIdOf(canonicalCandidate, canonicalTarget) || isReactDoctorShortIdOf(canonicalTarget, canonicalCandidate);
|
|
37567
|
+
};
|
|
37675
37568
|
const getEquivalentRuleKeys = (ruleKey) => {
|
|
37676
37569
|
const nativeRuleKey = canonicalizeRuleKey(ruleKey);
|
|
37677
37570
|
return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
|
|
@@ -37681,12 +37574,182 @@ const stripDescriptionTail = (ruleList) => {
|
|
|
37681
37574
|
if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
|
|
37682
37575
|
return ruleList.slice(0, descriptionMatch.index);
|
|
37683
37576
|
};
|
|
37684
|
-
const
|
|
37577
|
+
const tokenizeRuleList = (ruleList) => {
|
|
37685
37578
|
const trimmed = ruleList?.trim();
|
|
37686
|
-
if (!trimmed) return
|
|
37579
|
+
if (!trimmed) return [];
|
|
37687
37580
|
const ruleSection = stripDescriptionTail(trimmed).trim();
|
|
37688
|
-
if (!ruleSection) return
|
|
37689
|
-
return ruleSection.split(/[,\s]+/).
|
|
37581
|
+
if (!ruleSection) return [];
|
|
37582
|
+
return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
|
|
37583
|
+
};
|
|
37584
|
+
const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
|
|
37585
|
+
const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
|
|
37586
|
+
const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
|
|
37587
|
+
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}\`.`;
|
|
37588
|
+
const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
|
|
37589
|
+
const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
|
|
37590
|
+
const candidates = [{
|
|
37591
|
+
line: lines[diagnosticLineIndex],
|
|
37592
|
+
requiredScope: "line"
|
|
37593
|
+
}, {
|
|
37594
|
+
line: lines[diagnosticLineIndex - 1],
|
|
37595
|
+
requiredScope: "next-line"
|
|
37596
|
+
}];
|
|
37597
|
+
for (const { line, requiredScope } of candidates) {
|
|
37598
|
+
const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
|
|
37599
|
+
if (!match) continue;
|
|
37600
|
+
const [, tool, scope, ruleList] = match;
|
|
37601
|
+
if (scope !== requiredScope) continue;
|
|
37602
|
+
const tokens = tokenizeRuleList(ruleList);
|
|
37603
|
+
if (tokens.includes(ruleId)) continue;
|
|
37604
|
+
for (const token of tokens) if (tokenMisnamesRule(token, ruleId)) return buildHint(tool, token, ruleId);
|
|
37605
|
+
}
|
|
37606
|
+
return null;
|
|
37607
|
+
};
|
|
37608
|
+
const detectBlockNearMiss = (lines, diagnosticLineIndex, ruleId) => {
|
|
37609
|
+
let openMisname = null;
|
|
37610
|
+
const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
|
|
37611
|
+
for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
|
|
37612
|
+
const line = lines[lineIndex];
|
|
37613
|
+
if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
|
|
37614
|
+
const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
|
|
37615
|
+
if (disableMatch) {
|
|
37616
|
+
const [, tool, ruleList] = disableMatch;
|
|
37617
|
+
const tokens = tokenizeRuleList(ruleList);
|
|
37618
|
+
if (tokens.includes(ruleId)) openMisname = null;
|
|
37619
|
+
else {
|
|
37620
|
+
const misnamed = tokens.find((token) => tokenMisnamesRule(token, ruleId));
|
|
37621
|
+
if (misnamed) openMisname = {
|
|
37622
|
+
tool,
|
|
37623
|
+
token: misnamed
|
|
37624
|
+
};
|
|
37625
|
+
}
|
|
37626
|
+
continue;
|
|
37627
|
+
}
|
|
37628
|
+
const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
|
|
37629
|
+
if (enableMatch) {
|
|
37630
|
+
const enabledRules = tokenizeRuleList(enableMatch[1]);
|
|
37631
|
+
if (enabledRules.length === 0 || enabledRules.some((rule) => isSameRuleKey(rule, ruleId))) openMisname = null;
|
|
37632
|
+
}
|
|
37633
|
+
}
|
|
37634
|
+
return openMisname ? buildHint(openMisname.tool, openMisname.token, ruleId) : null;
|
|
37635
|
+
};
|
|
37636
|
+
const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
|
|
37637
|
+
if (!ruleId.startsWith("react-doctor/")) return null;
|
|
37638
|
+
return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
|
|
37639
|
+
};
|
|
37640
|
+
const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
|
|
37641
|
+
const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
|
|
37642
|
+
const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
|
|
37643
|
+
let stringDelimiter = null;
|
|
37644
|
+
for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
|
|
37645
|
+
const character = line[charIndex];
|
|
37646
|
+
if (stringDelimiter !== null) {
|
|
37647
|
+
if (character === "\\") {
|
|
37648
|
+
charIndex++;
|
|
37649
|
+
continue;
|
|
37650
|
+
}
|
|
37651
|
+
if (character === stringDelimiter) stringDelimiter = null;
|
|
37652
|
+
continue;
|
|
37653
|
+
}
|
|
37654
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
37655
|
+
stringDelimiter = character;
|
|
37656
|
+
continue;
|
|
37657
|
+
}
|
|
37658
|
+
if (character === "/" && line[charIndex + 1] === "/") return true;
|
|
37659
|
+
}
|
|
37660
|
+
return false;
|
|
37661
|
+
};
|
|
37662
|
+
const findOpenerTagOnLine = (line) => {
|
|
37663
|
+
for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
|
|
37664
|
+
if (match.index === void 0) continue;
|
|
37665
|
+
if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
|
|
37666
|
+
}
|
|
37667
|
+
return null;
|
|
37668
|
+
};
|
|
37669
|
+
const findJsxOpenerSpan = (lines, openerLineIndex) => {
|
|
37670
|
+
const openerLine = lines[openerLineIndex];
|
|
37671
|
+
if (openerLine === void 0) return null;
|
|
37672
|
+
const opener = findOpenerTagOnLine(openerLine);
|
|
37673
|
+
if (!opener) return null;
|
|
37674
|
+
const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
|
|
37675
|
+
let braceDepth = 0;
|
|
37676
|
+
let innerAngleDepth = 0;
|
|
37677
|
+
let stringDelimiter = null;
|
|
37678
|
+
for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
|
|
37679
|
+
const currentLine = lines[lineIndex];
|
|
37680
|
+
const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
|
|
37681
|
+
for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
|
|
37682
|
+
const character = currentLine[charIndex];
|
|
37683
|
+
if (stringDelimiter !== null) {
|
|
37684
|
+
if (character === "\\") {
|
|
37685
|
+
charIndex++;
|
|
37686
|
+
continue;
|
|
37687
|
+
}
|
|
37688
|
+
if (character === stringDelimiter) stringDelimiter = null;
|
|
37689
|
+
continue;
|
|
37690
|
+
}
|
|
37691
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
37692
|
+
stringDelimiter = character;
|
|
37693
|
+
continue;
|
|
37694
|
+
}
|
|
37695
|
+
if (character === "{") {
|
|
37696
|
+
braceDepth++;
|
|
37697
|
+
continue;
|
|
37698
|
+
}
|
|
37699
|
+
if (character === "}") {
|
|
37700
|
+
braceDepth--;
|
|
37701
|
+
continue;
|
|
37702
|
+
}
|
|
37703
|
+
if (braceDepth !== 0) continue;
|
|
37704
|
+
if (character === "<") {
|
|
37705
|
+
const followCharacter = currentLine[charIndex + 1];
|
|
37706
|
+
if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
|
|
37707
|
+
continue;
|
|
37708
|
+
}
|
|
37709
|
+
if (character !== ">") continue;
|
|
37710
|
+
const previousCharacter = currentLine[charIndex - 1];
|
|
37711
|
+
const nextCharacter = currentLine[charIndex + 1];
|
|
37712
|
+
if (previousCharacter === "=" || nextCharacter === "=") continue;
|
|
37713
|
+
if (innerAngleDepth > 0) {
|
|
37714
|
+
innerAngleDepth--;
|
|
37715
|
+
continue;
|
|
37716
|
+
}
|
|
37717
|
+
return lineIndex;
|
|
37718
|
+
}
|
|
37719
|
+
}
|
|
37720
|
+
return null;
|
|
37721
|
+
};
|
|
37722
|
+
const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
|
|
37723
|
+
for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
|
|
37724
|
+
const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
|
|
37725
|
+
if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
|
|
37726
|
+
}
|
|
37727
|
+
return null;
|
|
37728
|
+
};
|
|
37729
|
+
const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
|
|
37730
|
+
const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
|
|
37731
|
+
const collected = [];
|
|
37732
|
+
let isStillInChain = true;
|
|
37733
|
+
for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
|
|
37734
|
+
const candidateLine = lines[candidateIndex];
|
|
37735
|
+
if (candidateLine === void 0) break;
|
|
37736
|
+
const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
|
|
37737
|
+
if (match) {
|
|
37738
|
+
collected.push({
|
|
37739
|
+
commentLineIndex: candidateIndex,
|
|
37740
|
+
ruleList: match[1],
|
|
37741
|
+
isInChain: isStillInChain
|
|
37742
|
+
});
|
|
37743
|
+
continue;
|
|
37744
|
+
}
|
|
37745
|
+
isStillInChain = false;
|
|
37746
|
+
}
|
|
37747
|
+
return collected;
|
|
37748
|
+
};
|
|
37749
|
+
const isRuleListedInComment = (ruleList, ruleId) => {
|
|
37750
|
+
const tokens = tokenizeRuleList(ruleList);
|
|
37751
|
+
if (tokens.length === 0) return true;
|
|
37752
|
+
return tokens.some((token) => isSameRuleKey(token, ruleId));
|
|
37690
37753
|
};
|
|
37691
37754
|
const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
|
|
37692
37755
|
const formatLineGap = (gapLineCount) => `${gapLineCount} line${gapLineCount === 1 ? "" : "s"}`;
|
|
@@ -37730,7 +37793,7 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
|
|
|
37730
37793
|
};
|
|
37731
37794
|
return {
|
|
37732
37795
|
isSuppressed: false,
|
|
37733
|
-
nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId)
|
|
37796
|
+
nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId) ?? detectForeignDisableNearMiss(lines, diagnosticLineIndex, ruleId)
|
|
37734
37797
|
};
|
|
37735
37798
|
};
|
|
37736
37799
|
/**
|
|
@@ -38520,7 +38583,6 @@ const PACKAGE_JSON_FILENAME = "package.json";
|
|
|
38520
38583
|
const PACKAGE_JSON_CONFIG_KEY$1 = "reactDoctor";
|
|
38521
38584
|
const LEGACY_CONFIG_FILENAME = "react-doctor.config.json";
|
|
38522
38585
|
const jiti = createJiti(import.meta.url);
|
|
38523
|
-
const formatError = (error) => error instanceof Error ? error.message : String(error);
|
|
38524
38586
|
const importDefaultExport = async (jitiInstance, filePath) => {
|
|
38525
38587
|
const imported = await jitiInstance.import(filePath);
|
|
38526
38588
|
return imported?.default ?? imported;
|
|
@@ -38552,7 +38614,7 @@ const loadModuleConfig = async (filePath) => {
|
|
|
38552
38614
|
try {
|
|
38553
38615
|
return await importDefaultExport(aliasJiti, filePath);
|
|
38554
38616
|
} catch (retryError) {
|
|
38555
|
-
throw new Error(`${
|
|
38617
|
+
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
38618
|
}
|
|
38557
38619
|
}
|
|
38558
38620
|
};
|
|
@@ -38601,7 +38663,7 @@ const loadLegacyConfig = (directory) => {
|
|
|
38601
38663
|
}
|
|
38602
38664
|
warn(`${LEGACY_CONFIG_FILENAME} must contain an object, ignoring.`);
|
|
38603
38665
|
} catch (error) {
|
|
38604
|
-
warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${
|
|
38666
|
+
warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${messageFromUnknown(error)}`);
|
|
38605
38667
|
}
|
|
38606
38668
|
return {
|
|
38607
38669
|
status: "invalid",
|
|
@@ -38628,7 +38690,7 @@ const loadConfigFromDirectory = async (directory) => {
|
|
|
38628
38690
|
warn(`${CONFIG_BASENAME}.${extension} must export an object, ignoring.`);
|
|
38629
38691
|
sawBrokenConfigFile = true;
|
|
38630
38692
|
} catch (error) {
|
|
38631
|
-
warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${
|
|
38693
|
+
warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${messageFromUnknown(error)}`);
|
|
38632
38694
|
sawBrokenConfigFile = true;
|
|
38633
38695
|
}
|
|
38634
38696
|
}
|
|
@@ -39692,15 +39754,10 @@ const buildCapabilities = (project) => {
|
|
|
39692
39754
|
}
|
|
39693
39755
|
if (project.tailwindVersion !== null) {
|
|
39694
39756
|
capabilities.add("tailwind");
|
|
39695
|
-
|
|
39696
|
-
if (isTailwindAtLeast(tailwind, {
|
|
39757
|
+
if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
|
|
39697
39758
|
major: 3,
|
|
39698
39759
|
minor: 4
|
|
39699
39760
|
})) capabilities.add("tailwind:3.4");
|
|
39700
|
-
if (tailwind !== null && isTailwindAtLeast(tailwind, {
|
|
39701
|
-
major: 4,
|
|
39702
|
-
minor: 0
|
|
39703
|
-
})) capabilities.add("tailwind:4");
|
|
39704
39761
|
}
|
|
39705
39762
|
if (project.zodVersion !== null) {
|
|
39706
39763
|
capabilities.add("zod");
|
|
@@ -39908,7 +39965,7 @@ const readIgnoreFile = (filePath) => {
|
|
|
39908
39965
|
try {
|
|
39909
39966
|
content = NFS.readFileSync(filePath, "utf-8");
|
|
39910
39967
|
} catch (error) {
|
|
39911
|
-
const errnoCode = error
|
|
39968
|
+
const errnoCode = isErrnoException(error) ? error.code : void 0;
|
|
39912
39969
|
if (errnoCode && errnoCode !== "ENOENT") runSync(warn$1(`Could not read ignore file ${filePath}: ${errnoCode}`));
|
|
39913
39970
|
return [];
|
|
39914
39971
|
}
|
|
@@ -39946,8 +40003,8 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
39946
40003
|
cachedPatternsByRoot.set(rootDirectory, patterns);
|
|
39947
40004
|
return patterns;
|
|
39948
40005
|
};
|
|
40006
|
+
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39949
40007
|
const KNIP_JSON_FILENAME = "knip.json";
|
|
39950
|
-
const isRecord$1$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39951
40008
|
const readJsonFileSafe = (filePath) => {
|
|
39952
40009
|
let rawContents;
|
|
39953
40010
|
try {
|
|
@@ -39963,10 +40020,10 @@ const readJsonFileSafe = (filePath) => {
|
|
|
39963
40020
|
};
|
|
39964
40021
|
const readKnipConfig = (rootDirectory) => {
|
|
39965
40022
|
const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
|
|
39966
|
-
if (isRecord$
|
|
40023
|
+
if (isRecord$2(knipJson)) return knipJson;
|
|
39967
40024
|
const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
|
|
39968
|
-
const packageKnipConfig = isRecord$
|
|
39969
|
-
return isRecord$
|
|
40025
|
+
const packageKnipConfig = isRecord$2(packageJson) ? packageJson.knip : null;
|
|
40026
|
+
return isRecord$2(packageKnipConfig) ? packageKnipConfig : null;
|
|
39970
40027
|
};
|
|
39971
40028
|
const normalizePatternList = (value) => {
|
|
39972
40029
|
if (typeof value === "string" && value.length > 0) return [value];
|
|
@@ -39978,10 +40035,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
|
|
|
39978
40035
|
return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
|
|
39979
40036
|
};
|
|
39980
40037
|
const collectKnipWorkspacePatterns = (workspaces, settingName) => {
|
|
39981
|
-
if (!isRecord$
|
|
40038
|
+
if (!isRecord$2(workspaces)) return [];
|
|
39982
40039
|
const patterns = [];
|
|
39983
40040
|
for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
|
|
39984
|
-
if (!isRecord$
|
|
40041
|
+
if (!isRecord$2(workspaceConfig)) continue;
|
|
39985
40042
|
patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
|
|
39986
40043
|
}
|
|
39987
40044
|
return patterns;
|
|
@@ -40026,8 +40083,6 @@ const toCanonicalPath = (filePath) => {
|
|
|
40026
40083
|
};
|
|
40027
40084
|
const DEAD_CODE_PLUGIN = "deslop";
|
|
40028
40085
|
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
40086
|
const DEAD_CODE_WORKER_SCRIPT = `
|
|
40032
40087
|
const inputChunks = [];
|
|
40033
40088
|
process.stdin.on("data", (chunk) => inputChunks.push(chunk));
|
|
@@ -40085,7 +40140,7 @@ process.stdin.on("end", () => {
|
|
|
40085
40140
|
});
|
|
40086
40141
|
`;
|
|
40087
40142
|
const resolveTsConfigPath = (rootDirectory) => {
|
|
40088
|
-
for (const filename of TSCONFIG_FILENAMES
|
|
40143
|
+
for (const filename of TSCONFIG_FILENAMES) {
|
|
40089
40144
|
const candidate = Path.join(rootDirectory, filename);
|
|
40090
40145
|
if (NFS.existsSync(candidate)) return candidate;
|
|
40091
40146
|
}
|
|
@@ -40466,15 +40521,13 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
|
|
|
40466
40521
|
})()) }));
|
|
40467
40522
|
static layerOf = (diagnostics) => succeed$3(DeadCode, DeadCode.of({ run: () => fromIterable$1(diagnostics) }));
|
|
40468
40523
|
};
|
|
40469
|
-
const createNodeReadFileLinesSync = (rootDirectory) => {
|
|
40470
|
-
|
|
40471
|
-
|
|
40472
|
-
|
|
40473
|
-
|
|
40474
|
-
|
|
40475
|
-
|
|
40476
|
-
}
|
|
40477
|
-
};
|
|
40524
|
+
const createNodeReadFileLinesSync = (rootDirectory) => (filePath) => {
|
|
40525
|
+
const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
|
|
40526
|
+
try {
|
|
40527
|
+
return NFS.readFileSync(absolutePath, "utf-8").split("\n");
|
|
40528
|
+
} catch {
|
|
40529
|
+
return null;
|
|
40530
|
+
}
|
|
40478
40531
|
};
|
|
40479
40532
|
var Files = class Files extends Service()("react-doctor/Files") {
|
|
40480
40533
|
static layerNode = succeed$3(Files, Files.of({
|
|
@@ -40685,7 +40738,10 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40685
40738
|
directory: input.directory,
|
|
40686
40739
|
cause
|
|
40687
40740
|
}) });
|
|
40688
|
-
})
|
|
40741
|
+
}), withSpan("git.exec", { attributes: {
|
|
40742
|
+
"git.command": input.command,
|
|
40743
|
+
"git.subcommand": input.args[0] ?? ""
|
|
40744
|
+
} }));
|
|
40689
40745
|
const runGit = (directory, args) => runCommand({
|
|
40690
40746
|
command: "git",
|
|
40691
40747
|
args,
|
|
@@ -40713,7 +40769,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40713
40769
|
]);
|
|
40714
40770
|
if (candidates.status !== 0) return null;
|
|
40715
40771
|
return trimOrNull(candidates.stdout.split("\n")[0] ?? "");
|
|
40716
|
-
});
|
|
40772
|
+
}).pipe(withSpan("Git.defaultBranch"));
|
|
40717
40773
|
const branchExists = (directory, branch) => runGit(directory, [
|
|
40718
40774
|
"rev-parse",
|
|
40719
40775
|
"--verify",
|
|
@@ -40760,7 +40816,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40760
40816
|
const result = resultOption.value;
|
|
40761
40817
|
if (result.status !== 0) return null;
|
|
40762
40818
|
return parseGithubViewerPermission(result.stdout);
|
|
40763
|
-
}).pipe(catch_$1(() => succeed$2(null)));
|
|
40819
|
+
}).pipe(catch_$1(() => succeed$2(null)), withSpan("Git.githubViewerPermission"));
|
|
40764
40820
|
/**
|
|
40765
40821
|
* Resolves a `--diff A..B` / `A...B` commit range into a changed-file
|
|
40766
40822
|
* selection. Each endpoint is validated with `isSafeGitRevision`
|
|
@@ -40874,7 +40930,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40874
40930
|
changedFiles: splitNullSeparated(diff.stdout),
|
|
40875
40931
|
isCurrentChanges: false
|
|
40876
40932
|
};
|
|
40877
|
-
}),
|
|
40933
|
+
}).pipe(withSpan("Git.diffSelection")),
|
|
40878
40934
|
stagedFilePaths: (directory) => runGit(directory, [
|
|
40879
40935
|
"diff",
|
|
40880
40936
|
"--cached",
|
|
@@ -40916,7 +40972,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40916
40972
|
status: result.status,
|
|
40917
40973
|
stdout: result.stdout
|
|
40918
40974
|
};
|
|
40919
|
-
}),
|
|
40975
|
+
}).pipe(withSpan("Git.grep")),
|
|
40920
40976
|
changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
|
|
40921
40977
|
if (files.length === 0) return [];
|
|
40922
40978
|
if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
|
|
@@ -40932,7 +40988,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40932
40988
|
]);
|
|
40933
40989
|
if (result.status !== 0) return null;
|
|
40934
40990
|
return parseChangedLineRanges(result.stdout);
|
|
40935
|
-
})
|
|
40991
|
+
}).pipe(withSpan("Git.changedLineRanges"))
|
|
40936
40992
|
});
|
|
40937
40993
|
})).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
|
|
40938
40994
|
/**
|
|
@@ -41147,7 +41203,7 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
|
|
|
41147
41203
|
for (const [absolutePath, originalContent] of originalContents) try {
|
|
41148
41204
|
NFS.writeFileSync(absolutePath, originalContent);
|
|
41149
41205
|
} catch (error) {
|
|
41150
|
-
process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${
|
|
41206
|
+
process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${messageFromUnknown(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
|
|
41151
41207
|
}
|
|
41152
41208
|
};
|
|
41153
41209
|
const onExit = () => restore();
|
|
@@ -41253,7 +41309,7 @@ const resolveUserPlugin = (spec, configSourceDirectory) => {
|
|
|
41253
41309
|
try {
|
|
41254
41310
|
resolvedSpecifier = isRelative ? Path.resolve(configSourceDirectory, spec) : candidateRequire.resolve(spec);
|
|
41255
41311
|
} catch (error) {
|
|
41256
|
-
warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${
|
|
41312
|
+
warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${messageFromUnknown(error)}`);
|
|
41257
41313
|
return null;
|
|
41258
41314
|
}
|
|
41259
41315
|
const { name, ruleNames } = readPluginShape(resolvedSpecifier, (target) => candidateRequire(target));
|
|
@@ -41386,7 +41442,6 @@ const resolveOxlintBinary = () => {
|
|
|
41386
41442
|
return Path.join(oxlintPackageDirectory, "bin", "oxlint");
|
|
41387
41443
|
};
|
|
41388
41444
|
const resolvePluginPath = () => esmRequire.resolve("oxlint-plugin-react-doctor");
|
|
41389
|
-
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
41390
41445
|
const resolveTsConfigRelativePath = (rootDirectory) => {
|
|
41391
41446
|
for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
|
|
41392
41447
|
return null;
|
|
@@ -41758,7 +41813,7 @@ const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
|
|
|
41758
41813
|
const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
|
|
41759
41814
|
let currentNode = identifier.parent;
|
|
41760
41815
|
while (currentNode) {
|
|
41761
|
-
if (
|
|
41816
|
+
if (isScopeBoundary(currentNode)) {
|
|
41762
41817
|
if (scopeContainsNonImportBinding(currentNode, currentNode, identifier.text)) return true;
|
|
41763
41818
|
}
|
|
41764
41819
|
if (currentNode === sourceFile) return false;
|
|
@@ -41849,11 +41904,10 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
|
|
|
41849
41904
|
});
|
|
41850
41905
|
return resolution;
|
|
41851
41906
|
};
|
|
41852
|
-
const isScopeNode = isScopeBoundary;
|
|
41853
41907
|
const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, visitedDeclarations = /* @__PURE__ */ new Set()) => {
|
|
41854
41908
|
let currentNode = identifier.parent;
|
|
41855
41909
|
while (currentNode) {
|
|
41856
|
-
if (
|
|
41910
|
+
if (isScopeBoundary(currentNode)) {
|
|
41857
41911
|
const resolution = findResolutionInScope(currentNode, identifier.text, reactImportBindings, sourceFile, visitedDeclarations);
|
|
41858
41912
|
if (resolution) return resolution;
|
|
41859
41913
|
}
|
|
@@ -42101,7 +42155,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
42101
42155
|
child.kill("SIGKILL");
|
|
42102
42156
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
42103
42157
|
kind: "timeout",
|
|
42104
|
-
detail: `${spawnTimeoutMs /
|
|
42158
|
+
detail: `${spawnTimeoutMs / MILLISECONDS_PER_SECOND}s budget exceeded`
|
|
42105
42159
|
}) }));
|
|
42106
42160
|
}, spawnTimeoutMs);
|
|
42107
42161
|
timeoutHandle.unref?.();
|
|
@@ -43223,7 +43277,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43223
43277
|
}))))))));
|
|
43224
43278
|
const deadCodeFailureState = yield* get$2(deadCodeFailure);
|
|
43225
43279
|
const scanElapsedMilliseconds = Date.now() - scanStartTime;
|
|
43226
|
-
const scanElapsedSeconds = (scanElapsedMilliseconds /
|
|
43280
|
+
const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
|
|
43227
43281
|
if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
|
|
43228
43282
|
else if (input.suppressScanSummary) yield* scanProgress.stop();
|
|
43229
43283
|
else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
|
|
@@ -43460,7 +43514,7 @@ var StagedFiles = class StagedFiles extends Service()("react-doctor/StagedFiles"
|
|
|
43460
43514
|
static layerNode = effect(StagedFiles, gen(function* () {
|
|
43461
43515
|
const git = yield* Git;
|
|
43462
43516
|
return StagedFiles.of({
|
|
43463
|
-
discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile))),
|
|
43517
|
+
discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile)), withSpan("StagedFiles.discoverSourceFiles")),
|
|
43464
43518
|
materialize: ({ directory, stagedFiles, tempDirectory }) => materializeSourceTree({
|
|
43465
43519
|
directory,
|
|
43466
43520
|
files: stagedFiles,
|
|
@@ -43470,7 +43524,7 @@ var StagedFiles = class StagedFiles extends Service()("react-doctor/StagedFiles"
|
|
|
43470
43524
|
tempDirectory: tree.tempDirectory,
|
|
43471
43525
|
stagedFiles: tree.materializedFiles,
|
|
43472
43526
|
cleanup: tree.cleanup
|
|
43473
|
-
})))
|
|
43527
|
+
})), withSpan("StagedFiles.materialize"))
|
|
43474
43528
|
});
|
|
43475
43529
|
}));
|
|
43476
43530
|
/**
|
|
@@ -43603,6 +43657,7 @@ const buildJsonReport = (input) => {
|
|
|
43603
43657
|
score: result.score,
|
|
43604
43658
|
skippedChecks: result.skippedChecks,
|
|
43605
43659
|
...result.skippedCheckReasons ? { skippedCheckReasons: result.skippedCheckReasons } : {},
|
|
43660
|
+
...typeof result.scannedFileCount === "number" ? { scannedFileCount: result.scannedFileCount } : {},
|
|
43606
43661
|
elapsedMilliseconds: result.elapsedMilliseconds
|
|
43607
43662
|
}));
|
|
43608
43663
|
const flattenedDiagnostics = projects.flatMap((entry) => entry.diagnostics);
|
|
@@ -43877,7 +43932,7 @@ const FALSY_CI_FLAG_VALUES = new Set([
|
|
|
43877
43932
|
"false"
|
|
43878
43933
|
]);
|
|
43879
43934
|
const isCiFlagSet = (value) => value !== void 0 && !FALSY_CI_FLAG_VALUES.has(value.toLowerCase());
|
|
43880
|
-
const isCiEnvironment = () => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(
|
|
43935
|
+
const isCiEnvironment = (env = process.env) => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(env[environmentVariable])) || isCiFlagSet(env.CI);
|
|
43881
43936
|
const detectCiProvider = () => {
|
|
43882
43937
|
for (const [environmentVariable, provider] of CI_PROVIDER_BY_ENVIRONMENT_VARIABLE) if (process.env[environmentVariable]) return provider;
|
|
43883
43938
|
return isCiFlagSet(process.env.CI) ? "unknown" : null;
|
|
@@ -43902,6 +43957,53 @@ const detectCodingAgent = () => {
|
|
|
43902
43957
|
const isCodingAgentEnvironment = () => detectCodingAgent() !== null;
|
|
43903
43958
|
const isCiOrCodingAgentEnvironment = () => isCiEnvironment() || isCodingAgentEnvironment();
|
|
43904
43959
|
//#endregion
|
|
43960
|
+
//#region src/cli/utils/detect-terminal-kind.ts
|
|
43961
|
+
const TERMINAL_BY_TERM_PROGRAM = [
|
|
43962
|
+
["vscode", "vscode"],
|
|
43963
|
+
["iTerm.app", "iterm"],
|
|
43964
|
+
["Apple_Terminal", "apple-terminal"],
|
|
43965
|
+
["WezTerm", "wezterm"],
|
|
43966
|
+
["ghostty", "ghostty"],
|
|
43967
|
+
["Hyper", "hyper"],
|
|
43968
|
+
["Tabby", "tabby"],
|
|
43969
|
+
["rio", "rio"]
|
|
43970
|
+
];
|
|
43971
|
+
/**
|
|
43972
|
+
* Best-effort label for the terminal emulator / editor hosting the CLI,
|
|
43973
|
+
* derived from terminal-identity env vars. Recorded as the `terminalKind` run
|
|
43974
|
+
* tag so we can see where React Doctor is actually run (nvim, VS Code, iTerm,
|
|
43975
|
+
* …) — the split Sentry can't otherwise see. Low-cardinality and free of any
|
|
43976
|
+
* username/path/secret, so it's safe as a tag. Editor terminals (nvim/vim)
|
|
43977
|
+
* win over the outer emulator because that's the surface a user is reading in;
|
|
43978
|
+
* "ci" marks a run with no interactive terminal; "unknown" when nothing matches.
|
|
43979
|
+
*/
|
|
43980
|
+
const detectTerminalKind = (env = process.env) => {
|
|
43981
|
+
if (env.NVIM) return "neovim";
|
|
43982
|
+
if (env.VIM_TERMINAL) return "vim";
|
|
43983
|
+
const termProgram = env.TERM_PROGRAM;
|
|
43984
|
+
if (termProgram) {
|
|
43985
|
+
for (const [marker, label] of TERMINAL_BY_TERM_PROGRAM) if (termProgram === marker) return label;
|
|
43986
|
+
}
|
|
43987
|
+
if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return "kitty";
|
|
43988
|
+
if (env.WT_SESSION) return "windows-terminal";
|
|
43989
|
+
if (env.ALACRITTY_WINDOW_ID || env.TERM === "alacritty") return "alacritty";
|
|
43990
|
+
if (env.VTE_VERSION) return "vte";
|
|
43991
|
+
if (env.TMUX) return "tmux";
|
|
43992
|
+
if (isCiEnvironment(env)) return "ci";
|
|
43993
|
+
return "unknown";
|
|
43994
|
+
};
|
|
43995
|
+
//#endregion
|
|
43996
|
+
//#region src/cli/utils/is-debug-flag.ts
|
|
43997
|
+
/**
|
|
43998
|
+
* Whether the user passed `--debug` (surface the run's Sentry trace id, and
|
|
43999
|
+
* force performance tracing on so there's a trace to surface). Read straight
|
|
44000
|
+
* from argv rather than Commander's parsed flags because `initializeSentry()`
|
|
44001
|
+
* runs before Commander parses — the same reason `shouldEnableSentry()` reads
|
|
44002
|
+
* `--no-score` from argv. Sharing this one reader keeps the init-time sampling
|
|
44003
|
+
* override and the end-of-run print in agreement.
|
|
44004
|
+
*/
|
|
44005
|
+
const isDebugFlagEnabled = (argv = process.argv) => argv.includes("--debug");
|
|
44006
|
+
//#endregion
|
|
43905
44007
|
//#region src/cli/utils/is-git-hook-environment.ts
|
|
43906
44008
|
const isGitHookEnvironment = () => Boolean(process.env.GIT_DIR);
|
|
43907
44009
|
//#endregion
|
|
@@ -43924,6 +44026,7 @@ const NON_INTERACTIVE_ENVIRONMENT_VARIABLES = [
|
|
|
43924
44026
|
const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || isCodingAgentEnvironment();
|
|
43925
44027
|
//#endregion
|
|
43926
44028
|
//#region src/cli/utils/constants.ts
|
|
44029
|
+
const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
|
|
43927
44030
|
const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
|
|
43928
44031
|
const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
|
|
43929
44032
|
const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
|
|
@@ -44008,7 +44111,7 @@ const makeNoopConsole = () => ({
|
|
|
44008
44111
|
});
|
|
44009
44112
|
//#endregion
|
|
44010
44113
|
//#region src/cli/utils/version.ts
|
|
44011
|
-
const VERSION = "0.5.6-dev.
|
|
44114
|
+
const VERSION = "0.5.6-dev.44db3e0";
|
|
44012
44115
|
//#endregion
|
|
44013
44116
|
//#region src/cli/utils/json-mode.ts
|
|
44014
44117
|
let context = null;
|
|
@@ -44158,7 +44261,9 @@ const buildRunContext = () => {
|
|
|
44158
44261
|
viaAction: isOfficialGithubAction(),
|
|
44159
44262
|
codingAgent: detectCodingAgent(),
|
|
44160
44263
|
interactive: !isNonInteractiveEnvironment(),
|
|
44264
|
+
terminalKind: detectTerminalKind(),
|
|
44161
44265
|
jsonMode: isJsonModeActive(),
|
|
44266
|
+
debug: isDebugFlagEnabled(),
|
|
44162
44267
|
invokedVia: detectInvokedVia()
|
|
44163
44268
|
};
|
|
44164
44269
|
};
|
|
@@ -44228,7 +44333,9 @@ const buildSentryScope = (runContext = buildRunContext()) => {
|
|
|
44228
44333
|
viaAction: runContext.viaAction,
|
|
44229
44334
|
codingAgent: runContext.codingAgent,
|
|
44230
44335
|
interactive: runContext.interactive,
|
|
44336
|
+
terminalKind: runContext.terminalKind,
|
|
44231
44337
|
jsonMode: runContext.jsonMode,
|
|
44338
|
+
debug: runContext.debug,
|
|
44232
44339
|
invokedVia: runContext.invokedVia,
|
|
44233
44340
|
nodeMajor: runContext.nodeMajor
|
|
44234
44341
|
};
|
|
@@ -44366,13 +44473,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
44366
44473
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
44367
44474
|
* standard `SENTRY_RELEASE` override.
|
|
44368
44475
|
*/
|
|
44369
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.
|
|
44476
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.44db3e0`;
|
|
44370
44477
|
/**
|
|
44371
44478
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
44372
44479
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
44373
44480
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
44374
44481
|
*/
|
|
44375
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.
|
|
44482
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.44db3e0") ? "development" : "production");
|
|
44376
44483
|
/**
|
|
44377
44484
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
44378
44485
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -44436,7 +44543,7 @@ const flushSentry = async () => {
|
|
|
44436
44543
|
const initializeSentry = () => {
|
|
44437
44544
|
if (isInitialized || !shouldEnableSentry()) return;
|
|
44438
44545
|
isInitialized = true;
|
|
44439
|
-
resolvedTracesSampleRate = resolveTracesSampleRate();
|
|
44546
|
+
resolvedTracesSampleRate = isDebugFlagEnabled() ? 1 : resolveTracesSampleRate();
|
|
44440
44547
|
const { tags, contexts } = buildSentryScope();
|
|
44441
44548
|
Sentry.init({
|
|
44442
44549
|
dsn: process.env.SENTRY_DSN || "https://f253d570240a59b8dbd77b7a548ef133@o4510226365743104.ingest.us.sentry.io/4511487817809920",
|
|
@@ -47652,6 +47759,11 @@ const setActiveRunTrace = (trace) => {
|
|
|
47652
47759
|
activeRunTrace = trace;
|
|
47653
47760
|
};
|
|
47654
47761
|
const getActiveRunTrace = () => activeRunTrace;
|
|
47762
|
+
let lastRunTraceId = null;
|
|
47763
|
+
const recordRunTraceId = (traceId) => {
|
|
47764
|
+
lastRunTraceId = traceId;
|
|
47765
|
+
};
|
|
47766
|
+
const getLastRunTraceId = () => lastRunTraceId;
|
|
47655
47767
|
//#endregion
|
|
47656
47768
|
//#region src/cli/utils/to-span-attributes.ts
|
|
47657
47769
|
/**
|
|
@@ -47714,14 +47826,13 @@ const withSentryRunSpan = (run, options = {}) => {
|
|
|
47714
47826
|
op: "cli.inspect",
|
|
47715
47827
|
attributes: toSpanAttributes(tags)
|
|
47716
47828
|
}, (rootSpan) => {
|
|
47717
|
-
|
|
47718
|
-
|
|
47719
|
-
|
|
47720
|
-
|
|
47721
|
-
|
|
47722
|
-
|
|
47723
|
-
|
|
47724
|
-
}
|
|
47829
|
+
const spanContext = rootSpan.spanContext();
|
|
47830
|
+
recordRunTraceId(spanContext.traceId);
|
|
47831
|
+
if (options.concurrentScan !== true) setActiveRunTrace({
|
|
47832
|
+
traceId: spanContext.traceId,
|
|
47833
|
+
spanId: spanContext.spanId,
|
|
47834
|
+
sampled: (spanContext.traceFlags & 1) === 1
|
|
47835
|
+
});
|
|
47725
47836
|
return run(rootSpan);
|
|
47726
47837
|
});
|
|
47727
47838
|
};
|
|
@@ -48229,6 +48340,15 @@ const boxText = (content, innerWidth) => {
|
|
|
48229
48340
|
].join("\n");
|
|
48230
48341
|
};
|
|
48231
48342
|
//#endregion
|
|
48343
|
+
//#region src/cli/utils/resolve-absolute-path.ts
|
|
48344
|
+
/**
|
|
48345
|
+
* Resolves a diagnostic's `filePath` (relative to its project root, or
|
|
48346
|
+
* already absolute) to an absolute path. Shared by the code-frame reader and
|
|
48347
|
+
* the terminal hyperlink builder so both turn a relative path into the same
|
|
48348
|
+
* on-disk location.
|
|
48349
|
+
*/
|
|
48350
|
+
const resolveAbsolutePath = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
|
|
48351
|
+
//#endregion
|
|
48232
48352
|
//#region src/cli/utils/build-code-frame.ts
|
|
48233
48353
|
/**
|
|
48234
48354
|
* Renders a syntax-highlighted source excerpt around a diagnostic site
|
|
@@ -48239,7 +48359,7 @@ const boxText = (content, innerWidth) => {
|
|
|
48239
48359
|
*/
|
|
48240
48360
|
const buildCodeFrame = (input) => {
|
|
48241
48361
|
if (input.line <= 0) return null;
|
|
48242
|
-
const absolutePath =
|
|
48362
|
+
const absolutePath = resolveAbsolutePath(input.filePath, input.rootDirectory);
|
|
48243
48363
|
let source;
|
|
48244
48364
|
try {
|
|
48245
48365
|
source = NFS.readFileSync(absolutePath, "utf8");
|
|
@@ -48279,6 +48399,16 @@ const resolveMeasureWidth = (reservedColumns = 0) => resolveClampedWidth({
|
|
|
48279
48399
|
const DIVIDER_INDENT = " ";
|
|
48280
48400
|
const buildSectionDivider = () => highlighter.dim(`${DIVIDER_INDENT}${"─".repeat(resolveMeasureWidth(2))}`);
|
|
48281
48401
|
//#endregion
|
|
48402
|
+
//#region src/cli/utils/format-hyperlink.ts
|
|
48403
|
+
const OSC = "\x1B]";
|
|
48404
|
+
const ST = "\x1B\\";
|
|
48405
|
+
/**
|
|
48406
|
+
* Wraps `text` in an OSC 8 hyperlink pointing at `uri`. The visible characters
|
|
48407
|
+
* are exactly `text`; the link is carried in escape sequences a capable
|
|
48408
|
+
* terminal turns into a click target.
|
|
48409
|
+
*/
|
|
48410
|
+
const formatHyperlink = (text, uri) => `${OSC}8;;${uri}${ST}${text}${OSC}8;;${ST}`;
|
|
48411
|
+
//#endregion
|
|
48282
48412
|
//#region src/cli/utils/indent-multiline-text.ts
|
|
48283
48413
|
const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
|
|
48284
48414
|
//#endregion
|
|
@@ -48432,17 +48562,23 @@ const clusterNearbyDiagnostics = (diagnostics) => {
|
|
|
48432
48562
|
}
|
|
48433
48563
|
return clusters;
|
|
48434
48564
|
};
|
|
48435
|
-
const
|
|
48565
|
+
const formatClusterLocationText = (cluster) => {
|
|
48566
|
+
const { filePath } = cluster.diagnostics[0];
|
|
48567
|
+
if (cluster.startLine <= 0) return filePath;
|
|
48568
|
+
if (cluster.endLine > cluster.startLine) return `${filePath}:${cluster.startLine}-${cluster.endLine}`;
|
|
48569
|
+
return `${filePath}:${cluster.startLine}`;
|
|
48570
|
+
};
|
|
48571
|
+
const formatClusterLocation = (cluster, resolveSourceRoot, hyperlinks) => {
|
|
48436
48572
|
const lead = cluster.diagnostics[0];
|
|
48437
48573
|
const contextTag = formatFileContextTag(lead);
|
|
48438
|
-
|
|
48439
|
-
if (
|
|
48440
|
-
return `${lead.filePath
|
|
48574
|
+
const location = formatClusterLocationText(cluster);
|
|
48575
|
+
if (!hyperlinks) return `${location}${contextTag}`;
|
|
48576
|
+
return `${formatHyperlink(location, pathToFileURL(resolveAbsolutePath(lead.filePath, resolveSourceRoot(lead))).href)}${contextTag}`;
|
|
48441
48577
|
};
|
|
48442
|
-
const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame) => {
|
|
48578
|
+
const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame, hyperlinks) => {
|
|
48443
48579
|
const lead = cluster.diagnostics[0];
|
|
48444
48580
|
const isMultiSite = cluster.diagnostics.length > 1;
|
|
48445
|
-
const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster)}`)];
|
|
48581
|
+
const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster, resolveSourceRoot, hyperlinks)}`)];
|
|
48446
48582
|
const codeFrame = renderCodeFrame ? buildCodeFrame({
|
|
48447
48583
|
filePath: lead.filePath,
|
|
48448
48584
|
line: cluster.startLine,
|
|
@@ -48461,7 +48597,7 @@ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame
|
|
|
48461
48597
|
}
|
|
48462
48598
|
return lines;
|
|
48463
48599
|
};
|
|
48464
|
-
const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment) => {
|
|
48600
|
+
const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment, hyperlinks) => {
|
|
48465
48601
|
const representative = pickRepresentativeDiagnostic(ruleDiagnostics);
|
|
48466
48602
|
const { severity } = representative;
|
|
48467
48603
|
const trailingBadge = formatTrailingSiteBadge(ruleDiagnostics.length);
|
|
@@ -48481,7 +48617,7 @@ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, rende
|
|
|
48481
48617
|
}
|
|
48482
48618
|
const renderCodeFrame = severity === "error";
|
|
48483
48619
|
const sites = renderEverySite ? ruleDiagnostics : [representative];
|
|
48484
|
-
if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame));
|
|
48620
|
+
if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame, hyperlinks));
|
|
48485
48621
|
return lines;
|
|
48486
48622
|
};
|
|
48487
48623
|
const selectErrorRuleGroups = (diagnostics, rulePriority) => buildSortedRuleGroups(diagnostics.filter((diagnostic) => diagnostic.severity === "error"), rulePriority);
|
|
@@ -48494,7 +48630,7 @@ const buildOverflowSummaryLine = (diagnostics, rulePriority) => {
|
|
|
48494
48630
|
return ` ${highlighter.dim("Run")} ${command} ${highlighter.dim("to list every error and warning")}`;
|
|
48495
48631
|
};
|
|
48496
48632
|
const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(selectTopErrorRuleGroups(diagnostics, limit, rulePriority).map(([ruleKey]) => ruleKey));
|
|
48497
|
-
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) => {
|
|
48633
|
+
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, hyperlinks, rulePriority) => {
|
|
48498
48634
|
const topRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority).slice(0, 3);
|
|
48499
48635
|
if (topRuleGroups.length === 0) return {
|
|
48500
48636
|
lines: [],
|
|
@@ -48504,7 +48640,7 @@ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) =>
|
|
|
48504
48640
|
const blockOffsets = [];
|
|
48505
48641
|
for (const [ruleKey, ruleDiagnostics] of topRuleGroups) {
|
|
48506
48642
|
blockOffsets.push(lines.length);
|
|
48507
|
-
lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false));
|
|
48643
|
+
lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false, hyperlinks));
|
|
48508
48644
|
lines.push("");
|
|
48509
48645
|
}
|
|
48510
48646
|
return {
|
|
@@ -48542,18 +48678,18 @@ const buildOverviewHeaderLines = (diagnostics) => {
|
|
|
48542
48678
|
* single Effect.forEach over Console.log so failures or fiber
|
|
48543
48679
|
* interruption produce predictable partial output.
|
|
48544
48680
|
*/
|
|
48545
|
-
const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}) => gen(function* () {
|
|
48681
|
+
const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}, hyperlinks = false) => gen(function* () {
|
|
48546
48682
|
const sectionPause = onboarding.sectionPause ?? void_;
|
|
48547
48683
|
const animateCountUp = onboarding.animateCountUp ?? false;
|
|
48548
48684
|
const resolveSourceRoot = typeof sourceRoot === "function" ? sourceRoot : () => sourceRoot;
|
|
48549
48685
|
let detailLines;
|
|
48550
48686
|
let topErrorBlockOffsets = [];
|
|
48551
48687
|
if (!isVerbose) {
|
|
48552
|
-
const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, rulePriority);
|
|
48688
|
+
const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, hyperlinks, rulePriority);
|
|
48553
48689
|
detailLines = topErrors.lines;
|
|
48554
48690
|
topErrorBlockOffsets = topErrors.blockOffsets;
|
|
48555
48691
|
} else detailLines = buildSortedRuleGroups(diagnostics, rulePriority).flatMap(([ruleKey, ruleDiagnostics]) => {
|
|
48556
|
-
return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment), ""];
|
|
48692
|
+
return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment, hyperlinks), ""];
|
|
48557
48693
|
});
|
|
48558
48694
|
const overflowLine = isVerbose ? void 0 : buildOverflowSummaryLine(diagnostics, rulePriority);
|
|
48559
48695
|
const categoryTallies = buildCategoryDiagnosticGroups(diagnostics, rulePriority).map(buildCategoryTally);
|
|
@@ -48614,6 +48750,48 @@ const computeProjectedScore = async (topErrorSource, rescoreSource, currentScore
|
|
|
48614
48750
|
//#endregion
|
|
48615
48751
|
//#region src/cli/utils/filter-diagnostics-by-categories.ts
|
|
48616
48752
|
const filterDiagnosticsByCategories = (diagnostics, categories) => categories.size === 0 ? [...diagnostics] : diagnostics.filter((diagnostic) => categories.has(diagnostic.category));
|
|
48753
|
+
//#endregion
|
|
48754
|
+
//#region src/cli/utils/supports-hyperlinks.ts
|
|
48755
|
+
const HYPERLINK_CAPABLE_TERM_PROGRAMS = new Set([
|
|
48756
|
+
"iTerm.app",
|
|
48757
|
+
"WezTerm",
|
|
48758
|
+
"vscode",
|
|
48759
|
+
"Hyper",
|
|
48760
|
+
"ghostty",
|
|
48761
|
+
"Tabby",
|
|
48762
|
+
"rio"
|
|
48763
|
+
]);
|
|
48764
|
+
const parseVteVersion = (raw) => {
|
|
48765
|
+
const parsed = Number.parseInt(raw ?? "", 10);
|
|
48766
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
48767
|
+
};
|
|
48768
|
+
/**
|
|
48769
|
+
* Whether `stream` is a terminal that renders OSC 8 hyperlinks. Auto-detected
|
|
48770
|
+
* from terminal-identity env vars; the de-facto `FORCE_HYPERLINK` env var
|
|
48771
|
+
* overrides detection (`FORCE_HYPERLINK=0`/`false` forces off, any other value
|
|
48772
|
+
* forces on), mirroring how the ecosystem's terminal libraries gate the same
|
|
48773
|
+
* feature. Off for non-TTYs, `TERM=dumb`, and CI (whose log viewers render the
|
|
48774
|
+
* raw escape rather than a link). Unknown terminals default to off.
|
|
48775
|
+
*/
|
|
48776
|
+
const supportsHyperlinks = (stream = process.stdout, env = process.env) => {
|
|
48777
|
+
const forced = env.FORCE_HYPERLINK;
|
|
48778
|
+
if (forced !== void 0 && forced !== "") return forced !== "0" && forced.toLowerCase() !== "false";
|
|
48779
|
+
if (stream.isTTY !== true) return false;
|
|
48780
|
+
if (env.TERM === "dumb") return false;
|
|
48781
|
+
if (isCiEnvironment(env)) return false;
|
|
48782
|
+
if (env.WT_SESSION) return true;
|
|
48783
|
+
if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return true;
|
|
48784
|
+
if (parseVteVersion(env.VTE_VERSION) >= 5e3) return true;
|
|
48785
|
+
return Boolean(env.TERM_PROGRAM && HYPERLINK_CAPABLE_TERM_PROGRAMS.has(env.TERM_PROGRAM));
|
|
48786
|
+
};
|
|
48787
|
+
//#endregion
|
|
48788
|
+
//#region src/cli/utils/should-render-hyperlinks.ts
|
|
48789
|
+
/**
|
|
48790
|
+
* Whether to emit OSC 8 clickable `file:line` locations for this run: a
|
|
48791
|
+
* hyperlink-capable terminal AND not a coding agent (whose output parsers
|
|
48792
|
+
* would choke on the escape sequences).
|
|
48793
|
+
*/
|
|
48794
|
+
const shouldRenderHyperlinks = (stream = process.stdout) => supportsHyperlinks(stream) && !isCodingAgentEnvironment();
|
|
48617
48795
|
const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
|
|
48618
48796
|
const FALSY_FLAG_VALUES = new Set([
|
|
48619
48797
|
"",
|
|
@@ -48633,10 +48811,9 @@ const canAnimateOnboarding = (stream = process.stdout) => {
|
|
|
48633
48811
|
};
|
|
48634
48812
|
//#endregion
|
|
48635
48813
|
//#region src/cli/utils/onboarding-state.ts
|
|
48636
|
-
const GLOBAL_CONFIG_PROJECT_NAME$2 = "react-doctor";
|
|
48637
48814
|
const ONBOARDED_AT_KEY = "onboardedAt";
|
|
48638
48815
|
const getOnboardingStore = (options = {}) => new Conf({
|
|
48639
|
-
projectName:
|
|
48816
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
48640
48817
|
cwd: options.cwd
|
|
48641
48818
|
});
|
|
48642
48819
|
const hasCompletedOnboarding = (options = {}) => {
|
|
@@ -49092,6 +49269,78 @@ const resolveCliCategories = (categoryFlag) => {
|
|
|
49092
49269
|
return resolvedCategories.length > 0 ? resolvedCategories : void 0;
|
|
49093
49270
|
};
|
|
49094
49271
|
//#endregion
|
|
49272
|
+
//#region src/cli/utils/git-hook-shared.ts
|
|
49273
|
+
const HOOK_FILE_NAME = "pre-commit";
|
|
49274
|
+
const HOOK_RELATIVE_PATH = "hooks/pre-commit";
|
|
49275
|
+
const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
|
|
49276
|
+
const HUSKY_HOOKS_PATH = ".husky";
|
|
49277
|
+
const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
|
|
49278
|
+
const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
|
|
49279
|
+
const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
|
|
49280
|
+
const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
|
|
49281
|
+
const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
|
|
49282
|
+
const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
|
|
49283
|
+
"react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
|
|
49284
|
+
`if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
|
|
49285
|
+
"rm -f \"$react_doctor_output\";",
|
|
49286
|
+
"else",
|
|
49287
|
+
"rm -f \"$react_doctor_output\";",
|
|
49288
|
+
`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;`,
|
|
49289
|
+
"fi"
|
|
49290
|
+
].join(" ");
|
|
49291
|
+
const PACKAGE_JSON_FILE_NAME = "package.json";
|
|
49292
|
+
const runGit = (projectRoot, args) => {
|
|
49293
|
+
try {
|
|
49294
|
+
return execFileSync("git", [...args], {
|
|
49295
|
+
cwd: projectRoot,
|
|
49296
|
+
encoding: "utf8",
|
|
49297
|
+
stdio: [
|
|
49298
|
+
"ignore",
|
|
49299
|
+
"pipe",
|
|
49300
|
+
"ignore"
|
|
49301
|
+
]
|
|
49302
|
+
}).trim();
|
|
49303
|
+
} catch {
|
|
49304
|
+
return null;
|
|
49305
|
+
}
|
|
49306
|
+
};
|
|
49307
|
+
const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
|
|
49308
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49309
|
+
const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
|
|
49310
|
+
const readPackageJson = (projectRoot) => {
|
|
49311
|
+
try {
|
|
49312
|
+
return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
|
|
49313
|
+
} catch {
|
|
49314
|
+
return null;
|
|
49315
|
+
}
|
|
49316
|
+
};
|
|
49317
|
+
const writeJsonFile$1 = (filePath, value) => {
|
|
49318
|
+
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
49319
|
+
};
|
|
49320
|
+
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
49321
|
+
const packageJson = readPackageJson(projectRoot);
|
|
49322
|
+
if (!isRecord$1(packageJson)) return false;
|
|
49323
|
+
return [
|
|
49324
|
+
"dependencies",
|
|
49325
|
+
"devDependencies",
|
|
49326
|
+
"optionalDependencies"
|
|
49327
|
+
].some((fieldName) => {
|
|
49328
|
+
const dependencies = packageJson[fieldName];
|
|
49329
|
+
return isRecord$1(dependencies) && typeof dependencies[dependencyName] === "string";
|
|
49330
|
+
});
|
|
49331
|
+
};
|
|
49332
|
+
const packageHasRecordKey = (projectRoot, key) => {
|
|
49333
|
+
const packageJson = readPackageJson(projectRoot);
|
|
49334
|
+
return isRecord$1(packageJson) && isRecord$1(packageJson[key]);
|
|
49335
|
+
};
|
|
49336
|
+
const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
|
|
49337
|
+
const packageJson = readPackageJson(projectRoot);
|
|
49338
|
+
if (!isRecord$1(packageJson)) return false;
|
|
49339
|
+
const value = packageJson[key];
|
|
49340
|
+
return isRecord$1(value) && isRecord$1(value[nestedKey]);
|
|
49341
|
+
};
|
|
49342
|
+
const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
|
|
49343
|
+
//#endregion
|
|
49095
49344
|
//#region src/cli/utils/scan-result-cache.ts
|
|
49096
49345
|
const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
|
|
49097
49346
|
const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
@@ -49102,7 +49351,7 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
|
49102
49351
|
"eslint-plugin-react-hooks/package.json"
|
|
49103
49352
|
];
|
|
49104
49353
|
const bundledRequire = createRequire(import.meta.url);
|
|
49105
|
-
const isRecord
|
|
49354
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49106
49355
|
const normalizeForStableJson = (value) => {
|
|
49107
49356
|
if (value === null) return null;
|
|
49108
49357
|
if (value === void 0) return void 0;
|
|
@@ -49131,24 +49380,9 @@ const stringifyStableJson = (value) => {
|
|
|
49131
49380
|
}
|
|
49132
49381
|
};
|
|
49133
49382
|
const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
|
|
49134
|
-
const
|
|
49135
|
-
try {
|
|
49136
|
-
return execFileSync("git", [...args], {
|
|
49137
|
-
cwd: directory,
|
|
49138
|
-
encoding: "utf8",
|
|
49139
|
-
stdio: [
|
|
49140
|
-
"ignore",
|
|
49141
|
-
"pipe",
|
|
49142
|
-
"ignore"
|
|
49143
|
-
]
|
|
49144
|
-
}).trim();
|
|
49145
|
-
} catch {
|
|
49146
|
-
return null;
|
|
49147
|
-
}
|
|
49148
|
-
};
|
|
49149
|
-
const readHeadSha = (projectDirectory) => runGit$1(projectDirectory, ["rev-parse", "HEAD"]);
|
|
49383
|
+
const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
|
|
49150
49384
|
const isWorktreeClean = (projectDirectory) => {
|
|
49151
|
-
const status = runGit
|
|
49385
|
+
const status = runGit(projectDirectory, [
|
|
49152
49386
|
"status",
|
|
49153
49387
|
"--porcelain=v1",
|
|
49154
49388
|
"--untracked-files=normal"
|
|
@@ -49156,7 +49390,7 @@ const isWorktreeClean = (projectDirectory) => {
|
|
|
49156
49390
|
return status !== null && status.length === 0;
|
|
49157
49391
|
};
|
|
49158
49392
|
const hasHiddenTrackedFileState = (projectDirectory) => {
|
|
49159
|
-
const output = runGit
|
|
49393
|
+
const output = runGit(projectDirectory, ["ls-files", "-v"]);
|
|
49160
49394
|
if (output === null) return true;
|
|
49161
49395
|
return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
|
|
49162
49396
|
};
|
|
@@ -49169,7 +49403,7 @@ const resolveCacheFilePath = (projectDirectory) => {
|
|
|
49169
49403
|
const readPersistedCache = (cacheFilePath) => {
|
|
49170
49404
|
try {
|
|
49171
49405
|
const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
|
|
49172
|
-
if (!isRecord
|
|
49406
|
+
if (!isRecord(parsed) || parsed.version !== 1) return {
|
|
49173
49407
|
version: 1,
|
|
49174
49408
|
entries: []
|
|
49175
49409
|
};
|
|
@@ -49179,8 +49413,8 @@ const readPersistedCache = (cacheFilePath) => {
|
|
|
49179
49413
|
};
|
|
49180
49414
|
const entries = [];
|
|
49181
49415
|
for (const entry of parsed.entries) {
|
|
49182
|
-
if (!isRecord
|
|
49183
|
-
if (!isRecord
|
|
49416
|
+
if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
|
|
49417
|
+
if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
|
|
49184
49418
|
entries.push(entry);
|
|
49185
49419
|
}
|
|
49186
49420
|
return {
|
|
@@ -49712,6 +49946,7 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49712
49946
|
}
|
|
49713
49947
|
const animateRender = !options.silent && !options.verbose && canAnimateOnboarding(process.stdout);
|
|
49714
49948
|
const pause = onboardingSectionPause(animateRender);
|
|
49949
|
+
const useHyperlinks = shouldRenderHyperlinks(process.stdout);
|
|
49715
49950
|
const demotedDiagnosticCount = diagnostics.length - surfaceDiagnostics.length;
|
|
49716
49951
|
const lintSourceFileCount = options.includePaths.length > 0 ? options.includePaths.length : project.sourceFileCount;
|
|
49717
49952
|
if (printedDiagnostics.length === 0) {
|
|
@@ -49737,7 +49972,7 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49737
49972
|
yield* printDiagnostics([...printedDiagnostics], options.verbose, directory, buildRulePriorityMap([score]), isCodingAgentEnvironment(), {
|
|
49738
49973
|
sectionPause: pause,
|
|
49739
49974
|
animateCountUp: animateRender
|
|
49740
|
-
});
|
|
49975
|
+
}, useHyperlinks);
|
|
49741
49976
|
if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
|
|
49742
49977
|
if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
|
|
49743
49978
|
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.`));
|
|
@@ -49930,16 +50165,7 @@ const buildHandoffPayload = (input) => {
|
|
|
49930
50165
|
try {
|
|
49931
50166
|
outputDirectory = writeDiagnosticsDirectory([...input.diagnostics], input.outputDirectory);
|
|
49932
50167
|
} catch {}
|
|
49933
|
-
const lines = [
|
|
49934
|
-
"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:",
|
|
49935
|
-
`- It's used by teams at ${CI_TRUST_COMPANIES}.`,
|
|
49936
|
-
"- I don't have to fix everything at once; CI keeps new PRs clean while I incrementally pay down the existing issues.",
|
|
49937
|
-
`- Setup guide: ${CI_URL}`,
|
|
49938
|
-
"If I say yes, run `npx react-doctor install` (or add the GitHub Actions workflow). Either way, then continue with the fixes below.",
|
|
49939
|
-
"",
|
|
49940
|
-
`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.`,
|
|
49941
|
-
""
|
|
49942
|
-
];
|
|
50168
|
+
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.`, ""];
|
|
49943
50169
|
topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
|
|
49944
50170
|
const representative = ruleDiagnostics[0];
|
|
49945
50171
|
const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
|
|
@@ -50000,78 +50226,6 @@ const detectAvailableAgents = async () => {
|
|
|
50000
50226
|
return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
|
|
50001
50227
|
};
|
|
50002
50228
|
//#endregion
|
|
50003
|
-
//#region src/cli/utils/git-hook-shared.ts
|
|
50004
|
-
const HOOK_FILE_NAME = "pre-commit";
|
|
50005
|
-
const HOOK_RELATIVE_PATH = "hooks/pre-commit";
|
|
50006
|
-
const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
|
|
50007
|
-
const HUSKY_HOOKS_PATH = ".husky";
|
|
50008
|
-
const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
|
|
50009
|
-
const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
|
|
50010
|
-
const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
|
|
50011
|
-
const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
|
|
50012
|
-
const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
|
|
50013
|
-
const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
|
|
50014
|
-
"react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
|
|
50015
|
-
`if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
|
|
50016
|
-
"rm -f \"$react_doctor_output\";",
|
|
50017
|
-
"else",
|
|
50018
|
-
"rm -f \"$react_doctor_output\";",
|
|
50019
|
-
`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;`,
|
|
50020
|
-
"fi"
|
|
50021
|
-
].join(" ");
|
|
50022
|
-
const PACKAGE_JSON_FILE_NAME = "package.json";
|
|
50023
|
-
const runGit = (projectRoot, args) => {
|
|
50024
|
-
try {
|
|
50025
|
-
return execFileSync("git", [...args], {
|
|
50026
|
-
cwd: projectRoot,
|
|
50027
|
-
encoding: "utf8",
|
|
50028
|
-
stdio: [
|
|
50029
|
-
"ignore",
|
|
50030
|
-
"pipe",
|
|
50031
|
-
"ignore"
|
|
50032
|
-
]
|
|
50033
|
-
}).trim();
|
|
50034
|
-
} catch {
|
|
50035
|
-
return null;
|
|
50036
|
-
}
|
|
50037
|
-
};
|
|
50038
|
-
const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
|
|
50039
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
50040
|
-
const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
|
|
50041
|
-
const readPackageJson = (projectRoot) => {
|
|
50042
|
-
try {
|
|
50043
|
-
return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
|
|
50044
|
-
} catch {
|
|
50045
|
-
return null;
|
|
50046
|
-
}
|
|
50047
|
-
};
|
|
50048
|
-
const writeJsonFile$1 = (filePath, value) => {
|
|
50049
|
-
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
50050
|
-
};
|
|
50051
|
-
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
50052
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50053
|
-
if (!isRecord(packageJson)) return false;
|
|
50054
|
-
return [
|
|
50055
|
-
"dependencies",
|
|
50056
|
-
"devDependencies",
|
|
50057
|
-
"optionalDependencies"
|
|
50058
|
-
].some((fieldName) => {
|
|
50059
|
-
const dependencies = packageJson[fieldName];
|
|
50060
|
-
return isRecord(dependencies) && typeof dependencies[dependencyName] === "string";
|
|
50061
|
-
});
|
|
50062
|
-
};
|
|
50063
|
-
const packageHasRecordKey = (projectRoot, key) => {
|
|
50064
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50065
|
-
return isRecord(packageJson) && isRecord(packageJson[key]);
|
|
50066
|
-
};
|
|
50067
|
-
const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
|
|
50068
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50069
|
-
if (!isRecord(packageJson)) return false;
|
|
50070
|
-
const value = packageJson[key];
|
|
50071
|
-
return isRecord(value) && isRecord(value[nestedKey]);
|
|
50072
|
-
};
|
|
50073
|
-
const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
|
|
50074
|
-
//#endregion
|
|
50075
50229
|
//#region src/cli/utils/install-doctor-script.ts
|
|
50076
50230
|
const DOCTOR_SCRIPT_NAME = "doctor";
|
|
50077
50231
|
const FALLBACK_DOCTOR_SCRIPT_NAME = "react-doctor";
|
|
@@ -50097,31 +50251,31 @@ const findNearestPackageDirectory = (startDirectory, stopDirectory) => {
|
|
|
50097
50251
|
};
|
|
50098
50252
|
const hasDoctorScript = (projectRoot) => {
|
|
50099
50253
|
const packageJson = readPackageJson(findNearestPackageDirectory(projectRoot) ?? projectRoot);
|
|
50100
|
-
if (!isRecord(packageJson)) return false;
|
|
50254
|
+
if (!isRecord$1(packageJson)) return false;
|
|
50101
50255
|
const scripts = packageJson.scripts;
|
|
50102
|
-
if (!isRecord(scripts)) return false;
|
|
50256
|
+
if (!isRecord$1(scripts)) return false;
|
|
50103
50257
|
return isReactDoctorScriptCommand(scripts[DOCTOR_SCRIPT_NAME]) || isReactDoctorScriptCommand(scripts[FALLBACK_DOCTOR_SCRIPT_NAME]);
|
|
50104
50258
|
};
|
|
50105
50259
|
const hasDoctorDependency = (packageJson) => DEPENDENCY_FIELD_NAMES.some((fieldName) => {
|
|
50106
50260
|
const dependencies = packageJson[fieldName];
|
|
50107
|
-
return isRecord(dependencies) && Object.hasOwn(dependencies, "react-doctor");
|
|
50261
|
+
return isRecord$1(dependencies) && Object.hasOwn(dependencies, "react-doctor");
|
|
50108
50262
|
});
|
|
50109
50263
|
const installDoctorScript = (options) => {
|
|
50110
50264
|
const packageDirectory = findNearestPackageDirectory(options.projectRoot) ?? options.projectRoot;
|
|
50111
50265
|
const packageJsonPath = getPackageJsonPath(packageDirectory);
|
|
50112
50266
|
const packageJson = readPackageJson(packageDirectory);
|
|
50113
|
-
if (!isRecord(packageJson)) return {
|
|
50267
|
+
if (!isRecord$1(packageJson)) return {
|
|
50114
50268
|
packageJsonPath,
|
|
50115
50269
|
scriptStatus: "skipped",
|
|
50116
50270
|
scriptReason: "missing-or-invalid-package-json"
|
|
50117
50271
|
};
|
|
50118
50272
|
const scripts = packageJson.scripts;
|
|
50119
50273
|
const scriptTarget = (() => {
|
|
50120
|
-
if (scripts !== void 0 && !isRecord(scripts)) return {
|
|
50274
|
+
if (scripts !== void 0 && !isRecord$1(scripts)) return {
|
|
50121
50275
|
status: "skipped",
|
|
50122
50276
|
reason: "invalid-scripts"
|
|
50123
50277
|
};
|
|
50124
|
-
const scriptRecord = isRecord(scripts) ? scripts : {};
|
|
50278
|
+
const scriptRecord = isRecord$1(scripts) ? scripts : {};
|
|
50125
50279
|
if (isReactDoctorScriptCommand(scriptRecord[DOCTOR_SCRIPT_NAME])) return {
|
|
50126
50280
|
scriptName: DOCTOR_SCRIPT_NAME,
|
|
50127
50281
|
status: "existing"
|
|
@@ -50155,7 +50309,7 @@ const installDoctorScript = (options) => {
|
|
|
50155
50309
|
if (scriptStatus === "created") writeJsonFile$1(packageJsonPath, {
|
|
50156
50310
|
...packageJson,
|
|
50157
50311
|
scripts: {
|
|
50158
|
-
...isRecord(scripts) ? scripts : {},
|
|
50312
|
+
...isRecord$1(scripts) ? scripts : {},
|
|
50159
50313
|
[scriptTarget.scriptName ?? DOCTOR_SCRIPT_NAME]: DOCTOR_SCRIPT_COMMAND
|
|
50160
50314
|
}
|
|
50161
50315
|
});
|
|
@@ -50309,38 +50463,52 @@ const upgradeReactDoctorWorkflowInPlace = (projectRoot) => {
|
|
|
50309
50463
|
//#region src/cli/utils/hash-project-root.ts
|
|
50310
50464
|
const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
|
|
50311
50465
|
//#endregion
|
|
50312
|
-
//#region src/cli/utils/
|
|
50313
|
-
const
|
|
50314
|
-
const
|
|
50315
|
-
|
|
50316
|
-
|
|
50317
|
-
});
|
|
50318
|
-
|
|
50319
|
-
|
|
50320
|
-
|
|
50321
|
-
|
|
50322
|
-
|
|
50323
|
-
|
|
50324
|
-
|
|
50325
|
-
};
|
|
50326
|
-
const recordActionUpgradeDecision = (projectRoot, outcome, storeOptions = {}) => {
|
|
50327
|
-
try {
|
|
50328
|
-
const store = getActionUpgradeStore(storeOptions);
|
|
50329
|
-
const upgrades = store.get("actionUpgrades", {});
|
|
50330
|
-
store.set("actionUpgrades", {
|
|
50331
|
-
...upgrades,
|
|
50332
|
-
[hashProjectRoot(projectRoot)]: {
|
|
50333
|
-
rootDirectory: Path.resolve(projectRoot),
|
|
50334
|
-
outcome,
|
|
50335
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
50466
|
+
//#region src/cli/utils/project-decision-store.ts
|
|
50467
|
+
const createProjectDecisionStore = (storeKey) => {
|
|
50468
|
+
const getStore = (options = {}) => new Conf({
|
|
50469
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
50470
|
+
cwd: options.cwd
|
|
50471
|
+
});
|
|
50472
|
+
return {
|
|
50473
|
+
getConfigPath: (options = {}) => getStore(options).path,
|
|
50474
|
+
hasHandled: (projectRoot, options = {}) => {
|
|
50475
|
+
try {
|
|
50476
|
+
return Boolean(getStore(options).get(storeKey, {})[hashProjectRoot(projectRoot)]);
|
|
50477
|
+
} catch {
|
|
50478
|
+
return true;
|
|
50336
50479
|
}
|
|
50337
|
-
}
|
|
50338
|
-
|
|
50339
|
-
|
|
50340
|
-
|
|
50341
|
-
|
|
50480
|
+
},
|
|
50481
|
+
record: (projectRoot, outcome, options = {}) => {
|
|
50482
|
+
try {
|
|
50483
|
+
const store = getStore(options);
|
|
50484
|
+
store.set(storeKey, {
|
|
50485
|
+
...store.get(storeKey, {}),
|
|
50486
|
+
[hashProjectRoot(projectRoot)]: {
|
|
50487
|
+
rootDirectory: Path.resolve(projectRoot),
|
|
50488
|
+
outcome,
|
|
50489
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
50490
|
+
}
|
|
50491
|
+
});
|
|
50492
|
+
return true;
|
|
50493
|
+
} catch {
|
|
50494
|
+
return false;
|
|
50495
|
+
}
|
|
50496
|
+
}
|
|
50497
|
+
};
|
|
50342
50498
|
};
|
|
50343
50499
|
//#endregion
|
|
50500
|
+
//#region src/cli/utils/action-upgrade-prompt.ts
|
|
50501
|
+
const store$1 = createProjectDecisionStore("actionUpgrades");
|
|
50502
|
+
store$1.getConfigPath;
|
|
50503
|
+
const hasHandledActionUpgrade = store$1.hasHandled;
|
|
50504
|
+
const recordActionUpgradeDecision = store$1.record;
|
|
50505
|
+
//#endregion
|
|
50506
|
+
//#region src/cli/utils/ci-prompt-decision.ts
|
|
50507
|
+
const store = createProjectDecisionStore("ciPrompts");
|
|
50508
|
+
store.getConfigPath;
|
|
50509
|
+
const hasHandledCiPrompt = store.hasHandled;
|
|
50510
|
+
const recordCiPromptDecision = store.record;
|
|
50511
|
+
//#endregion
|
|
50344
50512
|
//#region src/cli/utils/open-url.ts
|
|
50345
50513
|
const resolveOpenCommand = (url) => {
|
|
50346
50514
|
if (process$1.platform === "darwin") return {
|
|
@@ -50969,13 +51137,13 @@ const installPackageJsonHook = (options, strategy) => {
|
|
|
50969
51137
|
const packageJsonPath = getPackageJsonPath(options.projectRoot);
|
|
50970
51138
|
const didHookExist = NFS.existsSync(packageJsonPath);
|
|
50971
51139
|
const packageJson = readPackageJson(options.projectRoot);
|
|
50972
|
-
const nextPackageJson = isRecord(packageJson) ? { ...packageJson } : {};
|
|
51140
|
+
const nextPackageJson = isRecord$1(packageJson) ? { ...packageJson } : {};
|
|
50973
51141
|
const parentKeys = strategy.path.slice(0, -1);
|
|
50974
51142
|
const leafKey = strategy.path[strategy.path.length - 1];
|
|
50975
51143
|
let parent = nextPackageJson;
|
|
50976
51144
|
for (const key of parentKeys) {
|
|
50977
51145
|
const existing = parent[key];
|
|
50978
|
-
const cloned = isRecord(existing) ? { ...existing } : {};
|
|
51146
|
+
const cloned = isRecord$1(existing) ? { ...existing } : {};
|
|
50979
51147
|
parent[key] = cloned;
|
|
50980
51148
|
parent = cloned;
|
|
50981
51149
|
}
|
|
@@ -51146,7 +51314,7 @@ const isHuskyProject = (projectRoot) => NFS.existsSync(Path.join(projectRoot, ".
|
|
|
51146
51314
|
const isVitePlusProject = (projectRoot) => packageHasDependency(projectRoot, "vite-plus");
|
|
51147
51315
|
const isSimpleGitHooksProject = (projectRoot) => {
|
|
51148
51316
|
const packageJson = readPackageJson(projectRoot);
|
|
51149
|
-
return isRecord(packageJson) && isRecord(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
|
|
51317
|
+
return isRecord$1(packageJson) && isRecord$1(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
|
|
51150
51318
|
};
|
|
51151
51319
|
const getLefthookConfigPath = (projectRoot) => {
|
|
51152
51320
|
for (const fileName of LEFTHOOK_CONFIG_FILES) {
|
|
@@ -51312,7 +51480,7 @@ const detectPackageManager = (projectRoot) => {
|
|
|
51312
51480
|
let currentDirectory = Path.resolve(projectRoot);
|
|
51313
51481
|
while (true) {
|
|
51314
51482
|
const packageJson = readPackageJson(currentDirectory);
|
|
51315
|
-
if (isRecord(packageJson) && typeof packageJson.packageManager === "string") {
|
|
51483
|
+
if (isRecord$1(packageJson) && typeof packageJson.packageManager === "string") {
|
|
51316
51484
|
const packageManagerName = packageJson.packageManager.split("@")[0];
|
|
51317
51485
|
if (packageManagerName === "pnpm" || packageManagerName === "yarn" || packageManagerName === "bun" || packageManagerName === "npm") return packageManagerName;
|
|
51318
51486
|
}
|
|
@@ -51388,12 +51556,12 @@ const isSupplyChainTrustError = (error) => {
|
|
|
51388
51556
|
const formatInstallCommand = (input) => [input.command, ...input.args].join(" ");
|
|
51389
51557
|
const installReactDoctorDependency = async (options) => {
|
|
51390
51558
|
const packageJson = readPackageJson(options.projectRoot);
|
|
51391
|
-
if (!isRecord(packageJson)) return {
|
|
51559
|
+
if (!isRecord$1(packageJson)) return {
|
|
51392
51560
|
dependencyStatus: "skipped",
|
|
51393
51561
|
dependencyReason: "missing-or-invalid-package-json"
|
|
51394
51562
|
};
|
|
51395
51563
|
if (hasDoctorDependency(packageJson)) return { dependencyStatus: "existing" };
|
|
51396
|
-
if (packageJson.devDependencies !== void 0 && !isRecord(packageJson.devDependencies)) return {
|
|
51564
|
+
if (packageJson.devDependencies !== void 0 && !isRecord$1(packageJson.devDependencies)) return {
|
|
51397
51565
|
dependencyStatus: "skipped",
|
|
51398
51566
|
dependencyReason: "invalid-dev-dependencies"
|
|
51399
51567
|
};
|
|
@@ -51557,10 +51725,12 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
51557
51725
|
const existingWorkflow = readReactDoctorWorkflow(projectRoot);
|
|
51558
51726
|
const canInstallWorkflow = !NFS.existsSync(workflowTargetPath);
|
|
51559
51727
|
const canUpgradeWorkflow = existingWorkflow !== null && workflowUsesV1Action(existingWorkflow.content) && !hasHandledActionUpgrade(projectRoot);
|
|
51560
|
-
const
|
|
51728
|
+
const ciPromptOutcome = canInstallWorkflow && !options.yes && !skipPrompts && !hasHandledCiPrompt(projectRoot) ? await askAddToGitHubActions(prompt) : null;
|
|
51729
|
+
const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || ciPromptOutcome === "yes");
|
|
51561
51730
|
const upgradePromptOutcome = canUpgradeWorkflow && !options.yes && !skipPrompts ? await askUpgradeActionVersion(prompt) : null;
|
|
51562
51731
|
const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
|
|
51563
51732
|
if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
|
|
51733
|
+
if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
|
|
51564
51734
|
const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
|
|
51565
51735
|
type: "multiselect",
|
|
51566
51736
|
name: "agents",
|
|
@@ -51807,18 +51977,24 @@ const handoffToAgent = async (input) => {
|
|
|
51807
51977
|
if (!input.interactive || input.diagnostics.length === 0) return;
|
|
51808
51978
|
cliLogger.break();
|
|
51809
51979
|
const projectRootForCi = findNearestPackageDirectory(input.rootDirectory) ?? input.rootDirectory;
|
|
51810
|
-
|
|
51980
|
+
const isGitHubActionsConfigured = isReactDoctorWorkflowInstalled(projectRootForCi);
|
|
51981
|
+
if (!isGitHubActionsConfigured && !hasHandledCiPrompt(projectRootForCi)) {
|
|
51811
51982
|
const ciOutcome = await askAddToGitHubActions();
|
|
51812
51983
|
recordCount(METRIC.agentHandoff, 1, {
|
|
51813
51984
|
outcome: `ci-${ciOutcome}`,
|
|
51814
51985
|
diagnosticsCount: input.diagnostics.length
|
|
51815
51986
|
});
|
|
51816
51987
|
if (ciOutcome === "cancel") return;
|
|
51988
|
+
recordCiPromptDecision(projectRootForCi, ciOutcome === "yes" ? "accepted" : "declined");
|
|
51817
51989
|
if (ciOutcome === "yes") {
|
|
51818
51990
|
await setUpGitHubActions({ rootDirectory: input.rootDirectory });
|
|
51819
51991
|
cliLogger.break();
|
|
51820
51992
|
}
|
|
51821
|
-
} else await maybeOfferActionUpgrade(projectRootForCi);
|
|
51993
|
+
} else if (isGitHubActionsConfigured) await maybeOfferActionUpgrade(projectRootForCi);
|
|
51994
|
+
else recordCount(METRIC.agentHandoff, 1, {
|
|
51995
|
+
outcome: "ci-suppressed",
|
|
51996
|
+
diagnosticsCount: input.diagnostics.length
|
|
51997
|
+
});
|
|
51822
51998
|
const { handoffTarget } = await prompts({
|
|
51823
51999
|
type: "select",
|
|
51824
52000
|
name: "handoffTarget",
|
|
@@ -52041,6 +52217,7 @@ const reportErrorToSentry = async (error) => {
|
|
|
52041
52217
|
sampled: runTrace.sampled,
|
|
52042
52218
|
sampleRand: Math.random()
|
|
52043
52219
|
});
|
|
52220
|
+
recordRunTraceId(scope.getPropagationContext().traceId);
|
|
52044
52221
|
return Sentry.captureException(error);
|
|
52045
52222
|
});
|
|
52046
52223
|
await Sentry.flush(SENTRY_FLUSH_TIMEOUT_MS);
|
|
@@ -52124,7 +52301,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52124
52301
|
yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
|
|
52125
52302
|
if (displayDiagnostics.length > 0) {
|
|
52126
52303
|
yield* log("");
|
|
52127
|
-
yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender });
|
|
52304
|
+
yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender }, shouldRenderHyperlinks(process.stdout));
|
|
52128
52305
|
}
|
|
52129
52306
|
const lowestScoredScan = findLowestScoredScan(completedScans);
|
|
52130
52307
|
const aggregateScore = lowestScoredScan?.result.score ?? null;
|
|
@@ -52162,9 +52339,8 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52162
52339
|
});
|
|
52163
52340
|
//#endregion
|
|
52164
52341
|
//#region src/cli/utils/prompt-install-setup.ts
|
|
52165
|
-
const GLOBAL_CONFIG_PROJECT_NAME = "react-doctor";
|
|
52166
52342
|
const getSetupPromptStore = (options = {}) => new Conf({
|
|
52167
|
-
projectName:
|
|
52343
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
52168
52344
|
cwd: options.cwd
|
|
52169
52345
|
});
|
|
52170
52346
|
const getSetupPromptProjectKey = (projectRoot) => hashProjectRoot(projectRoot);
|
|
@@ -52175,6 +52351,24 @@ const hasDisabledSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
|
52175
52351
|
return false;
|
|
52176
52352
|
}
|
|
52177
52353
|
};
|
|
52354
|
+
const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
52355
|
+
try {
|
|
52356
|
+
const store = getSetupPromptStore(storeOptions);
|
|
52357
|
+
const projects = store.get("projects", {});
|
|
52358
|
+
const projectKey = getSetupPromptProjectKey(projectRoot);
|
|
52359
|
+
store.set("projects", {
|
|
52360
|
+
...projects,
|
|
52361
|
+
[projectKey]: {
|
|
52362
|
+
...projects[projectKey] ?? {},
|
|
52363
|
+
rootDirectory: Path.resolve(projectRoot),
|
|
52364
|
+
setupPrompt: false
|
|
52365
|
+
}
|
|
52366
|
+
});
|
|
52367
|
+
return true;
|
|
52368
|
+
} catch {
|
|
52369
|
+
return false;
|
|
52370
|
+
}
|
|
52371
|
+
};
|
|
52178
52372
|
const resolveInstallSetupProjectRoot = (options) => {
|
|
52179
52373
|
if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
|
|
52180
52374
|
const packageDirectories = /* @__PURE__ */ new Set();
|
|
@@ -52581,6 +52775,14 @@ const runExplain = async (fileLineArgument, context) => {
|
|
|
52581
52775
|
const colorizedRule = colorizeRuleByDiagnostic(ruleIdentifier, diagnostic.severity);
|
|
52582
52776
|
const severityLabel = colorizeRuleByDiagnostic(diagnostic.severity, diagnostic.severity);
|
|
52583
52777
|
cliLogger.log(`${severitySymbol} ${colorizedRule} ${highlighter.dim(`(${severityLabel})`)} — ${diagnostic.message}`);
|
|
52778
|
+
const codeFrame = buildCodeFrame({
|
|
52779
|
+
filePath: diagnostic.filePath,
|
|
52780
|
+
line: diagnostic.line,
|
|
52781
|
+
column: diagnostic.column,
|
|
52782
|
+
endLine: diagnostic.endLine,
|
|
52783
|
+
rootDirectory: targetDirectory
|
|
52784
|
+
});
|
|
52785
|
+
if (codeFrame) cliLogger.log(indentMultilineText(codeFrame, " "));
|
|
52584
52786
|
if (diagnostic.category) cliLogger.dim(` Category: ${diagnostic.category}`);
|
|
52585
52787
|
if (diagnostic.help) cliLogger.dim(` ${diagnostic.help}`);
|
|
52586
52788
|
cliLogger.dim(` If this needs follow-up or looks like a false positive, open: ${buildDiagnosticIssueUrl({
|
|
@@ -52630,6 +52832,10 @@ const validateModeFlags = (flags) => {
|
|
|
52630
52832
|
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.`);
|
|
52631
52833
|
if (flags.score && flags.json) throw new CliInputError("Cannot combine --score and --json; pick one output mode.");
|
|
52632
52834
|
if (flags.score && flags.telemetry === false) throw new CliInputError("Cannot combine --score with --no-telemetry; --score prints the score that --no-telemetry disables.");
|
|
52835
|
+
if (flags.debug && (flags.score === false || flags.telemetry === false)) {
|
|
52836
|
+
const disablingFlag = flags.score === false ? "--no-score" : "--no-telemetry";
|
|
52837
|
+
throw new CliInputError(`Cannot combine --debug with ${disablingFlag}; ${disablingFlag} disables the Sentry reporting --debug needs to capture a trace.`);
|
|
52838
|
+
}
|
|
52633
52839
|
};
|
|
52634
52840
|
//#endregion
|
|
52635
52841
|
//#region src/cli/commands/inspect.ts
|
|
@@ -52977,11 +53183,13 @@ const inspectAction = async (directory, flags) => {
|
|
|
52977
53183
|
})) {
|
|
52978
53184
|
printAgentInstallHint();
|
|
52979
53185
|
recordCount(METRIC.agentInstallHintShown, 1);
|
|
53186
|
+
disableSetupPrompt(setupProjectRoot);
|
|
52980
53187
|
}
|
|
52981
53188
|
}
|
|
52982
53189
|
} catch (error) {
|
|
52983
53190
|
const isUserError = isExpectedUserError(error);
|
|
52984
53191
|
const sentryEventId = isUserError ? void 0 : await reportErrorToSentry(error);
|
|
53192
|
+
if (isDebugFlagEnabled()) await flushSentry();
|
|
52985
53193
|
if (isJsonMode) {
|
|
52986
53194
|
writeJsonErrorReport(error, sentryEventId);
|
|
52987
53195
|
process.exitCode = 1;
|
|
@@ -53704,6 +53912,33 @@ const normalizeHelpInvocation = (argv, knownCommands) => {
|
|
|
53704
53912
|
return [...nodeArguments, "--help"];
|
|
53705
53913
|
};
|
|
53706
53914
|
//#endregion
|
|
53915
|
+
//#region src/cli/utils/print-debug-trace.ts
|
|
53916
|
+
/**
|
|
53917
|
+
* The `--debug` end-of-run line, pure so it's testable without the Sentry SDK.
|
|
53918
|
+
* Mirrors the crash-reference phrasing in `handle-error.ts` ("mention this when
|
|
53919
|
+
* reporting") so users learn one habit for both paths. A `null` trace says why,
|
|
53920
|
+
* so `--debug` never silently does nothing.
|
|
53921
|
+
*/
|
|
53922
|
+
const buildDebugTraceMessage = (traceId) => traceId === null ? "Sentry trace unavailable for this run (no trace was recorded)." : `Sentry trace (mention this when reporting): ${traceId}`;
|
|
53923
|
+
/**
|
|
53924
|
+
* Prints the run's Sentry trace id to stderr at the end of a `--debug` run, so
|
|
53925
|
+
* maintainers can pull the full trace from a pasted id. Runs from the process
|
|
53926
|
+
* `exit` handler, so it's the last line on both the success path and the error
|
|
53927
|
+
* funnels (which `process.exit()` before the promise chain could resume).
|
|
53928
|
+
*
|
|
53929
|
+
* Writes straight to `process.stderr` (not `Console`) for three reasons: the
|
|
53930
|
+
* exit handler is synchronous, JSON mode patches the global console to no-ops —
|
|
53931
|
+
* a diagnostic the user explicitly asked for must survive that — and stderr
|
|
53932
|
+
* keeps `--json` / `--score` stdout machine-clean. The write is wrapped because
|
|
53933
|
+
* a diagnostic must never throw out of an exit handler.
|
|
53934
|
+
*/
|
|
53935
|
+
const printDebugTrace = () => {
|
|
53936
|
+
if (!Sentry.isInitialized()) return;
|
|
53937
|
+
try {
|
|
53938
|
+
process.stderr.write(`${highlighter.dim(buildDebugTraceMessage(getLastRunTraceId()))}\n`);
|
|
53939
|
+
} catch {}
|
|
53940
|
+
};
|
|
53941
|
+
//#endregion
|
|
53707
53942
|
//#region src/cli/utils/removed-cli-flags.ts
|
|
53708
53943
|
const REMOVED_FLAGS = new Map([
|
|
53709
53944
|
["--full", "use `--diff false` to force a full scan"],
|
|
@@ -53730,6 +53965,7 @@ const ROOT_FLAG_SPEC = {
|
|
|
53730
53965
|
longOptionsWithoutValues: new Set([
|
|
53731
53966
|
"--color",
|
|
53732
53967
|
"--dead-code",
|
|
53968
|
+
"--debug",
|
|
53733
53969
|
"--help",
|
|
53734
53970
|
"--json",
|
|
53735
53971
|
"--json-compact",
|
|
@@ -53897,6 +54133,9 @@ const stripUnknownCliFlags = (argv) => {
|
|
|
53897
54133
|
initializeSentry();
|
|
53898
54134
|
process.on("SIGINT", exitGracefully);
|
|
53899
54135
|
process.on("SIGTERM", exitGracefully);
|
|
54136
|
+
process.on("exit", () => {
|
|
54137
|
+
if (isDebugFlagEnabled()) printDebugTrace();
|
|
54138
|
+
});
|
|
53900
54139
|
unrefStdin();
|
|
53901
54140
|
guardStdin();
|
|
53902
54141
|
const formatExampleLines = (examples) => {
|
|
@@ -53941,7 +54180,7 @@ ${highlighter.dim("Learn more:")}
|
|
|
53941
54180
|
${highlighter.info(CANONICAL_GITHUB_URL)}
|
|
53942
54181
|
`;
|
|
53943
54182
|
const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
|
|
53944
|
-
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);
|
|
54183
|
+
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);
|
|
53945
54184
|
program.action(inspectAction);
|
|
53946
54185
|
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));
|
|
53947
54186
|
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);
|
|
@@ -53984,4 +54223,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
53984
54223
|
export {};
|
|
53985
54224
|
|
|
53986
54225
|
//# sourceMappingURL=cli.js.map
|
|
53987
|
-
//# debugId=
|
|
54226
|
+
//# debugId=e076a347-15ab-55bb-b11a-a813bb818760
|