react-doctor 0.1.3 → 0.1.5
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/README.md +84 -22
- package/dist/cli.js +253 -94
- package/dist/eslint-plugin.js +1 -1
- package/dist/index.d.ts +48 -1
- package/dist/index.js +209 -21
- package/package.json +8 -3
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs, { accessSync, constants, existsSync, mkdirSync, mkdtempSync, readdirS
|
|
|
3
3
|
import os, { tmpdir } from "node:os";
|
|
4
4
|
import path, { join } from "node:path";
|
|
5
5
|
import { performance } from "node:perf_hooks";
|
|
6
|
-
import { Command
|
|
6
|
+
import { Command } from "commander";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { SKILL_MANIFEST_FILE, detectInstalledSkillAgents, getSkillAgentConfig, getSkillAgentTypes, installSkillsFromSource } from "agent-install";
|
|
9
9
|
import pc from "picocolors";
|
|
@@ -214,7 +214,7 @@ const finalize = (method, originalText, displayText) => {
|
|
|
214
214
|
sharedInstance.stop();
|
|
215
215
|
ora({
|
|
216
216
|
text: displayText,
|
|
217
|
-
indent:
|
|
217
|
+
indent: 0
|
|
218
218
|
}).start()[method](displayText);
|
|
219
219
|
const [remainingText] = pendingTexts;
|
|
220
220
|
if (remainingText) sharedInstance.text = remainingText;
|
|
@@ -226,7 +226,7 @@ const spinner = (text) => ({ start() {
|
|
|
226
226
|
pendingTexts.add(text);
|
|
227
227
|
if (!sharedInstance) sharedInstance = ora({
|
|
228
228
|
text,
|
|
229
|
-
indent:
|
|
229
|
+
indent: 0
|
|
230
230
|
}).start();
|
|
231
231
|
else sharedInstance.text = text;
|
|
232
232
|
const handle = {
|
|
@@ -304,26 +304,45 @@ const runInstallSkill = async (options = {}) => {
|
|
|
304
304
|
}
|
|
305
305
|
};
|
|
306
306
|
//#endregion
|
|
307
|
-
//#region src/
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
307
|
+
//#region src/errors.ts
|
|
308
|
+
var ReactDoctorError = class extends Error {
|
|
309
|
+
name = "ReactDoctorError";
|
|
310
|
+
constructor(message, options) {
|
|
311
|
+
super(message, options);
|
|
312
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
var NoReactDependencyError = class extends ReactDoctorError {
|
|
316
|
+
name = "NoReactDependencyError";
|
|
317
|
+
directory;
|
|
318
|
+
constructor(directory, options) {
|
|
319
|
+
super(buildNoReactDependencyError(directory), options);
|
|
320
|
+
this.directory = directory;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
var PackageJsonNotFoundError = class extends ReactDoctorError {
|
|
324
|
+
name = "PackageJsonNotFoundError";
|
|
325
|
+
directory;
|
|
326
|
+
constructor(directory, options) {
|
|
327
|
+
super(`No package.json found in ${directory}`, options);
|
|
328
|
+
this.directory = directory;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region src/utils/resolve-config-root-dir.ts
|
|
333
|
+
const resolveConfigRootDir = (config, configSourceDirectory) => {
|
|
334
|
+
if (!config || !configSourceDirectory) return null;
|
|
335
|
+
const rawRootDir = config.rootDir;
|
|
336
|
+
if (typeof rawRootDir !== "string") return null;
|
|
337
|
+
const trimmedRootDir = rawRootDir.trim();
|
|
338
|
+
if (trimmedRootDir.length === 0) return null;
|
|
339
|
+
const resolvedRootDir = path.isAbsolute(trimmedRootDir) ? trimmedRootDir : path.resolve(configSourceDirectory, trimmedRootDir);
|
|
340
|
+
if (resolvedRootDir === configSourceDirectory) return null;
|
|
341
|
+
if (!fs.existsSync(resolvedRootDir) || !fs.statSync(resolvedRootDir).isDirectory()) {
|
|
342
|
+
logger.warn(`react-doctor config "rootDir" points to "${rawRootDir}" (resolved to ${resolvedRootDir}), which is not a directory. Ignoring.`);
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
return resolvedRootDir;
|
|
327
346
|
};
|
|
328
347
|
//#endregion
|
|
329
348
|
//#region src/utils/build-hidden-diagnostics-summary.ts
|
|
@@ -824,7 +843,7 @@ const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
|
|
|
824
843
|
};
|
|
825
844
|
//#endregion
|
|
826
845
|
//#region src/utils/find-stacked-disable-comments.ts
|
|
827
|
-
const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([\
|
|
846
|
+
const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
|
|
828
847
|
const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
|
|
829
848
|
const collected = [];
|
|
830
849
|
let isStillInChain = true;
|
|
@@ -846,13 +865,21 @@ const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
|
|
|
846
865
|
};
|
|
847
866
|
//#endregion
|
|
848
867
|
//#region src/utils/is-rule-listed-in-comment.ts
|
|
868
|
+
const stripDescriptionTail = (ruleList) => {
|
|
869
|
+
const descriptionMatch = ruleList.match(/(?:^|\s)--\s/);
|
|
870
|
+
if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
|
|
871
|
+
return ruleList.slice(0, descriptionMatch.index);
|
|
872
|
+
};
|
|
849
873
|
const isRuleListedInComment = (ruleList, ruleId) => {
|
|
850
|
-
|
|
851
|
-
|
|
874
|
+
const trimmed = ruleList?.trim();
|
|
875
|
+
if (!trimmed) return true;
|
|
876
|
+
const ruleSection = stripDescriptionTail(trimmed).trim();
|
|
877
|
+
if (!ruleSection) return true;
|
|
878
|
+
return ruleSection.split(/[,\s]+/).some((token) => token.trim() === ruleId);
|
|
852
879
|
};
|
|
853
880
|
//#endregion
|
|
854
881
|
//#region src/utils/evaluate-suppression.ts
|
|
855
|
-
const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([\
|
|
882
|
+
const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
|
|
856
883
|
const formatLineGap = (gapLineCount) => `${gapLineCount} line${gapLineCount === 1 ? "" : "s"}`;
|
|
857
884
|
const hasChainSuppressor = (comments, ruleId) => comments.some((comment) => comment.isInChain && isRuleListedInComment(comment.ruleList, ruleId));
|
|
858
885
|
const findAdjacentRuleListMismatch = (comments, ruleId) => comments.find((comment) => comment.isInChain && Boolean(comment.ruleList?.trim()) && !isRuleListedInComment(comment.ruleList, ruleId));
|
|
@@ -1273,9 +1300,9 @@ const resolveCatalogVersionFromCollection = (catalogs, packageName, catalogRefer
|
|
|
1273
1300
|
for (const namedCatalog of Object.values(catalogs.namedCatalogs)) if (namedCatalog[packageName]) return namedCatalog[packageName];
|
|
1274
1301
|
return null;
|
|
1275
1302
|
};
|
|
1276
|
-
const resolveCatalogVersion = (packageJson, packageName, rootDirectory) => {
|
|
1303
|
+
const resolveCatalogVersion = (packageJson, packageName, rootDirectory, explicitCatalogReference) => {
|
|
1277
1304
|
const rawVersion = collectAllDependencies(packageJson)[packageName];
|
|
1278
|
-
const catalogName = rawVersion ? extractCatalogName(rawVersion) : null;
|
|
1305
|
+
const catalogName = explicitCatalogReference ?? (rawVersion ? extractCatalogName(rawVersion) : null);
|
|
1279
1306
|
if (isPlainObject(packageJson.catalog)) {
|
|
1280
1307
|
const version = resolveVersionFromCatalog(packageJson.catalog, packageName);
|
|
1281
1308
|
if (version) return version;
|
|
@@ -1292,9 +1319,22 @@ const resolveCatalogVersion = (packageJson, packageName, rootDirectory) => {
|
|
|
1292
1319
|
}
|
|
1293
1320
|
}
|
|
1294
1321
|
const workspaces = packageJson.workspaces;
|
|
1295
|
-
if (workspaces && !Array.isArray(workspaces)
|
|
1296
|
-
|
|
1297
|
-
|
|
1322
|
+
if (workspaces && !Array.isArray(workspaces)) {
|
|
1323
|
+
if (isPlainObject(workspaces.catalog)) {
|
|
1324
|
+
const version = resolveVersionFromCatalog(workspaces.catalog, packageName);
|
|
1325
|
+
if (version) return version;
|
|
1326
|
+
}
|
|
1327
|
+
if (isPlainObject(workspaces.catalogs)) {
|
|
1328
|
+
const namedCatalog = catalogName ? workspaces.catalogs[catalogName] : void 0;
|
|
1329
|
+
if (namedCatalog && isPlainObject(namedCatalog)) {
|
|
1330
|
+
const version = resolveVersionFromCatalog(namedCatalog, packageName);
|
|
1331
|
+
if (version) return version;
|
|
1332
|
+
}
|
|
1333
|
+
for (const catalogEntries of Object.values(workspaces.catalogs)) if (isPlainObject(catalogEntries)) {
|
|
1334
|
+
const version = resolveVersionFromCatalog(catalogEntries, packageName);
|
|
1335
|
+
if (version) return version;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1298
1338
|
}
|
|
1299
1339
|
if (rootDirectory) {
|
|
1300
1340
|
const pnpmVersion = resolveCatalogVersionFromCollection(parsePnpmWorkspaceCatalogs(rootDirectory), packageName, catalogName);
|
|
@@ -1381,7 +1421,8 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
1381
1421
|
};
|
|
1382
1422
|
const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
|
|
1383
1423
|
const rootInfo = extractDependencyInfo(rootPackageJson);
|
|
1384
|
-
const
|
|
1424
|
+
const leafPackageJsonPath = path.join(directory, "package.json");
|
|
1425
|
+
const catalogVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot, isFile(leafPackageJsonPath) ? extractCatalogName(collectAllDependencies(readPackageJson(leafPackageJsonPath)).react ?? "") ?? null : null);
|
|
1385
1426
|
const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
|
|
1386
1427
|
return {
|
|
1387
1428
|
reactVersion: rootInfo.reactVersion ?? catalogVersion ?? workspaceInfo.reactVersion,
|
|
@@ -1509,15 +1550,16 @@ const discoverProject = (directory) => {
|
|
|
1509
1550
|
const cached = cachedProjectInfos.get(directory);
|
|
1510
1551
|
if (cached !== void 0) return cached;
|
|
1511
1552
|
const packageJsonPath = path.join(directory, "package.json");
|
|
1512
|
-
if (!isFile(packageJsonPath)) throw new
|
|
1553
|
+
if (!isFile(packageJsonPath)) throw new PackageJsonNotFoundError(directory);
|
|
1513
1554
|
const packageJson = readPackageJson(packageJsonPath);
|
|
1514
1555
|
let { reactVersion, framework } = extractDependencyInfo(packageJson);
|
|
1515
|
-
|
|
1556
|
+
const leafCatalogReference = extractCatalogName(collectAllDependencies(packageJson).react ?? "") ?? null;
|
|
1557
|
+
if (!reactVersion) reactVersion = resolveCatalogVersion(packageJson, "react", directory, leafCatalogReference);
|
|
1516
1558
|
if (!reactVersion) {
|
|
1517
1559
|
const monorepoRoot = findMonorepoRoot(directory);
|
|
1518
1560
|
if (monorepoRoot) {
|
|
1519
1561
|
const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
|
|
1520
|
-
if (isFile(monorepoPackageJsonPath)) reactVersion = resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), "react", monorepoRoot);
|
|
1562
|
+
if (isFile(monorepoPackageJsonPath)) reactVersion = resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), "react", monorepoRoot, leafCatalogReference);
|
|
1521
1563
|
}
|
|
1522
1564
|
}
|
|
1523
1565
|
if (!reactVersion || framework === "unknown") {
|
|
@@ -1591,6 +1633,7 @@ const BOOLEAN_FIELD_NAMES = [
|
|
|
1591
1633
|
"respectInlineDisables",
|
|
1592
1634
|
"adoptExistingLintConfig"
|
|
1593
1635
|
];
|
|
1636
|
+
const STRING_FIELD_NAMES = ["rootDir"];
|
|
1594
1637
|
const warnConfigField = (message) => {
|
|
1595
1638
|
process.stderr.write(`[react-doctor] ${message}\n`);
|
|
1596
1639
|
};
|
|
@@ -1606,6 +1649,10 @@ const coerceMaybeBooleanString = (fieldName, value) => {
|
|
|
1606
1649
|
}
|
|
1607
1650
|
warnConfigField(`config field "${fieldName}" must be a boolean (got ${typeof value}); ignoring this field.`);
|
|
1608
1651
|
};
|
|
1652
|
+
const validateString = (fieldName, value) => {
|
|
1653
|
+
if (typeof value === "string") return value;
|
|
1654
|
+
warnConfigField(`config field "${fieldName}" must be a string (got ${typeof value}); ignoring this field.`);
|
|
1655
|
+
};
|
|
1609
1656
|
const validateConfigTypes = (config) => {
|
|
1610
1657
|
const validated = { ...config };
|
|
1611
1658
|
for (const fieldName of BOOLEAN_FIELD_NAMES) {
|
|
@@ -1615,6 +1662,13 @@ const validateConfigTypes = (config) => {
|
|
|
1615
1662
|
if (coerced === void 0) delete validated[fieldName];
|
|
1616
1663
|
else validated[fieldName] = coerced;
|
|
1617
1664
|
}
|
|
1665
|
+
for (const fieldName of STRING_FIELD_NAMES) {
|
|
1666
|
+
const original = config[fieldName];
|
|
1667
|
+
if (original === void 0) continue;
|
|
1668
|
+
const validatedString = validateString(fieldName, original);
|
|
1669
|
+
if (validatedString === void 0) delete validated[fieldName];
|
|
1670
|
+
else validated[fieldName] = validatedString;
|
|
1671
|
+
}
|
|
1618
1672
|
return validated;
|
|
1619
1673
|
};
|
|
1620
1674
|
//#endregion
|
|
@@ -1626,7 +1680,10 @@ const loadConfigFromDirectory = (directory) => {
|
|
|
1626
1680
|
if (isFile(configFilePath)) try {
|
|
1627
1681
|
const fileContent = fs.readFileSync(configFilePath, "utf-8");
|
|
1628
1682
|
const parsed = JSON.parse(fileContent);
|
|
1629
|
-
if (isPlainObject(parsed)) return
|
|
1683
|
+
if (isPlainObject(parsed)) return {
|
|
1684
|
+
config: validateConfigTypes(parsed),
|
|
1685
|
+
sourceDirectory: directory
|
|
1686
|
+
};
|
|
1630
1687
|
logger.warn(`${CONFIG_FILENAME} must be a JSON object, ignoring.`);
|
|
1631
1688
|
} catch (error) {
|
|
1632
1689
|
logger.warn(`Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1637,7 +1694,10 @@ const loadConfigFromDirectory = (directory) => {
|
|
|
1637
1694
|
const packageJson = JSON.parse(fileContent);
|
|
1638
1695
|
if (isPlainObject(packageJson)) {
|
|
1639
1696
|
const embeddedConfig = packageJson[PACKAGE_JSON_CONFIG_KEY];
|
|
1640
|
-
if (isPlainObject(embeddedConfig)) return
|
|
1697
|
+
if (isPlainObject(embeddedConfig)) return {
|
|
1698
|
+
config: validateConfigTypes(embeddedConfig),
|
|
1699
|
+
sourceDirectory: directory
|
|
1700
|
+
};
|
|
1641
1701
|
}
|
|
1642
1702
|
} catch {
|
|
1643
1703
|
return null;
|
|
@@ -1646,7 +1706,7 @@ const loadConfigFromDirectory = (directory) => {
|
|
|
1646
1706
|
};
|
|
1647
1707
|
const isProjectBoundary$1 = (directory) => fs.existsSync(path.join(directory, ".git")) || isMonorepoRoot(directory);
|
|
1648
1708
|
const cachedConfigs = /* @__PURE__ */ new Map();
|
|
1649
|
-
const
|
|
1709
|
+
const loadConfigWithSource = (rootDirectory) => {
|
|
1650
1710
|
const cached = cachedConfigs.get(rootDirectory);
|
|
1651
1711
|
if (cached !== void 0) return cached;
|
|
1652
1712
|
const localConfig = loadConfigFromDirectory(rootDirectory);
|
|
@@ -1675,6 +1735,32 @@ const loadConfig = (rootDirectory) => {
|
|
|
1675
1735
|
return null;
|
|
1676
1736
|
};
|
|
1677
1737
|
//#endregion
|
|
1738
|
+
//#region src/utils/wrap-indented-text.ts
|
|
1739
|
+
const wrapLine = (lineText, contentWidth) => {
|
|
1740
|
+
if (lineText.length <= contentWidth) return [lineText];
|
|
1741
|
+
const wrappedLines = [];
|
|
1742
|
+
let remainingText = lineText.trim();
|
|
1743
|
+
while (remainingText.length > contentWidth) {
|
|
1744
|
+
const candidateText = remainingText.slice(0, contentWidth);
|
|
1745
|
+
const breakIndex = candidateText.lastIndexOf(" ");
|
|
1746
|
+
if (breakIndex <= 0) {
|
|
1747
|
+
wrappedLines.push(candidateText);
|
|
1748
|
+
remainingText = remainingText.slice(contentWidth).trimStart();
|
|
1749
|
+
continue;
|
|
1750
|
+
}
|
|
1751
|
+
wrappedLines.push(remainingText.slice(0, breakIndex));
|
|
1752
|
+
remainingText = remainingText.slice(breakIndex + 1).trimStart();
|
|
1753
|
+
}
|
|
1754
|
+
if (remainingText.length > 0) wrappedLines.push(remainingText);
|
|
1755
|
+
return wrappedLines;
|
|
1756
|
+
};
|
|
1757
|
+
const wrapIndentedText = (text, linePrefix, width) => {
|
|
1758
|
+
const contentWidth = width - linePrefix.length;
|
|
1759
|
+
if (contentWidth <= 0) return indentOnly(text, linePrefix);
|
|
1760
|
+
return text.split("\n").flatMap((lineText) => wrapLine(lineText, contentWidth)).map((lineText) => `${linePrefix}${lineText}`).join("\n");
|
|
1761
|
+
};
|
|
1762
|
+
const indentOnly = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
|
|
1763
|
+
//#endregion
|
|
1678
1764
|
//#region src/utils/resolve-compatible-node.ts
|
|
1679
1765
|
const parseNodeVersion = (versionString) => {
|
|
1680
1766
|
const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
|
|
@@ -2256,6 +2342,16 @@ const TANSTACK_START_RULES = {
|
|
|
2256
2342
|
"react-doctor/tanstack-start-redirect-in-try-catch": "warn",
|
|
2257
2343
|
"react-doctor/tanstack-start-loader-parallel-fetch": "warn"
|
|
2258
2344
|
};
|
|
2345
|
+
const YOU_MIGHT_NOT_NEED_EFFECT_RULES = {
|
|
2346
|
+
"effect/no-derived-state": "warn",
|
|
2347
|
+
"effect/no-chain-state-updates": "warn",
|
|
2348
|
+
"effect/no-event-handler": "warn",
|
|
2349
|
+
"effect/no-adjust-state-on-prop-change": "warn",
|
|
2350
|
+
"effect/no-reset-all-state-on-prop-change": "warn",
|
|
2351
|
+
"effect/no-pass-live-state-to-parent": "warn",
|
|
2352
|
+
"effect/no-pass-data-to-parent": "warn",
|
|
2353
|
+
"effect/no-initialize-state": "warn"
|
|
2354
|
+
};
|
|
2259
2355
|
const REACT_COMPILER_RULES = {
|
|
2260
2356
|
"react-hooks-js/set-state-in-render": "error",
|
|
2261
2357
|
"react-hooks-js/immutability": "error",
|
|
@@ -2300,6 +2396,23 @@ const resolveReactHooksJsPlugin = (hasReactCompiler, customRulesOnly) => {
|
|
|
2300
2396
|
availableRuleNames: readPluginRuleNames(pluginSpecifier)
|
|
2301
2397
|
};
|
|
2302
2398
|
};
|
|
2399
|
+
const YOU_MIGHT_NOT_NEED_EFFECT_NAMESPACE = "effect";
|
|
2400
|
+
const resolveYouMightNotNeedEffectPlugin = (customRulesOnly) => {
|
|
2401
|
+
if (customRulesOnly) return null;
|
|
2402
|
+
let pluginSpecifier;
|
|
2403
|
+
try {
|
|
2404
|
+
pluginSpecifier = esmRequire$1.resolve("eslint-plugin-react-you-might-not-need-an-effect");
|
|
2405
|
+
} catch {
|
|
2406
|
+
return null;
|
|
2407
|
+
}
|
|
2408
|
+
return {
|
|
2409
|
+
entry: {
|
|
2410
|
+
name: YOU_MIGHT_NOT_NEED_EFFECT_NAMESPACE,
|
|
2411
|
+
specifier: pluginSpecifier
|
|
2412
|
+
},
|
|
2413
|
+
availableRuleNames: readPluginRuleNames(pluginSpecifier)
|
|
2414
|
+
};
|
|
2415
|
+
};
|
|
2303
2416
|
const filterRulesToAvailable = (rules, pluginNamespace, availableRuleNames) => {
|
|
2304
2417
|
if (availableRuleNames.size === 0) return rules;
|
|
2305
2418
|
const ruleKeyPrefix = `${pluginNamespace}/`;
|
|
@@ -2510,6 +2623,11 @@ const filterRulesByReactMajor = (rules, reactMajorVersion) => {
|
|
|
2510
2623
|
const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, hasTanStackQuery, customRulesOnly = false, reactMajorVersion = null, extendsPaths = [] }) => {
|
|
2511
2624
|
const reactHooksJsPlugin = resolveReactHooksJsPlugin(hasReactCompiler, customRulesOnly);
|
|
2512
2625
|
const reactCompilerRules = reactHooksJsPlugin ? filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames) : {};
|
|
2626
|
+
const youMightNotNeedEffectPlugin = resolveYouMightNotNeedEffectPlugin(customRulesOnly);
|
|
2627
|
+
const youMightNotNeedEffectRules = youMightNotNeedEffectPlugin ? filterRulesToAvailable(YOU_MIGHT_NOT_NEED_EFFECT_RULES, YOU_MIGHT_NOT_NEED_EFFECT_NAMESPACE, youMightNotNeedEffectPlugin.availableRuleNames) : {};
|
|
2628
|
+
const jsPlugins = [];
|
|
2629
|
+
if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
|
|
2630
|
+
if (youMightNotNeedEffectPlugin) jsPlugins.push(youMightNotNeedEffectPlugin.entry);
|
|
2513
2631
|
return {
|
|
2514
2632
|
...extendsPaths.length > 0 ? { extends: extendsPaths } : {},
|
|
2515
2633
|
categories: {
|
|
@@ -2522,11 +2640,12 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, hasTanSta
|
|
|
2522
2640
|
nursery: "off"
|
|
2523
2641
|
},
|
|
2524
2642
|
plugins: customRulesOnly ? [] : ["react", "jsx-a11y"],
|
|
2525
|
-
jsPlugins:
|
|
2643
|
+
jsPlugins: [...jsPlugins, pluginPath],
|
|
2526
2644
|
rules: {
|
|
2527
2645
|
...customRulesOnly ? {} : BUILTIN_REACT_RULES,
|
|
2528
2646
|
...customRulesOnly ? {} : BUILTIN_A11Y_RULES,
|
|
2529
2647
|
...reactCompilerRules,
|
|
2648
|
+
...youMightNotNeedEffectRules,
|
|
2530
2649
|
...filterRulesByReactMajor(GLOBAL_REACT_DOCTOR_RULES, reactMajorVersion),
|
|
2531
2650
|
...framework === "nextjs" ? NEXTJS_RULES : {},
|
|
2532
2651
|
...framework === "expo" || framework === "react-native" ? REACT_NATIVE_RULES : {},
|
|
@@ -2640,6 +2759,7 @@ const PLUGIN_CATEGORY_MAP = {
|
|
|
2640
2759
|
"react-doctor": "Other",
|
|
2641
2760
|
"jsx-a11y": "Accessibility",
|
|
2642
2761
|
knip: "Dead Code",
|
|
2762
|
+
effect: "State & Effects",
|
|
2643
2763
|
eslint: "Correctness",
|
|
2644
2764
|
oxc: "Correctness",
|
|
2645
2765
|
typescript: "Correctness",
|
|
@@ -3154,6 +3274,7 @@ const parseOxlintOutput = (stdout) => {
|
|
|
3154
3274
|
severity: diagnostic.severity,
|
|
3155
3275
|
message: cleaned.message,
|
|
3156
3276
|
help: cleaned.help,
|
|
3277
|
+
url: diagnostic.url,
|
|
3157
3278
|
line: primaryLabel?.span.line ?? 0,
|
|
3158
3279
|
column: primaryLabel?.span.column ?? 0,
|
|
3159
3280
|
category: resolveDiagnosticCategory(plugin, rule)
|
|
@@ -3285,6 +3406,11 @@ const buildVerboseSiteMap = (diagnostics) => {
|
|
|
3285
3406
|
return fileSites;
|
|
3286
3407
|
};
|
|
3287
3408
|
const formatSiteCountBadge = (count) => count > 1 ? `×${count}` : "";
|
|
3409
|
+
const formatIssueCount = (count) => `${count} ${count === 1 ? "issue" : "issues"}`;
|
|
3410
|
+
const toRuleTitle = (ruleName) => {
|
|
3411
|
+
const readableRuleName = ruleName.replace(/^(no|prefer|require|use)-/, "").replace(/^(nextjs|tanstack-start)-/, "").replaceAll("-", " ");
|
|
3412
|
+
return (readableRuleName.charAt(0).toUpperCase() + readableRuleName.slice(1)).replace(/\b(css|html|url|svg|jsx|api|ua)\b/gi, (match) => match.toUpperCase());
|
|
3413
|
+
};
|
|
3288
3414
|
const computeRuleNameColumnWidth = (ruleKeys) => {
|
|
3289
3415
|
const longestRuleNameLength = ruleKeys.reduce((longest, ruleKey) => Math.max(longest, ruleKey.length), 0);
|
|
3290
3416
|
return Math.max(36, longestRuleNameLength);
|
|
@@ -3296,6 +3422,9 @@ const padRuleNameToColumn = (ruleName, columnWidth) => {
|
|
|
3296
3422
|
const grayLine = (text) => {
|
|
3297
3423
|
logger.log(highlighter.gray(text));
|
|
3298
3424
|
};
|
|
3425
|
+
const grayWrappedLine = (text, linePrefix) => {
|
|
3426
|
+
grayLine(wrapIndentedText(text, linePrefix, 88));
|
|
3427
|
+
};
|
|
3299
3428
|
const printCompactRuleGroupLine = (ruleKey, ruleDiagnostics, ruleNameColumnWidth) => {
|
|
3300
3429
|
const firstDiagnostic = ruleDiagnostics[0];
|
|
3301
3430
|
const icon = colorizeBySeverity(firstDiagnostic.severity === "error" ? "✗" : "⚠", firstDiagnostic.severity);
|
|
@@ -3304,13 +3433,38 @@ const printCompactRuleGroupLine = (ruleKey, ruleDiagnostics, ruleNameColumnWidth
|
|
|
3304
3433
|
const trailingBadge = siteCountBadge.length > 0 ? ` ${highlighter.gray(siteCountBadge)}` : "";
|
|
3305
3434
|
logger.log(` ${icon} ${ruleNameRendering}${trailingBadge}`);
|
|
3306
3435
|
};
|
|
3307
|
-
const
|
|
3308
|
-
|
|
3436
|
+
const getWorstSeverity = (diagnostics) => diagnostics.some((diagnostic) => diagnostic.severity === "error") ? "error" : "warning";
|
|
3437
|
+
const buildCategoryDiagnosticGroups = (diagnostics) => {
|
|
3438
|
+
return [...groupBy(diagnostics, (diagnostic) => diagnostic.category).entries()].map(([category, categoryDiagnostics]) => {
|
|
3439
|
+
return {
|
|
3440
|
+
category,
|
|
3441
|
+
diagnostics: categoryDiagnostics,
|
|
3442
|
+
ruleGroups: sortByImportance([...groupBy(categoryDiagnostics, (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`).entries()])
|
|
3443
|
+
};
|
|
3444
|
+
}).toSorted((categoryGroupA, categoryGroupB) => {
|
|
3445
|
+
const severityDelta = SEVERITY_ORDER[getWorstSeverity(categoryGroupA.diagnostics)] - SEVERITY_ORDER[getWorstSeverity(categoryGroupB.diagnostics)];
|
|
3446
|
+
if (severityDelta !== 0) return severityDelta;
|
|
3447
|
+
if (categoryGroupA.diagnostics.length !== categoryGroupB.diagnostics.length) return categoryGroupB.diagnostics.length - categoryGroupA.diagnostics.length;
|
|
3448
|
+
return categoryGroupA.category.localeCompare(categoryGroupB.category);
|
|
3449
|
+
});
|
|
3450
|
+
};
|
|
3451
|
+
const printDefaultRuleGroup = (ruleKey, ruleDiagnostics, rootDirectory) => {
|
|
3309
3452
|
const firstDiagnostic = ruleDiagnostics[0];
|
|
3310
|
-
|
|
3311
|
-
|
|
3453
|
+
const ruleTitle = toRuleTitle(firstDiagnostic.rule);
|
|
3454
|
+
const icon = colorizeBySeverity(firstDiagnostic.severity === "error" ? "✗" : "⚠", firstDiagnostic.severity);
|
|
3455
|
+
const siteCountBadge = formatSiteCountBadge(ruleDiagnostics.length);
|
|
3456
|
+
const trailingBadge = siteCountBadge.length > 0 ? ` ${highlighter.gray(siteCountBadge)}` : "";
|
|
3457
|
+
logger.log(` ${icon} ${ruleTitle}${trailingBadge}`);
|
|
3458
|
+
grayWrappedLine(firstDiagnostic.message, " ");
|
|
3459
|
+
if (firstDiagnostic.help) grayWrappedLine(firstDiagnostic.help, " ");
|
|
3460
|
+
if (firstDiagnostic.url) grayLine(` ${firstDiagnostic.url}`);
|
|
3312
3461
|
const firstLocation = ruleDiagnostics.find((diagnostic) => diagnostic.line > 0);
|
|
3313
|
-
if (firstLocation) grayLine(`
|
|
3462
|
+
if (firstLocation) grayLine(` ${toRelativePath(firstLocation.filePath, rootDirectory)}:${firstLocation.line}`);
|
|
3463
|
+
};
|
|
3464
|
+
const printDefaultCategoryGroup = (categoryGroup, visibleRuleGroups, rootDirectory) => {
|
|
3465
|
+
const issueCount = formatIssueCount(categoryGroup.diagnostics.length);
|
|
3466
|
+
logger.log(`${highlighter.bold(categoryGroup.category)} ${highlighter.dim(issueCount)}`);
|
|
3467
|
+
for (const [ruleKey, ruleDiagnostics] of visibleRuleGroups) printDefaultRuleGroup(ruleKey, ruleDiagnostics, rootDirectory);
|
|
3314
3468
|
logger.break();
|
|
3315
3469
|
};
|
|
3316
3470
|
const printVerboseRuleGroup = (ruleKey, ruleDiagnostics, ruleNameColumnWidth) => {
|
|
@@ -3326,22 +3480,36 @@ const printVerboseRuleGroup = (ruleKey, ruleDiagnostics, ruleNameColumnWidth) =>
|
|
|
3326
3480
|
else grayLine(` ${filePath}`);
|
|
3327
3481
|
logger.break();
|
|
3328
3482
|
};
|
|
3483
|
+
const printDefaultDiagnostics = (diagnostics, rootDirectory) => {
|
|
3484
|
+
const categoryGroups = buildCategoryDiagnosticGroups(diagnostics);
|
|
3485
|
+
const hiddenRuleGroups = [];
|
|
3486
|
+
const visibleCategoryGroups = categoryGroups.slice(0, 5);
|
|
3487
|
+
const hiddenCategoryGroups = categoryGroups.slice(5);
|
|
3488
|
+
for (const categoryGroup of visibleCategoryGroups) {
|
|
3489
|
+
const visibleRuleGroups = categoryGroup.ruleGroups.slice(0, 3);
|
|
3490
|
+
const remainingRuleGroups = categoryGroup.ruleGroups.slice(3);
|
|
3491
|
+
printDefaultCategoryGroup(categoryGroup, visibleRuleGroups, rootDirectory);
|
|
3492
|
+
hiddenRuleGroups.push(...remainingRuleGroups);
|
|
3493
|
+
}
|
|
3494
|
+
hiddenRuleGroups.push(...hiddenCategoryGroups.flatMap((categoryGroup) => categoryGroup.ruleGroups));
|
|
3495
|
+
if (hiddenRuleGroups.length > 0) printHiddenDiagnosticsSummary(hiddenRuleGroups);
|
|
3496
|
+
};
|
|
3329
3497
|
const printDiagnostics = (diagnostics, isVerbose, rootDirectory) => {
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3498
|
+
if (!isVerbose) {
|
|
3499
|
+
printDefaultDiagnostics(diagnostics, rootDirectory);
|
|
3500
|
+
return;
|
|
3501
|
+
}
|
|
3502
|
+
const visibleRuleGroups = sortByImportance([...groupBy(diagnostics, (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`).entries()]);
|
|
3333
3503
|
const ruleNameColumnWidth = computeRuleNameColumnWidth(visibleRuleGroups.map(([ruleKey]) => ruleKey));
|
|
3334
3504
|
visibleRuleGroups.forEach(([ruleKey, ruleDiagnostics]) => {
|
|
3335
|
-
|
|
3336
|
-
printVerboseRuleGroup(ruleKey, ruleDiagnostics, ruleNameColumnWidth);
|
|
3337
|
-
return;
|
|
3338
|
-
}
|
|
3339
|
-
printDetailedRuleGroup(ruleKey, ruleDiagnostics, rootDirectory, ruleNameColumnWidth);
|
|
3505
|
+
printVerboseRuleGroup(ruleKey, ruleDiagnostics, ruleNameColumnWidth);
|
|
3340
3506
|
});
|
|
3341
|
-
if (hiddenRuleGroups.length > 0) printHiddenDiagnosticsSummary(hiddenRuleGroups);
|
|
3342
3507
|
};
|
|
3343
3508
|
const printHiddenDiagnosticsSummary = (hiddenRuleGroups) => {
|
|
3344
|
-
const renderedParts = buildHiddenDiagnosticsSummary(hiddenRuleGroups.flatMap(([, ruleDiagnostics]) => ruleDiagnostics)).map((part) =>
|
|
3509
|
+
const renderedParts = buildHiddenDiagnosticsSummary(hiddenRuleGroups.flatMap(([, ruleDiagnostics]) => ruleDiagnostics)).map((part) => {
|
|
3510
|
+
const [icon, ...labelParts] = part.text.split(" ");
|
|
3511
|
+
return `${colorizeBySeverity(icon, part.severity)} ${highlighter.dim(labelParts.join(" "))}`;
|
|
3512
|
+
});
|
|
3345
3513
|
logger.log(` ${renderedParts.join(" ")}`);
|
|
3346
3514
|
grayLine(" Run `npx react-doctor@latest . --verbose` to get all details");
|
|
3347
3515
|
logger.break();
|
|
@@ -3361,6 +3529,7 @@ const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
|
|
|
3361
3529
|
firstDiagnostic.message
|
|
3362
3530
|
];
|
|
3363
3531
|
if (firstDiagnostic.help) sections.push("", `Suggestion: ${firstDiagnostic.help}`);
|
|
3532
|
+
if (firstDiagnostic.url) sections.push("", `Docs: ${firstDiagnostic.url}`);
|
|
3364
3533
|
sections.push("", "Files:");
|
|
3365
3534
|
const fileSites = buildVerboseSiteMap(ruleDiagnostics);
|
|
3366
3535
|
for (const [filePath, sites] of fileSites) if (sites.length > 0) for (const site of sites) {
|
|
@@ -3432,32 +3601,6 @@ const printNoScoreHeader = (noScoreMessage) => {
|
|
|
3432
3601
|
logger.log(` ${highlighter.gray(noScoreMessage)}`);
|
|
3433
3602
|
logger.break();
|
|
3434
3603
|
};
|
|
3435
|
-
const buildCategoryBar = (count, maximumCount, useErrorColor) => {
|
|
3436
|
-
if (maximumCount === 0) return highlighter.dim("░".repeat(16));
|
|
3437
|
-
const filledCount = Math.max(1, Math.round(count / maximumCount * 16));
|
|
3438
|
-
const cappedFilledCount = Math.min(filledCount, 16);
|
|
3439
|
-
const emptyCount = 16 - cappedFilledCount;
|
|
3440
|
-
const filledSegment = "█".repeat(cappedFilledCount);
|
|
3441
|
-
const emptySegment = "░".repeat(emptyCount);
|
|
3442
|
-
return `${useErrorColor ? highlighter.error(filledSegment) : highlighter.warn(filledSegment)}${highlighter.dim(emptySegment)}`;
|
|
3443
|
-
};
|
|
3444
|
-
const padCategoryLabel = (categoryLabel) => {
|
|
3445
|
-
if (categoryLabel.length >= 18) return categoryLabel;
|
|
3446
|
-
return categoryLabel + " ".repeat(18 - categoryLabel.length);
|
|
3447
|
-
};
|
|
3448
|
-
const printCategoryBreakdown = (entries) => {
|
|
3449
|
-
if (entries.length === 0) return;
|
|
3450
|
-
const maximumCount = Math.max(...entries.map((entry) => entry.totalCount));
|
|
3451
|
-
logger.dim(" By category");
|
|
3452
|
-
for (const entry of entries) {
|
|
3453
|
-
const paddedLabel = padCategoryLabel(entry.category);
|
|
3454
|
-
const categoryBar = buildCategoryBar(entry.totalCount, maximumCount, entry.errorCount > 0);
|
|
3455
|
-
const totalCountDisplay = String(entry.totalCount);
|
|
3456
|
-
const errorBadge = entry.errorCount > 0 ? ` ${highlighter.error(`${entry.errorCount}×`)}` : "";
|
|
3457
|
-
logger.log(` ${paddedLabel}${categoryBar} ${totalCountDisplay}${errorBadge}`);
|
|
3458
|
-
}
|
|
3459
|
-
logger.break();
|
|
3460
|
-
};
|
|
3461
3604
|
const buildShareUrl = (diagnostics, scoreResult, projectName) => {
|
|
3462
3605
|
const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
3463
3606
|
const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
|
|
@@ -3483,7 +3626,6 @@ const printCountsSummaryLine = (diagnostics, totalSourceFileCount, elapsedMillis
|
|
|
3483
3626
|
logger.log(` ${issueCountColor(issueCountText)} ${highlighter.dim(`${fileCountText} ${elapsedTimeText}`)}`);
|
|
3484
3627
|
};
|
|
3485
3628
|
const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, projectName, totalSourceFileCount, noScoreMessage, isOffline) => {
|
|
3486
|
-
printCategoryBreakdown(buildCategoryBreakdown(diagnostics));
|
|
3487
3629
|
if (scoreResult) printScoreHeader(scoreResult);
|
|
3488
3630
|
else printNoScoreHeader(noScoreMessage);
|
|
3489
3631
|
printCountsSummaryLine(diagnostics, totalSourceFileCount, elapsedMilliseconds);
|
|
@@ -3565,7 +3707,15 @@ const printProjectDetection = (projectInfo, userConfig, isDiffMode, includePaths
|
|
|
3565
3707
|
};
|
|
3566
3708
|
const scan = async (directory, inputOptions = {}) => {
|
|
3567
3709
|
const startTime = performance.now();
|
|
3568
|
-
|
|
3710
|
+
let scanDirectory = directory;
|
|
3711
|
+
let userConfig;
|
|
3712
|
+
if (inputOptions.configOverride !== void 0) userConfig = inputOptions.configOverride;
|
|
3713
|
+
else {
|
|
3714
|
+
const loadedConfig = loadConfigWithSource(directory);
|
|
3715
|
+
const redirectedDirectory = resolveConfigRootDir(loadedConfig?.config ?? null, loadedConfig?.sourceDirectory ?? null);
|
|
3716
|
+
if (redirectedDirectory) scanDirectory = redirectedDirectory;
|
|
3717
|
+
userConfig = loadedConfig?.config ?? null;
|
|
3718
|
+
}
|
|
3569
3719
|
const options = mergeScanOptions(inputOptions, userConfig);
|
|
3570
3720
|
const wasLoggerSilent = isLoggerSilent();
|
|
3571
3721
|
const wasSpinnerSilent = isSpinnerSilent();
|
|
@@ -3574,7 +3724,7 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
3574
3724
|
setSpinnerSilent(true);
|
|
3575
3725
|
}
|
|
3576
3726
|
try {
|
|
3577
|
-
return await runScan(
|
|
3727
|
+
return await runScan(scanDirectory, options, userConfig, startTime);
|
|
3578
3728
|
} finally {
|
|
3579
3729
|
if (options.silent) {
|
|
3580
3730
|
setLoggerSilent(wasLoggerSilent);
|
|
@@ -3586,7 +3736,7 @@ const runScan = async (directory, options, userConfig, startTime) => {
|
|
|
3586
3736
|
const projectInfo = discoverProject(directory);
|
|
3587
3737
|
const { includePaths } = options;
|
|
3588
3738
|
const isDiffMode = includePaths.length > 0;
|
|
3589
|
-
if (!projectInfo.reactVersion) throw new
|
|
3739
|
+
if (!projectInfo.reactVersion) throw new NoReactDependencyError(directory);
|
|
3590
3740
|
const lintIncludePaths = computeJsxIncludePaths(includePaths) ?? resolveLintIncludePaths(directory, userConfig);
|
|
3591
3741
|
const lintSourceFileCount = lintIncludePaths?.length ?? projectInfo.sourceFileCount;
|
|
3592
3742
|
if (!options.scoreOnly) printProjectDetection(projectInfo, userConfig, isDiffMode, includePaths, lintSourceFileCount);
|
|
@@ -4102,7 +4252,7 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
|
|
|
4102
4252
|
};
|
|
4103
4253
|
//#endregion
|
|
4104
4254
|
//#region src/cli.ts
|
|
4105
|
-
const VERSION = "0.1.
|
|
4255
|
+
const VERSION = "0.1.5";
|
|
4106
4256
|
const VALID_FAIL_ON_LEVELS = new Set([
|
|
4107
4257
|
"error",
|
|
4108
4258
|
"warning",
|
|
@@ -4281,20 +4431,28 @@ const validateModeFlags = (flags) => {
|
|
|
4281
4431
|
if (flags.explain !== void 0 && flags.why !== void 0) throw new Error("Use --explain or --why, not both — they're aliases of the same flag.");
|
|
4282
4432
|
if ((flags.explain ?? flags.why) !== void 0 && (flags.json || flags.score || flags.annotations || flags.staged)) throw new Error("--explain cannot be combined with --json, --score, --annotations, or --staged.");
|
|
4283
4433
|
};
|
|
4284
|
-
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 detection").option("--no-dead-code", "skip dead code detection").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--full", "force a full scan (overrides any `diff` value in config or `--diff`)").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch (pass `false` to disable; overridden by --full)").option("--offline", "skip telemetry (anonymous, not stored, only used to calculate score)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--fail-on <level>", "exit with error code on diagnostics: error, warning, none", "error").option("--annotations", "output diagnostics as GitHub Actions annotations").option("--explain <file:line>", "diagnose why a rule fired or why a suppression didn't apply at a specific location").
|
|
4434
|
+
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 detection").option("--no-dead-code", "skip dead code detection").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--full", "force a full scan (overrides any `diff` value in config or `--diff`)").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch (pass `false` to disable; overridden by --full)").option("--offline", "skip telemetry (anonymous, not stored, only used to calculate score)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--fail-on <level>", "exit with error code on diagnostics: error, warning, none", "error").option("--annotations", "output diagnostics as GitHub Actions annotations").option("--explain <file:line>", "diagnose why a rule fired or why a suppression didn't apply at a specific location").option("--why <file:line>", "alias for --explain").option("--respect-inline-disables", "respect inline `// eslint-disable*` / `// oxlint-disable*` comments (default)").option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").action(async (directory, flags) => {
|
|
4285
4435
|
const isScoreOnly = flags.score;
|
|
4286
4436
|
const isJsonMode = flags.json;
|
|
4287
4437
|
const isQuiet = isScoreOnly || isJsonMode;
|
|
4288
|
-
const
|
|
4438
|
+
const requestedDirectory = path.resolve(directory);
|
|
4289
4439
|
const jsonStartTime = performance.now();
|
|
4290
4440
|
isJsonModeActive = isJsonMode;
|
|
4291
4441
|
isCompactJsonOutput = Boolean(flags.jsonCompact);
|
|
4292
|
-
resolvedDirectoryForCancel =
|
|
4442
|
+
resolvedDirectoryForCancel = requestedDirectory;
|
|
4293
4443
|
cancelStartTime = jsonStartTime;
|
|
4294
4444
|
if (isJsonMode) setLoggerSilent(true);
|
|
4295
4445
|
try {
|
|
4296
4446
|
validateModeFlags(flags);
|
|
4297
|
-
const
|
|
4447
|
+
const loadedConfig = loadConfigWithSource(requestedDirectory);
|
|
4448
|
+
const userConfig = loadedConfig?.config ?? null;
|
|
4449
|
+
const redirectedDirectory = resolveConfigRootDir(loadedConfig?.config ?? null, loadedConfig?.sourceDirectory ?? null);
|
|
4450
|
+
const resolvedDirectory = redirectedDirectory ?? requestedDirectory;
|
|
4451
|
+
resolvedDirectoryForCancel = resolvedDirectory;
|
|
4452
|
+
if (redirectedDirectory && !isQuiet) {
|
|
4453
|
+
logger.dim(`Redirected to ${highlighter.info(toRelativePath(resolvedDirectory, requestedDirectory))} via react-doctor config "rootDir".`);
|
|
4454
|
+
logger.break();
|
|
4455
|
+
}
|
|
4298
4456
|
const explainArgument = flags.explain ?? flags.why;
|
|
4299
4457
|
if (explainArgument !== void 0) {
|
|
4300
4458
|
await runExplain(explainArgument, {
|
|
@@ -4368,7 +4526,7 @@ const program = new Command().name("react-doctor").description("Diagnose React c
|
|
|
4368
4526
|
totalElapsedMilliseconds: performance.now() - jsonStartTime
|
|
4369
4527
|
}));
|
|
4370
4528
|
if (flags.annotations) printAnnotations(remappedDiagnostics, isJsonMode);
|
|
4371
|
-
if (shouldFailForDiagnostics(remappedDiagnostics, resolveFailOnLevel(program, flags, userConfig))) process.exitCode = 1;
|
|
4529
|
+
if (!isScoreOnly && shouldFailForDiagnostics(remappedDiagnostics, resolveFailOnLevel(program, flags, userConfig))) process.exitCode = 1;
|
|
4372
4530
|
} finally {
|
|
4373
4531
|
cleanupSnapshot?.();
|
|
4374
4532
|
}
|
|
@@ -4412,7 +4570,8 @@ const program = new Command().name("react-doctor").description("Diagnose React c
|
|
|
4412
4570
|
}
|
|
4413
4571
|
const scanResult = await scan(projectDirectory, {
|
|
4414
4572
|
...scanOptions,
|
|
4415
|
-
includePaths
|
|
4573
|
+
includePaths,
|
|
4574
|
+
configOverride: userConfig
|
|
4416
4575
|
});
|
|
4417
4576
|
allDiagnostics.push(...scanResult.diagnostics);
|
|
4418
4577
|
completedScans.push({
|
|
@@ -4431,13 +4590,13 @@ const program = new Command().name("react-doctor").description("Diagnose React c
|
|
|
4431
4590
|
totalElapsedMilliseconds: performance.now() - jsonStartTime
|
|
4432
4591
|
}));
|
|
4433
4592
|
if (flags.annotations) printAnnotations(allDiagnostics, isJsonMode);
|
|
4434
|
-
if (shouldFailForDiagnostics(allDiagnostics, resolveFailOnLevel(program, flags, userConfig))) process.exitCode = 1;
|
|
4593
|
+
if (!isScoreOnly && shouldFailForDiagnostics(allDiagnostics, resolveFailOnLevel(program, flags, userConfig))) process.exitCode = 1;
|
|
4435
4594
|
} catch (error) {
|
|
4436
4595
|
try {
|
|
4437
4596
|
if (isJsonMode) {
|
|
4438
4597
|
writeJsonReport(buildJsonReportError({
|
|
4439
4598
|
version: VERSION,
|
|
4440
|
-
directory:
|
|
4599
|
+
directory: resolvedDirectoryForCancel ?? requestedDirectory,
|
|
4441
4600
|
error,
|
|
4442
4601
|
elapsedMilliseconds: performance.now() - jsonStartTime,
|
|
4443
4602
|
mode: currentReportMode
|
package/dist/eslint-plugin.js
CHANGED