react-doctor 0.5.6-dev.431e515 → 0.5.6-dev.50999f4
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 +421 -251
- package/dist/index.js +53 -67
- package/dist/lsp.js +72 -89
- 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]="496f49cf-4038-5332-b301-ffedc55210ba")}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";
|
|
@@ -35893,6 +35893,7 @@ const isLargeMinifiedFile = (absolutePath) => {
|
|
|
35893
35893
|
if (sizeBytes < 2e4) return false;
|
|
35894
35894
|
return isMinifiedSource(absolutePath);
|
|
35895
35895
|
};
|
|
35896
|
+
const isErrnoException = (error) => error instanceof Error && "code" in error;
|
|
35896
35897
|
const IGNORABLE_READDIR_ERROR_CODES = new Set([
|
|
35897
35898
|
"EACCES",
|
|
35898
35899
|
"EPERM",
|
|
@@ -35902,11 +35903,7 @@ const IGNORABLE_READDIR_ERROR_CODES = new Set([
|
|
|
35902
35903
|
"ELOOP",
|
|
35903
35904
|
"ENAMETOOLONG"
|
|
35904
35905
|
]);
|
|
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
|
-
};
|
|
35906
|
+
const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
|
|
35910
35907
|
const readDirectoryEntries = (directoryPath) => {
|
|
35911
35908
|
try {
|
|
35912
35909
|
return NFS.readdirSync(directoryPath, { withFileTypes: true });
|
|
@@ -35953,7 +35950,7 @@ const readPackageJsonUncached = (packageJsonPath) => {
|
|
|
35953
35950
|
return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
|
|
35954
35951
|
} catch (error) {
|
|
35955
35952
|
if (error instanceof SyntaxError) return {};
|
|
35956
|
-
if (error
|
|
35953
|
+
if (isErrnoException(error)) {
|
|
35957
35954
|
const { code } = error;
|
|
35958
35955
|
if (code === "EISDIR" || code === "EACCES" || code === "EPERM" || code === "ENOENT") return {};
|
|
35959
35956
|
}
|
|
@@ -36678,17 +36675,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
|
|
|
36678
36675
|
return false;
|
|
36679
36676
|
};
|
|
36680
36677
|
const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
|
|
36681
|
-
const
|
|
36682
|
-
const spec = packageJson.dependencies?.
|
|
36678
|
+
const getDependencySpec = (packageJson, packageName) => {
|
|
36679
|
+
const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
|
|
36683
36680
|
return typeof spec === "string" ? spec : null;
|
|
36684
36681
|
};
|
|
36685
|
-
const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson,
|
|
36682
|
+
const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
|
|
36686
36683
|
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);
|
|
36684
|
+
const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
|
|
36692
36685
|
const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
|
|
36693
36686
|
if (version === null || !isCatalogReference(version)) return version;
|
|
36694
36687
|
const catalogName = extractCatalogName(version);
|
|
@@ -36700,11 +36693,7 @@ const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson,
|
|
|
36700
36693
|
if (!isFile(monorepoPackageJsonPath)) return version;
|
|
36701
36694
|
return resolveCatalogVersion(readPackageJson$1(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
|
|
36702
36695
|
};
|
|
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);
|
|
36696
|
+
const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
|
|
36708
36697
|
const getPreactVersion = (packageJson) => {
|
|
36709
36698
|
return {
|
|
36710
36699
|
...packageJson.peerDependencies,
|
|
@@ -36793,6 +36782,11 @@ const ES_TARGET_YEAR_BY_NAME = {
|
|
|
36793
36782
|
esnext: 9999
|
|
36794
36783
|
};
|
|
36795
36784
|
/**
|
|
36785
|
+
* tsconfig filenames probed when resolving a project's TypeScript
|
|
36786
|
+
* compiler options — the root config first, then a monorepo base config.
|
|
36787
|
+
*/
|
|
36788
|
+
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
36789
|
+
/**
|
|
36796
36790
|
* Project-config files that `StagedFiles.materialize` copies into
|
|
36797
36791
|
* the temp directory alongside staged sources so oxlint resolves
|
|
36798
36792
|
* `tsconfig` / `package.json` / lint configs the same way it would
|
|
@@ -37314,6 +37308,7 @@ const isTailwindAtLeast = (detected, required) => {
|
|
|
37314
37308
|
if (detected.major !== required.major) return detected.major > required.major;
|
|
37315
37309
|
return detected.minor >= required.minor;
|
|
37316
37310
|
};
|
|
37311
|
+
const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
|
|
37317
37312
|
var InvalidGlobPatternError = class extends Error {
|
|
37318
37313
|
pattern;
|
|
37319
37314
|
reason;
|
|
@@ -37342,7 +37337,7 @@ const compileGlobPattern = (rawPattern) => {
|
|
|
37342
37337
|
try {
|
|
37343
37338
|
return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
|
|
37344
37339
|
} catch (caughtError) {
|
|
37345
|
-
throw new InvalidGlobPatternError(rawPattern,
|
|
37340
|
+
throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
|
|
37346
37341
|
}
|
|
37347
37342
|
};
|
|
37348
37343
|
const compileGlobPatternsLenient = (patterns, onInvalid) => {
|
|
@@ -38520,7 +38515,6 @@ const PACKAGE_JSON_FILENAME = "package.json";
|
|
|
38520
38515
|
const PACKAGE_JSON_CONFIG_KEY$1 = "reactDoctor";
|
|
38521
38516
|
const LEGACY_CONFIG_FILENAME = "react-doctor.config.json";
|
|
38522
38517
|
const jiti = createJiti(import.meta.url);
|
|
38523
|
-
const formatError = (error) => error instanceof Error ? error.message : String(error);
|
|
38524
38518
|
const importDefaultExport = async (jitiInstance, filePath) => {
|
|
38525
38519
|
const imported = await jitiInstance.import(filePath);
|
|
38526
38520
|
return imported?.default ?? imported;
|
|
@@ -38552,7 +38546,7 @@ const loadModuleConfig = async (filePath) => {
|
|
|
38552
38546
|
try {
|
|
38553
38547
|
return await importDefaultExport(aliasJiti, filePath);
|
|
38554
38548
|
} catch (retryError) {
|
|
38555
|
-
throw new Error(`${
|
|
38549
|
+
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
38550
|
}
|
|
38557
38551
|
}
|
|
38558
38552
|
};
|
|
@@ -38601,7 +38595,7 @@ const loadLegacyConfig = (directory) => {
|
|
|
38601
38595
|
}
|
|
38602
38596
|
warn(`${LEGACY_CONFIG_FILENAME} must contain an object, ignoring.`);
|
|
38603
38597
|
} catch (error) {
|
|
38604
|
-
warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${
|
|
38598
|
+
warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${messageFromUnknown(error)}`);
|
|
38605
38599
|
}
|
|
38606
38600
|
return {
|
|
38607
38601
|
status: "invalid",
|
|
@@ -38628,7 +38622,7 @@ const loadConfigFromDirectory = async (directory) => {
|
|
|
38628
38622
|
warn(`${CONFIG_BASENAME}.${extension} must export an object, ignoring.`);
|
|
38629
38623
|
sawBrokenConfigFile = true;
|
|
38630
38624
|
} catch (error) {
|
|
38631
|
-
warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${
|
|
38625
|
+
warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${messageFromUnknown(error)}`);
|
|
38632
38626
|
sawBrokenConfigFile = true;
|
|
38633
38627
|
}
|
|
38634
38628
|
}
|
|
@@ -39692,15 +39686,10 @@ const buildCapabilities = (project) => {
|
|
|
39692
39686
|
}
|
|
39693
39687
|
if (project.tailwindVersion !== null) {
|
|
39694
39688
|
capabilities.add("tailwind");
|
|
39695
|
-
|
|
39696
|
-
if (isTailwindAtLeast(tailwind, {
|
|
39689
|
+
if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
|
|
39697
39690
|
major: 3,
|
|
39698
39691
|
minor: 4
|
|
39699
39692
|
})) capabilities.add("tailwind:3.4");
|
|
39700
|
-
if (tailwind !== null && isTailwindAtLeast(tailwind, {
|
|
39701
|
-
major: 4,
|
|
39702
|
-
minor: 0
|
|
39703
|
-
})) capabilities.add("tailwind:4");
|
|
39704
39693
|
}
|
|
39705
39694
|
if (project.zodVersion !== null) {
|
|
39706
39695
|
capabilities.add("zod");
|
|
@@ -39908,7 +39897,7 @@ const readIgnoreFile = (filePath) => {
|
|
|
39908
39897
|
try {
|
|
39909
39898
|
content = NFS.readFileSync(filePath, "utf-8");
|
|
39910
39899
|
} catch (error) {
|
|
39911
|
-
const errnoCode = error
|
|
39900
|
+
const errnoCode = isErrnoException(error) ? error.code : void 0;
|
|
39912
39901
|
if (errnoCode && errnoCode !== "ENOENT") runSync(warn$1(`Could not read ignore file ${filePath}: ${errnoCode}`));
|
|
39913
39902
|
return [];
|
|
39914
39903
|
}
|
|
@@ -39946,8 +39935,8 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
39946
39935
|
cachedPatternsByRoot.set(rootDirectory, patterns);
|
|
39947
39936
|
return patterns;
|
|
39948
39937
|
};
|
|
39938
|
+
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39949
39939
|
const KNIP_JSON_FILENAME = "knip.json";
|
|
39950
|
-
const isRecord$1$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39951
39940
|
const readJsonFileSafe = (filePath) => {
|
|
39952
39941
|
let rawContents;
|
|
39953
39942
|
try {
|
|
@@ -39963,10 +39952,10 @@ const readJsonFileSafe = (filePath) => {
|
|
|
39963
39952
|
};
|
|
39964
39953
|
const readKnipConfig = (rootDirectory) => {
|
|
39965
39954
|
const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
|
|
39966
|
-
if (isRecord$
|
|
39955
|
+
if (isRecord$2(knipJson)) return knipJson;
|
|
39967
39956
|
const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
|
|
39968
|
-
const packageKnipConfig = isRecord$
|
|
39969
|
-
return isRecord$
|
|
39957
|
+
const packageKnipConfig = isRecord$2(packageJson) ? packageJson.knip : null;
|
|
39958
|
+
return isRecord$2(packageKnipConfig) ? packageKnipConfig : null;
|
|
39970
39959
|
};
|
|
39971
39960
|
const normalizePatternList = (value) => {
|
|
39972
39961
|
if (typeof value === "string" && value.length > 0) return [value];
|
|
@@ -39978,10 +39967,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
|
|
|
39978
39967
|
return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
|
|
39979
39968
|
};
|
|
39980
39969
|
const collectKnipWorkspacePatterns = (workspaces, settingName) => {
|
|
39981
|
-
if (!isRecord$
|
|
39970
|
+
if (!isRecord$2(workspaces)) return [];
|
|
39982
39971
|
const patterns = [];
|
|
39983
39972
|
for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
|
|
39984
|
-
if (!isRecord$
|
|
39973
|
+
if (!isRecord$2(workspaceConfig)) continue;
|
|
39985
39974
|
patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
|
|
39986
39975
|
}
|
|
39987
39976
|
return patterns;
|
|
@@ -40026,8 +40015,6 @@ const toCanonicalPath = (filePath) => {
|
|
|
40026
40015
|
};
|
|
40027
40016
|
const DEAD_CODE_PLUGIN = "deslop";
|
|
40028
40017
|
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
40018
|
const DEAD_CODE_WORKER_SCRIPT = `
|
|
40032
40019
|
const inputChunks = [];
|
|
40033
40020
|
process.stdin.on("data", (chunk) => inputChunks.push(chunk));
|
|
@@ -40085,7 +40072,7 @@ process.stdin.on("end", () => {
|
|
|
40085
40072
|
});
|
|
40086
40073
|
`;
|
|
40087
40074
|
const resolveTsConfigPath = (rootDirectory) => {
|
|
40088
|
-
for (const filename of TSCONFIG_FILENAMES
|
|
40075
|
+
for (const filename of TSCONFIG_FILENAMES) {
|
|
40089
40076
|
const candidate = Path.join(rootDirectory, filename);
|
|
40090
40077
|
if (NFS.existsSync(candidate)) return candidate;
|
|
40091
40078
|
}
|
|
@@ -40466,15 +40453,13 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
|
|
|
40466
40453
|
})()) }));
|
|
40467
40454
|
static layerOf = (diagnostics) => succeed$3(DeadCode, DeadCode.of({ run: () => fromIterable$1(diagnostics) }));
|
|
40468
40455
|
};
|
|
40469
|
-
const createNodeReadFileLinesSync = (rootDirectory) => {
|
|
40470
|
-
|
|
40471
|
-
|
|
40472
|
-
|
|
40473
|
-
|
|
40474
|
-
|
|
40475
|
-
|
|
40476
|
-
}
|
|
40477
|
-
};
|
|
40456
|
+
const createNodeReadFileLinesSync = (rootDirectory) => (filePath) => {
|
|
40457
|
+
const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
|
|
40458
|
+
try {
|
|
40459
|
+
return NFS.readFileSync(absolutePath, "utf-8").split("\n");
|
|
40460
|
+
} catch {
|
|
40461
|
+
return null;
|
|
40462
|
+
}
|
|
40478
40463
|
};
|
|
40479
40464
|
var Files = class Files extends Service()("react-doctor/Files") {
|
|
40480
40465
|
static layerNode = succeed$3(Files, Files.of({
|
|
@@ -40685,7 +40670,10 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40685
40670
|
directory: input.directory,
|
|
40686
40671
|
cause
|
|
40687
40672
|
}) });
|
|
40688
|
-
})
|
|
40673
|
+
}), withSpan("git.exec", { attributes: {
|
|
40674
|
+
"git.command": input.command,
|
|
40675
|
+
"git.subcommand": input.args[0] ?? ""
|
|
40676
|
+
} }));
|
|
40689
40677
|
const runGit = (directory, args) => runCommand({
|
|
40690
40678
|
command: "git",
|
|
40691
40679
|
args,
|
|
@@ -40713,7 +40701,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40713
40701
|
]);
|
|
40714
40702
|
if (candidates.status !== 0) return null;
|
|
40715
40703
|
return trimOrNull(candidates.stdout.split("\n")[0] ?? "");
|
|
40716
|
-
});
|
|
40704
|
+
}).pipe(withSpan("Git.defaultBranch"));
|
|
40717
40705
|
const branchExists = (directory, branch) => runGit(directory, [
|
|
40718
40706
|
"rev-parse",
|
|
40719
40707
|
"--verify",
|
|
@@ -40760,7 +40748,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40760
40748
|
const result = resultOption.value;
|
|
40761
40749
|
if (result.status !== 0) return null;
|
|
40762
40750
|
return parseGithubViewerPermission(result.stdout);
|
|
40763
|
-
}).pipe(catch_$1(() => succeed$2(null)));
|
|
40751
|
+
}).pipe(catch_$1(() => succeed$2(null)), withSpan("Git.githubViewerPermission"));
|
|
40764
40752
|
/**
|
|
40765
40753
|
* Resolves a `--diff A..B` / `A...B` commit range into a changed-file
|
|
40766
40754
|
* selection. Each endpoint is validated with `isSafeGitRevision`
|
|
@@ -40874,7 +40862,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40874
40862
|
changedFiles: splitNullSeparated(diff.stdout),
|
|
40875
40863
|
isCurrentChanges: false
|
|
40876
40864
|
};
|
|
40877
|
-
}),
|
|
40865
|
+
}).pipe(withSpan("Git.diffSelection")),
|
|
40878
40866
|
stagedFilePaths: (directory) => runGit(directory, [
|
|
40879
40867
|
"diff",
|
|
40880
40868
|
"--cached",
|
|
@@ -40916,7 +40904,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40916
40904
|
status: result.status,
|
|
40917
40905
|
stdout: result.stdout
|
|
40918
40906
|
};
|
|
40919
|
-
}),
|
|
40907
|
+
}).pipe(withSpan("Git.grep")),
|
|
40920
40908
|
changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
|
|
40921
40909
|
if (files.length === 0) return [];
|
|
40922
40910
|
if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
|
|
@@ -40932,7 +40920,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40932
40920
|
]);
|
|
40933
40921
|
if (result.status !== 0) return null;
|
|
40934
40922
|
return parseChangedLineRanges(result.stdout);
|
|
40935
|
-
})
|
|
40923
|
+
}).pipe(withSpan("Git.changedLineRanges"))
|
|
40936
40924
|
});
|
|
40937
40925
|
})).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
|
|
40938
40926
|
/**
|
|
@@ -41147,7 +41135,7 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
|
|
|
41147
41135
|
for (const [absolutePath, originalContent] of originalContents) try {
|
|
41148
41136
|
NFS.writeFileSync(absolutePath, originalContent);
|
|
41149
41137
|
} catch (error) {
|
|
41150
|
-
process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${
|
|
41138
|
+
process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${messageFromUnknown(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
|
|
41151
41139
|
}
|
|
41152
41140
|
};
|
|
41153
41141
|
const onExit = () => restore();
|
|
@@ -41253,7 +41241,7 @@ const resolveUserPlugin = (spec, configSourceDirectory) => {
|
|
|
41253
41241
|
try {
|
|
41254
41242
|
resolvedSpecifier = isRelative ? Path.resolve(configSourceDirectory, spec) : candidateRequire.resolve(spec);
|
|
41255
41243
|
} catch (error) {
|
|
41256
|
-
warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${
|
|
41244
|
+
warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${messageFromUnknown(error)}`);
|
|
41257
41245
|
return null;
|
|
41258
41246
|
}
|
|
41259
41247
|
const { name, ruleNames } = readPluginShape(resolvedSpecifier, (target) => candidateRequire(target));
|
|
@@ -41386,7 +41374,6 @@ const resolveOxlintBinary = () => {
|
|
|
41386
41374
|
return Path.join(oxlintPackageDirectory, "bin", "oxlint");
|
|
41387
41375
|
};
|
|
41388
41376
|
const resolvePluginPath = () => esmRequire.resolve("oxlint-plugin-react-doctor");
|
|
41389
|
-
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
41390
41377
|
const resolveTsConfigRelativePath = (rootDirectory) => {
|
|
41391
41378
|
for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
|
|
41392
41379
|
return null;
|
|
@@ -41758,7 +41745,7 @@ const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
|
|
|
41758
41745
|
const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
|
|
41759
41746
|
let currentNode = identifier.parent;
|
|
41760
41747
|
while (currentNode) {
|
|
41761
|
-
if (
|
|
41748
|
+
if (isScopeBoundary(currentNode)) {
|
|
41762
41749
|
if (scopeContainsNonImportBinding(currentNode, currentNode, identifier.text)) return true;
|
|
41763
41750
|
}
|
|
41764
41751
|
if (currentNode === sourceFile) return false;
|
|
@@ -41849,11 +41836,10 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
|
|
|
41849
41836
|
});
|
|
41850
41837
|
return resolution;
|
|
41851
41838
|
};
|
|
41852
|
-
const isScopeNode = isScopeBoundary;
|
|
41853
41839
|
const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, visitedDeclarations = /* @__PURE__ */ new Set()) => {
|
|
41854
41840
|
let currentNode = identifier.parent;
|
|
41855
41841
|
while (currentNode) {
|
|
41856
|
-
if (
|
|
41842
|
+
if (isScopeBoundary(currentNode)) {
|
|
41857
41843
|
const resolution = findResolutionInScope(currentNode, identifier.text, reactImportBindings, sourceFile, visitedDeclarations);
|
|
41858
41844
|
if (resolution) return resolution;
|
|
41859
41845
|
}
|
|
@@ -42101,7 +42087,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
42101
42087
|
child.kill("SIGKILL");
|
|
42102
42088
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
42103
42089
|
kind: "timeout",
|
|
42104
|
-
detail: `${spawnTimeoutMs /
|
|
42090
|
+
detail: `${spawnTimeoutMs / MILLISECONDS_PER_SECOND}s budget exceeded`
|
|
42105
42091
|
}) }));
|
|
42106
42092
|
}, spawnTimeoutMs);
|
|
42107
42093
|
timeoutHandle.unref?.();
|
|
@@ -43223,7 +43209,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43223
43209
|
}))))))));
|
|
43224
43210
|
const deadCodeFailureState = yield* get$2(deadCodeFailure);
|
|
43225
43211
|
const scanElapsedMilliseconds = Date.now() - scanStartTime;
|
|
43226
|
-
const scanElapsedSeconds = (scanElapsedMilliseconds /
|
|
43212
|
+
const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
|
|
43227
43213
|
if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
|
|
43228
43214
|
else if (input.suppressScanSummary) yield* scanProgress.stop();
|
|
43229
43215
|
else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
|
|
@@ -43460,7 +43446,7 @@ var StagedFiles = class StagedFiles extends Service()("react-doctor/StagedFiles"
|
|
|
43460
43446
|
static layerNode = effect(StagedFiles, gen(function* () {
|
|
43461
43447
|
const git = yield* Git;
|
|
43462
43448
|
return StagedFiles.of({
|
|
43463
|
-
discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile))),
|
|
43449
|
+
discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile)), withSpan("StagedFiles.discoverSourceFiles")),
|
|
43464
43450
|
materialize: ({ directory, stagedFiles, tempDirectory }) => materializeSourceTree({
|
|
43465
43451
|
directory,
|
|
43466
43452
|
files: stagedFiles,
|
|
@@ -43470,7 +43456,7 @@ var StagedFiles = class StagedFiles extends Service()("react-doctor/StagedFiles"
|
|
|
43470
43456
|
tempDirectory: tree.tempDirectory,
|
|
43471
43457
|
stagedFiles: tree.materializedFiles,
|
|
43472
43458
|
cleanup: tree.cleanup
|
|
43473
|
-
})))
|
|
43459
|
+
})), withSpan("StagedFiles.materialize"))
|
|
43474
43460
|
});
|
|
43475
43461
|
}));
|
|
43476
43462
|
/**
|
|
@@ -43877,7 +43863,7 @@ const FALSY_CI_FLAG_VALUES = new Set([
|
|
|
43877
43863
|
"false"
|
|
43878
43864
|
]);
|
|
43879
43865
|
const isCiFlagSet = (value) => value !== void 0 && !FALSY_CI_FLAG_VALUES.has(value.toLowerCase());
|
|
43880
|
-
const isCiEnvironment = () => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(
|
|
43866
|
+
const isCiEnvironment = (env = process.env) => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(env[environmentVariable])) || isCiFlagSet(env.CI);
|
|
43881
43867
|
const detectCiProvider = () => {
|
|
43882
43868
|
for (const [environmentVariable, provider] of CI_PROVIDER_BY_ENVIRONMENT_VARIABLE) if (process.env[environmentVariable]) return provider;
|
|
43883
43869
|
return isCiFlagSet(process.env.CI) ? "unknown" : null;
|
|
@@ -43902,6 +43888,53 @@ const detectCodingAgent = () => {
|
|
|
43902
43888
|
const isCodingAgentEnvironment = () => detectCodingAgent() !== null;
|
|
43903
43889
|
const isCiOrCodingAgentEnvironment = () => isCiEnvironment() || isCodingAgentEnvironment();
|
|
43904
43890
|
//#endregion
|
|
43891
|
+
//#region src/cli/utils/detect-terminal-kind.ts
|
|
43892
|
+
const TERMINAL_BY_TERM_PROGRAM = [
|
|
43893
|
+
["vscode", "vscode"],
|
|
43894
|
+
["iTerm.app", "iterm"],
|
|
43895
|
+
["Apple_Terminal", "apple-terminal"],
|
|
43896
|
+
["WezTerm", "wezterm"],
|
|
43897
|
+
["ghostty", "ghostty"],
|
|
43898
|
+
["Hyper", "hyper"],
|
|
43899
|
+
["Tabby", "tabby"],
|
|
43900
|
+
["rio", "rio"]
|
|
43901
|
+
];
|
|
43902
|
+
/**
|
|
43903
|
+
* Best-effort label for the terminal emulator / editor hosting the CLI,
|
|
43904
|
+
* derived from terminal-identity env vars. Recorded as the `terminalKind` run
|
|
43905
|
+
* tag so we can see where React Doctor is actually run (nvim, VS Code, iTerm,
|
|
43906
|
+
* …) — the split Sentry can't otherwise see. Low-cardinality and free of any
|
|
43907
|
+
* username/path/secret, so it's safe as a tag. Editor terminals (nvim/vim)
|
|
43908
|
+
* win over the outer emulator because that's the surface a user is reading in;
|
|
43909
|
+
* "ci" marks a run with no interactive terminal; "unknown" when nothing matches.
|
|
43910
|
+
*/
|
|
43911
|
+
const detectTerminalKind = (env = process.env) => {
|
|
43912
|
+
if (env.NVIM) return "neovim";
|
|
43913
|
+
if (env.VIM_TERMINAL) return "vim";
|
|
43914
|
+
const termProgram = env.TERM_PROGRAM;
|
|
43915
|
+
if (termProgram) {
|
|
43916
|
+
for (const [marker, label] of TERMINAL_BY_TERM_PROGRAM) if (termProgram === marker) return label;
|
|
43917
|
+
}
|
|
43918
|
+
if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return "kitty";
|
|
43919
|
+
if (env.WT_SESSION) return "windows-terminal";
|
|
43920
|
+
if (env.ALACRITTY_WINDOW_ID || env.TERM === "alacritty") return "alacritty";
|
|
43921
|
+
if (env.VTE_VERSION) return "vte";
|
|
43922
|
+
if (env.TMUX) return "tmux";
|
|
43923
|
+
if (isCiEnvironment(env)) return "ci";
|
|
43924
|
+
return "unknown";
|
|
43925
|
+
};
|
|
43926
|
+
//#endregion
|
|
43927
|
+
//#region src/cli/utils/is-debug-flag.ts
|
|
43928
|
+
/**
|
|
43929
|
+
* Whether the user passed `--debug` (surface the run's Sentry trace id, and
|
|
43930
|
+
* force performance tracing on so there's a trace to surface). Read straight
|
|
43931
|
+
* from argv rather than Commander's parsed flags because `initializeSentry()`
|
|
43932
|
+
* runs before Commander parses — the same reason `shouldEnableSentry()` reads
|
|
43933
|
+
* `--no-score` from argv. Sharing this one reader keeps the init-time sampling
|
|
43934
|
+
* override and the end-of-run print in agreement.
|
|
43935
|
+
*/
|
|
43936
|
+
const isDebugFlagEnabled = (argv = process.argv) => argv.includes("--debug");
|
|
43937
|
+
//#endregion
|
|
43905
43938
|
//#region src/cli/utils/is-git-hook-environment.ts
|
|
43906
43939
|
const isGitHookEnvironment = () => Boolean(process.env.GIT_DIR);
|
|
43907
43940
|
//#endregion
|
|
@@ -43924,6 +43957,7 @@ const NON_INTERACTIVE_ENVIRONMENT_VARIABLES = [
|
|
|
43924
43957
|
const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || isCodingAgentEnvironment();
|
|
43925
43958
|
//#endregion
|
|
43926
43959
|
//#region src/cli/utils/constants.ts
|
|
43960
|
+
const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
|
|
43927
43961
|
const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
|
|
43928
43962
|
const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
|
|
43929
43963
|
const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
|
|
@@ -44008,7 +44042,7 @@ const makeNoopConsole = () => ({
|
|
|
44008
44042
|
});
|
|
44009
44043
|
//#endregion
|
|
44010
44044
|
//#region src/cli/utils/version.ts
|
|
44011
|
-
const VERSION = "0.5.6-dev.
|
|
44045
|
+
const VERSION = "0.5.6-dev.50999f4";
|
|
44012
44046
|
//#endregion
|
|
44013
44047
|
//#region src/cli/utils/json-mode.ts
|
|
44014
44048
|
let context = null;
|
|
@@ -44158,7 +44192,9 @@ const buildRunContext = () => {
|
|
|
44158
44192
|
viaAction: isOfficialGithubAction(),
|
|
44159
44193
|
codingAgent: detectCodingAgent(),
|
|
44160
44194
|
interactive: !isNonInteractiveEnvironment(),
|
|
44195
|
+
terminalKind: detectTerminalKind(),
|
|
44161
44196
|
jsonMode: isJsonModeActive(),
|
|
44197
|
+
debug: isDebugFlagEnabled(),
|
|
44162
44198
|
invokedVia: detectInvokedVia()
|
|
44163
44199
|
};
|
|
44164
44200
|
};
|
|
@@ -44228,7 +44264,9 @@ const buildSentryScope = (runContext = buildRunContext()) => {
|
|
|
44228
44264
|
viaAction: runContext.viaAction,
|
|
44229
44265
|
codingAgent: runContext.codingAgent,
|
|
44230
44266
|
interactive: runContext.interactive,
|
|
44267
|
+
terminalKind: runContext.terminalKind,
|
|
44231
44268
|
jsonMode: runContext.jsonMode,
|
|
44269
|
+
debug: runContext.debug,
|
|
44232
44270
|
invokedVia: runContext.invokedVia,
|
|
44233
44271
|
nodeMajor: runContext.nodeMajor
|
|
44234
44272
|
};
|
|
@@ -44366,13 +44404,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
44366
44404
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
44367
44405
|
* standard `SENTRY_RELEASE` override.
|
|
44368
44406
|
*/
|
|
44369
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.
|
|
44407
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.50999f4`;
|
|
44370
44408
|
/**
|
|
44371
44409
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
44372
44410
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
44373
44411
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
44374
44412
|
*/
|
|
44375
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.
|
|
44413
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.50999f4") ? "development" : "production");
|
|
44376
44414
|
/**
|
|
44377
44415
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
44378
44416
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -44436,7 +44474,7 @@ const flushSentry = async () => {
|
|
|
44436
44474
|
const initializeSentry = () => {
|
|
44437
44475
|
if (isInitialized || !shouldEnableSentry()) return;
|
|
44438
44476
|
isInitialized = true;
|
|
44439
|
-
resolvedTracesSampleRate = resolveTracesSampleRate();
|
|
44477
|
+
resolvedTracesSampleRate = isDebugFlagEnabled() ? 1 : resolveTracesSampleRate();
|
|
44440
44478
|
const { tags, contexts } = buildSentryScope();
|
|
44441
44479
|
Sentry.init({
|
|
44442
44480
|
dsn: process.env.SENTRY_DSN || "https://f253d570240a59b8dbd77b7a548ef133@o4510226365743104.ingest.us.sentry.io/4511487817809920",
|
|
@@ -47652,6 +47690,11 @@ const setActiveRunTrace = (trace) => {
|
|
|
47652
47690
|
activeRunTrace = trace;
|
|
47653
47691
|
};
|
|
47654
47692
|
const getActiveRunTrace = () => activeRunTrace;
|
|
47693
|
+
let lastRunTraceId = null;
|
|
47694
|
+
const recordRunTraceId = (traceId) => {
|
|
47695
|
+
lastRunTraceId = traceId;
|
|
47696
|
+
};
|
|
47697
|
+
const getLastRunTraceId = () => lastRunTraceId;
|
|
47655
47698
|
//#endregion
|
|
47656
47699
|
//#region src/cli/utils/to-span-attributes.ts
|
|
47657
47700
|
/**
|
|
@@ -47714,14 +47757,13 @@ const withSentryRunSpan = (run, options = {}) => {
|
|
|
47714
47757
|
op: "cli.inspect",
|
|
47715
47758
|
attributes: toSpanAttributes(tags)
|
|
47716
47759
|
}, (rootSpan) => {
|
|
47717
|
-
|
|
47718
|
-
|
|
47719
|
-
|
|
47720
|
-
|
|
47721
|
-
|
|
47722
|
-
|
|
47723
|
-
|
|
47724
|
-
}
|
|
47760
|
+
const spanContext = rootSpan.spanContext();
|
|
47761
|
+
recordRunTraceId(spanContext.traceId);
|
|
47762
|
+
if (options.concurrentScan !== true) setActiveRunTrace({
|
|
47763
|
+
traceId: spanContext.traceId,
|
|
47764
|
+
spanId: spanContext.spanId,
|
|
47765
|
+
sampled: (spanContext.traceFlags & 1) === 1
|
|
47766
|
+
});
|
|
47725
47767
|
return run(rootSpan);
|
|
47726
47768
|
});
|
|
47727
47769
|
};
|
|
@@ -48229,6 +48271,15 @@ const boxText = (content, innerWidth) => {
|
|
|
48229
48271
|
].join("\n");
|
|
48230
48272
|
};
|
|
48231
48273
|
//#endregion
|
|
48274
|
+
//#region src/cli/utils/resolve-absolute-path.ts
|
|
48275
|
+
/**
|
|
48276
|
+
* Resolves a diagnostic's `filePath` (relative to its project root, or
|
|
48277
|
+
* already absolute) to an absolute path. Shared by the code-frame reader and
|
|
48278
|
+
* the terminal hyperlink builder so both turn a relative path into the same
|
|
48279
|
+
* on-disk location.
|
|
48280
|
+
*/
|
|
48281
|
+
const resolveAbsolutePath = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
|
|
48282
|
+
//#endregion
|
|
48232
48283
|
//#region src/cli/utils/build-code-frame.ts
|
|
48233
48284
|
/**
|
|
48234
48285
|
* Renders a syntax-highlighted source excerpt around a diagnostic site
|
|
@@ -48239,7 +48290,7 @@ const boxText = (content, innerWidth) => {
|
|
|
48239
48290
|
*/
|
|
48240
48291
|
const buildCodeFrame = (input) => {
|
|
48241
48292
|
if (input.line <= 0) return null;
|
|
48242
|
-
const absolutePath =
|
|
48293
|
+
const absolutePath = resolveAbsolutePath(input.filePath, input.rootDirectory);
|
|
48243
48294
|
let source;
|
|
48244
48295
|
try {
|
|
48245
48296
|
source = NFS.readFileSync(absolutePath, "utf8");
|
|
@@ -48279,6 +48330,16 @@ const resolveMeasureWidth = (reservedColumns = 0) => resolveClampedWidth({
|
|
|
48279
48330
|
const DIVIDER_INDENT = " ";
|
|
48280
48331
|
const buildSectionDivider = () => highlighter.dim(`${DIVIDER_INDENT}${"─".repeat(resolveMeasureWidth(2))}`);
|
|
48281
48332
|
//#endregion
|
|
48333
|
+
//#region src/cli/utils/format-hyperlink.ts
|
|
48334
|
+
const OSC = "\x1B]";
|
|
48335
|
+
const ST = "\x1B\\";
|
|
48336
|
+
/**
|
|
48337
|
+
* Wraps `text` in an OSC 8 hyperlink pointing at `uri`. The visible characters
|
|
48338
|
+
* are exactly `text`; the link is carried in escape sequences a capable
|
|
48339
|
+
* terminal turns into a click target.
|
|
48340
|
+
*/
|
|
48341
|
+
const formatHyperlink = (text, uri) => `${OSC}8;;${uri}${ST}${text}${OSC}8;;${ST}`;
|
|
48342
|
+
//#endregion
|
|
48282
48343
|
//#region src/cli/utils/indent-multiline-text.ts
|
|
48283
48344
|
const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
|
|
48284
48345
|
//#endregion
|
|
@@ -48432,17 +48493,23 @@ const clusterNearbyDiagnostics = (diagnostics) => {
|
|
|
48432
48493
|
}
|
|
48433
48494
|
return clusters;
|
|
48434
48495
|
};
|
|
48435
|
-
const
|
|
48496
|
+
const formatClusterLocationText = (cluster) => {
|
|
48497
|
+
const { filePath } = cluster.diagnostics[0];
|
|
48498
|
+
if (cluster.startLine <= 0) return filePath;
|
|
48499
|
+
if (cluster.endLine > cluster.startLine) return `${filePath}:${cluster.startLine}-${cluster.endLine}`;
|
|
48500
|
+
return `${filePath}:${cluster.startLine}`;
|
|
48501
|
+
};
|
|
48502
|
+
const formatClusterLocation = (cluster, resolveSourceRoot, hyperlinks) => {
|
|
48436
48503
|
const lead = cluster.diagnostics[0];
|
|
48437
48504
|
const contextTag = formatFileContextTag(lead);
|
|
48438
|
-
|
|
48439
|
-
if (
|
|
48440
|
-
return `${lead.filePath
|
|
48505
|
+
const location = formatClusterLocationText(cluster);
|
|
48506
|
+
if (!hyperlinks) return `${location}${contextTag}`;
|
|
48507
|
+
return `${formatHyperlink(location, pathToFileURL(resolveAbsolutePath(lead.filePath, resolveSourceRoot(lead))).href)}${contextTag}`;
|
|
48441
48508
|
};
|
|
48442
|
-
const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame) => {
|
|
48509
|
+
const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame, hyperlinks) => {
|
|
48443
48510
|
const lead = cluster.diagnostics[0];
|
|
48444
48511
|
const isMultiSite = cluster.diagnostics.length > 1;
|
|
48445
|
-
const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster)}`)];
|
|
48512
|
+
const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster, resolveSourceRoot, hyperlinks)}`)];
|
|
48446
48513
|
const codeFrame = renderCodeFrame ? buildCodeFrame({
|
|
48447
48514
|
filePath: lead.filePath,
|
|
48448
48515
|
line: cluster.startLine,
|
|
@@ -48461,7 +48528,7 @@ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame
|
|
|
48461
48528
|
}
|
|
48462
48529
|
return lines;
|
|
48463
48530
|
};
|
|
48464
|
-
const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment) => {
|
|
48531
|
+
const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment, hyperlinks) => {
|
|
48465
48532
|
const representative = pickRepresentativeDiagnostic(ruleDiagnostics);
|
|
48466
48533
|
const { severity } = representative;
|
|
48467
48534
|
const trailingBadge = formatTrailingSiteBadge(ruleDiagnostics.length);
|
|
@@ -48481,7 +48548,7 @@ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, rende
|
|
|
48481
48548
|
}
|
|
48482
48549
|
const renderCodeFrame = severity === "error";
|
|
48483
48550
|
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));
|
|
48551
|
+
if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame, hyperlinks));
|
|
48485
48552
|
return lines;
|
|
48486
48553
|
};
|
|
48487
48554
|
const selectErrorRuleGroups = (diagnostics, rulePriority) => buildSortedRuleGroups(diagnostics.filter((diagnostic) => diagnostic.severity === "error"), rulePriority);
|
|
@@ -48494,7 +48561,7 @@ const buildOverflowSummaryLine = (diagnostics, rulePriority) => {
|
|
|
48494
48561
|
return ` ${highlighter.dim("Run")} ${command} ${highlighter.dim("to list every error and warning")}`;
|
|
48495
48562
|
};
|
|
48496
48563
|
const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(selectTopErrorRuleGroups(diagnostics, limit, rulePriority).map(([ruleKey]) => ruleKey));
|
|
48497
|
-
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) => {
|
|
48564
|
+
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, hyperlinks, rulePriority) => {
|
|
48498
48565
|
const topRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority).slice(0, 3);
|
|
48499
48566
|
if (topRuleGroups.length === 0) return {
|
|
48500
48567
|
lines: [],
|
|
@@ -48504,7 +48571,7 @@ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) =>
|
|
|
48504
48571
|
const blockOffsets = [];
|
|
48505
48572
|
for (const [ruleKey, ruleDiagnostics] of topRuleGroups) {
|
|
48506
48573
|
blockOffsets.push(lines.length);
|
|
48507
|
-
lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false));
|
|
48574
|
+
lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false, hyperlinks));
|
|
48508
48575
|
lines.push("");
|
|
48509
48576
|
}
|
|
48510
48577
|
return {
|
|
@@ -48542,18 +48609,18 @@ const buildOverviewHeaderLines = (diagnostics) => {
|
|
|
48542
48609
|
* single Effect.forEach over Console.log so failures or fiber
|
|
48543
48610
|
* interruption produce predictable partial output.
|
|
48544
48611
|
*/
|
|
48545
|
-
const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}) => gen(function* () {
|
|
48612
|
+
const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}, hyperlinks = false) => gen(function* () {
|
|
48546
48613
|
const sectionPause = onboarding.sectionPause ?? void_;
|
|
48547
48614
|
const animateCountUp = onboarding.animateCountUp ?? false;
|
|
48548
48615
|
const resolveSourceRoot = typeof sourceRoot === "function" ? sourceRoot : () => sourceRoot;
|
|
48549
48616
|
let detailLines;
|
|
48550
48617
|
let topErrorBlockOffsets = [];
|
|
48551
48618
|
if (!isVerbose) {
|
|
48552
|
-
const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, rulePriority);
|
|
48619
|
+
const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, hyperlinks, rulePriority);
|
|
48553
48620
|
detailLines = topErrors.lines;
|
|
48554
48621
|
topErrorBlockOffsets = topErrors.blockOffsets;
|
|
48555
48622
|
} else detailLines = buildSortedRuleGroups(diagnostics, rulePriority).flatMap(([ruleKey, ruleDiagnostics]) => {
|
|
48556
|
-
return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment), ""];
|
|
48623
|
+
return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment, hyperlinks), ""];
|
|
48557
48624
|
});
|
|
48558
48625
|
const overflowLine = isVerbose ? void 0 : buildOverflowSummaryLine(diagnostics, rulePriority);
|
|
48559
48626
|
const categoryTallies = buildCategoryDiagnosticGroups(diagnostics, rulePriority).map(buildCategoryTally);
|
|
@@ -48614,6 +48681,48 @@ const computeProjectedScore = async (topErrorSource, rescoreSource, currentScore
|
|
|
48614
48681
|
//#endregion
|
|
48615
48682
|
//#region src/cli/utils/filter-diagnostics-by-categories.ts
|
|
48616
48683
|
const filterDiagnosticsByCategories = (diagnostics, categories) => categories.size === 0 ? [...diagnostics] : diagnostics.filter((diagnostic) => categories.has(diagnostic.category));
|
|
48684
|
+
//#endregion
|
|
48685
|
+
//#region src/cli/utils/supports-hyperlinks.ts
|
|
48686
|
+
const HYPERLINK_CAPABLE_TERM_PROGRAMS = new Set([
|
|
48687
|
+
"iTerm.app",
|
|
48688
|
+
"WezTerm",
|
|
48689
|
+
"vscode",
|
|
48690
|
+
"Hyper",
|
|
48691
|
+
"ghostty",
|
|
48692
|
+
"Tabby",
|
|
48693
|
+
"rio"
|
|
48694
|
+
]);
|
|
48695
|
+
const parseVteVersion = (raw) => {
|
|
48696
|
+
const parsed = Number.parseInt(raw ?? "", 10);
|
|
48697
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
48698
|
+
};
|
|
48699
|
+
/**
|
|
48700
|
+
* Whether `stream` is a terminal that renders OSC 8 hyperlinks. Auto-detected
|
|
48701
|
+
* from terminal-identity env vars; the de-facto `FORCE_HYPERLINK` env var
|
|
48702
|
+
* overrides detection (`FORCE_HYPERLINK=0`/`false` forces off, any other value
|
|
48703
|
+
* forces on), mirroring how the ecosystem's terminal libraries gate the same
|
|
48704
|
+
* feature. Off for non-TTYs, `TERM=dumb`, and CI (whose log viewers render the
|
|
48705
|
+
* raw escape rather than a link). Unknown terminals default to off.
|
|
48706
|
+
*/
|
|
48707
|
+
const supportsHyperlinks = (stream = process.stdout, env = process.env) => {
|
|
48708
|
+
const forced = env.FORCE_HYPERLINK;
|
|
48709
|
+
if (forced !== void 0 && forced !== "") return forced !== "0" && forced.toLowerCase() !== "false";
|
|
48710
|
+
if (stream.isTTY !== true) return false;
|
|
48711
|
+
if (env.TERM === "dumb") return false;
|
|
48712
|
+
if (isCiEnvironment(env)) return false;
|
|
48713
|
+
if (env.WT_SESSION) return true;
|
|
48714
|
+
if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return true;
|
|
48715
|
+
if (parseVteVersion(env.VTE_VERSION) >= 5e3) return true;
|
|
48716
|
+
return Boolean(env.TERM_PROGRAM && HYPERLINK_CAPABLE_TERM_PROGRAMS.has(env.TERM_PROGRAM));
|
|
48717
|
+
};
|
|
48718
|
+
//#endregion
|
|
48719
|
+
//#region src/cli/utils/should-render-hyperlinks.ts
|
|
48720
|
+
/**
|
|
48721
|
+
* Whether to emit OSC 8 clickable `file:line` locations for this run: a
|
|
48722
|
+
* hyperlink-capable terminal AND not a coding agent (whose output parsers
|
|
48723
|
+
* would choke on the escape sequences).
|
|
48724
|
+
*/
|
|
48725
|
+
const shouldRenderHyperlinks = (stream = process.stdout) => supportsHyperlinks(stream) && !isCodingAgentEnvironment();
|
|
48617
48726
|
const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
|
|
48618
48727
|
const FALSY_FLAG_VALUES = new Set([
|
|
48619
48728
|
"",
|
|
@@ -48633,10 +48742,9 @@ const canAnimateOnboarding = (stream = process.stdout) => {
|
|
|
48633
48742
|
};
|
|
48634
48743
|
//#endregion
|
|
48635
48744
|
//#region src/cli/utils/onboarding-state.ts
|
|
48636
|
-
const GLOBAL_CONFIG_PROJECT_NAME$2 = "react-doctor";
|
|
48637
48745
|
const ONBOARDED_AT_KEY = "onboardedAt";
|
|
48638
48746
|
const getOnboardingStore = (options = {}) => new Conf({
|
|
48639
|
-
projectName:
|
|
48747
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
48640
48748
|
cwd: options.cwd
|
|
48641
48749
|
});
|
|
48642
48750
|
const hasCompletedOnboarding = (options = {}) => {
|
|
@@ -49092,6 +49200,78 @@ const resolveCliCategories = (categoryFlag) => {
|
|
|
49092
49200
|
return resolvedCategories.length > 0 ? resolvedCategories : void 0;
|
|
49093
49201
|
};
|
|
49094
49202
|
//#endregion
|
|
49203
|
+
//#region src/cli/utils/git-hook-shared.ts
|
|
49204
|
+
const HOOK_FILE_NAME = "pre-commit";
|
|
49205
|
+
const HOOK_RELATIVE_PATH = "hooks/pre-commit";
|
|
49206
|
+
const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
|
|
49207
|
+
const HUSKY_HOOKS_PATH = ".husky";
|
|
49208
|
+
const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
|
|
49209
|
+
const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
|
|
49210
|
+
const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
|
|
49211
|
+
const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
|
|
49212
|
+
const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
|
|
49213
|
+
const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
|
|
49214
|
+
"react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
|
|
49215
|
+
`if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
|
|
49216
|
+
"rm -f \"$react_doctor_output\";",
|
|
49217
|
+
"else",
|
|
49218
|
+
"rm -f \"$react_doctor_output\";",
|
|
49219
|
+
`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;`,
|
|
49220
|
+
"fi"
|
|
49221
|
+
].join(" ");
|
|
49222
|
+
const PACKAGE_JSON_FILE_NAME = "package.json";
|
|
49223
|
+
const runGit = (projectRoot, args) => {
|
|
49224
|
+
try {
|
|
49225
|
+
return execFileSync("git", [...args], {
|
|
49226
|
+
cwd: projectRoot,
|
|
49227
|
+
encoding: "utf8",
|
|
49228
|
+
stdio: [
|
|
49229
|
+
"ignore",
|
|
49230
|
+
"pipe",
|
|
49231
|
+
"ignore"
|
|
49232
|
+
]
|
|
49233
|
+
}).trim();
|
|
49234
|
+
} catch {
|
|
49235
|
+
return null;
|
|
49236
|
+
}
|
|
49237
|
+
};
|
|
49238
|
+
const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
|
|
49239
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49240
|
+
const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
|
|
49241
|
+
const readPackageJson = (projectRoot) => {
|
|
49242
|
+
try {
|
|
49243
|
+
return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
|
|
49244
|
+
} catch {
|
|
49245
|
+
return null;
|
|
49246
|
+
}
|
|
49247
|
+
};
|
|
49248
|
+
const writeJsonFile$1 = (filePath, value) => {
|
|
49249
|
+
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
49250
|
+
};
|
|
49251
|
+
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
49252
|
+
const packageJson = readPackageJson(projectRoot);
|
|
49253
|
+
if (!isRecord$1(packageJson)) return false;
|
|
49254
|
+
return [
|
|
49255
|
+
"dependencies",
|
|
49256
|
+
"devDependencies",
|
|
49257
|
+
"optionalDependencies"
|
|
49258
|
+
].some((fieldName) => {
|
|
49259
|
+
const dependencies = packageJson[fieldName];
|
|
49260
|
+
return isRecord$1(dependencies) && typeof dependencies[dependencyName] === "string";
|
|
49261
|
+
});
|
|
49262
|
+
};
|
|
49263
|
+
const packageHasRecordKey = (projectRoot, key) => {
|
|
49264
|
+
const packageJson = readPackageJson(projectRoot);
|
|
49265
|
+
return isRecord$1(packageJson) && isRecord$1(packageJson[key]);
|
|
49266
|
+
};
|
|
49267
|
+
const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
|
|
49268
|
+
const packageJson = readPackageJson(projectRoot);
|
|
49269
|
+
if (!isRecord$1(packageJson)) return false;
|
|
49270
|
+
const value = packageJson[key];
|
|
49271
|
+
return isRecord$1(value) && isRecord$1(value[nestedKey]);
|
|
49272
|
+
};
|
|
49273
|
+
const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
|
|
49274
|
+
//#endregion
|
|
49095
49275
|
//#region src/cli/utils/scan-result-cache.ts
|
|
49096
49276
|
const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
|
|
49097
49277
|
const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
@@ -49102,7 +49282,7 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
|
49102
49282
|
"eslint-plugin-react-hooks/package.json"
|
|
49103
49283
|
];
|
|
49104
49284
|
const bundledRequire = createRequire(import.meta.url);
|
|
49105
|
-
const isRecord
|
|
49285
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49106
49286
|
const normalizeForStableJson = (value) => {
|
|
49107
49287
|
if (value === null) return null;
|
|
49108
49288
|
if (value === void 0) return void 0;
|
|
@@ -49131,24 +49311,9 @@ const stringifyStableJson = (value) => {
|
|
|
49131
49311
|
}
|
|
49132
49312
|
};
|
|
49133
49313
|
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"]);
|
|
49314
|
+
const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
|
|
49150
49315
|
const isWorktreeClean = (projectDirectory) => {
|
|
49151
|
-
const status = runGit
|
|
49316
|
+
const status = runGit(projectDirectory, [
|
|
49152
49317
|
"status",
|
|
49153
49318
|
"--porcelain=v1",
|
|
49154
49319
|
"--untracked-files=normal"
|
|
@@ -49156,7 +49321,7 @@ const isWorktreeClean = (projectDirectory) => {
|
|
|
49156
49321
|
return status !== null && status.length === 0;
|
|
49157
49322
|
};
|
|
49158
49323
|
const hasHiddenTrackedFileState = (projectDirectory) => {
|
|
49159
|
-
const output = runGit
|
|
49324
|
+
const output = runGit(projectDirectory, ["ls-files", "-v"]);
|
|
49160
49325
|
if (output === null) return true;
|
|
49161
49326
|
return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
|
|
49162
49327
|
};
|
|
@@ -49169,7 +49334,7 @@ const resolveCacheFilePath = (projectDirectory) => {
|
|
|
49169
49334
|
const readPersistedCache = (cacheFilePath) => {
|
|
49170
49335
|
try {
|
|
49171
49336
|
const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
|
|
49172
|
-
if (!isRecord
|
|
49337
|
+
if (!isRecord(parsed) || parsed.version !== 1) return {
|
|
49173
49338
|
version: 1,
|
|
49174
49339
|
entries: []
|
|
49175
49340
|
};
|
|
@@ -49179,8 +49344,8 @@ const readPersistedCache = (cacheFilePath) => {
|
|
|
49179
49344
|
};
|
|
49180
49345
|
const entries = [];
|
|
49181
49346
|
for (const entry of parsed.entries) {
|
|
49182
|
-
if (!isRecord
|
|
49183
|
-
if (!isRecord
|
|
49347
|
+
if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
|
|
49348
|
+
if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
|
|
49184
49349
|
entries.push(entry);
|
|
49185
49350
|
}
|
|
49186
49351
|
return {
|
|
@@ -49712,6 +49877,7 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49712
49877
|
}
|
|
49713
49878
|
const animateRender = !options.silent && !options.verbose && canAnimateOnboarding(process.stdout);
|
|
49714
49879
|
const pause = onboardingSectionPause(animateRender);
|
|
49880
|
+
const useHyperlinks = shouldRenderHyperlinks(process.stdout);
|
|
49715
49881
|
const demotedDiagnosticCount = diagnostics.length - surfaceDiagnostics.length;
|
|
49716
49882
|
const lintSourceFileCount = options.includePaths.length > 0 ? options.includePaths.length : project.sourceFileCount;
|
|
49717
49883
|
if (printedDiagnostics.length === 0) {
|
|
@@ -49737,7 +49903,7 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49737
49903
|
yield* printDiagnostics([...printedDiagnostics], options.verbose, directory, buildRulePriorityMap([score]), isCodingAgentEnvironment(), {
|
|
49738
49904
|
sectionPause: pause,
|
|
49739
49905
|
animateCountUp: animateRender
|
|
49740
|
-
});
|
|
49906
|
+
}, useHyperlinks);
|
|
49741
49907
|
if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
|
|
49742
49908
|
if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
|
|
49743
49909
|
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 +50096,7 @@ const buildHandoffPayload = (input) => {
|
|
|
49930
50096
|
try {
|
|
49931
50097
|
outputDirectory = writeDiagnosticsDirectory([...input.diagnostics], input.outputDirectory);
|
|
49932
50098
|
} 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
|
-
];
|
|
50099
|
+
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
50100
|
topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
|
|
49944
50101
|
const representative = ruleDiagnostics[0];
|
|
49945
50102
|
const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
|
|
@@ -50000,78 +50157,6 @@ const detectAvailableAgents = async () => {
|
|
|
50000
50157
|
return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
|
|
50001
50158
|
};
|
|
50002
50159
|
//#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
50160
|
//#region src/cli/utils/install-doctor-script.ts
|
|
50076
50161
|
const DOCTOR_SCRIPT_NAME = "doctor";
|
|
50077
50162
|
const FALLBACK_DOCTOR_SCRIPT_NAME = "react-doctor";
|
|
@@ -50097,31 +50182,31 @@ const findNearestPackageDirectory = (startDirectory, stopDirectory) => {
|
|
|
50097
50182
|
};
|
|
50098
50183
|
const hasDoctorScript = (projectRoot) => {
|
|
50099
50184
|
const packageJson = readPackageJson(findNearestPackageDirectory(projectRoot) ?? projectRoot);
|
|
50100
|
-
if (!isRecord(packageJson)) return false;
|
|
50185
|
+
if (!isRecord$1(packageJson)) return false;
|
|
50101
50186
|
const scripts = packageJson.scripts;
|
|
50102
|
-
if (!isRecord(scripts)) return false;
|
|
50187
|
+
if (!isRecord$1(scripts)) return false;
|
|
50103
50188
|
return isReactDoctorScriptCommand(scripts[DOCTOR_SCRIPT_NAME]) || isReactDoctorScriptCommand(scripts[FALLBACK_DOCTOR_SCRIPT_NAME]);
|
|
50104
50189
|
};
|
|
50105
50190
|
const hasDoctorDependency = (packageJson) => DEPENDENCY_FIELD_NAMES.some((fieldName) => {
|
|
50106
50191
|
const dependencies = packageJson[fieldName];
|
|
50107
|
-
return isRecord(dependencies) && Object.hasOwn(dependencies, "react-doctor");
|
|
50192
|
+
return isRecord$1(dependencies) && Object.hasOwn(dependencies, "react-doctor");
|
|
50108
50193
|
});
|
|
50109
50194
|
const installDoctorScript = (options) => {
|
|
50110
50195
|
const packageDirectory = findNearestPackageDirectory(options.projectRoot) ?? options.projectRoot;
|
|
50111
50196
|
const packageJsonPath = getPackageJsonPath(packageDirectory);
|
|
50112
50197
|
const packageJson = readPackageJson(packageDirectory);
|
|
50113
|
-
if (!isRecord(packageJson)) return {
|
|
50198
|
+
if (!isRecord$1(packageJson)) return {
|
|
50114
50199
|
packageJsonPath,
|
|
50115
50200
|
scriptStatus: "skipped",
|
|
50116
50201
|
scriptReason: "missing-or-invalid-package-json"
|
|
50117
50202
|
};
|
|
50118
50203
|
const scripts = packageJson.scripts;
|
|
50119
50204
|
const scriptTarget = (() => {
|
|
50120
|
-
if (scripts !== void 0 && !isRecord(scripts)) return {
|
|
50205
|
+
if (scripts !== void 0 && !isRecord$1(scripts)) return {
|
|
50121
50206
|
status: "skipped",
|
|
50122
50207
|
reason: "invalid-scripts"
|
|
50123
50208
|
};
|
|
50124
|
-
const scriptRecord = isRecord(scripts) ? scripts : {};
|
|
50209
|
+
const scriptRecord = isRecord$1(scripts) ? scripts : {};
|
|
50125
50210
|
if (isReactDoctorScriptCommand(scriptRecord[DOCTOR_SCRIPT_NAME])) return {
|
|
50126
50211
|
scriptName: DOCTOR_SCRIPT_NAME,
|
|
50127
50212
|
status: "existing"
|
|
@@ -50155,7 +50240,7 @@ const installDoctorScript = (options) => {
|
|
|
50155
50240
|
if (scriptStatus === "created") writeJsonFile$1(packageJsonPath, {
|
|
50156
50241
|
...packageJson,
|
|
50157
50242
|
scripts: {
|
|
50158
|
-
...isRecord(scripts) ? scripts : {},
|
|
50243
|
+
...isRecord$1(scripts) ? scripts : {},
|
|
50159
50244
|
[scriptTarget.scriptName ?? DOCTOR_SCRIPT_NAME]: DOCTOR_SCRIPT_COMMAND
|
|
50160
50245
|
}
|
|
50161
50246
|
});
|
|
@@ -50309,38 +50394,52 @@ const upgradeReactDoctorWorkflowInPlace = (projectRoot) => {
|
|
|
50309
50394
|
//#region src/cli/utils/hash-project-root.ts
|
|
50310
50395
|
const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
|
|
50311
50396
|
//#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()
|
|
50397
|
+
//#region src/cli/utils/project-decision-store.ts
|
|
50398
|
+
const createProjectDecisionStore = (storeKey) => {
|
|
50399
|
+
const getStore = (options = {}) => new Conf({
|
|
50400
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
50401
|
+
cwd: options.cwd
|
|
50402
|
+
});
|
|
50403
|
+
return {
|
|
50404
|
+
getConfigPath: (options = {}) => getStore(options).path,
|
|
50405
|
+
hasHandled: (projectRoot, options = {}) => {
|
|
50406
|
+
try {
|
|
50407
|
+
return Boolean(getStore(options).get(storeKey, {})[hashProjectRoot(projectRoot)]);
|
|
50408
|
+
} catch {
|
|
50409
|
+
return true;
|
|
50336
50410
|
}
|
|
50337
|
-
}
|
|
50338
|
-
|
|
50339
|
-
|
|
50340
|
-
|
|
50341
|
-
|
|
50411
|
+
},
|
|
50412
|
+
record: (projectRoot, outcome, options = {}) => {
|
|
50413
|
+
try {
|
|
50414
|
+
const store = getStore(options);
|
|
50415
|
+
store.set(storeKey, {
|
|
50416
|
+
...store.get(storeKey, {}),
|
|
50417
|
+
[hashProjectRoot(projectRoot)]: {
|
|
50418
|
+
rootDirectory: Path.resolve(projectRoot),
|
|
50419
|
+
outcome,
|
|
50420
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
50421
|
+
}
|
|
50422
|
+
});
|
|
50423
|
+
return true;
|
|
50424
|
+
} catch {
|
|
50425
|
+
return false;
|
|
50426
|
+
}
|
|
50427
|
+
}
|
|
50428
|
+
};
|
|
50342
50429
|
};
|
|
50343
50430
|
//#endregion
|
|
50431
|
+
//#region src/cli/utils/action-upgrade-prompt.ts
|
|
50432
|
+
const store$1 = createProjectDecisionStore("actionUpgrades");
|
|
50433
|
+
store$1.getConfigPath;
|
|
50434
|
+
const hasHandledActionUpgrade = store$1.hasHandled;
|
|
50435
|
+
const recordActionUpgradeDecision = store$1.record;
|
|
50436
|
+
//#endregion
|
|
50437
|
+
//#region src/cli/utils/ci-prompt-decision.ts
|
|
50438
|
+
const store = createProjectDecisionStore("ciPrompts");
|
|
50439
|
+
store.getConfigPath;
|
|
50440
|
+
const hasHandledCiPrompt = store.hasHandled;
|
|
50441
|
+
const recordCiPromptDecision = store.record;
|
|
50442
|
+
//#endregion
|
|
50344
50443
|
//#region src/cli/utils/open-url.ts
|
|
50345
50444
|
const resolveOpenCommand = (url) => {
|
|
50346
50445
|
if (process$1.platform === "darwin") return {
|
|
@@ -50969,13 +51068,13 @@ const installPackageJsonHook = (options, strategy) => {
|
|
|
50969
51068
|
const packageJsonPath = getPackageJsonPath(options.projectRoot);
|
|
50970
51069
|
const didHookExist = NFS.existsSync(packageJsonPath);
|
|
50971
51070
|
const packageJson = readPackageJson(options.projectRoot);
|
|
50972
|
-
const nextPackageJson = isRecord(packageJson) ? { ...packageJson } : {};
|
|
51071
|
+
const nextPackageJson = isRecord$1(packageJson) ? { ...packageJson } : {};
|
|
50973
51072
|
const parentKeys = strategy.path.slice(0, -1);
|
|
50974
51073
|
const leafKey = strategy.path[strategy.path.length - 1];
|
|
50975
51074
|
let parent = nextPackageJson;
|
|
50976
51075
|
for (const key of parentKeys) {
|
|
50977
51076
|
const existing = parent[key];
|
|
50978
|
-
const cloned = isRecord(existing) ? { ...existing } : {};
|
|
51077
|
+
const cloned = isRecord$1(existing) ? { ...existing } : {};
|
|
50979
51078
|
parent[key] = cloned;
|
|
50980
51079
|
parent = cloned;
|
|
50981
51080
|
}
|
|
@@ -51146,7 +51245,7 @@ const isHuskyProject = (projectRoot) => NFS.existsSync(Path.join(projectRoot, ".
|
|
|
51146
51245
|
const isVitePlusProject = (projectRoot) => packageHasDependency(projectRoot, "vite-plus");
|
|
51147
51246
|
const isSimpleGitHooksProject = (projectRoot) => {
|
|
51148
51247
|
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"));
|
|
51248
|
+
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
51249
|
};
|
|
51151
51250
|
const getLefthookConfigPath = (projectRoot) => {
|
|
51152
51251
|
for (const fileName of LEFTHOOK_CONFIG_FILES) {
|
|
@@ -51312,7 +51411,7 @@ const detectPackageManager = (projectRoot) => {
|
|
|
51312
51411
|
let currentDirectory = Path.resolve(projectRoot);
|
|
51313
51412
|
while (true) {
|
|
51314
51413
|
const packageJson = readPackageJson(currentDirectory);
|
|
51315
|
-
if (isRecord(packageJson) && typeof packageJson.packageManager === "string") {
|
|
51414
|
+
if (isRecord$1(packageJson) && typeof packageJson.packageManager === "string") {
|
|
51316
51415
|
const packageManagerName = packageJson.packageManager.split("@")[0];
|
|
51317
51416
|
if (packageManagerName === "pnpm" || packageManagerName === "yarn" || packageManagerName === "bun" || packageManagerName === "npm") return packageManagerName;
|
|
51318
51417
|
}
|
|
@@ -51388,12 +51487,12 @@ const isSupplyChainTrustError = (error) => {
|
|
|
51388
51487
|
const formatInstallCommand = (input) => [input.command, ...input.args].join(" ");
|
|
51389
51488
|
const installReactDoctorDependency = async (options) => {
|
|
51390
51489
|
const packageJson = readPackageJson(options.projectRoot);
|
|
51391
|
-
if (!isRecord(packageJson)) return {
|
|
51490
|
+
if (!isRecord$1(packageJson)) return {
|
|
51392
51491
|
dependencyStatus: "skipped",
|
|
51393
51492
|
dependencyReason: "missing-or-invalid-package-json"
|
|
51394
51493
|
};
|
|
51395
51494
|
if (hasDoctorDependency(packageJson)) return { dependencyStatus: "existing" };
|
|
51396
|
-
if (packageJson.devDependencies !== void 0 && !isRecord(packageJson.devDependencies)) return {
|
|
51495
|
+
if (packageJson.devDependencies !== void 0 && !isRecord$1(packageJson.devDependencies)) return {
|
|
51397
51496
|
dependencyStatus: "skipped",
|
|
51398
51497
|
dependencyReason: "invalid-dev-dependencies"
|
|
51399
51498
|
};
|
|
@@ -51557,10 +51656,12 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
51557
51656
|
const existingWorkflow = readReactDoctorWorkflow(projectRoot);
|
|
51558
51657
|
const canInstallWorkflow = !NFS.existsSync(workflowTargetPath);
|
|
51559
51658
|
const canUpgradeWorkflow = existingWorkflow !== null && workflowUsesV1Action(existingWorkflow.content) && !hasHandledActionUpgrade(projectRoot);
|
|
51560
|
-
const
|
|
51659
|
+
const ciPromptOutcome = canInstallWorkflow && !options.yes && !skipPrompts && !hasHandledCiPrompt(projectRoot) ? await askAddToGitHubActions(prompt) : null;
|
|
51660
|
+
const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || ciPromptOutcome === "yes");
|
|
51561
51661
|
const upgradePromptOutcome = canUpgradeWorkflow && !options.yes && !skipPrompts ? await askUpgradeActionVersion(prompt) : null;
|
|
51562
51662
|
const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
|
|
51563
51663
|
if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
|
|
51664
|
+
if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
|
|
51564
51665
|
const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
|
|
51565
51666
|
type: "multiselect",
|
|
51566
51667
|
name: "agents",
|
|
@@ -51807,18 +51908,24 @@ const handoffToAgent = async (input) => {
|
|
|
51807
51908
|
if (!input.interactive || input.diagnostics.length === 0) return;
|
|
51808
51909
|
cliLogger.break();
|
|
51809
51910
|
const projectRootForCi = findNearestPackageDirectory(input.rootDirectory) ?? input.rootDirectory;
|
|
51810
|
-
|
|
51911
|
+
const isGitHubActionsConfigured = isReactDoctorWorkflowInstalled(projectRootForCi);
|
|
51912
|
+
if (!isGitHubActionsConfigured && !hasHandledCiPrompt(projectRootForCi)) {
|
|
51811
51913
|
const ciOutcome = await askAddToGitHubActions();
|
|
51812
51914
|
recordCount(METRIC.agentHandoff, 1, {
|
|
51813
51915
|
outcome: `ci-${ciOutcome}`,
|
|
51814
51916
|
diagnosticsCount: input.diagnostics.length
|
|
51815
51917
|
});
|
|
51816
51918
|
if (ciOutcome === "cancel") return;
|
|
51919
|
+
recordCiPromptDecision(projectRootForCi, ciOutcome === "yes" ? "accepted" : "declined");
|
|
51817
51920
|
if (ciOutcome === "yes") {
|
|
51818
51921
|
await setUpGitHubActions({ rootDirectory: input.rootDirectory });
|
|
51819
51922
|
cliLogger.break();
|
|
51820
51923
|
}
|
|
51821
|
-
} else await maybeOfferActionUpgrade(projectRootForCi);
|
|
51924
|
+
} else if (isGitHubActionsConfigured) await maybeOfferActionUpgrade(projectRootForCi);
|
|
51925
|
+
else recordCount(METRIC.agentHandoff, 1, {
|
|
51926
|
+
outcome: "ci-suppressed",
|
|
51927
|
+
diagnosticsCount: input.diagnostics.length
|
|
51928
|
+
});
|
|
51822
51929
|
const { handoffTarget } = await prompts({
|
|
51823
51930
|
type: "select",
|
|
51824
51931
|
name: "handoffTarget",
|
|
@@ -52041,6 +52148,7 @@ const reportErrorToSentry = async (error) => {
|
|
|
52041
52148
|
sampled: runTrace.sampled,
|
|
52042
52149
|
sampleRand: Math.random()
|
|
52043
52150
|
});
|
|
52151
|
+
recordRunTraceId(scope.getPropagationContext().traceId);
|
|
52044
52152
|
return Sentry.captureException(error);
|
|
52045
52153
|
});
|
|
52046
52154
|
await Sentry.flush(SENTRY_FLUSH_TIMEOUT_MS);
|
|
@@ -52124,7 +52232,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52124
52232
|
yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
|
|
52125
52233
|
if (displayDiagnostics.length > 0) {
|
|
52126
52234
|
yield* log("");
|
|
52127
|
-
yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender });
|
|
52235
|
+
yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender }, shouldRenderHyperlinks(process.stdout));
|
|
52128
52236
|
}
|
|
52129
52237
|
const lowestScoredScan = findLowestScoredScan(completedScans);
|
|
52130
52238
|
const aggregateScore = lowestScoredScan?.result.score ?? null;
|
|
@@ -52162,9 +52270,8 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52162
52270
|
});
|
|
52163
52271
|
//#endregion
|
|
52164
52272
|
//#region src/cli/utils/prompt-install-setup.ts
|
|
52165
|
-
const GLOBAL_CONFIG_PROJECT_NAME = "react-doctor";
|
|
52166
52273
|
const getSetupPromptStore = (options = {}) => new Conf({
|
|
52167
|
-
projectName:
|
|
52274
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
52168
52275
|
cwd: options.cwd
|
|
52169
52276
|
});
|
|
52170
52277
|
const getSetupPromptProjectKey = (projectRoot) => hashProjectRoot(projectRoot);
|
|
@@ -52175,6 +52282,24 @@ const hasDisabledSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
|
52175
52282
|
return false;
|
|
52176
52283
|
}
|
|
52177
52284
|
};
|
|
52285
|
+
const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
52286
|
+
try {
|
|
52287
|
+
const store = getSetupPromptStore(storeOptions);
|
|
52288
|
+
const projects = store.get("projects", {});
|
|
52289
|
+
const projectKey = getSetupPromptProjectKey(projectRoot);
|
|
52290
|
+
store.set("projects", {
|
|
52291
|
+
...projects,
|
|
52292
|
+
[projectKey]: {
|
|
52293
|
+
...projects[projectKey] ?? {},
|
|
52294
|
+
rootDirectory: Path.resolve(projectRoot),
|
|
52295
|
+
setupPrompt: false
|
|
52296
|
+
}
|
|
52297
|
+
});
|
|
52298
|
+
return true;
|
|
52299
|
+
} catch {
|
|
52300
|
+
return false;
|
|
52301
|
+
}
|
|
52302
|
+
};
|
|
52178
52303
|
const resolveInstallSetupProjectRoot = (options) => {
|
|
52179
52304
|
if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
|
|
52180
52305
|
const packageDirectories = /* @__PURE__ */ new Set();
|
|
@@ -52581,6 +52706,14 @@ const runExplain = async (fileLineArgument, context) => {
|
|
|
52581
52706
|
const colorizedRule = colorizeRuleByDiagnostic(ruleIdentifier, diagnostic.severity);
|
|
52582
52707
|
const severityLabel = colorizeRuleByDiagnostic(diagnostic.severity, diagnostic.severity);
|
|
52583
52708
|
cliLogger.log(`${severitySymbol} ${colorizedRule} ${highlighter.dim(`(${severityLabel})`)} — ${diagnostic.message}`);
|
|
52709
|
+
const codeFrame = buildCodeFrame({
|
|
52710
|
+
filePath: diagnostic.filePath,
|
|
52711
|
+
line: diagnostic.line,
|
|
52712
|
+
column: diagnostic.column,
|
|
52713
|
+
endLine: diagnostic.endLine,
|
|
52714
|
+
rootDirectory: targetDirectory
|
|
52715
|
+
});
|
|
52716
|
+
if (codeFrame) cliLogger.log(indentMultilineText(codeFrame, " "));
|
|
52584
52717
|
if (diagnostic.category) cliLogger.dim(` Category: ${diagnostic.category}`);
|
|
52585
52718
|
if (diagnostic.help) cliLogger.dim(` ${diagnostic.help}`);
|
|
52586
52719
|
cliLogger.dim(` If this needs follow-up or looks like a false positive, open: ${buildDiagnosticIssueUrl({
|
|
@@ -52630,6 +52763,10 @@ const validateModeFlags = (flags) => {
|
|
|
52630
52763
|
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
52764
|
if (flags.score && flags.json) throw new CliInputError("Cannot combine --score and --json; pick one output mode.");
|
|
52632
52765
|
if (flags.score && flags.telemetry === false) throw new CliInputError("Cannot combine --score with --no-telemetry; --score prints the score that --no-telemetry disables.");
|
|
52766
|
+
if (flags.debug && (flags.score === false || flags.telemetry === false)) {
|
|
52767
|
+
const disablingFlag = flags.score === false ? "--no-score" : "--no-telemetry";
|
|
52768
|
+
throw new CliInputError(`Cannot combine --debug with ${disablingFlag}; ${disablingFlag} disables the Sentry reporting --debug needs to capture a trace.`);
|
|
52769
|
+
}
|
|
52633
52770
|
};
|
|
52634
52771
|
//#endregion
|
|
52635
52772
|
//#region src/cli/commands/inspect.ts
|
|
@@ -52977,11 +53114,13 @@ const inspectAction = async (directory, flags) => {
|
|
|
52977
53114
|
})) {
|
|
52978
53115
|
printAgentInstallHint();
|
|
52979
53116
|
recordCount(METRIC.agentInstallHintShown, 1);
|
|
53117
|
+
disableSetupPrompt(setupProjectRoot);
|
|
52980
53118
|
}
|
|
52981
53119
|
}
|
|
52982
53120
|
} catch (error) {
|
|
52983
53121
|
const isUserError = isExpectedUserError(error);
|
|
52984
53122
|
const sentryEventId = isUserError ? void 0 : await reportErrorToSentry(error);
|
|
53123
|
+
if (isDebugFlagEnabled()) await flushSentry();
|
|
52985
53124
|
if (isJsonMode) {
|
|
52986
53125
|
writeJsonErrorReport(error, sentryEventId);
|
|
52987
53126
|
process.exitCode = 1;
|
|
@@ -53704,6 +53843,33 @@ const normalizeHelpInvocation = (argv, knownCommands) => {
|
|
|
53704
53843
|
return [...nodeArguments, "--help"];
|
|
53705
53844
|
};
|
|
53706
53845
|
//#endregion
|
|
53846
|
+
//#region src/cli/utils/print-debug-trace.ts
|
|
53847
|
+
/**
|
|
53848
|
+
* The `--debug` end-of-run line, pure so it's testable without the Sentry SDK.
|
|
53849
|
+
* Mirrors the crash-reference phrasing in `handle-error.ts` ("mention this when
|
|
53850
|
+
* reporting") so users learn one habit for both paths. A `null` trace says why,
|
|
53851
|
+
* so `--debug` never silently does nothing.
|
|
53852
|
+
*/
|
|
53853
|
+
const buildDebugTraceMessage = (traceId) => traceId === null ? "Sentry trace unavailable for this run (no trace was recorded)." : `Sentry trace (mention this when reporting): ${traceId}`;
|
|
53854
|
+
/**
|
|
53855
|
+
* Prints the run's Sentry trace id to stderr at the end of a `--debug` run, so
|
|
53856
|
+
* maintainers can pull the full trace from a pasted id. Runs from the process
|
|
53857
|
+
* `exit` handler, so it's the last line on both the success path and the error
|
|
53858
|
+
* funnels (which `process.exit()` before the promise chain could resume).
|
|
53859
|
+
*
|
|
53860
|
+
* Writes straight to `process.stderr` (not `Console`) for three reasons: the
|
|
53861
|
+
* exit handler is synchronous, JSON mode patches the global console to no-ops —
|
|
53862
|
+
* a diagnostic the user explicitly asked for must survive that — and stderr
|
|
53863
|
+
* keeps `--json` / `--score` stdout machine-clean. The write is wrapped because
|
|
53864
|
+
* a diagnostic must never throw out of an exit handler.
|
|
53865
|
+
*/
|
|
53866
|
+
const printDebugTrace = () => {
|
|
53867
|
+
if (!Sentry.isInitialized()) return;
|
|
53868
|
+
try {
|
|
53869
|
+
process.stderr.write(`${highlighter.dim(buildDebugTraceMessage(getLastRunTraceId()))}\n`);
|
|
53870
|
+
} catch {}
|
|
53871
|
+
};
|
|
53872
|
+
//#endregion
|
|
53707
53873
|
//#region src/cli/utils/removed-cli-flags.ts
|
|
53708
53874
|
const REMOVED_FLAGS = new Map([
|
|
53709
53875
|
["--full", "use `--diff false` to force a full scan"],
|
|
@@ -53730,6 +53896,7 @@ const ROOT_FLAG_SPEC = {
|
|
|
53730
53896
|
longOptionsWithoutValues: new Set([
|
|
53731
53897
|
"--color",
|
|
53732
53898
|
"--dead-code",
|
|
53899
|
+
"--debug",
|
|
53733
53900
|
"--help",
|
|
53734
53901
|
"--json",
|
|
53735
53902
|
"--json-compact",
|
|
@@ -53897,6 +54064,9 @@ const stripUnknownCliFlags = (argv) => {
|
|
|
53897
54064
|
initializeSentry();
|
|
53898
54065
|
process.on("SIGINT", exitGracefully);
|
|
53899
54066
|
process.on("SIGTERM", exitGracefully);
|
|
54067
|
+
process.on("exit", () => {
|
|
54068
|
+
if (isDebugFlagEnabled()) printDebugTrace();
|
|
54069
|
+
});
|
|
53900
54070
|
unrefStdin();
|
|
53901
54071
|
guardStdin();
|
|
53902
54072
|
const formatExampleLines = (examples) => {
|
|
@@ -53941,7 +54111,7 @@ ${highlighter.dim("Learn more:")}
|
|
|
53941
54111
|
${highlighter.info(CANONICAL_GITHUB_URL)}
|
|
53942
54112
|
`;
|
|
53943
54113
|
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);
|
|
54114
|
+
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
54115
|
program.action(inspectAction);
|
|
53946
54116
|
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
54117
|
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 +54154,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
53984
54154
|
export {};
|
|
53985
54155
|
|
|
53986
54156
|
//# sourceMappingURL=cli.js.map
|
|
53987
|
-
//# debugId=
|
|
54157
|
+
//# debugId=496f49cf-4038-5332-b301-ffedc55210ba
|