react-doctor 0.2.8 → 0.2.9

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.
@@ -2045,6 +2045,14 @@ var PackageJsonNotFoundError = class extends Error {
2045
2045
  this.directory = directory;
2046
2046
  }
2047
2047
  };
2048
+ var NotADirectoryError = class extends Error {
2049
+ name = "NotADirectoryError";
2050
+ resolvedPath;
2051
+ constructor(resolvedPath, options) {
2052
+ super(`Resolved scan target "${resolvedPath}" is not a directory. Ensure the path exists and points to a project directory, not a file.`, options);
2053
+ this.resolvedPath = resolvedPath;
2054
+ }
2055
+ };
2048
2056
  var AmbiguousProjectError = class extends Error {
2049
2057
  name = "AmbiguousProjectError";
2050
2058
  directory;
@@ -4132,8 +4140,10 @@ const resolveScanTarget = (requestedDirectory) => {
4132
4140
  const configSourceDirectory = loadedConfig?.sourceDirectory ?? null;
4133
4141
  const redirectedDirectory = resolveConfigRootDir(userConfig, configSourceDirectory);
4134
4142
  const directoryAfterRedirect = redirectedDirectory ?? absoluteRequested;
4143
+ const resolvedDirectory = resolveDiagnoseTarget(directoryAfterRedirect) ?? directoryAfterRedirect;
4144
+ if (!isDirectory(resolvedDirectory)) throw existsSync(resolvedDirectory) ? new NotADirectoryError(resolvedDirectory) : new ProjectNotFoundError(resolvedDirectory);
4135
4145
  return {
4136
- resolvedDirectory: resolveDiagnoseTarget(directoryAfterRedirect) ?? directoryAfterRedirect,
4146
+ resolvedDirectory,
4137
4147
  requestedDirectory: absoluteRequested,
4138
4148
  userConfig,
4139
4149
  configSourceDirectory,
@@ -4614,7 +4624,7 @@ var Files = class Files extends Context.Service()("react-doctor/Files") {
4614
4624
  * pattern in react-doctor-evals' test layers.
4615
4625
  */
4616
4626
  static layerInMemory = (tree) => {
4617
- const resolveAbsolute = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
4627
+ const resolveAbsolute = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : `${rootDirectory}/${filePath}`;
4618
4628
  return Layer.succeed(Files, Files.of({
4619
4629
  readLines: (input) => Effect.sync(() => {
4620
4630
  const absolute = resolveAbsolute(input.filePath, input.rootDirectory);
@@ -4622,17 +4632,17 @@ var Files = class Files extends Context.Service()("react-doctor/Files") {
4622
4632
  return content === void 0 ? null : content.split("\n");
4623
4633
  }),
4624
4634
  listSourceFiles: (rootDirectory) => Effect.sync(() => {
4625
- const prefix = rootDirectory.endsWith(Path.sep) ? rootDirectory : `${rootDirectory}${Path.sep}`;
4635
+ const prefix = rootDirectory.endsWith("/") ? rootDirectory : `${rootDirectory}/`;
4626
4636
  const files = [];
4627
4637
  for (const absolute of tree.keys()) {
4628
4638
  if (!absolute.startsWith(prefix)) continue;
4629
- files.push(absolute.slice(prefix.length).split(Path.sep).join("/"));
4639
+ files.push(absolute.slice(prefix.length));
4630
4640
  }
4631
4641
  return files;
4632
4642
  }),
4633
4643
  isFile: (filePath) => Effect.sync(() => tree.has(filePath)),
4634
4644
  isDirectory: (filePath) => Effect.sync(() => {
4635
- const prefix = filePath.endsWith(Path.sep) ? filePath : `${filePath}${Path.sep}`;
4645
+ const prefix = filePath.endsWith("/") ? filePath : `${filePath}/`;
4636
4646
  for (const absolute of tree.keys()) if (absolute.startsWith(prefix)) return true;
4637
4647
  return false;
4638
4648
  })
@@ -5769,7 +5779,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
5769
5779
  const primaryLabel = diagnostic.labels[0];
5770
5780
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
5771
5781
  return {
5772
- filePath: diagnostic.filename,
5782
+ filePath: diagnostic.filename.replaceAll("\\", "/"),
5773
5783
  plugin,
5774
5784
  rule,
5775
5785
  severity: diagnostic.severity,
@@ -7046,4 +7056,4 @@ const cliLogger = {
7046
7056
  //#endregion
7047
7057
  export { isReactDoctorError as A, filterSourceFiles as C, groupBy as D, getDiffInfo as E, runInspect as F, toRelativePath as I, listWorkspacePackages as M, resolveScanTarget as N, highlighter as O, restoreLegacyThrow as P, filterDiagnosticsForSurface as S, formatReactDoctorError as T, Score as _, DeadCode as a, buildJsonReportError as b, LintPartialFailures as c, OXLINT_NODE_REQUIREMENT as d, Progress as f, SKILL_NAME as g, SHARE_BASE_URL as h, Config as i, layerOtlp as j, isMonorepoRoot as k, Linter as l, Reporter as m, cli_logger_exports as n, Files as o, Project as p, CANONICAL_GITHUB_URL as r, Git as s, cliLogger as t, NodeResolver as u, StagedFiles as v, formatErrorChain as w, discoverReactSubprojects as x, buildJsonReport as y };
7048
7058
 
7049
- //# sourceMappingURL=cli-logger-C35LXalM.js.map
7059
+ //# sourceMappingURL=cli-logger-BliQX9s8.js.map
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { i as __toESM, n as __exportAll, r as __require, t as __commonJSMin } from "./rolldown-runtime-uZX_iqCz.js";
2
- import { A as isReactDoctorError, C as filterSourceFiles, D as groupBy, E as getDiffInfo, F as runInspect, I as toRelativePath, M as listWorkspacePackages, N as resolveScanTarget, O as highlighter, P as restoreLegacyThrow, S as filterDiagnosticsForSurface, T as formatReactDoctorError, _ as Score, a as DeadCode, b as buildJsonReportError, c as LintPartialFailures, d as OXLINT_NODE_REQUIREMENT, f as Progress, g as SKILL_NAME, h as SHARE_BASE_URL, i as Config, j as layerOtlp, k as isMonorepoRoot, l as Linter, m as Reporter, o as Files, p as Project, r as CANONICAL_GITHUB_URL, s as Git, t as cliLogger, u as NodeResolver, v as StagedFiles, w as formatErrorChain, x as discoverReactSubprojects, y as buildJsonReport } from "./cli-logger-C35LXalM.js";
2
+ import { A as isReactDoctorError, C as filterSourceFiles, D as groupBy, E as getDiffInfo, F as runInspect, I as toRelativePath, M as listWorkspacePackages, N as resolveScanTarget, O as highlighter, P as restoreLegacyThrow, S as filterDiagnosticsForSurface, T as formatReactDoctorError, _ as Score, a as DeadCode, b as buildJsonReportError, c as LintPartialFailures, d as OXLINT_NODE_REQUIREMENT, f as Progress, g as SKILL_NAME, h as SHARE_BASE_URL, i as Config, j as layerOtlp, k as isMonorepoRoot, l as Linter, m as Reporter, o as Files, p as Project, r as CANONICAL_GITHUB_URL, s as Git, t as cliLogger, u as NodeResolver, v as StagedFiles, w as formatErrorChain, x as discoverReactSubprojects, y as buildJsonReport } from "./cli-logger-BliQX9s8.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { execFileSync, execSync } from "node:child_process";
5
5
  import path, { join } from "node:path";
@@ -6323,6 +6323,12 @@ const colorizeByScore = (text, score) => {
6323
6323
  //#region src/cli/utils/render-score-header.ts
6324
6324
  const SCORE_BAR_ANIMATION_FRAME_COUNT = 40;
6325
6325
  const SCORE_BAR_ANIMATION_FRAME_DELAY_MS = 50;
6326
+ const PERFECT_SCORE_RAINBOW_FRAME_COUNT = 16;
6327
+ const PERFECT_SCORE_RAINBOW_FRAME_DELAY_MS = 50;
6328
+ const RAINBOW_HUE_SHIFT_PER_FRAME = 9;
6329
+ const RAINBOW_GRADIENT_WIDTH = 80;
6330
+ const RAINBOW_OKLCH_LIGHTNESS = .638;
6331
+ const RAINBOW_OKLCH_CHROMA = .129;
6326
6332
  const easeOutCubic = (progress) => 1 - (1 - progress) ** 3;
6327
6333
  const sleep = (milliseconds) => Effect.promise(() => new Promise((resolve) => setTimeout(resolve, milliseconds)));
6328
6334
  const buildScoreBarSegments = (filledCount) => {
@@ -6333,6 +6339,36 @@ const buildScoreBarSegments = (filledCount) => {
6333
6339
  };
6334
6340
  };
6335
6341
  const getFilledCount = (score) => Math.round(score / 100 * 50);
6342
+ const joinScoreHeaderFrame = (lines) => `${lines[0]}\n\r${lines[1]}\n\r${lines[2]}\n\r${lines[3]}\n`;
6343
+ const buildRawScoreBar = (displayScore) => {
6344
+ const { filledSegment, emptySegment } = buildScoreBarSegments(getFilledCount(displayScore));
6345
+ return filledSegment + emptySegment;
6346
+ };
6347
+ const buildScoreHeaderLine = (faceLine, rightColumnContent) => {
6348
+ return ` ${faceLine}${rightColumnContent.length > 0 ? " " : ""}${rightColumnContent}`;
6349
+ };
6350
+ const getRightColumnOffset = (faceLine) => ` ${faceLine} `.length;
6351
+ const clampColorChannel = (value) => Math.max(0, Math.min(255, Math.round(value)));
6352
+ const encodeSrgb = (value) => value <= .0031308 ? value * 12.92 : 1.055 * value ** (1 / 2.4) - .055;
6353
+ const oklchToRgb = (lightness, chroma, hue) => {
6354
+ const hueRadians = hue * Math.PI / 180;
6355
+ const labA = chroma * Math.cos(hueRadians);
6356
+ const labB = chroma * Math.sin(hueRadians);
6357
+ const longCone = (lightness + .3963377774 * labA + .2158037573 * labB) ** 3;
6358
+ const mediumCone = (lightness - .1055613458 * labA - .0638541728 * labB) ** 3;
6359
+ const shortCone = (lightness - .0894841775 * labA - 1.291485548 * labB) ** 3;
6360
+ return {
6361
+ red: clampColorChannel(encodeSrgb(4.0767416621 * longCone - 3.3077115913 * mediumCone + .2309699292 * shortCone) * 255),
6362
+ green: clampColorChannel(encodeSrgb(-1.2684380046 * longCone + 2.6097574011 * mediumCone - .3413193965 * shortCone) * 255),
6363
+ blue: clampColorChannel(encodeSrgb(-.0041960863 * longCone - .7034186147 * mediumCone + 1.707614701 * shortCone) * 255)
6364
+ };
6365
+ };
6366
+ const colorizeTrueColor = (text, { red, green, blue }) => `\x1b[38;2;${red};${green};${blue}m${text}\x1b[39m`;
6367
+ const colorizeRainbowText = (text, frame, offset = 0) => [...text].map((character, index) => {
6368
+ if (character === " ") return character;
6369
+ return colorizeTrueColor(character, oklchToRgb(RAINBOW_OKLCH_LIGHTNESS, RAINBOW_OKLCH_CHROMA, ((index + offset) / RAINBOW_GRADIENT_WIDTH * 360 + frame * RAINBOW_HUE_SHIFT_PER_FRAME) % 360));
6370
+ }).join("");
6371
+ const buildRainbowHeaderLine = (faceLine, rightColumnContent, frame) => colorizeRainbowText(buildScoreHeaderLine(faceLine, rightColumnContent), frame);
6336
6372
  const buildScoreBar = (displayScore, colorScore = displayScore) => {
6337
6373
  const { filledSegment, emptySegment } = buildScoreBarSegments(getFilledCount(displayScore));
6338
6374
  return colorizeByScore(filledSegment, colorScore) + highlighter.dim(emptySegment);
@@ -6343,18 +6379,19 @@ const getDoctorFace = (score) => {
6343
6379
  return ["x x", " ▽ "];
6344
6380
  };
6345
6381
  const BRANDING_LINE = `React Doctor ${highlighter.dim("(https://react.doctor)")}`;
6346
- const buildFaceRenderedLines = (score) => {
6382
+ const RAW_BRANDING_LINE = "React Doctor (https://react.doctor)";
6383
+ const buildRawFaceLines = (score) => {
6347
6384
  const [eyes, mouth] = getDoctorFace(score);
6348
- const colorize = (text) => colorizeByScore(text, score);
6349
6385
  return [
6350
6386
  "┌─────┐",
6351
6387
  `│ ${eyes} │`,
6352
6388
  `│ ${mouth} │`,
6353
6389
  "└─────┘"
6354
- ].map(colorize);
6390
+ ];
6355
6391
  };
6356
- const buildScoreHeaderLine = (faceLine, rightColumnContent) => {
6357
- return ` ${faceLine}${rightColumnContent.length > 0 ? " " : ""}${rightColumnContent}`;
6392
+ const buildFaceRenderedLines = (score) => {
6393
+ const colorize = (text) => colorizeByScore(text, score);
6394
+ return buildRawFaceLines(score).map(colorize);
6358
6395
  };
6359
6396
  const writeScoreHeaderLine = (line) => Effect.sync(() => {
6360
6397
  process.stdout.write(line);
@@ -6365,29 +6402,97 @@ const buildScoreLine = (displayScore, finalScore, label, projectName) => {
6365
6402
  const projectSuffix = projectName ? ` ${highlighter.dim("·")} ${highlighter.dim(projectName)}` : "";
6366
6403
  return `${scoreNumber} ${highlighter.dim(`/ 100`)} ${scoreLabel}${projectSuffix}`;
6367
6404
  };
6405
+ const buildRawScoreLine = (displayScore, label, projectName) => {
6406
+ return `${displayScore} / 100 ${label}${projectName ? ` · ${projectName}` : ""}`;
6407
+ };
6408
+ const buildRainbowScoreHeaderFrame = ({ score, displayScore, label, frame, projectName }) => {
6409
+ const rawFaceLines = buildRawFaceLines(score);
6410
+ return joinScoreHeaderFrame([
6411
+ buildRainbowHeaderLine(rawFaceLines[0] ?? "", buildRawScoreLine(displayScore, label, projectName), frame),
6412
+ buildRainbowHeaderLine(rawFaceLines[1] ?? "", buildRawScoreBar(displayScore), frame),
6413
+ buildRainbowHeaderLine(rawFaceLines[2] ?? "", RAW_BRANDING_LINE, frame),
6414
+ buildRainbowHeaderLine(rawFaceLines[3] ?? "", "", frame)
6415
+ ]);
6416
+ };
6417
+ const buildFinalPerfectScoreHeaderFrame = (score, label, frame, projectName) => {
6418
+ const rawFaceLines = buildRawFaceLines(score);
6419
+ const renderedFaceLines = buildFaceRenderedLines(score);
6420
+ const rainbowBarLine = colorizeRainbowText(buildRawScoreBar(score), frame, getRightColumnOffset(rawFaceLines[1] ?? ""));
6421
+ return joinScoreHeaderFrame([
6422
+ buildScoreHeaderLine(renderedFaceLines[0] ?? "", buildScoreLine(score, score, label, projectName)),
6423
+ buildScoreHeaderLine(renderedFaceLines[1] ?? "", rainbowBarLine),
6424
+ buildScoreHeaderLine(renderedFaceLines[2] ?? "", BRANDING_LINE),
6425
+ buildScoreHeaderLine(renderedFaceLines[3] ?? "", "")
6426
+ ]);
6427
+ };
6428
+ const buildInitialScoreHeaderLine = ({ isPerfectScore, shouldAnimate, lineIndex, renderedFaceLine, rawFaceLine, rightColumnContent, rawRightColumnContent, score }) => {
6429
+ if (!isPerfectScore) return buildScoreHeaderLine(renderedFaceLine, rightColumnContent);
6430
+ if (shouldAnimate) return buildRainbowHeaderLine(rawFaceLine, rawRightColumnContent, 0);
6431
+ if (lineIndex !== 1) return buildScoreHeaderLine(renderedFaceLine, rightColumnContent);
6432
+ return buildScoreHeaderLine(renderedFaceLine, colorizeRainbowText(buildRawScoreBar(score), 0, getRightColumnOffset(rawFaceLine)));
6433
+ };
6368
6434
  const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectName) => Effect.gen(function* () {
6435
+ const isPerfectScore = score === 100;
6369
6436
  for (let frame = 0; frame <= SCORE_BAR_ANIMATION_FRAME_COUNT; frame += 1) {
6370
6437
  const progress = easeOutCubic(frame / SCORE_BAR_ANIMATION_FRAME_COUNT);
6371
6438
  const animatedScore = Math.round(score * progress);
6439
+ if (isPerfectScore) {
6440
+ yield* writeScoreHeaderLine(`${frame === 0 ? "" : "\x1B[4A"}\r${buildRainbowScoreHeaderFrame({
6441
+ score,
6442
+ displayScore: animatedScore,
6443
+ label,
6444
+ frame,
6445
+ projectName
6446
+ })}`);
6447
+ if (frame < SCORE_BAR_ANIMATION_FRAME_COUNT) yield* sleep(SCORE_BAR_ANIMATION_FRAME_DELAY_MS);
6448
+ continue;
6449
+ }
6372
6450
  const animatedScoreLine = buildScoreLine(animatedScore, score, label, projectName);
6373
6451
  const animatedBarLine = buildScoreBar(animatedScore, score);
6374
6452
  yield* writeScoreHeaderLine(`${frame === 0 ? "" : "\x1B[2A"}\r${buildScoreHeaderLine(scoreFaceLine, animatedScoreLine)}\n\r${buildScoreHeaderLine(barFaceLine, animatedBarLine)}\n`);
6375
6453
  if (frame < SCORE_BAR_ANIMATION_FRAME_COUNT) yield* sleep(SCORE_BAR_ANIMATION_FRAME_DELAY_MS);
6376
6454
  }
6455
+ if (!isPerfectScore) return;
6456
+ for (let frame = 0; frame < PERFECT_SCORE_RAINBOW_FRAME_COUNT; frame += 1) {
6457
+ yield* writeScoreHeaderLine(`\x1b[4A\r${buildRainbowScoreHeaderFrame({
6458
+ score,
6459
+ displayScore: score,
6460
+ label,
6461
+ frame,
6462
+ projectName
6463
+ })}`);
6464
+ yield* sleep(PERFECT_SCORE_RAINBOW_FRAME_DELAY_MS);
6465
+ }
6466
+ yield* writeScoreHeaderLine(`\x1b[4A\r${buildFinalPerfectScoreHeaderFrame(score, label, PERFECT_SCORE_RAINBOW_FRAME_COUNT, projectName)}\x1b[2A`);
6377
6467
  });
6378
6468
  const printScoreHeader = (scoreResult, projectName) => Effect.gen(function* () {
6469
+ const isPerfectScore = scoreResult.score === 100;
6379
6470
  const renderedFaceLines = buildFaceRenderedLines(scoreResult.score);
6471
+ const rawFaceLines = buildRawFaceLines(scoreResult.score);
6380
6472
  const shouldAnimate = !isSpinnerSilent() && isSpinnerInteractive(process.stdout);
6473
+ const displayScore = shouldAnimate ? 0 : scoreResult.score;
6381
6474
  const rightColumnLines = [
6382
- buildScoreLine(shouldAnimate ? 0 : scoreResult.score, scoreResult.score, scoreResult.label, projectName),
6475
+ buildScoreLine(displayScore, scoreResult.score, scoreResult.label, projectName),
6383
6476
  shouldAnimate ? buildScoreBar(0, scoreResult.score) : buildScoreBar(scoreResult.score),
6384
6477
  BRANDING_LINE,
6385
6478
  ""
6386
6479
  ];
6387
- for (let lineIndex = 0; lineIndex < renderedFaceLines.length; lineIndex += 1) {
6388
- const rightColumnContent = rightColumnLines[lineIndex] ?? "";
6389
- yield* Console.log(buildScoreHeaderLine(renderedFaceLines[lineIndex], rightColumnContent));
6390
- }
6480
+ const rawRightColumnLines = [
6481
+ buildRawScoreLine(displayScore, scoreResult.label, projectName),
6482
+ buildRawScoreBar(displayScore),
6483
+ RAW_BRANDING_LINE,
6484
+ ""
6485
+ ];
6486
+ for (let lineIndex = 0; lineIndex < renderedFaceLines.length; lineIndex += 1) yield* Console.log(buildInitialScoreHeaderLine({
6487
+ isPerfectScore,
6488
+ shouldAnimate,
6489
+ lineIndex,
6490
+ renderedFaceLine: renderedFaceLines[lineIndex] ?? "",
6491
+ rawFaceLine: rawFaceLines[lineIndex] ?? "",
6492
+ rightColumnContent: rightColumnLines[lineIndex] ?? "",
6493
+ rawRightColumnContent: rawRightColumnLines[lineIndex] ?? "",
6494
+ score: scoreResult.score
6495
+ }));
6391
6496
  yield* Console.log("");
6392
6497
  if (shouldAnimate) {
6393
6498
  yield* writeScoreHeaderLine("\x1B[5A");
@@ -6561,7 +6666,7 @@ const resolveOxlintNodeEffect = (isLintEnabled, isQuiet) => Effect.gen(function*
6561
6666
  const resolveOxlintNode = (isLintEnabled, isQuiet) => Effect.runPromise(resolveOxlintNodeEffect(isLintEnabled, isQuiet).pipe(Effect.provide(NodeResolver.layerNode)));
6562
6667
  //#endregion
6563
6668
  //#region src/cli/utils/version.ts
6564
- const VERSION = "0.2.8";
6669
+ const VERSION = "0.2.9";
6565
6670
  //#endregion
6566
6671
  //#region src/inspect.ts
6567
6672
  const silentConsole = makeNoopConsole();
@@ -7362,7 +7467,7 @@ const warnSetupPromptFailure = async (options, error) => {
7362
7467
  return;
7363
7468
  }
7364
7469
  try {
7365
- const { cliLogger } = await import("./cli-logger-C35LXalM.js").then((n) => n.n);
7470
+ const { cliLogger } = await import("./cli-logger-BliQX9s8.js").then((n) => n.n);
7366
7471
  cliLogger.warn(message);
7367
7472
  } catch {}
7368
7473
  };
@@ -8646,7 +8751,8 @@ const defaultInstallDependencyRunner = (input) => {
8646
8751
  env: {
8647
8752
  ...process.env,
8648
8753
  REACT_DOCTOR_INSTALL: "1"
8649
- }
8754
+ },
8755
+ shell: process.platform === "win32"
8650
8756
  });
8651
8757
  };
8652
8758
  const installReactDoctorDependency = async (options) => {
package/dist/index.d.ts CHANGED
@@ -355,8 +355,14 @@ interface DiagnoseResult {
355
355
  skippedCheckReasons?: Record<string, string>;
356
356
  project: ProjectInfo;
357
357
  elapsedMilliseconds: number;
358
- } //#endregion
359
- //#region src/types/handle-error.d.ts
358
+ }
359
+ /**
360
+ * A single project to scan as part of a `diagnoseProjects()` batch.
361
+ * Scan options (`deadCode`, `lint`, etc.) are flat on the entry and
362
+ * layer on top of the global defaults — omitted fields fall through.
363
+ * `config` is a full `ReactDoctorConfig` override that replaces the
364
+ * on-disk `react-doctor.config.json` for this project's scan.
365
+ */
360
366
  //#endregion
361
367
  //#region src/types/inspect.d.ts
362
368
  interface InspectResult {
@@ -492,13 +498,18 @@ declare class PackageJsonNotFoundError extends Error {
492
498
  readonly directory: string;
493
499
  constructor(directory: string, options?: ErrorOptions);
494
500
  }
501
+ declare class NotADirectoryError extends Error {
502
+ readonly name = "NotADirectoryError";
503
+ readonly resolvedPath: string;
504
+ constructor(resolvedPath: string, options?: ErrorOptions);
505
+ }
495
506
  declare class AmbiguousProjectError extends Error {
496
507
  readonly name = "AmbiguousProjectError";
497
508
  readonly directory: string;
498
509
  readonly candidates: readonly string[];
499
510
  constructor(directory: string, candidates: readonly string[], options?: ErrorOptions);
500
511
  }
501
- declare const isProjectDiscoveryError: (value: unknown) => value is ProjectNotFoundError | NoReactDependencyError | PackageJsonNotFoundError | AmbiguousProjectError; //#endregion
512
+ declare const isProjectDiscoveryError: (value: unknown) => value is ProjectNotFoundError | NoReactDependencyError | PackageJsonNotFoundError | NotADirectoryError | AmbiguousProjectError; //#endregion
502
513
  //#region src/project-info/utils/is-directory.d.ts
503
514
  //#endregion
504
515
  //#region src/errors.d.ts
@@ -651,7 +662,41 @@ declare const summarizeDiagnostics: (diagnostics: Diagnostic[], worstScore?: num
651
662
  //#endregion
652
663
  //#region ../api/dist/index.d.ts
653
664
  //#region src/diagnose.d.ts
654
- declare const diagnose: (directory: string, options?: DiagnoseOptions) => Promise<DiagnoseResult>; //#endregion
665
+ declare const diagnose: (directory: string, options?: DiagnoseOptions) => Promise<DiagnoseResult>;
666
+ /**
667
+ * Scan multiple projects in parallel and return per-project scores,
668
+ * diagnostics, and an aggregate score (worst-of across all projects).
669
+ *
670
+ * Each project runs its own independent `runInspect` pipeline — the
671
+ * same pipeline `diagnose()` uses — so per-project config overrides,
672
+ * dead-code analysis, and scoring all work identically to a single
673
+ * `diagnose()` call.
674
+ *
675
+ * Projects that fail (e.g. missing `package.json`, no React dependency)
676
+ * are included in the result with `ok: false` rather than aborting the
677
+ * entire batch, so callers always receive partial results.
678
+ *
679
+ * ```ts
680
+ * const result = await diagnoseProjects({
681
+ * projects: [
682
+ * { directory: "packages/app" },
683
+ * { directory: "packages/shared", deadCode: false },
684
+ * { directory: "packages/admin", config: {
685
+ * rules: { "react-doctor/no-array-index-as-key": "off" },
686
+ * }},
687
+ * ],
688
+ * concurrency: 4,
689
+ * });
690
+ *
691
+ * for (const project of result.projects) {
692
+ * if (project.ok) {
693
+ * console.log(project.directory, project.score);
694
+ * } else {
695
+ * console.error(project.directory, project.error);
696
+ * }
697
+ * }
698
+ * ```
699
+ */
655
700
  //#endregion
656
701
  //#region src/index.d.ts
657
702
  declare const clearCaches: () => void;
@@ -662,5 +707,5 @@ interface ToJsonReportOptions {
662
707
  }
663
708
  declare const toJsonReport: (result: DiagnoseResult, options: ToJsonReportOptions) => JsonReport;
664
709
  //#endregion
665
- export { AmbiguousProjectError, type DiagnoseOptions, type DiagnoseResult, type Diagnostic, type DiffInfo, type JsonReport, type JsonReportDiffInfo, type JsonReportError, type JsonReportMode, type JsonReportProjectEntry, type JsonReportSummary, NoReactDependencyError, PackageJsonNotFoundError, type ProjectInfo, ProjectNotFoundError, type ReactDoctorConfig, ReactDoctorError, type ScoreResult, buildJsonReport, buildJsonReportError, clearCaches, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
710
+ export { AmbiguousProjectError, type DiagnoseOptions, type DiagnoseResult, type Diagnostic, type DiffInfo, type JsonReport, type JsonReportDiffInfo, type JsonReportError, type JsonReportMode, type JsonReportProjectEntry, type JsonReportSummary, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, type ProjectInfo, ProjectNotFoundError, type ReactDoctorConfig, ReactDoctorError, type ScoreResult, buildJsonReport, buildJsonReportError, clearCaches, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
666
711
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -2067,6 +2067,14 @@ var PackageJsonNotFoundError = class extends Error {
2067
2067
  this.directory = directory;
2068
2068
  }
2069
2069
  };
2070
+ var NotADirectoryError = class extends Error {
2071
+ name = "NotADirectoryError";
2072
+ resolvedPath;
2073
+ constructor(resolvedPath, options) {
2074
+ super(`Resolved scan target "${resolvedPath}" is not a directory. Ensure the path exists and points to a project directory, not a file.`, options);
2075
+ this.resolvedPath = resolvedPath;
2076
+ }
2077
+ };
2070
2078
  var AmbiguousProjectError = class extends Error {
2071
2079
  name = "AmbiguousProjectError";
2072
2080
  directory;
@@ -2077,7 +2085,7 @@ var AmbiguousProjectError = class extends Error {
2077
2085
  this.candidates = candidates;
2078
2086
  }
2079
2087
  };
2080
- const isProjectDiscoveryError = (value) => value instanceof ProjectNotFoundError || value instanceof NoReactDependencyError || value instanceof PackageJsonNotFoundError || value instanceof AmbiguousProjectError;
2088
+ const isProjectDiscoveryError = (value) => value instanceof ProjectNotFoundError || value instanceof NoReactDependencyError || value instanceof PackageJsonNotFoundError || value instanceof NotADirectoryError || value instanceof AmbiguousProjectError;
2081
2089
  const isFile = (filePath) => {
2082
2090
  try {
2083
2091
  return fs.statSync(filePath).isFile();
@@ -4160,8 +4168,10 @@ const resolveScanTarget = (requestedDirectory) => {
4160
4168
  const configSourceDirectory = loadedConfig?.sourceDirectory ?? null;
4161
4169
  const redirectedDirectory = resolveConfigRootDir(userConfig, configSourceDirectory);
4162
4170
  const directoryAfterRedirect = redirectedDirectory ?? absoluteRequested;
4171
+ const resolvedDirectory = resolveDiagnoseTarget(directoryAfterRedirect) ?? directoryAfterRedirect;
4172
+ if (!isDirectory(resolvedDirectory)) throw existsSync(resolvedDirectory) ? new NotADirectoryError(resolvedDirectory) : new ProjectNotFoundError(resolvedDirectory);
4163
4173
  return {
4164
- resolvedDirectory: resolveDiagnoseTarget(directoryAfterRedirect) ?? directoryAfterRedirect,
4174
+ resolvedDirectory,
4165
4175
  requestedDirectory: absoluteRequested,
4166
4176
  userConfig,
4167
4177
  configSourceDirectory,
@@ -4645,7 +4655,7 @@ var Files = class Files extends Context.Service()("react-doctor/Files") {
4645
4655
  * pattern in react-doctor-evals' test layers.
4646
4656
  */
4647
4657
  static layerInMemory = (tree) => {
4648
- const resolveAbsolute = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
4658
+ const resolveAbsolute = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : `${rootDirectory}/${filePath}`;
4649
4659
  return Layer.succeed(Files, Files.of({
4650
4660
  readLines: (input) => Effect.sync(() => {
4651
4661
  const absolute = resolveAbsolute(input.filePath, input.rootDirectory);
@@ -4653,17 +4663,17 @@ var Files = class Files extends Context.Service()("react-doctor/Files") {
4653
4663
  return content === void 0 ? null : content.split("\n");
4654
4664
  }),
4655
4665
  listSourceFiles: (rootDirectory) => Effect.sync(() => {
4656
- const prefix = rootDirectory.endsWith(Path.sep) ? rootDirectory : `${rootDirectory}${Path.sep}`;
4666
+ const prefix = rootDirectory.endsWith("/") ? rootDirectory : `${rootDirectory}/`;
4657
4667
  const files = [];
4658
4668
  for (const absolute of tree.keys()) {
4659
4669
  if (!absolute.startsWith(prefix)) continue;
4660
- files.push(absolute.slice(prefix.length).split(Path.sep).join("/"));
4670
+ files.push(absolute.slice(prefix.length));
4661
4671
  }
4662
4672
  return files;
4663
4673
  }),
4664
4674
  isFile: (filePath) => Effect.sync(() => tree.has(filePath)),
4665
4675
  isDirectory: (filePath) => Effect.sync(() => {
4666
- const prefix = filePath.endsWith(Path.sep) ? filePath : `${filePath}${Path.sep}`;
4676
+ const prefix = filePath.endsWith("/") ? filePath : `${filePath}/`;
4667
4677
  for (const absolute of tree.keys()) if (absolute.startsWith(prefix)) return true;
4668
4678
  return false;
4669
4679
  })
@@ -5800,7 +5810,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
5800
5810
  const primaryLabel = diagnostic.labels[0];
5801
5811
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
5802
5812
  return {
5803
- filePath: diagnostic.filename,
5813
+ filePath: diagnostic.filename.replaceAll("\\", "/"),
5804
5814
  plugin,
5805
5815
  rule,
5806
5816
  severity: diagnostic.severity,
@@ -6602,22 +6612,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6602
6612
  "inspect.isCi": input.isCi,
6603
6613
  "inspect.scoreSurface": input.scoreSurface ?? "score"
6604
6614
  } }));
6605
- /**
6606
- * Default layer stack for the production CLI / programmatic API:
6607
- * real Node-side services for Project / Config / Files / Git / Linter /
6608
- * DeadCode; HTTP for Score; noop Progress (the CLI overrides with
6609
- * `Progress.layerOra(...)` for terminal feedback); the silent Reporter
6610
- * (the orchestrator already returns the diagnostic array via
6611
- * `Stream.runCollect`).
6612
- *
6613
- * Callers tweak by replacing individual layers: `--no-score` swaps
6614
- * `Score.layerHttp` for `Score.layerOf(null)`; `--no-lint` swaps
6615
- * `Linter.layerOxlint` for `Linter.layerOf([])`; `--no-dead-code`
6616
- * swaps `DeadCode.layerNode` for `DeadCode.layerOf([])`; a caller
6617
- * with a pre-loaded config swaps `Config.layerNode` for
6618
- * `Config.layerOf(resolved)`.
6619
- */
6620
- const layerInspectLive = Layer.mergeAll(Project.layerNode, Config.layerNode, DeadCode.layerNode, Files.layerNode, Git.layerNode, Linter.layerOxlint, LintPartialFailures.layerLive, Progress.layerNoop, Reporter.layerNoop, Score.layerHttp);
6615
+ Layer.mergeAll(Project.layerNode, Config.layerNode, DeadCode.layerNode, Files.layerNode, Git.layerNode, Linter.layerOxlint, LintPartialFailures.layerLive, Progress.layerNoop, Reporter.layerNoop, Score.layerHttp);
6621
6616
  const parseNodeVersion = (versionString) => {
6622
6617
  const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
6623
6618
  return {
@@ -7021,22 +7016,23 @@ import_picocolors.default.red, import_picocolors.default.yellow, import_picocolo
7021
7016
  const clearAutoSuppressionCaches = () => {};
7022
7017
  //#endregion
7023
7018
  //#region ../api/dist/index.js
7024
- const diagnose = async (directory, options = {}) => {
7025
- const startTime = globalThis.performance.now();
7026
- const scanTarget = resolveScanTarget(directory);
7019
+ const DEFAULT_LAYER = Layer.mergeAll(Project.layerNode, Config.layerNode, DeadCode.layerNode, Files.layerNode, Git.layerNode, Linter.layerOxlint, LintPartialFailures.layerLive, Progress.layerNoop, Reporter.layerNoop, Score.layerHttp);
7020
+ const buildInspectProgram = (scanTarget, options, configOverride) => {
7021
+ const effectiveConfig = configOverride ?? scanTarget.userConfig;
7027
7022
  const includePaths = options.includePaths ?? [];
7028
- const program = runInspect({
7023
+ return runInspect({
7029
7024
  directory: scanTarget.resolvedDirectory,
7030
7025
  includePaths,
7031
- customRulesOnly: scanTarget.userConfig?.customRulesOnly ?? false,
7032
- respectInlineDisables: options.respectInlineDisables ?? scanTarget.userConfig?.respectInlineDisables ?? true,
7033
- adoptExistingLintConfig: scanTarget.userConfig?.adoptExistingLintConfig ?? true,
7034
- ignoredTags: new Set(scanTarget.userConfig?.ignore?.tags ?? []),
7035
- runDeadCode: options.deadCode ?? scanTarget.userConfig?.deadCode ?? true,
7026
+ customRulesOnly: effectiveConfig?.customRulesOnly ?? false,
7027
+ respectInlineDisables: options.respectInlineDisables ?? effectiveConfig?.respectInlineDisables ?? true,
7028
+ adoptExistingLintConfig: effectiveConfig?.adoptExistingLintConfig ?? true,
7029
+ ignoredTags: new Set(effectiveConfig?.ignore?.tags ?? []),
7030
+ runDeadCode: options.deadCode ?? effectiveConfig?.deadCode ?? true,
7036
7031
  isCi: false,
7037
7032
  resolveLocalGithubViewerPermission: true
7038
7033
  });
7039
- const output = await Effect.runPromise(restoreLegacyThrow(program.pipe(Effect.provide(layerInspectLive), Effect.provide(layerOtlp))));
7034
+ };
7035
+ const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
7040
7036
  if (output.didLintFail && output.lintFailureReason !== null) console.error("Lint failed:", output.lintFailureReason);
7041
7037
  const skippedChecks = [];
7042
7038
  const skippedCheckReasons = {};
@@ -7050,9 +7046,14 @@ const diagnose = async (directory, options = {}) => {
7050
7046
  skippedChecks,
7051
7047
  ...Object.keys(skippedCheckReasons).length > 0 ? { skippedCheckReasons } : {},
7052
7048
  project: output.project,
7053
- elapsedMilliseconds: globalThis.performance.now() - startTime
7049
+ elapsedMilliseconds
7054
7050
  };
7055
7051
  };
7052
+ const diagnose = async (directory, options = {}) => {
7053
+ const startTime = globalThis.performance.now();
7054
+ const program = buildInspectProgram(resolveScanTarget(directory), options);
7055
+ return outputToDiagnoseResult(await Effect.runPromise(restoreLegacyThrow(program.pipe(Effect.provide(DEFAULT_LAYER), Effect.provide(layerOtlp)))), globalThis.performance.now() - startTime);
7056
+ };
7056
7057
  //#endregion
7057
7058
  //#region src/index.ts
7058
7059
  const clearCaches = () => {
@@ -7081,6 +7082,6 @@ const toJsonReport = (result, options) => buildJsonReport({
7081
7082
  totalElapsedMilliseconds: result.elapsedMilliseconds
7082
7083
  });
7083
7084
  //#endregion
7084
- export { AmbiguousProjectError, NoReactDependencyError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
7085
+ export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
7085
7086
 
7086
7087
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Diagnose and fix React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -58,21 +58,21 @@
58
58
  "oxlint": "^1.66.0",
59
59
  "prompts": "^2.4.2",
60
60
  "typescript": ">=5.0.4 <7",
61
- "oxlint-plugin-react-doctor": "0.2.8"
61
+ "oxlint-plugin-react-doctor": "0.2.9"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@types/prompts": "^2.4.9",
65
65
  "commander": "^14.0.3",
66
66
  "ora": "^9.4.0",
67
- "@react-doctor/api": "0.2.8",
68
- "@react-doctor/core": "0.2.8"
67
+ "@react-doctor/api": "0.2.9",
68
+ "@react-doctor/core": "0.2.9"
69
69
  },
70
70
  "engines": {
71
71
  "node": "^20.19.0 || >=22.12.0"
72
72
  },
73
73
  "scripts": {
74
74
  "dev": "vp pack --watch",
75
- "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && NODE_ENV=production vp pack",
75
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && cross-env NODE_ENV=production vp pack",
76
76
  "typecheck": "tsc --noEmit",
77
77
  "test": "vp test run"
78
78
  }