react-doctor 0.0.19 → 0.0.21
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 +74 -16
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -1
- package/dist/index.js.map +1 -1
- package/dist/react-doctor-plugin.js +5 -2
- package/dist/react-doctor-plugin.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -28,8 +28,10 @@ const SUMMARY_BOX_HORIZONTAL_PADDING_CHARS = 1;
|
|
|
28
28
|
const SUMMARY_BOX_OUTER_INDENT_CHARS = 2;
|
|
29
29
|
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
30
30
|
const SHARE_BASE_URL = "https://www.react.doctor/share";
|
|
31
|
+
const OPEN_BASE_URL = "https://www.react.doctor/open";
|
|
31
32
|
const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
32
33
|
const OFFLINE_MESSAGE = "You are offline, could not calculate score. Reconnect to calculate.";
|
|
34
|
+
const OFFLINE_FLAG_MESSAGE = "Score not calculated. Remove --offline to calculate score.";
|
|
33
35
|
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
34
36
|
|
|
35
37
|
//#endregion
|
|
@@ -765,6 +767,46 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler }) => ({
|
|
|
765
767
|
}
|
|
766
768
|
});
|
|
767
769
|
|
|
770
|
+
//#endregion
|
|
771
|
+
//#region src/utils/neutralize-disable-directives.ts
|
|
772
|
+
const findFilesWithDisableDirectives = (rootDirectory) => {
|
|
773
|
+
const result = spawnSync("git", [
|
|
774
|
+
"grep",
|
|
775
|
+
"-l",
|
|
776
|
+
"--untracked",
|
|
777
|
+
"-E",
|
|
778
|
+
"(eslint|oxlint)-disable"
|
|
779
|
+
], {
|
|
780
|
+
cwd: rootDirectory,
|
|
781
|
+
encoding: "utf-8",
|
|
782
|
+
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
|
|
783
|
+
});
|
|
784
|
+
if (result.error || result.status === null) return [];
|
|
785
|
+
return result.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
|
|
786
|
+
};
|
|
787
|
+
const neutralizeContent = (content) => content.replaceAll("eslint-disable", "eslint_disable").replaceAll("oxlint-disable", "oxlint_disable");
|
|
788
|
+
const neutralizeDisableDirectives = (rootDirectory) => {
|
|
789
|
+
const filePaths = findFilesWithDisableDirectives(rootDirectory);
|
|
790
|
+
const originalContents = /* @__PURE__ */ new Map();
|
|
791
|
+
for (const relativePath of filePaths) {
|
|
792
|
+
const absolutePath = path.join(rootDirectory, relativePath);
|
|
793
|
+
let originalContent;
|
|
794
|
+
try {
|
|
795
|
+
originalContent = fs.readFileSync(absolutePath, "utf-8");
|
|
796
|
+
} catch {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
const neutralizedContent = neutralizeContent(originalContent);
|
|
800
|
+
if (neutralizedContent !== originalContent) {
|
|
801
|
+
originalContents.set(absolutePath, originalContent);
|
|
802
|
+
fs.writeFileSync(absolutePath, neutralizedContent);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return () => {
|
|
806
|
+
for (const [absolutePath, originalContent] of originalContents) fs.writeFileSync(absolutePath, originalContent);
|
|
807
|
+
};
|
|
808
|
+
};
|
|
809
|
+
|
|
768
810
|
//#endregion
|
|
769
811
|
//#region src/utils/run-oxlint.ts
|
|
770
812
|
const esmRequire = createRequire(import.meta.url);
|
|
@@ -832,7 +874,7 @@ const RULE_CATEGORY_MAP = {
|
|
|
832
874
|
"react-doctor/async-parallel": "Performance"
|
|
833
875
|
};
|
|
834
876
|
const RULE_HELP_MAP = {
|
|
835
|
-
"no-derived-state-effect": "
|
|
877
|
+
"no-derived-state-effect": "For derived state, compute inline: `const x = fn(dep)`. For state resets on prop change, use a key prop: `<Component key={prop} />`",
|
|
836
878
|
"no-fetch-in-effect": "Use `useQuery()` from @tanstack/react-query, `useSWR()`, or fetch in a Server Component instead",
|
|
837
879
|
"no-cascading-set-state": "Combine into useReducer: `const [state, dispatch] = useReducer(reducer, initialState)`",
|
|
838
880
|
"no-effect-event-handler": "Move the conditional logic into onClick, onChange, or onSubmit handlers directly",
|
|
@@ -934,6 +976,7 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
|
|
|
934
976
|
framework,
|
|
935
977
|
hasReactCompiler
|
|
936
978
|
});
|
|
979
|
+
const restoreDisableDirectives = neutralizeDisableDirectives(rootDirectory);
|
|
937
980
|
try {
|
|
938
981
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
939
982
|
const args = [
|
|
@@ -989,6 +1032,7 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
|
|
|
989
1032
|
};
|
|
990
1033
|
});
|
|
991
1034
|
} finally {
|
|
1035
|
+
restoreDisableDirectives();
|
|
992
1036
|
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
|
|
993
1037
|
}
|
|
994
1038
|
};
|
|
@@ -1170,7 +1214,7 @@ const buildShareUrl = (diagnostics, scoreResult, projectName) => {
|
|
|
1170
1214
|
if (affectedFileCount > 0) params.set("f", String(affectedFileCount));
|
|
1171
1215
|
return `${SHARE_BASE_URL}?${params.toString()}`;
|
|
1172
1216
|
};
|
|
1173
|
-
const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, projectName, totalSourceFileCount) => {
|
|
1217
|
+
const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, projectName, totalSourceFileCount, noScoreMessage) => {
|
|
1174
1218
|
const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
1175
1219
|
const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
|
|
1176
1220
|
const affectedFileCount = collectAffectedFiles(diagnostics).size;
|
|
@@ -1212,7 +1256,7 @@ const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, projectName
|
|
|
1212
1256
|
} else {
|
|
1213
1257
|
summaryFramedLines.push(createFramedLine("React Doctor (www.react.doctor)", `React Doctor ${highlighter.dim("(www.react.doctor)")}`));
|
|
1214
1258
|
summaryFramedLines.push(createFramedLine(""));
|
|
1215
|
-
summaryFramedLines.push(createFramedLine(
|
|
1259
|
+
summaryFramedLines.push(createFramedLine(noScoreMessage, highlighter.dim(noScoreMessage)));
|
|
1216
1260
|
summaryFramedLines.push(createFramedLine(""));
|
|
1217
1261
|
}
|
|
1218
1262
|
summaryFramedLines.push(createFramedLine(summaryLinePartsPlain.join(" "), summaryLineParts.join(" ")));
|
|
@@ -1237,6 +1281,7 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
1237
1281
|
deadCode: inputOptions.deadCode ?? userConfig?.deadCode ?? true,
|
|
1238
1282
|
verbose: inputOptions.verbose ?? userConfig?.verbose ?? false,
|
|
1239
1283
|
scoreOnly: inputOptions.scoreOnly ?? false,
|
|
1284
|
+
offline: inputOptions.offline ?? false,
|
|
1240
1285
|
includePaths: inputOptions.includePaths
|
|
1241
1286
|
};
|
|
1242
1287
|
const includePaths = options.includePaths ?? [];
|
|
@@ -1266,7 +1311,10 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
1266
1311
|
return lintDiagnostics;
|
|
1267
1312
|
} catch (error) {
|
|
1268
1313
|
lintSpinner?.fail("Lint checks failed (non-fatal, skipping).");
|
|
1269
|
-
|
|
1314
|
+
if (error instanceof Error) {
|
|
1315
|
+
logger.error(error.message);
|
|
1316
|
+
if (error.stack) logger.dim(error.stack);
|
|
1317
|
+
} else logger.error(String(error));
|
|
1270
1318
|
return [];
|
|
1271
1319
|
}
|
|
1272
1320
|
})() : Promise.resolve([]);
|
|
@@ -1290,10 +1338,11 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
1290
1338
|
];
|
|
1291
1339
|
const diagnostics = userConfig ? filterIgnoredDiagnostics(allDiagnostics, userConfig) : allDiagnostics;
|
|
1292
1340
|
const elapsedMilliseconds = performance.now() - startTime;
|
|
1293
|
-
const scoreResult = await calculateScore(diagnostics);
|
|
1341
|
+
const scoreResult = options.offline ? null : await calculateScore(diagnostics);
|
|
1342
|
+
const noScoreMessage = options.offline ? OFFLINE_FLAG_MESSAGE : OFFLINE_MESSAGE;
|
|
1294
1343
|
if (options.scoreOnly) {
|
|
1295
1344
|
if (scoreResult) logger.log(`${scoreResult.score}`);
|
|
1296
|
-
else logger.dim(
|
|
1345
|
+
else logger.dim(noScoreMessage);
|
|
1297
1346
|
return;
|
|
1298
1347
|
}
|
|
1299
1348
|
if (diagnostics.length === 0) {
|
|
@@ -1302,12 +1351,12 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
1302
1351
|
if (scoreResult) {
|
|
1303
1352
|
printBranding(scoreResult.score);
|
|
1304
1353
|
printScoreGauge(scoreResult.score, scoreResult.label);
|
|
1305
|
-
} else logger.dim(` ${
|
|
1354
|
+
} else logger.dim(` ${noScoreMessage}`);
|
|
1306
1355
|
return;
|
|
1307
1356
|
}
|
|
1308
1357
|
printDiagnostics(diagnostics, options.verbose);
|
|
1309
1358
|
const displayedSourceFileCount = isDiffMode ? includePaths.length : projectInfo.sourceFileCount;
|
|
1310
|
-
printSummary(diagnostics, elapsedMilliseconds, scoreResult, projectInfo.projectName, displayedSourceFileCount);
|
|
1359
|
+
printSummary(diagnostics, elapsedMilliseconds, scoreResult, projectInfo.projectName, displayedSourceFileCount, noScoreMessage);
|
|
1311
1360
|
};
|
|
1312
1361
|
|
|
1313
1362
|
//#endregion
|
|
@@ -1614,7 +1663,7 @@ const maybePromptSkillInstall = async (shouldSkipPrompts) => {
|
|
|
1614
1663
|
|
|
1615
1664
|
//#endregion
|
|
1616
1665
|
//#region src/cli.ts
|
|
1617
|
-
const VERSION = "0.0.
|
|
1666
|
+
const VERSION = "0.0.21";
|
|
1618
1667
|
process.on("SIGINT", () => process.exit(0));
|
|
1619
1668
|
process.on("SIGTERM", () => process.exit(0));
|
|
1620
1669
|
const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isScoreOnly) => {
|
|
@@ -1639,7 +1688,7 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isSco
|
|
|
1639
1688
|
});
|
|
1640
1689
|
return Boolean(shouldScanBranchOnly);
|
|
1641
1690
|
};
|
|
1642
|
-
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("--no-lint", "skip linting").option("--no-dead-code", "skip dead code detection").option("--verbose", "show file details per rule").option("--score", "output only the score").option("-y, --yes", "skip prompts, scan all workspace projects").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch").option("--fix", "open Ami to auto-fix all issues").option("--prompt", "copy latest scan output to clipboard").action(async (directory, flags) => {
|
|
1691
|
+
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("--no-lint", "skip linting").option("--no-dead-code", "skip dead code detection").option("--verbose", "show file details per rule").option("--score", "output only the score").option("-y, --yes", "skip prompts, scan all workspace projects").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch").option("--offline", "skip telemetry (anonymous, not stored, only used to calculate score)").option("--fix", "open Ami to auto-fix all issues").option("--prompt", "copy latest scan output to clipboard").action(async (directory, flags) => {
|
|
1643
1692
|
const isScoreOnly = flags.score && !flags.prompt;
|
|
1644
1693
|
const shouldCopyPromptOutput = flags.prompt;
|
|
1645
1694
|
if (shouldCopyPromptOutput) startLoggerCapture();
|
|
@@ -1655,7 +1704,8 @@ const program = new Command().name("react-doctor").description("Diagnose React c
|
|
|
1655
1704
|
lint: isCliOverride("lint") ? flags.lint : userConfig?.lint ?? flags.lint,
|
|
1656
1705
|
deadCode: isCliOverride("deadCode") ? flags.deadCode : userConfig?.deadCode ?? flags.deadCode,
|
|
1657
1706
|
verbose: flags.prompt || (isCliOverride("verbose") ? Boolean(flags.verbose) : userConfig?.verbose ?? false),
|
|
1658
|
-
scoreOnly: isScoreOnly
|
|
1707
|
+
scoreOnly: isScoreOnly,
|
|
1708
|
+
offline: flags.offline
|
|
1659
1709
|
};
|
|
1660
1710
|
const isAutomatedEnvironment = [
|
|
1661
1711
|
process.env.CI,
|
|
@@ -1753,12 +1803,20 @@ const openUrl = (url) => {
|
|
|
1753
1803
|
}
|
|
1754
1804
|
execSync(process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`, { stdio: "ignore" });
|
|
1755
1805
|
};
|
|
1756
|
-
const
|
|
1757
|
-
|
|
1758
|
-
|
|
1806
|
+
const buildDeeplinkParams = (directory) => {
|
|
1807
|
+
const params = new URLSearchParams();
|
|
1808
|
+
params.set("cwd", path.resolve(directory));
|
|
1809
|
+
params.set("prompt", DEEPLINK_FIX_PROMPT);
|
|
1810
|
+
params.set("mode", "agent");
|
|
1811
|
+
params.set("autoSubmit", "true");
|
|
1812
|
+
return params;
|
|
1813
|
+
};
|
|
1814
|
+
const buildDeeplink = (directory) => `ami://open-project?${buildDeeplinkParams(directory).toString()}`;
|
|
1815
|
+
const buildWebDeeplink = (directory) => `${OPEN_BASE_URL}?${buildDeeplinkParams(directory).toString()}`;
|
|
1759
1816
|
const openAmiToFix = (directory) => {
|
|
1760
1817
|
const isInstalled = isAmiInstalled();
|
|
1761
1818
|
const deeplink = buildDeeplink(directory);
|
|
1819
|
+
const webDeeplink = buildWebDeeplink(directory);
|
|
1762
1820
|
if (!isInstalled) {
|
|
1763
1821
|
if (process.platform === "darwin") {
|
|
1764
1822
|
installAmi();
|
|
@@ -1769,7 +1827,7 @@ const openAmiToFix = (directory) => {
|
|
|
1769
1827
|
}
|
|
1770
1828
|
logger.break();
|
|
1771
1829
|
logger.dim("Once Ami is running, open this link to start fixing:");
|
|
1772
|
-
logger.info(
|
|
1830
|
+
logger.info(webDeeplink);
|
|
1773
1831
|
return;
|
|
1774
1832
|
}
|
|
1775
1833
|
logger.log("Opening Ami to fix react-doctor issues...");
|
|
@@ -1779,7 +1837,7 @@ const openAmiToFix = (directory) => {
|
|
|
1779
1837
|
} catch {
|
|
1780
1838
|
logger.break();
|
|
1781
1839
|
logger.dim("Could not open Ami automatically. Open this URL manually:");
|
|
1782
|
-
logger.info(
|
|
1840
|
+
logger.info(webDeeplink);
|
|
1783
1841
|
}
|
|
1784
1842
|
};
|
|
1785
1843
|
const buildPromptWithOutput = (reactDoctorOutput) => {
|