react-doctor 0.1.3 → 0.1.4

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 CHANGED
@@ -214,7 +214,7 @@ const finalize = (method, originalText, displayText) => {
214
214
  sharedInstance.stop();
215
215
  ora({
216
216
  text: displayText,
217
- indent: 2
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: 2
229
+ indent: 0
230
230
  }).start();
231
231
  else sharedInstance.text = text;
232
232
  const handle = {
@@ -304,28 +304,6 @@ const runInstallSkill = async (options = {}) => {
304
304
  }
305
305
  };
306
306
  //#endregion
307
- //#region src/utils/build-category-breakdown.ts
308
- const buildCategoryBreakdown = (diagnostics) => {
309
- const entriesByCategory = /* @__PURE__ */ new Map();
310
- for (const diagnostic of diagnostics) {
311
- const existingEntry = entriesByCategory.get(diagnostic.category) ?? {
312
- category: diagnostic.category,
313
- totalCount: 0,
314
- errorCount: 0,
315
- warningCount: 0
316
- };
317
- existingEntry.totalCount += 1;
318
- if (diagnostic.severity === "error") existingEntry.errorCount += 1;
319
- else existingEntry.warningCount += 1;
320
- entriesByCategory.set(diagnostic.category, existingEntry);
321
- }
322
- return [...entriesByCategory.values()].sort((entryA, entryB) => {
323
- if (entryA.errorCount !== entryB.errorCount) return entryB.errorCount - entryA.errorCount;
324
- if (entryA.totalCount !== entryB.totalCount) return entryB.totalCount - entryA.totalCount;
325
- return entryA.category.localeCompare(entryB.category);
326
- });
327
- };
328
- //#endregion
329
307
  //#region src/utils/build-hidden-diagnostics-summary.ts
330
308
  const buildHiddenDiagnosticsSummary = (hiddenDiagnostics) => {
331
309
  const errorCount = hiddenDiagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
@@ -1675,6 +1653,32 @@ const loadConfig = (rootDirectory) => {
1675
1653
  return null;
1676
1654
  };
1677
1655
  //#endregion
1656
+ //#region src/utils/wrap-indented-text.ts
1657
+ const wrapLine = (lineText, contentWidth) => {
1658
+ if (lineText.length <= contentWidth) return [lineText];
1659
+ const wrappedLines = [];
1660
+ let remainingText = lineText.trim();
1661
+ while (remainingText.length > contentWidth) {
1662
+ const candidateText = remainingText.slice(0, contentWidth);
1663
+ const breakIndex = candidateText.lastIndexOf(" ");
1664
+ if (breakIndex <= 0) {
1665
+ wrappedLines.push(candidateText);
1666
+ remainingText = remainingText.slice(contentWidth).trimStart();
1667
+ continue;
1668
+ }
1669
+ wrappedLines.push(remainingText.slice(0, breakIndex));
1670
+ remainingText = remainingText.slice(breakIndex + 1).trimStart();
1671
+ }
1672
+ if (remainingText.length > 0) wrappedLines.push(remainingText);
1673
+ return wrappedLines;
1674
+ };
1675
+ const wrapIndentedText = (text, linePrefix, width) => {
1676
+ const contentWidth = width - linePrefix.length;
1677
+ if (contentWidth <= 0) return indentOnly(text, linePrefix);
1678
+ return text.split("\n").flatMap((lineText) => wrapLine(lineText, contentWidth)).map((lineText) => `${linePrefix}${lineText}`).join("\n");
1679
+ };
1680
+ const indentOnly = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
1681
+ //#endregion
1678
1682
  //#region src/utils/resolve-compatible-node.ts
1679
1683
  const parseNodeVersion = (versionString) => {
1680
1684
  const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
@@ -3154,6 +3158,7 @@ const parseOxlintOutput = (stdout) => {
3154
3158
  severity: diagnostic.severity,
3155
3159
  message: cleaned.message,
3156
3160
  help: cleaned.help,
3161
+ url: diagnostic.url,
3157
3162
  line: primaryLabel?.span.line ?? 0,
3158
3163
  column: primaryLabel?.span.column ?? 0,
3159
3164
  category: resolveDiagnosticCategory(plugin, rule)
@@ -3285,6 +3290,11 @@ const buildVerboseSiteMap = (diagnostics) => {
3285
3290
  return fileSites;
3286
3291
  };
3287
3292
  const formatSiteCountBadge = (count) => count > 1 ? `×${count}` : "";
3293
+ const formatIssueCount = (count) => `${count} ${count === 1 ? "issue" : "issues"}`;
3294
+ const toRuleTitle = (ruleName) => {
3295
+ const readableRuleName = ruleName.replace(/^(no|prefer|require|use)-/, "").replace(/^(nextjs|tanstack-start)-/, "").replaceAll("-", " ");
3296
+ return (readableRuleName.charAt(0).toUpperCase() + readableRuleName.slice(1)).replace(/\b(css|html|url|svg|jsx|api|ua)\b/gi, (match) => match.toUpperCase());
3297
+ };
3288
3298
  const computeRuleNameColumnWidth = (ruleKeys) => {
3289
3299
  const longestRuleNameLength = ruleKeys.reduce((longest, ruleKey) => Math.max(longest, ruleKey.length), 0);
3290
3300
  return Math.max(36, longestRuleNameLength);
@@ -3296,6 +3306,9 @@ const padRuleNameToColumn = (ruleName, columnWidth) => {
3296
3306
  const grayLine = (text) => {
3297
3307
  logger.log(highlighter.gray(text));
3298
3308
  };
3309
+ const grayWrappedLine = (text, linePrefix) => {
3310
+ grayLine(wrapIndentedText(text, linePrefix, 88));
3311
+ };
3299
3312
  const printCompactRuleGroupLine = (ruleKey, ruleDiagnostics, ruleNameColumnWidth) => {
3300
3313
  const firstDiagnostic = ruleDiagnostics[0];
3301
3314
  const icon = colorizeBySeverity(firstDiagnostic.severity === "error" ? "✗" : "⚠", firstDiagnostic.severity);
@@ -3304,13 +3317,38 @@ const printCompactRuleGroupLine = (ruleKey, ruleDiagnostics, ruleNameColumnWidth
3304
3317
  const trailingBadge = siteCountBadge.length > 0 ? ` ${highlighter.gray(siteCountBadge)}` : "";
3305
3318
  logger.log(` ${icon} ${ruleNameRendering}${trailingBadge}`);
3306
3319
  };
3307
- const printDetailedRuleGroup = (ruleKey, ruleDiagnostics, rootDirectory, ruleNameColumnWidth) => {
3308
- printCompactRuleGroupLine(ruleKey, ruleDiagnostics, ruleNameColumnWidth);
3320
+ const getWorstSeverity = (diagnostics) => diagnostics.some((diagnostic) => diagnostic.severity === "error") ? "error" : "warning";
3321
+ const buildCategoryDiagnosticGroups = (diagnostics) => {
3322
+ return [...groupBy(diagnostics, (diagnostic) => diagnostic.category).entries()].map(([category, categoryDiagnostics]) => {
3323
+ return {
3324
+ category,
3325
+ diagnostics: categoryDiagnostics,
3326
+ ruleGroups: sortByImportance([...groupBy(categoryDiagnostics, (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`).entries()])
3327
+ };
3328
+ }).toSorted((categoryGroupA, categoryGroupB) => {
3329
+ const severityDelta = SEVERITY_ORDER[getWorstSeverity(categoryGroupA.diagnostics)] - SEVERITY_ORDER[getWorstSeverity(categoryGroupB.diagnostics)];
3330
+ if (severityDelta !== 0) return severityDelta;
3331
+ if (categoryGroupA.diagnostics.length !== categoryGroupB.diagnostics.length) return categoryGroupB.diagnostics.length - categoryGroupA.diagnostics.length;
3332
+ return categoryGroupA.category.localeCompare(categoryGroupB.category);
3333
+ });
3334
+ };
3335
+ const printDefaultRuleGroup = (ruleKey, ruleDiagnostics, rootDirectory) => {
3309
3336
  const firstDiagnostic = ruleDiagnostics[0];
3310
- grayLine(indentMultilineText(firstDiagnostic.message, " "));
3311
- if (firstDiagnostic.help) grayLine(indentMultilineText(`→ ${firstDiagnostic.help}`, " "));
3337
+ const ruleTitle = toRuleTitle(firstDiagnostic.rule);
3338
+ const icon = colorizeBySeverity(firstDiagnostic.severity === "error" ? "" : "⚠", firstDiagnostic.severity);
3339
+ const siteCountBadge = formatSiteCountBadge(ruleDiagnostics.length);
3340
+ const trailingBadge = siteCountBadge.length > 0 ? ` ${highlighter.gray(siteCountBadge)}` : "";
3341
+ logger.log(` ${icon} ${ruleTitle}${trailingBadge}`);
3342
+ grayWrappedLine(firstDiagnostic.message, " ");
3343
+ if (firstDiagnostic.help) grayWrappedLine(firstDiagnostic.help, " ");
3344
+ if (firstDiagnostic.url) grayLine(` ${firstDiagnostic.url}`);
3312
3345
  const firstLocation = ruleDiagnostics.find((diagnostic) => diagnostic.line > 0);
3313
- if (firstLocation) grayLine(` ${toRelativePath(firstLocation.filePath, rootDirectory)}:${firstLocation.line}`);
3346
+ if (firstLocation) grayLine(` ${toRelativePath(firstLocation.filePath, rootDirectory)}:${firstLocation.line}`);
3347
+ };
3348
+ const printDefaultCategoryGroup = (categoryGroup, visibleRuleGroups, rootDirectory) => {
3349
+ const issueCount = formatIssueCount(categoryGroup.diagnostics.length);
3350
+ logger.log(`${highlighter.bold(categoryGroup.category)} ${highlighter.dim(issueCount)}`);
3351
+ for (const [ruleKey, ruleDiagnostics] of visibleRuleGroups) printDefaultRuleGroup(ruleKey, ruleDiagnostics, rootDirectory);
3314
3352
  logger.break();
3315
3353
  };
3316
3354
  const printVerboseRuleGroup = (ruleKey, ruleDiagnostics, ruleNameColumnWidth) => {
@@ -3326,22 +3364,36 @@ const printVerboseRuleGroup = (ruleKey, ruleDiagnostics, ruleNameColumnWidth) =>
3326
3364
  else grayLine(` ${filePath}`);
3327
3365
  logger.break();
3328
3366
  };
3367
+ const printDefaultDiagnostics = (diagnostics, rootDirectory) => {
3368
+ const categoryGroups = buildCategoryDiagnosticGroups(diagnostics);
3369
+ const hiddenRuleGroups = [];
3370
+ const visibleCategoryGroups = categoryGroups.slice(0, 5);
3371
+ const hiddenCategoryGroups = categoryGroups.slice(5);
3372
+ for (const categoryGroup of visibleCategoryGroups) {
3373
+ const visibleRuleGroups = categoryGroup.ruleGroups.slice(0, 3);
3374
+ const remainingRuleGroups = categoryGroup.ruleGroups.slice(3);
3375
+ printDefaultCategoryGroup(categoryGroup, visibleRuleGroups, rootDirectory);
3376
+ hiddenRuleGroups.push(...remainingRuleGroups);
3377
+ }
3378
+ hiddenRuleGroups.push(...hiddenCategoryGroups.flatMap((categoryGroup) => categoryGroup.ruleGroups));
3379
+ if (hiddenRuleGroups.length > 0) printHiddenDiagnosticsSummary(hiddenRuleGroups);
3380
+ };
3329
3381
  const printDiagnostics = (diagnostics, isVerbose, rootDirectory) => {
3330
- const sortedRuleGroups = sortByImportance([...groupBy(diagnostics, (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`).entries()]);
3331
- const visibleRuleGroups = isVerbose ? sortedRuleGroups : sortedRuleGroups.slice(0, 5);
3332
- const hiddenRuleGroups = isVerbose ? [] : sortedRuleGroups.slice(5);
3382
+ if (!isVerbose) {
3383
+ printDefaultDiagnostics(diagnostics, rootDirectory);
3384
+ return;
3385
+ }
3386
+ const visibleRuleGroups = sortByImportance([...groupBy(diagnostics, (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`).entries()]);
3333
3387
  const ruleNameColumnWidth = computeRuleNameColumnWidth(visibleRuleGroups.map(([ruleKey]) => ruleKey));
3334
3388
  visibleRuleGroups.forEach(([ruleKey, ruleDiagnostics]) => {
3335
- if (isVerbose) {
3336
- printVerboseRuleGroup(ruleKey, ruleDiagnostics, ruleNameColumnWidth);
3337
- return;
3338
- }
3339
- printDetailedRuleGroup(ruleKey, ruleDiagnostics, rootDirectory, ruleNameColumnWidth);
3389
+ printVerboseRuleGroup(ruleKey, ruleDiagnostics, ruleNameColumnWidth);
3340
3390
  });
3341
- if (hiddenRuleGroups.length > 0) printHiddenDiagnosticsSummary(hiddenRuleGroups);
3342
3391
  };
3343
3392
  const printHiddenDiagnosticsSummary = (hiddenRuleGroups) => {
3344
- const renderedParts = buildHiddenDiagnosticsSummary(hiddenRuleGroups.flatMap(([, ruleDiagnostics]) => ruleDiagnostics)).map((part) => colorizeBySeverity(part.text, part.severity));
3393
+ const renderedParts = buildHiddenDiagnosticsSummary(hiddenRuleGroups.flatMap(([, ruleDiagnostics]) => ruleDiagnostics)).map((part) => {
3394
+ const [icon, ...labelParts] = part.text.split(" ");
3395
+ return `${colorizeBySeverity(icon, part.severity)} ${highlighter.dim(labelParts.join(" "))}`;
3396
+ });
3345
3397
  logger.log(` ${renderedParts.join(" ")}`);
3346
3398
  grayLine(" Run `npx react-doctor@latest . --verbose` to get all details");
3347
3399
  logger.break();
@@ -3361,6 +3413,7 @@ const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
3361
3413
  firstDiagnostic.message
3362
3414
  ];
3363
3415
  if (firstDiagnostic.help) sections.push("", `Suggestion: ${firstDiagnostic.help}`);
3416
+ if (firstDiagnostic.url) sections.push("", `Docs: ${firstDiagnostic.url}`);
3364
3417
  sections.push("", "Files:");
3365
3418
  const fileSites = buildVerboseSiteMap(ruleDiagnostics);
3366
3419
  for (const [filePath, sites] of fileSites) if (sites.length > 0) for (const site of sites) {
@@ -3432,32 +3485,6 @@ const printNoScoreHeader = (noScoreMessage) => {
3432
3485
  logger.log(` ${highlighter.gray(noScoreMessage)}`);
3433
3486
  logger.break();
3434
3487
  };
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
3488
  const buildShareUrl = (diagnostics, scoreResult, projectName) => {
3462
3489
  const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
3463
3490
  const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
@@ -3483,7 +3510,6 @@ const printCountsSummaryLine = (diagnostics, totalSourceFileCount, elapsedMillis
3483
3510
  logger.log(` ${issueCountColor(issueCountText)} ${highlighter.dim(`${fileCountText} ${elapsedTimeText}`)}`);
3484
3511
  };
3485
3512
  const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, projectName, totalSourceFileCount, noScoreMessage, isOffline) => {
3486
- printCategoryBreakdown(buildCategoryBreakdown(diagnostics));
3487
3513
  if (scoreResult) printScoreHeader(scoreResult);
3488
3514
  else printNoScoreHeader(noScoreMessage);
3489
3515
  printCountsSummaryLine(diagnostics, totalSourceFileCount, elapsedMilliseconds);
@@ -4102,7 +4128,7 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
4102
4128
  };
4103
4129
  //#endregion
4104
4130
  //#region src/cli.ts
4105
- const VERSION = "0.1.3";
4131
+ const VERSION = "0.1.4";
4106
4132
  const VALID_FAIL_ON_LEVELS = new Set([
4107
4133
  "error",
4108
4134
  "warning",
@@ -6946,7 +6946,7 @@ const ALL_RULES_AT_RECOMMENDED_SEVERITY = {
6946
6946
  const eslintPlugin = {
6947
6947
  meta: {
6948
6948
  name: PLUGIN_NAMESPACE,
6949
- version: "0.1.3"
6949
+ version: "0.1.4"
6950
6950
  },
6951
6951
  rules: eslintShapedRules,
6952
6952
  configs: {
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ interface Diagnostic {
18
18
  severity: "error" | "warning";
19
19
  message: string;
20
20
  help: string;
21
+ url?: string;
21
22
  line: number;
22
23
  column: number;
23
24
  category: string;
package/dist/index.js CHANGED
@@ -2821,6 +2821,7 @@ const parseOxlintOutput = (stdout) => {
2821
2821
  severity: diagnostic.severity,
2822
2822
  message: cleaned.message,
2823
2823
  help: cleaned.help,
2824
+ url: diagnostic.url,
2824
2825
  line: primaryLabel?.span.line ?? 0,
2825
2826
  column: primaryLabel?.span.column ?? 0,
2826
2827
  category: resolveDiagnosticCategory(plugin, rule)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Diagnose and fix React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
5
5
  "keywords": [
6
6
  "accessibility",