react-doctor 0.0.16 → 0.0.18
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 +70 -21
- package/dist/cli.js.map +1 -1
- package/dist/index.js +45 -18
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -242,23 +242,34 @@ const resolveWorkspaceDirectories = (rootDirectory, pattern) => {
|
|
|
242
242
|
if (!fs.existsSync(baseDirectory) || !fs.statSync(baseDirectory).isDirectory()) return [];
|
|
243
243
|
return fs.readdirSync(baseDirectory).map((entry) => path.join(baseDirectory, entry)).filter((entryPath) => fs.statSync(entryPath).isDirectory() && fs.existsSync(path.join(entryPath, "package.json")));
|
|
244
244
|
};
|
|
245
|
-
const
|
|
245
|
+
const isMonorepoRoot = (directory) => {
|
|
246
|
+
if (fs.existsSync(path.join(directory, "pnpm-workspace.yaml"))) return true;
|
|
247
|
+
const packageJsonPath = path.join(directory, "package.json");
|
|
248
|
+
if (!fs.existsSync(packageJsonPath)) return false;
|
|
249
|
+
const packageJson = readPackageJson(packageJsonPath);
|
|
250
|
+
return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
|
|
251
|
+
};
|
|
252
|
+
const findMonorepoRoot$1 = (startDirectory) => {
|
|
246
253
|
let currentDirectory = path.dirname(startDirectory);
|
|
247
|
-
const result = {
|
|
248
|
-
reactVersion: null,
|
|
249
|
-
framework: "unknown"
|
|
250
|
-
};
|
|
251
254
|
while (currentDirectory !== path.dirname(currentDirectory)) {
|
|
252
|
-
|
|
253
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
254
|
-
const info = extractDependencyInfo(readPackageJson(packageJsonPath));
|
|
255
|
-
if (!result.reactVersion && info.reactVersion) result.reactVersion = info.reactVersion;
|
|
256
|
-
if (result.framework === "unknown" && info.framework !== "unknown") result.framework = info.framework;
|
|
257
|
-
if (result.reactVersion && result.framework !== "unknown") return result;
|
|
258
|
-
}
|
|
255
|
+
if (isMonorepoRoot(currentDirectory)) return currentDirectory;
|
|
259
256
|
currentDirectory = path.dirname(currentDirectory);
|
|
260
257
|
}
|
|
261
|
-
return
|
|
258
|
+
return null;
|
|
259
|
+
};
|
|
260
|
+
const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
261
|
+
const monorepoRoot = findMonorepoRoot$1(directory);
|
|
262
|
+
if (!monorepoRoot) return {
|
|
263
|
+
reactVersion: null,
|
|
264
|
+
framework: "unknown"
|
|
265
|
+
};
|
|
266
|
+
const rootPackageJson = readPackageJson(path.join(monorepoRoot, "package.json"));
|
|
267
|
+
const rootInfo = extractDependencyInfo(rootPackageJson);
|
|
268
|
+
const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
|
|
269
|
+
return {
|
|
270
|
+
reactVersion: rootInfo.reactVersion ?? workspaceInfo.reactVersion,
|
|
271
|
+
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
272
|
+
};
|
|
262
273
|
};
|
|
263
274
|
const findReactInWorkspaces = (rootDirectory, packageJson) => {
|
|
264
275
|
const patterns = getWorkspacePatterns(rootDirectory, packageJson);
|
|
@@ -355,10 +366,10 @@ const discoverProject = (directory) => {
|
|
|
355
366
|
if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
|
|
356
367
|
if (framework === "unknown" && workspaceInfo.framework !== "unknown") framework = workspaceInfo.framework;
|
|
357
368
|
}
|
|
358
|
-
if (!reactVersion || framework === "unknown") {
|
|
359
|
-
const
|
|
360
|
-
if (!reactVersion) reactVersion =
|
|
361
|
-
if (framework === "unknown") framework =
|
|
369
|
+
if ((!reactVersion || framework === "unknown") && !isMonorepoRoot(directory)) {
|
|
370
|
+
const monorepoInfo = findDependencyInfoFromMonorepoRoot(directory);
|
|
371
|
+
if (!reactVersion) reactVersion = monorepoInfo.reactVersion;
|
|
372
|
+
if (framework === "unknown") framework = monorepoInfo.framework;
|
|
362
373
|
}
|
|
363
374
|
const projectName = packageJson.name ?? path.basename(directory);
|
|
364
375
|
const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
|
|
@@ -474,15 +485,18 @@ const silenced = async (fn) => {
|
|
|
474
485
|
const originalLog = console.log;
|
|
475
486
|
const originalInfo = console.info;
|
|
476
487
|
const originalWarn = console.warn;
|
|
488
|
+
const originalError = console.error;
|
|
477
489
|
console.log = () => {};
|
|
478
490
|
console.info = () => {};
|
|
479
491
|
console.warn = () => {};
|
|
492
|
+
console.error = () => {};
|
|
480
493
|
try {
|
|
481
494
|
return await fn();
|
|
482
495
|
} finally {
|
|
483
496
|
console.log = originalLog;
|
|
484
497
|
console.info = originalInfo;
|
|
485
498
|
console.warn = originalWarn;
|
|
499
|
+
console.error = originalError;
|
|
486
500
|
}
|
|
487
501
|
};
|
|
488
502
|
const findMonorepoRoot = (directory) => {
|
|
@@ -498,13 +512,26 @@ const findMonorepoRoot = (directory) => {
|
|
|
498
512
|
}
|
|
499
513
|
return null;
|
|
500
514
|
};
|
|
515
|
+
const CONFIG_LOADING_ERROR_PATTERN = /Error loading .*\/([a-z-]+)\.config\./;
|
|
516
|
+
const extractFailedPluginName = (error) => {
|
|
517
|
+
return String(error).match(CONFIG_LOADING_ERROR_PATTERN)?.[1] ?? null;
|
|
518
|
+
};
|
|
519
|
+
const MAX_KNIP_RETRIES = 5;
|
|
501
520
|
const runKnipWithOptions = async (knipCwd, workspaceName) => {
|
|
502
521
|
const options = await silenced(() => createOptions({
|
|
503
522
|
cwd: knipCwd,
|
|
504
523
|
isShowProgress: false,
|
|
505
524
|
...workspaceName ? { workspace: workspaceName } : {}
|
|
506
525
|
}));
|
|
507
|
-
|
|
526
|
+
const parsedConfig = options.parsedConfig;
|
|
527
|
+
for (let attempt = 0; attempt <= MAX_KNIP_RETRIES; attempt++) try {
|
|
528
|
+
return await silenced(() => main(options));
|
|
529
|
+
} catch (error) {
|
|
530
|
+
const failedPlugin = extractFailedPluginName(error);
|
|
531
|
+
if (!failedPlugin || attempt === MAX_KNIP_RETRIES) throw error;
|
|
532
|
+
parsedConfig[failedPlugin] = false;
|
|
533
|
+
}
|
|
534
|
+
throw new Error("Unreachable");
|
|
508
535
|
};
|
|
509
536
|
const hasNodeModules = (directory) => {
|
|
510
537
|
const nodeModulesPath = path.join(directory, "node_modules");
|
|
@@ -1161,8 +1188,9 @@ const scan = async (directory, options) => {
|
|
|
1161
1188
|
const lintDiagnostics = await runOxlint(directory, projectInfo.hasTypeScript, projectInfo.framework, projectInfo.hasReactCompiler);
|
|
1162
1189
|
lintSpinner?.succeed("Running lint checks.");
|
|
1163
1190
|
return lintDiagnostics;
|
|
1164
|
-
} catch {
|
|
1191
|
+
} catch (error) {
|
|
1165
1192
|
lintSpinner?.fail("Lint checks failed (non-fatal, skipping).");
|
|
1193
|
+
logger.error(String(error));
|
|
1166
1194
|
return [];
|
|
1167
1195
|
}
|
|
1168
1196
|
})() : Promise.resolve([]);
|
|
@@ -1172,8 +1200,9 @@ const scan = async (directory, options) => {
|
|
|
1172
1200
|
const knipDiagnostics = await runKnip(directory);
|
|
1173
1201
|
deadCodeSpinner?.succeed("Detecting dead code.");
|
|
1174
1202
|
return knipDiagnostics;
|
|
1175
|
-
} catch {
|
|
1203
|
+
} catch (error) {
|
|
1176
1204
|
deadCodeSpinner?.fail("Dead code detection failed (non-fatal, skipping).");
|
|
1205
|
+
logger.error(String(error));
|
|
1177
1206
|
return [];
|
|
1178
1207
|
}
|
|
1179
1208
|
})() : Promise.resolve([]);
|
|
@@ -1203,6 +1232,14 @@ const scan = async (directory, options) => {
|
|
|
1203
1232
|
printSummary(diagnostics, elapsedMilliseconds, scoreResult, projectInfo.projectName, projectInfo.sourceFileCount);
|
|
1204
1233
|
};
|
|
1205
1234
|
|
|
1235
|
+
//#endregion
|
|
1236
|
+
//#region src/utils/should-auto-select-current-choice.ts
|
|
1237
|
+
const shouldAutoSelectCurrentChoice = (choiceStates, cursor) => {
|
|
1238
|
+
if (choiceStates.some((choiceState) => choiceState.selected)) return false;
|
|
1239
|
+
const currentChoice = choiceStates[cursor];
|
|
1240
|
+
return Boolean(currentChoice) && !currentChoice.disabled;
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1206
1243
|
//#endregion
|
|
1207
1244
|
//#region src/utils/should-select-all-choices.ts
|
|
1208
1245
|
const shouldSelectAllChoices = (choiceStates) => {
|
|
@@ -1214,6 +1251,7 @@ const shouldSelectAllChoices = (choiceStates) => {
|
|
|
1214
1251
|
const require = createRequire(import.meta.url);
|
|
1215
1252
|
const PROMPTS_MULTISELECT_MODULE_PATH = "prompts/lib/elements/multiselect";
|
|
1216
1253
|
let didPatchMultiselectToggleAll = false;
|
|
1254
|
+
let didPatchMultiselectSubmit = false;
|
|
1217
1255
|
const onCancel = () => {
|
|
1218
1256
|
logger.break();
|
|
1219
1257
|
logger.log("Cancelled.");
|
|
@@ -1238,8 +1276,19 @@ const patchMultiselectToggleAll = () => {
|
|
|
1238
1276
|
this.render();
|
|
1239
1277
|
};
|
|
1240
1278
|
};
|
|
1279
|
+
const patchMultiselectSubmit = () => {
|
|
1280
|
+
if (didPatchMultiselectSubmit) return;
|
|
1281
|
+
didPatchMultiselectSubmit = true;
|
|
1282
|
+
const multiselectPromptConstructor = require(PROMPTS_MULTISELECT_MODULE_PATH);
|
|
1283
|
+
const originalSubmit = multiselectPromptConstructor.prototype.submit;
|
|
1284
|
+
multiselectPromptConstructor.prototype.submit = function() {
|
|
1285
|
+
if (shouldAutoSelectCurrentChoice(this.value, this.cursor)) this.value[this.cursor].selected = true;
|
|
1286
|
+
originalSubmit.call(this);
|
|
1287
|
+
};
|
|
1288
|
+
};
|
|
1241
1289
|
const prompts = (questions) => {
|
|
1242
1290
|
patchMultiselectToggleAll();
|
|
1291
|
+
patchMultiselectSubmit();
|
|
1243
1292
|
return basePrompts(questions, { onCancel });
|
|
1244
1293
|
};
|
|
1245
1294
|
|
|
@@ -1414,7 +1463,7 @@ const copyToClipboard = (text) => {
|
|
|
1414
1463
|
|
|
1415
1464
|
//#endregion
|
|
1416
1465
|
//#region src/cli.ts
|
|
1417
|
-
const VERSION = "0.0.
|
|
1466
|
+
const VERSION = "0.0.18";
|
|
1418
1467
|
process.on("SIGINT", () => process.exit(0));
|
|
1419
1468
|
process.on("SIGTERM", () => process.exit(0));
|
|
1420
1469
|
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("--fix", "open Ami to auto-fix all issues").option("--prompt", "copy latest scan output to clipboard").action(async (directory, flags) => {
|