react-doctor 0.1.0 → 0.1.2

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 CHANGED
@@ -197,6 +197,29 @@ const counts = summarizeDiagnostics(result.diagnostics);
197
197
 
198
198
  `react-doctor/api` re-exports `JsonReport`, `JsonReportSummary`, `JsonReportProjectEntry`, `JsonReportMode`, plus the lower-level `buildJsonReport` and `buildJsonReportError` builders. See [`packages/react-doctor/src/api.ts`](https://github.com/millionco/react-doctor/blob/main/packages/react-doctor/src/api.ts) for the full types.
199
199
 
200
+ ## Leaderboard
201
+
202
+ Top React codebases scanned by React Doctor, ranked by score. Updated automatically from [millionco/react-doctor-benchmarks](https://github.com/millionco/react-doctor-benchmarks).
203
+
204
+ <!-- LEADERBOARD:START -->
205
+ <!-- prettier-ignore -->
206
+ | # | Repo | Score |
207
+ | -- | ---- | ----: |
208
+ | 1 | [executor](https://github.com/RhysSullivan/executor) | 96 |
209
+ | 2 | [nodejs.org](https://github.com/nodejs/nodejs.org) | 87 |
210
+ | 3 | [tldraw](https://github.com/tldraw/tldraw) | 76 |
211
+ | 4 | [t3code](https://github.com/pingdotgg/t3code) | 75 |
212
+ | 5 | [mastra](https://github.com/mastra-ai/mastra) | 70 |
213
+ | 6 | [excalidraw](https://github.com/excalidraw/excalidraw) | 69 |
214
+ | 7 | [payload](https://github.com/payloadcms/payload) | 69 |
215
+ | 8 | [better-auth](https://github.com/better-auth/better-auth) | 69 |
216
+ | 9 | [rocket.chat](https://github.com/RocketChat/Rocket.Chat) | 67 |
217
+ | 10 | [typebot](https://github.com/baptisteArno/typebot.io) | 66 |
218
+
219
+ <!-- LEADERBOARD:END -->
220
+
221
+ See the [full leaderboard](https://www.react.doctor/leaderboard).
222
+
200
223
  ## Resources & Contributing Back
201
224
 
202
225
  Want to try it out? Check out [the demo](https://react.doctor).
package/dist/cli.js CHANGED
@@ -89,7 +89,9 @@ const highlighter = {
89
89
  warn: pc.yellow,
90
90
  info: pc.cyan,
91
91
  success: pc.green,
92
- dim: pc.dim
92
+ dim: pc.dim,
93
+ gray: pc.gray,
94
+ bold: pc.bold
93
95
  };
94
96
  //#endregion
95
97
  //#region src/utils/logger.ts
@@ -210,7 +212,10 @@ const finalize = (method, originalText, displayText) => {
210
212
  return;
211
213
  }
212
214
  sharedInstance.stop();
213
- ora(displayText).start()[method](displayText);
215
+ ora({
216
+ text: displayText,
217
+ indent: 2
218
+ }).start()[method](displayText);
214
219
  const [remainingText] = pendingTexts;
215
220
  if (remainingText) sharedInstance.text = remainingText;
216
221
  sharedInstance.start();
@@ -219,7 +224,10 @@ const spinner = (text) => ({ start() {
219
224
  if (isSilent) return noopHandle;
220
225
  activeCount++;
221
226
  pendingTexts.add(text);
222
- if (!sharedInstance) sharedInstance = ora({ text }).start();
227
+ if (!sharedInstance) sharedInstance = ora({
228
+ text,
229
+ indent: 2
230
+ }).start();
223
231
  else sharedInstance.text = text;
224
232
  const handle = {
225
233
  succeed: (displayText) => {
@@ -296,6 +304,28 @@ const runInstallSkill = async (options = {}) => {
296
304
  }
297
305
  };
298
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
299
329
  //#region src/utils/build-hidden-diagnostics-summary.ts
300
330
  const buildHiddenDiagnosticsSummary = (hiddenDiagnostics) => {
301
331
  const errorCount = hiddenDiagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
@@ -1462,32 +1492,6 @@ const formatErrorMessage = (error) => error instanceof Error ? error.message ||
1462
1492
  const formatErrorChain = (rootError) => collectErrorChain(rootError).map(formatErrorMessage).join(" → ");
1463
1493
  const getErrorChainMessages = (rootError) => collectErrorChain(rootError).map(formatErrorMessage);
1464
1494
  //#endregion
1465
- //#region src/utils/framed-box.ts
1466
- const createFramedLine = (plainText, renderedText = plainText) => ({
1467
- plainText,
1468
- renderedText
1469
- });
1470
- const renderFramedBoxString = (framedLines) => {
1471
- if (framedLines.length === 0) return "";
1472
- const borderColorizer = highlighter.dim;
1473
- const outerIndent = " ".repeat(2);
1474
- const horizontalPadding = " ".repeat(1);
1475
- const maximumLineLength = Math.max(...framedLines.map((framedLine) => framedLine.plainText.length));
1476
- const borderLine = "─".repeat(maximumLineLength + 2);
1477
- const lines = [];
1478
- lines.push(`${outerIndent}${borderColorizer(`┌${borderLine}┐`)}`);
1479
- for (const framedLine of framedLines) {
1480
- const trailingSpaces = " ".repeat(maximumLineLength - framedLine.plainText.length);
1481
- lines.push(`${outerIndent}${borderColorizer("│")}${horizontalPadding}${framedLine.renderedText}${trailingSpaces}${horizontalPadding}${borderColorizer("│")}`);
1482
- }
1483
- lines.push(`${outerIndent}${borderColorizer(`└${borderLine}┘`)}`);
1484
- return lines.join("\n");
1485
- };
1486
- const printFramedBox = (framedLines) => {
1487
- const rendered = renderFramedBoxString(framedLines);
1488
- if (rendered) logger.log(rendered);
1489
- };
1490
- //#endregion
1491
1495
  //#region src/utils/group-by.ts
1492
1496
  const groupBy = (items, keyFn) => {
1493
1497
  const groups = /* @__PURE__ */ new Map();
@@ -3162,8 +3166,6 @@ const runOxlint = async (options) => {
3162
3166
  return await spawnLintBatches();
3163
3167
  } catch (error) {
3164
3168
  if (extendsPaths.length === 0) throw error;
3165
- const reason = error instanceof Error ? error.message : String(error);
3166
- process.stderr.write(`[react-doctor] could not adopt existing lint config (${reason.split("\n")[0]}); retrying without extends. Set "adoptExistingLintConfig": false to silence.\n`);
3167
3169
  writeOxlintConfig(createOxlintConfig({
3168
3170
  pluginPath,
3169
3171
  framework,
@@ -3208,33 +3210,66 @@ const buildVerboseSiteMap = (diagnostics) => {
3208
3210
  }
3209
3211
  return fileSites;
3210
3212
  };
3211
- const printDiagnostics = (diagnostics, isVerbose) => {
3213
+ const formatSiteCountBadge = (count) => count > 1 ? `×${count}` : "";
3214
+ const computeRuleNameColumnWidth = (ruleKeys) => {
3215
+ const longestRuleNameLength = ruleKeys.reduce((longest, ruleKey) => Math.max(longest, ruleKey.length), 0);
3216
+ return Math.max(36, longestRuleNameLength);
3217
+ };
3218
+ const padRuleNameToColumn = (ruleName, columnWidth) => {
3219
+ if (ruleName.length >= columnWidth) return ruleName;
3220
+ return ruleName + " ".repeat(columnWidth - ruleName.length);
3221
+ };
3222
+ const grayLine = (text) => {
3223
+ logger.log(highlighter.gray(text));
3224
+ };
3225
+ const printCompactRuleGroupLine = (ruleKey, ruleDiagnostics, ruleNameColumnWidth) => {
3226
+ const firstDiagnostic = ruleDiagnostics[0];
3227
+ const icon = colorizeBySeverity(firstDiagnostic.severity === "error" ? "✗" : "⚠", firstDiagnostic.severity);
3228
+ const siteCountBadge = formatSiteCountBadge(ruleDiagnostics.length);
3229
+ const ruleNameRendering = siteCountBadge.length > 0 ? colorizeBySeverity(padRuleNameToColumn(ruleKey, ruleNameColumnWidth), firstDiagnostic.severity) : colorizeBySeverity(ruleKey, firstDiagnostic.severity);
3230
+ const trailingBadge = siteCountBadge.length > 0 ? ` ${highlighter.gray(siteCountBadge)}` : "";
3231
+ logger.log(` ${icon} ${ruleNameRendering}${trailingBadge}`);
3232
+ };
3233
+ const printDetailedRuleGroup = (ruleKey, ruleDiagnostics, rootDirectory, ruleNameColumnWidth) => {
3234
+ printCompactRuleGroupLine(ruleKey, ruleDiagnostics, ruleNameColumnWidth);
3235
+ const firstDiagnostic = ruleDiagnostics[0];
3236
+ grayLine(indentMultilineText(firstDiagnostic.message, " "));
3237
+ if (firstDiagnostic.help) grayLine(indentMultilineText(`→ ${firstDiagnostic.help}`, " "));
3238
+ const firstLocation = ruleDiagnostics.find((diagnostic) => diagnostic.line > 0);
3239
+ if (firstLocation) grayLine(` ${toRelativePath(firstLocation.filePath, rootDirectory)}:${firstLocation.line}`);
3240
+ logger.break();
3241
+ };
3242
+ const printVerboseRuleGroup = (ruleKey, ruleDiagnostics, ruleNameColumnWidth) => {
3243
+ printCompactRuleGroupLine(ruleKey, ruleDiagnostics, ruleNameColumnWidth);
3244
+ const firstDiagnostic = ruleDiagnostics[0];
3245
+ grayLine(indentMultilineText(firstDiagnostic.message, " "));
3246
+ if (firstDiagnostic.help) grayLine(indentMultilineText(`→ ${firstDiagnostic.help}`, " "));
3247
+ const fileSites = buildVerboseSiteMap(ruleDiagnostics);
3248
+ for (const [filePath, sites] of fileSites) if (sites.length > 0) for (const site of sites) {
3249
+ grayLine(` ${filePath}:${site.line}`);
3250
+ if (site.suppressionHint) grayLine(` ↳ ${site.suppressionHint}`);
3251
+ }
3252
+ else grayLine(` ${filePath}`);
3253
+ logger.break();
3254
+ };
3255
+ const printDiagnostics = (diagnostics, isVerbose, rootDirectory) => {
3212
3256
  const sortedRuleGroups = sortByImportance([...groupBy(diagnostics, (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`).entries()]);
3213
- const visibleRuleGroups = isVerbose ? sortedRuleGroups : sortedRuleGroups.slice(0, 3);
3214
- const hiddenRuleGroups = isVerbose ? [] : sortedRuleGroups.slice(3);
3215
- for (const [, ruleDiagnostics] of visibleRuleGroups) {
3216
- const firstDiagnostic = ruleDiagnostics[0];
3217
- const icon = colorizeBySeverity(firstDiagnostic.severity === "error" ? "✗" : "⚠", firstDiagnostic.severity);
3218
- const count = ruleDiagnostics.length;
3219
- const countLabel = count > 1 ? colorizeBySeverity(` (${count})`, firstDiagnostic.severity) : "";
3220
- logger.log(` ${icon} ${firstDiagnostic.message}${countLabel}`);
3221
- if (firstDiagnostic.help) logger.dim(indentMultilineText(firstDiagnostic.help, " "));
3257
+ const visibleRuleGroups = isVerbose ? sortedRuleGroups : sortedRuleGroups.slice(0, 5);
3258
+ const hiddenRuleGroups = isVerbose ? [] : sortedRuleGroups.slice(5);
3259
+ const ruleNameColumnWidth = computeRuleNameColumnWidth(visibleRuleGroups.map(([ruleKey]) => ruleKey));
3260
+ visibleRuleGroups.forEach(([ruleKey, ruleDiagnostics]) => {
3222
3261
  if (isVerbose) {
3223
- const fileSites = buildVerboseSiteMap(ruleDiagnostics);
3224
- for (const [filePath, sites] of fileSites) if (sites.length > 0) for (const site of sites) {
3225
- logger.dim(` ${filePath}:${site.line}`);
3226
- if (site.suppressionHint) logger.dim(` ↳ ${site.suppressionHint}`);
3227
- }
3228
- else logger.dim(` ${filePath}`);
3262
+ printVerboseRuleGroup(ruleKey, ruleDiagnostics, ruleNameColumnWidth);
3263
+ return;
3229
3264
  }
3230
- logger.break();
3231
- }
3265
+ printDetailedRuleGroup(ruleKey, ruleDiagnostics, rootDirectory, ruleNameColumnWidth);
3266
+ });
3232
3267
  if (hiddenRuleGroups.length > 0) printHiddenDiagnosticsSummary(hiddenRuleGroups);
3233
3268
  };
3234
3269
  const printHiddenDiagnosticsSummary = (hiddenRuleGroups) => {
3235
3270
  const renderedParts = buildHiddenDiagnosticsSummary(hiddenRuleGroups.flatMap(([, ruleDiagnostics]) => ruleDiagnostics)).map((part) => colorizeBySeverity(part.text, part.severity));
3236
3271
  logger.log(` ${renderedParts.join(" ")}`);
3237
- logger.dim(" Run `npx react-doctor@latest . --verbose` to get all details");
3272
+ grayLine(" Run `npx react-doctor@latest . --verbose` to get all details");
3238
3273
  logger.break();
3239
3274
  };
3240
3275
  const formatElapsedTime = (elapsedMilliseconds) => {
@@ -3277,37 +3312,76 @@ const buildScoreBarSegments = (score) => {
3277
3312
  emptySegment: "░".repeat(emptyCount)
3278
3313
  };
3279
3314
  };
3280
- const buildPlainScoreBar = (score) => {
3281
- const { filledSegment, emptySegment } = buildScoreBarSegments(score);
3282
- return `${filledSegment}${emptySegment}`;
3283
- };
3284
3315
  const buildScoreBar = (score) => {
3285
3316
  const { filledSegment, emptySegment } = buildScoreBarSegments(score);
3286
3317
  return colorizeByScore(filledSegment, score) + highlighter.dim(emptySegment);
3287
3318
  };
3288
- const printScoreGauge = (score, label) => {
3289
- const scoreDisplay = colorizeByScore(`${score}`, score);
3290
- const labelDisplay = colorizeByScore(label, score);
3291
- logger.log(` ${scoreDisplay} / 100 ${labelDisplay}`);
3292
- logger.break();
3293
- logger.log(` ${buildScoreBar(score)}`);
3294
- logger.break();
3295
- };
3296
3319
  const getDoctorFace = (score) => {
3297
3320
  if (score >= 75) return ["◠ ◠", " ▽ "];
3298
3321
  if (score >= 50) return ["• •", " ─ "];
3299
3322
  return ["x x", " ▽ "];
3300
3323
  };
3301
- const printBranding = (score) => {
3302
- if (score !== void 0) {
3303
- const [eyes, mouth] = getDoctorFace(score);
3304
- const colorize = (text) => colorizeByScore(text, score);
3305
- logger.log(colorize(" ┌─────┐"));
3306
- logger.log(colorize(` │ ${eyes} │`));
3307
- logger.log(colorize(` │ ${mouth} │`));
3308
- logger.log(colorize(" └─────┘"));
3324
+ const BRANDING_LINE = `React Doctor ${highlighter.dim("(www.react.doctor)")}`;
3325
+ const buildFaceRenderedLines = (score) => {
3326
+ const [eyes, mouth] = getDoctorFace(score);
3327
+ const colorize = (text) => colorizeByScore(text, score);
3328
+ return [
3329
+ "┌─────┐",
3330
+ `│ ${eyes} │`,
3331
+ `│ ${mouth} │`,
3332
+ "└─────┘"
3333
+ ].map(colorize);
3334
+ };
3335
+ const printScoreHeader = (scoreResult) => {
3336
+ const renderedFaceLines = buildFaceRenderedLines(scoreResult.score);
3337
+ const scoreNumber = colorizeByScore(`${scoreResult.score}`, scoreResult.score);
3338
+ const scoreLabel = colorizeByScore(scoreResult.label, scoreResult.score);
3339
+ const rightColumnLines = [
3340
+ `${scoreNumber} ${highlighter.dim(`/ 100`)} ${scoreLabel}`,
3341
+ buildScoreBar(scoreResult.score),
3342
+ BRANDING_LINE,
3343
+ ""
3344
+ ];
3345
+ for (let lineIndex = 0; lineIndex < renderedFaceLines.length; lineIndex += 1) {
3346
+ const rightColumnContent = rightColumnLines[lineIndex] ?? "";
3347
+ const separator = rightColumnContent.length > 0 ? " " : "";
3348
+ logger.log(` ${renderedFaceLines[lineIndex]}${separator}${rightColumnContent}`);
3349
+ }
3350
+ logger.break();
3351
+ };
3352
+ const printBrandingOnlyHeader = () => {
3353
+ logger.log(` ${BRANDING_LINE}`);
3354
+ logger.break();
3355
+ };
3356
+ const printNoScoreHeader = (noScoreMessage) => {
3357
+ logger.log(` ${BRANDING_LINE}`);
3358
+ logger.log(` ${highlighter.gray(noScoreMessage)}`);
3359
+ logger.break();
3360
+ };
3361
+ const buildCategoryBar = (count, maximumCount, useErrorColor) => {
3362
+ if (maximumCount === 0) return highlighter.dim("░".repeat(16));
3363
+ const filledCount = Math.max(1, Math.round(count / maximumCount * 16));
3364
+ const cappedFilledCount = Math.min(filledCount, 16);
3365
+ const emptyCount = 16 - cappedFilledCount;
3366
+ const filledSegment = "█".repeat(cappedFilledCount);
3367
+ const emptySegment = "░".repeat(emptyCount);
3368
+ return `${useErrorColor ? highlighter.error(filledSegment) : highlighter.warn(filledSegment)}${highlighter.dim(emptySegment)}`;
3369
+ };
3370
+ const padCategoryLabel = (categoryLabel) => {
3371
+ if (categoryLabel.length >= 18) return categoryLabel;
3372
+ return categoryLabel + " ".repeat(18 - categoryLabel.length);
3373
+ };
3374
+ const printCategoryBreakdown = (entries) => {
3375
+ if (entries.length === 0) return;
3376
+ const maximumCount = Math.max(...entries.map((entry) => entry.totalCount));
3377
+ logger.dim(" By category");
3378
+ for (const entry of entries) {
3379
+ const paddedLabel = padCategoryLabel(entry.category);
3380
+ const categoryBar = buildCategoryBar(entry.totalCount, maximumCount, entry.errorCount > 0);
3381
+ const totalCountDisplay = String(entry.totalCount);
3382
+ const errorBadge = entry.errorCount > 0 ? ` ${highlighter.error(`${entry.errorCount}×`)}` : "";
3383
+ logger.log(` ${paddedLabel}${categoryBar} ${totalCountDisplay}${errorBadge}`);
3309
3384
  }
3310
- logger.log(` React Doctor ${highlighter.dim("(www.react.doctor)")}`);
3311
3385
  logger.break();
3312
3386
  };
3313
3387
  const buildShareUrl = (diagnostics, scoreResult, projectName) => {
@@ -3322,67 +3396,31 @@ const buildShareUrl = (diagnostics, scoreResult, projectName) => {
3322
3396
  if (affectedFileCount > 0) params.set("f", String(affectedFileCount));
3323
3397
  return `${SHARE_BASE_URL}?${params.toString()}`;
3324
3398
  };
3325
- const buildBrandingLines = (scoreResult, noScoreMessage) => {
3326
- const lines = [];
3327
- if (scoreResult) {
3328
- const [eyes, mouth] = getDoctorFace(scoreResult.score);
3329
- const scoreColorizer = (text) => colorizeByScore(text, scoreResult.score);
3330
- lines.push(createFramedLine("┌─────┐", scoreColorizer("┌─────┐")));
3331
- lines.push(createFramedLine(`│ ${eyes} │`, scoreColorizer(`│ ${eyes} │`)));
3332
- lines.push(createFramedLine(`│ ${mouth} │`, scoreColorizer(`│ ${mouth} │`)));
3333
- lines.push(createFramedLine("└─────┘", scoreColorizer("└─────┘")));
3334
- lines.push(createFramedLine("React Doctor (www.react.doctor)", `React Doctor ${highlighter.dim("(www.react.doctor)")}`));
3335
- lines.push(createFramedLine(""));
3336
- const scoreLinePlainText = `${scoreResult.score} / 100 ${scoreResult.label}`;
3337
- const scoreLineRenderedText = `${colorizeByScore(String(scoreResult.score), scoreResult.score)} / 100 ${colorizeByScore(scoreResult.label, scoreResult.score)}`;
3338
- lines.push(createFramedLine(scoreLinePlainText, scoreLineRenderedText));
3339
- lines.push(createFramedLine(""));
3340
- lines.push(createFramedLine(buildPlainScoreBar(scoreResult.score), buildScoreBar(scoreResult.score)));
3341
- lines.push(createFramedLine(""));
3342
- } else {
3343
- lines.push(createFramedLine("React Doctor (www.react.doctor)", `React Doctor ${highlighter.dim("(www.react.doctor)")}`));
3344
- lines.push(createFramedLine(""));
3345
- lines.push(createFramedLine(noScoreMessage, highlighter.dim(noScoreMessage)));
3346
- lines.push(createFramedLine(""));
3347
- }
3348
- return lines;
3349
- };
3350
- const buildCountsSummaryLine = (diagnostics, totalSourceFileCount, elapsedMilliseconds) => {
3399
+ const printCountsSummaryLine = (diagnostics, totalSourceFileCount, elapsedMilliseconds) => {
3351
3400
  const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
3352
3401
  const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
3353
3402
  const affectedFileCount = collectAffectedFiles(diagnostics).size;
3354
- const elapsed = formatElapsedTime(elapsedMilliseconds);
3355
- const plainParts = [];
3356
- const renderedParts = [];
3357
- if (errorCount > 0) {
3358
- const errorText = `✗ ${errorCount} error${errorCount === 1 ? "" : "s"}`;
3359
- plainParts.push(errorText);
3360
- renderedParts.push(highlighter.error(errorText));
3361
- }
3362
- if (warningCount > 0) {
3363
- const warningText = `⚠ ${warningCount} warning${warningCount === 1 ? "" : "s"}`;
3364
- plainParts.push(warningText);
3365
- renderedParts.push(highlighter.warn(warningText));
3366
- }
3403
+ const totalIssueCount = diagnostics.length;
3404
+ const elapsedTimeLabel = formatElapsedTime(elapsedMilliseconds);
3405
+ const issueCountColor = errorCount > 0 ? highlighter.error : warningCount > 0 ? highlighter.warn : highlighter.dim;
3406
+ const issueCountText = `${totalIssueCount} ${totalIssueCount === 1 ? "issue" : "issues"}`;
3367
3407
  const fileCountText = totalSourceFileCount > 0 ? `across ${affectedFileCount}/${totalSourceFileCount} files` : `across ${affectedFileCount} file${affectedFileCount === 1 ? "" : "s"}`;
3368
- const elapsedTimeText = `in ${elapsed}`;
3369
- plainParts.push(fileCountText, elapsedTimeText);
3370
- renderedParts.push(highlighter.dim(fileCountText), highlighter.dim(elapsedTimeText));
3371
- return createFramedLine(plainParts.join(" "), renderedParts.join(" "));
3408
+ const elapsedTimeText = `in ${elapsedTimeLabel}`;
3409
+ logger.log(` ${issueCountColor(issueCountText)} ${highlighter.dim(`${fileCountText} ${elapsedTimeText}`)}`);
3372
3410
  };
3373
3411
  const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, projectName, totalSourceFileCount, noScoreMessage, isOffline) => {
3374
- printFramedBox([...buildBrandingLines(scoreResult, noScoreMessage), buildCountsSummaryLine(diagnostics, totalSourceFileCount, elapsedMilliseconds)]);
3412
+ printCategoryBreakdown(buildCategoryBreakdown(diagnostics));
3413
+ if (scoreResult) printScoreHeader(scoreResult);
3414
+ else printNoScoreHeader(noScoreMessage);
3415
+ printCountsSummaryLine(diagnostics, totalSourceFileCount, elapsedMilliseconds);
3375
3416
  try {
3376
3417
  const diagnosticsDirectory = writeDiagnosticsDirectory(diagnostics);
3377
- logger.break();
3378
- logger.dim(` Full diagnostics written to ${diagnosticsDirectory}`);
3379
- } catch {
3380
- logger.break();
3381
- }
3418
+ logger.log(highlighter.gray(` Full diagnostics written to ${diagnosticsDirectory}`));
3419
+ } catch {}
3382
3420
  if (!isOffline) {
3383
- const shareUrl = buildShareUrl(diagnostics, scoreResult, projectName);
3384
3421
  logger.break();
3385
- logger.dim(` Share your results: ${highlighter.info(shareUrl)}`);
3422
+ const shareUrl = buildShareUrl(diagnostics, scoreResult, projectName);
3423
+ logger.log(` ${highlighter.bold("→ Share your results:")} ${highlighter.info(shareUrl)}`);
3386
3424
  }
3387
3425
  };
3388
3426
  const resolveOxlintNode = async (isLintEnabled, isQuiet) => {
@@ -3565,15 +3603,14 @@ const runScan = async (directory, options, userConfig, startTime) => {
3565
3603
  } else logger.success("No issues found!");
3566
3604
  logger.break();
3567
3605
  if (hasSkippedChecks) {
3568
- printBranding();
3569
- logger.dim(" Score not shown — some checks could not complete.");
3570
- } else if (scoreResult) {
3571
- printBranding(scoreResult.score);
3572
- printScoreGauge(scoreResult.score, scoreResult.label);
3573
- } else logger.dim(` ${noScoreMessage}`);
3606
+ printBrandingOnlyHeader();
3607
+ logger.log(highlighter.gray(" Score not shown — some checks could not complete."));
3608
+ } else if (scoreResult) printScoreHeader(scoreResult);
3609
+ else printNoScoreHeader(noScoreMessage);
3574
3610
  return buildResult();
3575
3611
  }
3576
- printDiagnostics(diagnostics, options.verbose);
3612
+ logger.break();
3613
+ printDiagnostics(diagnostics, options.verbose, directory);
3577
3614
  const displayedSourceFileCount = isDiffMode ? includePaths.length : lintSourceFileCount;
3578
3615
  const shouldShowShareLink = !options.offline && options.share;
3579
3616
  printSummary(diagnostics, elapsedMilliseconds, scoreResult, projectInfo.projectName, displayedSourceFileCount, noScoreMessage, !shouldShowShareLink);
@@ -3991,7 +4028,7 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
3991
4028
  };
3992
4029
  //#endregion
3993
4030
  //#region src/cli.ts
3994
- const VERSION = "0.1.0";
4031
+ const VERSION = "0.1.2";
3995
4032
  const VALID_FAIL_ON_LEVELS = new Set([
3996
4033
  "error",
3997
4034
  "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.0"
6949
+ version: "0.1.2"
6950
6950
  },
6951
6951
  rules: eslintShapedRules,
6952
6952
  configs: {
package/dist/index.js CHANGED
@@ -487,7 +487,9 @@ const highlighter = {
487
487
  warn: pc.yellow,
488
488
  info: pc.cyan,
489
489
  success: pc.green,
490
- dim: pc.dim
490
+ dim: pc.dim,
491
+ gray: pc.gray,
492
+ bold: pc.bold
491
493
  };
492
494
  const logger = {
493
495
  error(...args) {
@@ -2831,8 +2833,6 @@ const runOxlint = async (options) => {
2831
2833
  return await spawnLintBatches();
2832
2834
  } catch (error) {
2833
2835
  if (extendsPaths.length === 0) throw error;
2834
- const reason = error instanceof Error ? error.message : String(error);
2835
- process.stderr.write(`[react-doctor] could not adopt existing lint config (${reason.split("\n")[0]}); retrying without extends. Set "adoptExistingLintConfig": false to silence.\n`);
2836
2836
  writeOxlintConfig(createOxlintConfig({
2837
2837
  pluginPath,
2838
2838
  framework,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Diagnose and fix React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
5
5
  "keywords": [
6
6
  "accessibility",