react-doctor 0.2.9 → 0.2.10

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.
@@ -22,6 +22,7 @@ import * as Option from "effect/Option";
22
22
  import * as Ref from "effect/Ref";
23
23
  import * as Stream from "effect/Stream";
24
24
  import * as Cache from "effect/Cache";
25
+ import { Worker } from "node:worker_threads";
25
26
  import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner";
26
27
  import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem";
27
28
  import * as NodePath from "@effect/platform-node-shared/NodePath";
@@ -2272,11 +2273,13 @@ const FRAMEWORK_DISPLAY_NAMES = {
2272
2273
  gatsby: "Gatsby",
2273
2274
  expo: "Expo",
2274
2275
  "react-native": "React Native",
2276
+ preact: "Preact",
2275
2277
  unknown: "React"
2276
2278
  };
2277
2279
  const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
2278
2280
  const detectFramework = (dependencies) => {
2279
2281
  for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
2282
+ if (dependencies.preact && !dependencies.react) return "preact";
2280
2283
  return "unknown";
2281
2284
  };
2282
2285
  const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
@@ -2740,6 +2743,13 @@ const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => {
2740
2743
  }
2741
2744
  return false;
2742
2745
  };
2746
+ const hasPreact = (packageJson) => {
2747
+ return "preact" in {
2748
+ ...packageJson.peerDependencies,
2749
+ ...packageJson.dependencies,
2750
+ ...packageJson.devDependencies
2751
+ };
2752
+ };
2743
2753
  const TANSTACK_QUERY_PACKAGES = new Set([
2744
2754
  "@tanstack/react-query",
2745
2755
  "@tanstack/query-core",
@@ -2777,7 +2787,8 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
2777
2787
  const REACT_DEPENDENCY_NAMES = new Set([
2778
2788
  "react",
2779
2789
  "react-native",
2780
- "next"
2790
+ "next",
2791
+ "preact"
2781
2792
  ]);
2782
2793
  const hasReactDependency = (packageJson) => {
2783
2794
  const allDependencies = {
@@ -2936,12 +2947,47 @@ const discoverProject = (directory) => {
2936
2947
  hasTypeScript,
2937
2948
  hasReactCompiler: detectReactCompiler(directory, packageJson),
2938
2949
  hasTanStackQuery: hasTanStackQuery(packageJson),
2950
+ hasPreact: hasPreact(packageJson),
2939
2951
  hasReactNativeWorkspace,
2940
2952
  sourceFileCount
2941
2953
  };
2942
2954
  cachedProjectInfos.set(directory, projectInfo);
2943
2955
  return projectInfo;
2944
2956
  };
2957
+ const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
2958
+ const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
2959
+ const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
2960
+ const parseReactMajorMinor = (reactVersion) => {
2961
+ if (typeof reactVersion !== "string") return null;
2962
+ const trimmed = reactVersion.trim();
2963
+ if (trimmed.length === 0) return null;
2964
+ const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
2965
+ if (lowerBoundsOnly.length === 0) return null;
2966
+ const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
2967
+ if (majorMinorMatch) {
2968
+ const major = Number.parseInt(majorMinorMatch[1], 10);
2969
+ const minor = Number.parseInt(majorMinorMatch[2], 10);
2970
+ if (!Number.isFinite(major) || major <= 0) return null;
2971
+ if (!Number.isFinite(minor) || minor < 0) return null;
2972
+ return {
2973
+ major,
2974
+ minor
2975
+ };
2976
+ }
2977
+ const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
2978
+ if (!majorOnlyMatch) return null;
2979
+ const major = Number.parseInt(majorOnlyMatch[1], 10);
2980
+ if (!Number.isFinite(major) || major <= 0) return null;
2981
+ return {
2982
+ major,
2983
+ minor: 0
2984
+ };
2985
+ };
2986
+ const isReactAtLeast = (detected, required) => {
2987
+ if (detected === null) return true;
2988
+ if (detected.major !== required.major) return detected.major > required.major;
2989
+ return detected.minor >= required.minor;
2990
+ };
2945
2991
  const parseTailwindMajorMinor = (tailwindVersion) => {
2946
2992
  if (typeof tailwindVersion !== "string") return null;
2947
2993
  const trimmed = tailwindVersion.trim();
@@ -2972,6 +3018,7 @@ const isTailwindAtLeast = (detected, required) => {
2972
3018
  return detected.minor >= required.minor;
2973
3019
  };
2974
3020
  const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
3021
+ const MILLISECONDS_PER_SECOND = 1e3;
2975
3022
  const SCORE_API_URL = "https://www.react.doctor/api/score";
2976
3023
  const SHARE_BASE_URL = "https://www.react.doctor/share";
2977
3024
  const FETCH_TIMEOUT_MS = 1e4;
@@ -4491,6 +4538,49 @@ const collectIgnorePatterns = (rootDirectory) => {
4491
4538
  return patterns;
4492
4539
  };
4493
4540
  const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
4541
+ const DEAD_CODE_WORKER_SCRIPT = `
4542
+ const { parentPort, workerData } = require("node:worker_threads");
4543
+
4544
+ const normalizeResult = (result) => ({
4545
+ unusedFiles: result.unusedFiles.map((unusedFile) => ({
4546
+ path: unusedFile.path,
4547
+ })),
4548
+ unusedExports: result.unusedExports.map((unusedExport) => ({
4549
+ path: unusedExport.path,
4550
+ name: unusedExport.name,
4551
+ line: unusedExport.line,
4552
+ column: unusedExport.column,
4553
+ isTypeOnly: unusedExport.isTypeOnly,
4554
+ })),
4555
+ unusedDependencies: result.unusedDependencies.map((unusedDependency) => ({
4556
+ name: unusedDependency.name,
4557
+ isDevDependency: unusedDependency.isDevDependency,
4558
+ })),
4559
+ circularDependencies: result.circularDependencies.map((cycle) => ({
4560
+ files: cycle.files,
4561
+ })),
4562
+ });
4563
+
4564
+ const serializeError = (error) =>
4565
+ error instanceof Error
4566
+ ? { name: error.name, message: error.message, stack: error.stack }
4567
+ : { message: String(error) };
4568
+
4569
+ (async () => {
4570
+ try {
4571
+ const { analyze, defineConfig } = await import(workerData.deslopJsModuleSpecifier);
4572
+ const config = {
4573
+ rootDir: workerData.rootDirectory,
4574
+ ...(workerData.tsConfigPath ? { tsConfigPath: workerData.tsConfigPath } : {}),
4575
+ ...(workerData.ignorePatterns.length > 0 ? { ignorePatterns: workerData.ignorePatterns } : {}),
4576
+ };
4577
+ const result = await analyze(defineConfig(config));
4578
+ parentPort.postMessage({ ok: true, result: normalizeResult(result) });
4579
+ } catch (error) {
4580
+ parentPort.postMessage({ ok: false, error: serializeError(error) });
4581
+ }
4582
+ })();
4583
+ `;
4494
4584
  const resolveTsConfigPath = (rootDirectory) => {
4495
4585
  for (const filename of TSCONFIG_FILENAMES$1) {
4496
4586
  const candidate = path.join(rootDirectory, filename);
@@ -4511,16 +4601,180 @@ const toRelativeFilePath = (rootDirectory, filePath) => {
4511
4601
  const relative = toRelativePath(filePath, rootDirectory);
4512
4602
  return relative.length > 0 ? relative : filePath.replace(/\\/g, "/");
4513
4603
  };
4604
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4605
+ const parseArray = (value, label) => {
4606
+ if (!Array.isArray(value)) throw new Error(`Dead-code worker returned invalid ${label}.`);
4607
+ return value;
4608
+ };
4609
+ const parseString = (value, label) => {
4610
+ if (typeof value !== "string") throw new Error(`Dead-code worker returned invalid ${label}.`);
4611
+ return value;
4612
+ };
4613
+ const parseNumber = (value, label) => {
4614
+ if (typeof value !== "number") throw new Error(`Dead-code worker returned invalid ${label}.`);
4615
+ return value;
4616
+ };
4617
+ const parseBoolean = (value, label) => {
4618
+ if (typeof value !== "boolean") throw new Error(`Dead-code worker returned invalid ${label}.`);
4619
+ return value;
4620
+ };
4621
+ const parseStringArray = (value, label) => {
4622
+ return parseArray(value, label).map((entry, index) => parseString(entry, `${label}[${index}]`));
4623
+ };
4624
+ const parseUnusedFiles = (value) => {
4625
+ const values = parseArray(value, "unusedFiles");
4626
+ const unusedFiles = [];
4627
+ for (const [index, entry] of values.entries()) {
4628
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedFiles[${index}].`);
4629
+ unusedFiles.push({ path: parseString(entry.path, `unusedFiles[${index}].path`) });
4630
+ }
4631
+ return unusedFiles;
4632
+ };
4633
+ const parseUnusedExports = (value) => {
4634
+ const values = parseArray(value, "unusedExports");
4635
+ const unusedExports = [];
4636
+ for (const [index, entry] of values.entries()) {
4637
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedExports[${index}].`);
4638
+ unusedExports.push({
4639
+ path: parseString(entry.path, `unusedExports[${index}].path`),
4640
+ name: parseString(entry.name, `unusedExports[${index}].name`),
4641
+ line: parseNumber(entry.line, `unusedExports[${index}].line`),
4642
+ column: parseNumber(entry.column, `unusedExports[${index}].column`),
4643
+ isTypeOnly: parseBoolean(entry.isTypeOnly, `unusedExports[${index}].isTypeOnly`)
4644
+ });
4645
+ }
4646
+ return unusedExports;
4647
+ };
4648
+ const parseUnusedDependencies = (value) => {
4649
+ const values = parseArray(value, "unusedDependencies");
4650
+ const unusedDependencies = [];
4651
+ for (const [index, entry] of values.entries()) {
4652
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedDependencies[${index}].`);
4653
+ unusedDependencies.push({
4654
+ name: parseString(entry.name, `unusedDependencies[${index}].name`),
4655
+ isDevDependency: parseBoolean(entry.isDevDependency, `unusedDependencies[${index}].isDevDependency`)
4656
+ });
4657
+ }
4658
+ return unusedDependencies;
4659
+ };
4660
+ const parseCircularDependencies = (value) => {
4661
+ const values = parseArray(value, "circularDependencies");
4662
+ const circularDependencies = [];
4663
+ for (const [index, entry] of values.entries()) {
4664
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid circularDependencies[${index}].`);
4665
+ circularDependencies.push({ files: parseStringArray(entry.files, `circularDependencies[${index}].files`) });
4666
+ }
4667
+ return circularDependencies;
4668
+ };
4669
+ const parseDeadCodeWorkerResult = (value) => {
4670
+ if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid result.");
4671
+ return {
4672
+ unusedFiles: parseUnusedFiles(value.unusedFiles),
4673
+ unusedExports: parseUnusedExports(value.unusedExports),
4674
+ unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
4675
+ circularDependencies: parseCircularDependencies(value.circularDependencies)
4676
+ };
4677
+ };
4678
+ const parseDeadCodeWorkerError = (value) => {
4679
+ if (!isRecord(value) || typeof value.message !== "string") return { message: "Dead-code worker failed." };
4680
+ return {
4681
+ ...typeof value.name === "string" ? { name: value.name } : {},
4682
+ message: value.message,
4683
+ ...typeof value.stack === "string" ? { stack: value.stack } : {}
4684
+ };
4685
+ };
4686
+ const parseDeadCodeWorkerMessage = (value) => {
4687
+ if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid message.");
4688
+ if (value.ok === true) return {
4689
+ ok: true,
4690
+ result: value.result
4691
+ };
4692
+ if (value.ok === false) return {
4693
+ ok: false,
4694
+ error: parseDeadCodeWorkerError(value.error)
4695
+ };
4696
+ throw new Error("Dead-code worker returned an invalid status.");
4697
+ };
4698
+ const buildDeadCodeWorkerError = (workerError) => {
4699
+ const error = new Error(workerError.message);
4700
+ if (workerError.name !== void 0) error.name = workerError.name;
4701
+ if (workerError.stack !== void 0) error.stack = workerError.stack;
4702
+ return error;
4703
+ };
4704
+ const createDeadCodeWorker = (input) => {
4705
+ const worker = new Worker(DEAD_CODE_WORKER_SCRIPT, {
4706
+ eval: true,
4707
+ workerData: input
4708
+ });
4709
+ let didSettle = false;
4710
+ return {
4711
+ result: new Promise((resolve, reject) => {
4712
+ const settle = (callback) => {
4713
+ if (didSettle) return;
4714
+ didSettle = true;
4715
+ worker.removeAllListeners();
4716
+ callback();
4717
+ };
4718
+ worker.once("message", (message) => {
4719
+ try {
4720
+ const parsedMessage = parseDeadCodeWorkerMessage(message);
4721
+ if (parsedMessage.ok) {
4722
+ settle(() => resolve(parsedMessage.result));
4723
+ return;
4724
+ }
4725
+ settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
4726
+ } catch (error) {
4727
+ settle(() => reject(error));
4728
+ }
4729
+ });
4730
+ worker.once("error", (error) => {
4731
+ settle(() => reject(error));
4732
+ });
4733
+ worker.once("exit", (exitCode) => {
4734
+ if (exitCode === 0) return;
4735
+ settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker exited with code ${exitCode}.`)));
4736
+ });
4737
+ }),
4738
+ terminate: () => {
4739
+ didSettle = true;
4740
+ worker.removeAllListeners();
4741
+ return worker.terminate();
4742
+ }
4743
+ };
4744
+ };
4745
+ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
4746
+ let didSettle = false;
4747
+ const timeoutHandle = setTimeout(() => {
4748
+ if (didSettle) return;
4749
+ didSettle = true;
4750
+ handle.terminate?.();
4751
+ reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
4752
+ }, timeoutMs);
4753
+ timeoutHandle.unref?.();
4754
+ handle.result.then((value) => {
4755
+ if (didSettle) return;
4756
+ didSettle = true;
4757
+ clearTimeout(timeoutHandle);
4758
+ handle.terminate?.();
4759
+ resolve(value);
4760
+ }, (error) => {
4761
+ if (didSettle) return;
4762
+ didSettle = true;
4763
+ clearTimeout(timeoutHandle);
4764
+ handle.terminate?.();
4765
+ reject(error);
4766
+ });
4767
+ });
4514
4768
  const checkDeadCode = async (options) => {
4515
4769
  const { rootDirectory, userConfig } = options;
4516
4770
  if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
4517
- const { analyze, defineConfig } = await import("deslop-js");
4518
4771
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
4519
- const result = await analyze(defineConfig({
4520
- rootDir: rootDirectory,
4772
+ const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
4773
+ rootDirectory,
4521
4774
  tsConfigPath: resolveTsConfigPath(rootDirectory),
4522
- ...ignorePatterns.length > 0 ? { ignorePatterns } : {}
4523
- }));
4775
+ ignorePatterns,
4776
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
4777
+ }), options.workerTimeoutMs ?? 12e4));
4524
4778
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
4525
4779
  const diagnostics = [];
4526
4780
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -5144,7 +5398,15 @@ const buildCapabilities = (project) => {
5144
5398
  capabilities.add(project.framework);
5145
5399
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
5146
5400
  const reactMajor = project.reactMajorVersion;
5147
- if (reactMajor !== null) for (let major = 17; major <= reactMajor; major++) capabilities.add(`react:${major}`);
5401
+ if (reactMajor !== null) {
5402
+ for (let major = 17; major <= reactMajor; major++) capabilities.add(`react:${major}`);
5403
+ if (reactMajor >= 19) {
5404
+ if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
5405
+ major: 19,
5406
+ minor: 2
5407
+ })) capabilities.add("react:19.2");
5408
+ }
5409
+ }
5148
5410
  if (project.tailwindVersion !== null) {
5149
5411
  capabilities.add("tailwind");
5150
5412
  if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
@@ -5155,6 +5417,10 @@ const buildCapabilities = (project) => {
5155
5417
  if (project.hasReactCompiler) capabilities.add("react-compiler");
5156
5418
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
5157
5419
  if (project.hasTypeScript) capabilities.add("typescript");
5420
+ if (project.hasPreact) {
5421
+ capabilities.add("preact");
5422
+ if (project.reactVersion === null) capabilities.add("pure-preact");
5423
+ }
5158
5424
  return capabilities;
5159
5425
  };
5160
5426
  const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
@@ -6484,17 +6750,6 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6484
6750
  didFail: false,
6485
6751
  reason: null
6486
6752
  });
6487
- const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
6488
- const deadCodeFiber = yield* Effect.forkChild(shouldRunDeadCode ? Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
6489
- rootDirectory: scanDirectory,
6490
- userConfig: resolvedConfig.config
6491
- }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6492
- yield* Ref.set(deadCodeFailure, {
6493
- didFail: true,
6494
- reason: error.message
6495
- });
6496
- return Stream.empty;
6497
- })))))) : Effect.succeed([]));
6498
6753
  const scanProgress = yield* progressService.start("Scanning...");
6499
6754
  const scanStartTime = Date.now();
6500
6755
  let lastReportedTotalFileCount = 0;
@@ -6524,11 +6779,18 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6524
6779
  const lintCollected = yield* Stream.runCollect(applyPerElementPipeline(rawLintStream));
6525
6780
  const lintFailureState = yield* Ref.get(lintFailure);
6526
6781
  yield* afterLint(lintFailureState.didFail);
6527
- if (lintFailureState.didFail) {
6528
- yield* Fiber.interrupt(deadCodeFiber);
6529
- yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
6530
- }
6531
- const deadCodeCollected = lintFailureState.didFail ? [] : yield* Fiber.join(deadCodeFiber);
6782
+ if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
6783
+ const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
6784
+ const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update("Analyzing dead code...").pipe(Effect.andThen(Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
6785
+ rootDirectory: scanDirectory,
6786
+ userConfig: resolvedConfig.config
6787
+ }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6788
+ yield* Ref.set(deadCodeFailure, {
6789
+ didFail: true,
6790
+ reason: error.message
6791
+ });
6792
+ return Stream.empty;
6793
+ }))))))));
6532
6794
  const deadCodeFailureState = yield* Ref.get(deadCodeFailure);
6533
6795
  const scanElapsedSeconds = ((Date.now() - scanStartTime) / 1e3).toFixed(1);
6534
6796
  const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
@@ -7023,7 +7285,7 @@ var cli_logger_exports = /* @__PURE__ */ __exportAll({ cliLogger: () => cliLogge
7023
7285
  /**
7024
7286
  * Thin synchronous façade over Effect's `Console` module. Used by
7025
7287
  * the imperative CLI helper files (`select-projects`, `run-explain`,
7026
- * `install-skill`, the legacy paths in `cli/commands/inspect.ts`)
7288
+ * `install-react-doctor`, the legacy paths in `cli/commands/inspect.ts`)
7027
7289
  * that aren't yet Effect-typed. Every call drains into a single
7028
7290
  * `Console.*` Effect via `Effect.runSync`, so the underlying logging
7029
7291
  * pipeline is identical to the canonical `yield* Console.log(...)`
@@ -7056,4 +7318,4 @@ const cliLogger = {
7056
7318
  //#endregion
7057
7319
  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 };
7058
7320
 
7059
- //# sourceMappingURL=cli-logger-BliQX9s8.js.map
7321
+ //# sourceMappingURL=cli-logger-BRBUS1pE.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-BliQX9s8.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-BRBUS1pE.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";
@@ -6320,11 +6320,11 @@ const colorizeByScore = (text, score) => {
6320
6320
  return highlighter.error(text);
6321
6321
  };
6322
6322
  //#endregion
6323
+ //#region src/cli/utils/constants.ts
6324
+ const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
6325
+ const INTERNAL_ERROR_JSON_FALLBACK = "{\"schemaVersion\":1,\"ok\":false,\"error\":{\"message\":\"Internal error\",\"name\":\"Error\",\"chain\":[]}}\n";
6326
+ //#endregion
6323
6327
  //#region src/cli/utils/render-score-header.ts
6324
- const SCORE_BAR_ANIMATION_FRAME_COUNT = 40;
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
6328
  const RAINBOW_HUE_SHIFT_PER_FRAME = 9;
6329
6329
  const RAINBOW_GRADIENT_WIDTH = 80;
6330
6330
  const RAINBOW_OKLCH_LIGHTNESS = .638;
@@ -6433,8 +6433,8 @@ const buildInitialScoreHeaderLine = ({ isPerfectScore, shouldAnimate, lineIndex,
6433
6433
  };
6434
6434
  const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectName) => Effect.gen(function* () {
6435
6435
  const isPerfectScore = score === 100;
6436
- for (let frame = 0; frame <= SCORE_BAR_ANIMATION_FRAME_COUNT; frame += 1) {
6437
- const progress = easeOutCubic(frame / SCORE_BAR_ANIMATION_FRAME_COUNT);
6436
+ for (let frame = 0; frame <= 40; frame += 1) {
6437
+ const progress = easeOutCubic(frame / 40);
6438
6438
  const animatedScore = Math.round(score * progress);
6439
6439
  if (isPerfectScore) {
6440
6440
  yield* writeScoreHeaderLine(`${frame === 0 ? "" : "\x1B[4A"}\r${buildRainbowScoreHeaderFrame({
@@ -6444,16 +6444,16 @@ const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectNam
6444
6444
  frame,
6445
6445
  projectName
6446
6446
  })}`);
6447
- if (frame < SCORE_BAR_ANIMATION_FRAME_COUNT) yield* sleep(SCORE_BAR_ANIMATION_FRAME_DELAY_MS);
6447
+ if (frame < 40) yield* sleep(50);
6448
6448
  continue;
6449
6449
  }
6450
6450
  const animatedScoreLine = buildScoreLine(animatedScore, score, label, projectName);
6451
6451
  const animatedBarLine = buildScoreBar(animatedScore, score);
6452
6452
  yield* writeScoreHeaderLine(`${frame === 0 ? "" : "\x1B[2A"}\r${buildScoreHeaderLine(scoreFaceLine, animatedScoreLine)}\n\r${buildScoreHeaderLine(barFaceLine, animatedBarLine)}\n`);
6453
- if (frame < SCORE_BAR_ANIMATION_FRAME_COUNT) yield* sleep(SCORE_BAR_ANIMATION_FRAME_DELAY_MS);
6453
+ if (frame < 40) yield* sleep(50);
6454
6454
  }
6455
6455
  if (!isPerfectScore) return;
6456
- for (let frame = 0; frame < PERFECT_SCORE_RAINBOW_FRAME_COUNT; frame += 1) {
6456
+ for (let frame = 0; frame < 16; frame += 1) {
6457
6457
  yield* writeScoreHeaderLine(`\x1b[4A\r${buildRainbowScoreHeaderFrame({
6458
6458
  score,
6459
6459
  displayScore: score,
@@ -6461,9 +6461,9 @@ const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectNam
6461
6461
  frame,
6462
6462
  projectName
6463
6463
  })}`);
6464
- yield* sleep(PERFECT_SCORE_RAINBOW_FRAME_DELAY_MS);
6464
+ yield* sleep(50);
6465
6465
  }
6466
- yield* writeScoreHeaderLine(`\x1b[4A\r${buildFinalPerfectScoreHeaderFrame(score, label, PERFECT_SCORE_RAINBOW_FRAME_COUNT, projectName)}\x1b[2A`);
6466
+ yield* writeScoreHeaderLine(`\x1b[4A\r${buildFinalPerfectScoreHeaderFrame(score, label, 16, projectName)}\x1b[2A`);
6467
6467
  });
6468
6468
  const printScoreHeader = (scoreResult, projectName) => Effect.gen(function* () {
6469
6469
  const isPerfectScore = scoreResult.score === 100;
@@ -6666,7 +6666,7 @@ const resolveOxlintNodeEffect = (isLintEnabled, isQuiet) => Effect.gen(function*
6666
6666
  const resolveOxlintNode = (isLintEnabled, isQuiet) => Effect.runPromise(resolveOxlintNodeEffect(isLintEnabled, isQuiet).pipe(Effect.provide(NodeResolver.layerNode)));
6667
6667
  //#endregion
6668
6668
  //#region src/cli/utils/version.ts
6669
- const VERSION = "0.2.9";
6669
+ const VERSION = "0.2.10";
6670
6670
  //#endregion
6671
6671
  //#region src/inspect.ts
6672
6672
  const silentConsole = makeNoopConsole();
@@ -6854,10 +6854,6 @@ const finalizeAndRender = (input) => Effect.gen(function* () {
6854
6854
  return buildResult();
6855
6855
  });
6856
6856
  //#endregion
6857
- //#region src/cli/utils/constants.ts
6858
- const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
6859
- const INTERNAL_ERROR_JSON_FALLBACK = "{\"schemaVersion\":1,\"ok\":false,\"error\":{\"message\":\"Internal error\",\"name\":\"Error\",\"chain\":[]}}\n";
6860
- //#endregion
6861
6857
  //#region src/cli/utils/get-staged-files.ts
6862
6858
  const stagedFilesLayer = StagedFiles.layerNode.pipe(Layer.provide(Git.layerNode));
6863
6859
  const getStagedSourceFiles = async (directory) => {
@@ -7416,7 +7412,7 @@ const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
7416
7412
  return true;
7417
7413
  };
7418
7414
  const shouldPromptInstallSetup = (options) => {
7419
- if (!options.hasScoredScan) return false;
7415
+ if (!(options.hasCompletedScan ?? options.hasScoredScan ?? false)) return false;
7420
7416
  if (options.isJsonMode) return false;
7421
7417
  if (options.isScoreOnly) return false;
7422
7418
  if (options.isStaged) return false;
@@ -7426,13 +7422,13 @@ const shouldPromptInstallSetup = (options) => {
7426
7422
  return !hasDoctorScript(options.projectRoot);
7427
7423
  };
7428
7424
  const resolveInstallSetupProjectRoot = (options) => {
7429
- if (options.completedScanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
7425
+ if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
7430
7426
  const packageDirectories = /* @__PURE__ */ new Set();
7431
- for (const scanDirectory of options.completedScanDirectories) {
7427
+ for (const scanDirectory of options.scanDirectories) {
7432
7428
  const packageDirectory = findNearestPackageDirectory(scanDirectory, options.scanRoot) ?? findNearestPackageDirectory(scanDirectory) ?? scanDirectory;
7433
7429
  packageDirectories.add(packageDirectory);
7434
7430
  }
7435
- if (packageDirectories.size !== 1) return null;
7431
+ if (packageDirectories.size !== 1) return findNearestPackageDirectory(options.scanRoot, options.scanRoot);
7436
7432
  return [...packageDirectories][0] ?? null;
7437
7433
  };
7438
7434
  const defaultWait = (milliseconds) => new Promise((resolve) => {
@@ -7467,7 +7463,7 @@ const warnSetupPromptFailure = async (options, error) => {
7467
7463
  return;
7468
7464
  }
7469
7465
  try {
7470
- const { cliLogger } = await import("./cli-logger-BliQX9s8.js").then((n) => n.n);
7466
+ const { cliLogger } = await import("./cli-logger-BRBUS1pE.js").then((n) => n.n);
7471
7467
  cliLogger.warn(message);
7472
7468
  } catch {}
7473
7469
  };
@@ -7483,7 +7479,7 @@ const promptInstallSetup = async (options) => {
7483
7479
  writeLine("You can always run `npx react-doctor@latest install` to set it up later.");
7484
7480
  return;
7485
7481
  }
7486
- const install = options.install ?? (await Promise.resolve().then(() => install_skill_exports)).runInstallSkill;
7482
+ const install = options.install ?? (await Promise.resolve().then(() => install_react_doctor_exports)).runInstallReactDoctor;
7487
7483
  const previousExitCode = process.exitCode;
7488
7484
  let setupExitCode;
7489
7485
  try {
@@ -7502,7 +7498,7 @@ const promptInstallSetup = async (options) => {
7502
7498
  }
7503
7499
  };
7504
7500
  const shouldShowAgentInstallHint = (options) => {
7505
- if (!options.hasScoredScan) return false;
7501
+ if (!(options.hasCompletedScan ?? options.hasScoredScan ?? false)) return false;
7506
7502
  if (options.isJsonMode) return false;
7507
7503
  if (options.isScoreOnly) return false;
7508
7504
  if (options.isStaged) return false;
@@ -7567,7 +7563,7 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isQui
7567
7563
  const { scanScope } = await prompts({
7568
7564
  type: "select",
7569
7565
  name: "scanScope",
7570
- message: "Select",
7566
+ message: "Choose what to scan",
7571
7567
  choices: [{
7572
7568
  title: "Full codebase",
7573
7569
  value: "full"
@@ -7709,7 +7705,7 @@ const selectProjects = async (rootDirectory, projectFlag, skipPrompts) => {
7709
7705
  }
7710
7706
  if (packages.length === 0) return [rootDirectory];
7711
7707
  if (packages.length === 1) {
7712
- cliLogger.log(`${highlighter.success("✔")} Select projects to scan ${highlighter.dim("›")} ${packages[0].name}`);
7708
+ cliLogger.log(`${highlighter.success("✔")} Select projects ${highlighter.dim("›")} ${packages[0].name}`);
7713
7709
  return [packages[0].directory];
7714
7710
  }
7715
7711
  if (projectFlag) return resolveProjectFlag(projectFlag, packages);
@@ -7733,13 +7729,13 @@ const resolveProjectFlag = (projectFlag, workspacePackages) => {
7733
7729
  return resolvedDirectories;
7734
7730
  };
7735
7731
  const printDiscoveredProjects = (packages) => {
7736
- cliLogger.log(`${highlighter.success("✔")} Select projects to scan ${highlighter.dim("›")} ${packages.map((workspacePackage) => workspacePackage.name).join(", ")}`);
7732
+ cliLogger.log(`${highlighter.success("✔")} Select projects ${highlighter.dim("›")} ${packages.map((workspacePackage) => workspacePackage.name).join(", ")}`);
7737
7733
  };
7738
7734
  const promptProjectSelection = async (workspacePackages, rootDirectory) => {
7739
7735
  const { selectedDirectories } = await prompts({
7740
7736
  type: "multiselect",
7741
7737
  name: "selectedDirectories",
7742
- message: "Select projects to scan",
7738
+ message: "Select projects",
7743
7739
  choices: workspacePackages.map((workspacePackage) => ({
7744
7740
  title: workspacePackage.name,
7745
7741
  description: path.relative(rootDirectory, workspacePackage.directory),
@@ -7996,13 +7992,13 @@ const inspectAction = async (directory, flags) => {
7996
7992
  });
7997
7993
  const setupProjectRoot = resolveInstallSetupProjectRoot({
7998
7994
  scanRoot: resolvedDirectory,
7999
- completedScanDirectories: completedScans.map((scan) => scan.directory)
7995
+ scanDirectories: projectDirectories
8000
7996
  });
8001
7997
  if (setupProjectRoot !== null) {
8002
- const hasScoredScan = completedScans.some((scan) => scan.result.score !== null);
7998
+ const hasCompletedScan = completedScans.length > 0;
8003
7999
  await promptInstallSetup({
8004
8000
  projectRoot: setupProjectRoot,
8005
- hasScoredScan,
8001
+ hasCompletedScan,
8006
8002
  issueCount: filterDiagnosticsForSurface(allDiagnostics, scanOptions.outputSurface ?? "cli", userConfig).length,
8007
8003
  isJsonMode,
8008
8004
  isScoreOnly,
@@ -8011,7 +8007,7 @@ const inspectAction = async (directory, flags) => {
8011
8007
  });
8012
8008
  if (shouldShowAgentInstallHint({
8013
8009
  projectRoot: setupProjectRoot,
8014
- hasScoredScan,
8010
+ hasCompletedScan,
8015
8011
  isJsonMode,
8016
8012
  isScoreOnly,
8017
8013
  isStaged: Boolean(flags.staged)
@@ -8643,8 +8639,12 @@ const installReactDoctorGitHook = (options) => {
8643
8639
  return installDirectGitHook(options);
8644
8640
  };
8645
8641
  //#endregion
8646
- //#region src/cli/utils/install-skill.ts
8647
- var install_skill_exports = /* @__PURE__ */ __exportAll({ runInstallSkill: () => runInstallSkill });
8642
+ //#region src/cli/utils/install-react-doctor.ts
8643
+ var install_react_doctor_exports = /* @__PURE__ */ __exportAll({ runInstallReactDoctor: () => runInstallReactDoctor });
8644
+ const SETUP_OPTION_GIT_HOOK = "git-hook";
8645
+ const SETUP_OPTION_AGENT_HOOKS = "agent-hooks";
8646
+ const SETUP_OPTION_WORKFLOW = "workflow";
8647
+ const SETUP_OPTION_SKIP = "skip";
8648
8648
  const CONFIG_ONLY_GIT_HOOK_KINDS = new Set([
8649
8649
  "ghooks",
8650
8650
  "git-hooks-js",
@@ -8825,7 +8825,30 @@ const getSkillSourceDirectory = () => {
8825
8825
  const distDirectory = path.dirname(fileURLToPath(import.meta.url));
8826
8826
  return path.join(distDirectory, "skills", SKILL_NAME);
8827
8827
  };
8828
- const runInstallSkill = async (options = {}) => {
8828
+ const canInstallNativeAgentHooks = (agents) => agents.some((agent) => agent === "claude-code" || agent === "cursor");
8829
+ const buildWorkflowContent = () => [
8830
+ "name: React Doctor",
8831
+ "",
8832
+ "on:",
8833
+ " pull_request:",
8834
+ " branches: [main]",
8835
+ "",
8836
+ "permissions:",
8837
+ " contents: read",
8838
+ " pull-requests: write",
8839
+ "",
8840
+ "jobs:",
8841
+ " react-doctor:",
8842
+ " runs-on: ubuntu-latest",
8843
+ " steps:",
8844
+ " - uses: actions/checkout@v4",
8845
+ " - uses: millionco/react-doctor@main",
8846
+ " with:",
8847
+ " github-token: ${{ secrets.GITHUB_TOKEN }}",
8848
+ " diff: main",
8849
+ ""
8850
+ ].join("\n");
8851
+ const runInstallReactDoctor = async (options = {}) => {
8829
8852
  const requestedProjectRoot = options.projectRoot ?? process.cwd();
8830
8853
  const projectRoot = findNearestPackageDirectory(requestedProjectRoot) ?? requestedProjectRoot;
8831
8854
  const sourceDir = options.sourceDir ?? getSkillSourceDirectory();
@@ -8846,7 +8869,8 @@ const runInstallSkill = async (options = {}) => {
8846
8869
  const gitHookTarget = options.gitHookPath === void 0 ? detectGitHookTarget(projectRoot) : options.gitHookPath === null ? null : buildManualGitHookTarget(options.gitHookPath, projectRoot);
8847
8870
  const gitHookPath = gitHookTarget?.hookPath;
8848
8871
  const promptOptions = options.onPromptCancel === void 0 ? {} : { onCancel: options.onPromptCancel };
8849
- const selectedAgents = skipPrompts ? detectedAgents : (await prompts({
8872
+ const prompt = options.prompt ?? prompts;
8873
+ const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
8850
8874
  type: "multiselect",
8851
8875
  name: "agents",
8852
8876
  message: `Install the ${highlighter.info(`/react-doctor`)} skill for:`,
@@ -8859,13 +8883,48 @@ const runInstallSkill = async (options = {}) => {
8859
8883
  min: 1
8860
8884
  }, promptOptions)).agents ?? [];
8861
8885
  if (selectedAgents.length === 0) return;
8862
- const shouldInstallGitHook = gitHookPath !== null && gitHookPath !== void 0 && (Boolean(options.yes) || !skipPrompts && Boolean((await prompts({
8863
- type: "confirm",
8864
- name: "installGitHook",
8865
- message: "Check for issues before each commit?",
8866
- initial: true
8867
- }, promptOptions)).installGitHook));
8868
- const shouldInstallAgentHooks = Boolean(options.agentHooks);
8886
+ const workflowsDirectory = path.join(projectRoot, ".github", "workflows");
8887
+ const workflowTargetPath = path.join(workflowsDirectory, "react-doctor.yml");
8888
+ const hasExistingWorkflows = existsSync(workflowsDirectory);
8889
+ const canInstallWorkflow = !existsSync(workflowTargetPath);
8890
+ const setupActionChoices = [
8891
+ ...gitHookPath === null || gitHookPath === void 0 ? [] : [{
8892
+ title: "Pre-commit hook",
8893
+ description: "Check staged changes before each commit",
8894
+ value: SETUP_OPTION_GIT_HOOK,
8895
+ selected: true
8896
+ }],
8897
+ ...canInstallNativeAgentHooks(selectedAgents) ? [{
8898
+ title: "Agent hooks",
8899
+ description: "Ask Claude Code or Cursor to scan after code edits",
8900
+ value: SETUP_OPTION_AGENT_HOOKS,
8901
+ selected: Boolean(options.agentHooks)
8902
+ }] : [],
8903
+ ...canInstallWorkflow ? [{
8904
+ title: "GitHub Actions workflow",
8905
+ description: "Scan pull requests in CI",
8906
+ value: SETUP_OPTION_WORKFLOW,
8907
+ selected: hasExistingWorkflows
8908
+ }] : []
8909
+ ];
8910
+ const setupChoices = setupActionChoices.length === 0 ? [] : [{
8911
+ title: "Skip optional setup",
8912
+ description: "Install only the agent skill and package setup",
8913
+ value: SETUP_OPTION_SKIP,
8914
+ selected: false
8915
+ }, ...setupActionChoices];
8916
+ const selectedSetupOptions = skipPrompts || setupChoices.length === 0 ? [] : (await prompt({
8917
+ type: "multiselect",
8918
+ name: "setupOptions",
8919
+ message: "Select additional React Doctor setup:",
8920
+ choices: setupChoices,
8921
+ instructions: false
8922
+ }, promptOptions)).setupOptions ?? [];
8923
+ const selectedSetupActions = selectedSetupOptions.filter((setupOption) => setupOption !== SETUP_OPTION_SKIP);
8924
+ const didSkipOptionalSetup = selectedSetupActions.length === 0 && selectedSetupOptions.includes(SETUP_OPTION_SKIP);
8925
+ const shouldInstallGitHook = gitHookPath !== null && gitHookPath !== void 0 && (Boolean(options.yes) || !didSkipOptionalSetup && selectedSetupActions.includes(SETUP_OPTION_GIT_HOOK));
8926
+ const shouldInstallAgentHooks = Boolean(options.agentHooks) || !didSkipOptionalSetup && selectedSetupActions.includes(SETUP_OPTION_AGENT_HOOKS);
8927
+ const shouldInstallWorkflow = !skipPrompts && !didSkipOptionalSetup && canInstallWorkflow && selectedSetupActions.includes(SETUP_OPTION_WORKFLOW);
8869
8928
  if (options.dryRun) {
8870
8929
  cliLogger.log(`Dry run — would install ${SKILL_NAME} skill for:`);
8871
8930
  for (const agent of selectedAgents) cliLogger.dim(` - ${getSkillAgentConfig(agent).displayName}`);
@@ -8874,6 +8933,7 @@ const runInstallSkill = async (options = {}) => {
8874
8933
  cliLogger.dim(" Dev dependency: react-doctor");
8875
8934
  if (shouldInstallGitHook) cliLogger.dim(` Git hook: ${gitHookPath}`);
8876
8935
  if (shouldInstallAgentHooks) cliLogger.dim(" Agent hooks: Claude Code / Cursor when selected");
8936
+ if (shouldInstallWorkflow) cliLogger.dim(` GitHub Actions workflow: ${path.relative(projectRoot, workflowTargetPath)}`);
8877
8937
  return;
8878
8938
  }
8879
8939
  const installSpinner = spinner(`Installing ${SKILL_NAME} skill...`).start();
@@ -8921,47 +8981,15 @@ const runInstallSkill = async (options = {}) => {
8921
8981
  throw error;
8922
8982
  }
8923
8983
  }
8924
- const workflowsDirectory = path.join(projectRoot, ".github", "workflows");
8925
- const workflowTargetPath = path.join(workflowsDirectory, "react-doctor.yml");
8926
- if (!existsSync(workflowTargetPath) && !skipPrompts) {
8927
- const hasExistingWorkflows = existsSync(workflowsDirectory);
8928
- const { shouldInstallWorkflow } = await prompts({
8929
- type: "confirm",
8930
- name: "shouldInstallWorkflow",
8931
- message: "Add a GitHub Actions workflow to scan PRs?",
8932
- initial: hasExistingWorkflows
8933
- }, promptOptions);
8934
- if (shouldInstallWorkflow) {
8935
- if (!hasExistingWorkflows) mkdirSync(workflowsDirectory, { recursive: true });
8936
- const workflowSpinner = spinner("Adding GitHub Actions workflow...").start();
8937
- try {
8938
- writeFileSync(workflowTargetPath, [
8939
- "name: React Doctor",
8940
- "",
8941
- "on:",
8942
- " pull_request:",
8943
- " branches: [main]",
8944
- "",
8945
- "permissions:",
8946
- " contents: read",
8947
- " pull-requests: write",
8948
- "",
8949
- "jobs:",
8950
- " react-doctor:",
8951
- " runs-on: ubuntu-latest",
8952
- " steps:",
8953
- " - uses: actions/checkout@v4",
8954
- " - uses: millionco/react-doctor@main",
8955
- " with:",
8956
- " github-token: ${{ secrets.GITHUB_TOKEN }}",
8957
- " diff: main",
8958
- ""
8959
- ].join("\n"));
8960
- workflowSpinner.succeed(`GitHub Actions workflow added at ${path.relative(projectRoot, workflowTargetPath)}.`);
8961
- } catch (error) {
8962
- workflowSpinner.fail("Failed to add GitHub Actions workflow.");
8963
- throw error;
8964
- }
8984
+ if (shouldInstallWorkflow) {
8985
+ if (!hasExistingWorkflows) mkdirSync(workflowsDirectory, { recursive: true });
8986
+ const workflowSpinner = spinner("Adding GitHub Actions workflow...").start();
8987
+ try {
8988
+ writeFileSync(workflowTargetPath, buildWorkflowContent());
8989
+ workflowSpinner.succeed(`GitHub Actions workflow added at ${path.relative(projectRoot, workflowTargetPath)}.`);
8990
+ } catch (error) {
8991
+ workflowSpinner.fail("Failed to add GitHub Actions workflow.");
8992
+ throw error;
8965
8993
  }
8966
8994
  }
8967
8995
  };
@@ -8971,7 +8999,7 @@ const installAction = async (options, command) => {
8971
8999
  Effect.runSync(printBrandedHeader);
8972
9000
  try {
8973
9001
  const parentOptions = command?.parent?.opts?.();
8974
- await runInstallSkill({
9002
+ await runInstallReactDoctor({
8975
9003
  yes: options.yes ?? parentOptions?.yes,
8976
9004
  dryRun: options.dryRun,
8977
9005
  agentHooks: options.agentHooks,
package/dist/index.d.ts CHANGED
@@ -232,7 +232,9 @@ interface ReactDoctorConfig {
232
232
  * `categories` field, but keyed by React Doctor's display
233
233
  * categories (`"Server"`, `"React Native"`, `"Architecture"`,
234
234
  * `"Bundle Size"`, `"State & Effects"`, `"Security"`,
235
- * `"Accessibility"`, `"Performance"`, `"Correctness"`, …).
235
+ * `"Accessibility"`, `"Performance"`, `"Correctness"`,
236
+ * `"Next.js"`, `"Preact"`, `"TanStack Query"`,
237
+ * `"TanStack Start"`, …).
236
238
  *
237
239
  * ```json
238
240
  * { "categories": { "React Native": "warn", "Server": "off" } }
@@ -296,7 +298,7 @@ interface Diagnostic {
296
298
  }
297
299
  //#endregion
298
300
  //#region src/types/project-info.d.ts
299
- type Framework = "nextjs" | "vite" | "cra" | "remix" | "gatsby" | "expo" | "react-native" | "tanstack-start" | "unknown";
301
+ type Framework = "nextjs" | "vite" | "cra" | "remix" | "gatsby" | "expo" | "react-native" | "tanstack-start" | "preact" | "unknown";
300
302
  interface ProjectInfo {
301
303
  rootDirectory: string;
302
304
  projectName: string;
@@ -307,6 +309,16 @@ interface ProjectInfo {
307
309
  hasTypeScript: boolean;
308
310
  hasReactCompiler: boolean;
309
311
  hasTanStackQuery: boolean;
312
+ /**
313
+ * `true` when `preact` is declared anywhere in the project's
314
+ * dependency manifest. Drives the `preact` capability in
315
+ * `buildCapabilities`, which gates every `preact-*` rule. Modeled
316
+ * on `hasTanStackQuery` rather than the `framework` field because
317
+ * the dominant Preact setup today is Preact-on-Vite — those
318
+ * projects classify as `framework: "vite"` for build-tool reasons
319
+ * but still need Preact-specific rules to fire.
320
+ */
321
+ hasPreact: boolean;
310
322
  /**
311
323
  * `true` when the project (or any of its workspace packages) declares
312
324
  * React Native or Expo as a dependency. Enables the `react-native`
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ import * as Option from "effect/Option";
21
21
  import * as Ref from "effect/Ref";
22
22
  import * as Stream from "effect/Stream";
23
23
  import * as Cache from "effect/Cache";
24
+ import { Worker } from "node:worker_threads";
24
25
  import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner";
25
26
  import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem";
26
27
  import * as NodePath from "@effect/platform-node-shared/NodePath";
@@ -2298,11 +2299,13 @@ const FRAMEWORK_DISPLAY_NAMES = {
2298
2299
  gatsby: "Gatsby",
2299
2300
  expo: "Expo",
2300
2301
  "react-native": "React Native",
2302
+ preact: "Preact",
2301
2303
  unknown: "React"
2302
2304
  };
2303
2305
  const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
2304
2306
  const detectFramework = (dependencies) => {
2305
2307
  for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
2308
+ if (dependencies.preact && !dependencies.react) return "preact";
2306
2309
  return "unknown";
2307
2310
  };
2308
2311
  const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
@@ -2766,6 +2769,13 @@ const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => {
2766
2769
  }
2767
2770
  return false;
2768
2771
  };
2772
+ const hasPreact = (packageJson) => {
2773
+ return "preact" in {
2774
+ ...packageJson.peerDependencies,
2775
+ ...packageJson.dependencies,
2776
+ ...packageJson.devDependencies
2777
+ };
2778
+ };
2769
2779
  const TANSTACK_QUERY_PACKAGES = new Set([
2770
2780
  "@tanstack/react-query",
2771
2781
  "@tanstack/query-core",
@@ -2803,7 +2813,8 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
2803
2813
  const REACT_DEPENDENCY_NAMES = new Set([
2804
2814
  "react",
2805
2815
  "react-native",
2806
- "next"
2816
+ "next",
2817
+ "preact"
2807
2818
  ]);
2808
2819
  const hasReactDependency = (packageJson) => {
2809
2820
  const allDependencies = {
@@ -2965,12 +2976,47 @@ const discoverProject = (directory) => {
2965
2976
  hasTypeScript,
2966
2977
  hasReactCompiler: detectReactCompiler(directory, packageJson),
2967
2978
  hasTanStackQuery: hasTanStackQuery(packageJson),
2979
+ hasPreact: hasPreact(packageJson),
2968
2980
  hasReactNativeWorkspace,
2969
2981
  sourceFileCount
2970
2982
  };
2971
2983
  cachedProjectInfos.set(directory, projectInfo);
2972
2984
  return projectInfo;
2973
2985
  };
2986
+ const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
2987
+ const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
2988
+ const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
2989
+ const parseReactMajorMinor = (reactVersion) => {
2990
+ if (typeof reactVersion !== "string") return null;
2991
+ const trimmed = reactVersion.trim();
2992
+ if (trimmed.length === 0) return null;
2993
+ const lowerBoundsOnly = trimmed.replace(UPPER_BOUND_COMPARATOR_PATTERN, " ").trim();
2994
+ if (lowerBoundsOnly.length === 0) return null;
2995
+ const majorMinorMatch = lowerBoundsOnly.match(MAJOR_MINOR_PATTERN);
2996
+ if (majorMinorMatch) {
2997
+ const major = Number.parseInt(majorMinorMatch[1], 10);
2998
+ const minor = Number.parseInt(majorMinorMatch[2], 10);
2999
+ if (!Number.isFinite(major) || major <= 0) return null;
3000
+ if (!Number.isFinite(minor) || minor < 0) return null;
3001
+ return {
3002
+ major,
3003
+ minor
3004
+ };
3005
+ }
3006
+ const majorOnlyMatch = lowerBoundsOnly.match(MAJOR_ONLY_PATTERN);
3007
+ if (!majorOnlyMatch) return null;
3008
+ const major = Number.parseInt(majorOnlyMatch[1], 10);
3009
+ if (!Number.isFinite(major) || major <= 0) return null;
3010
+ return {
3011
+ major,
3012
+ minor: 0
3013
+ };
3014
+ };
3015
+ const isReactAtLeast = (detected, required) => {
3016
+ if (detected === null) return true;
3017
+ if (detected.major !== required.major) return detected.major > required.major;
3018
+ return detected.minor >= required.minor;
3019
+ };
2974
3020
  const parseTailwindMajorMinor = (tailwindVersion) => {
2975
3021
  if (typeof tailwindVersion !== "string") return null;
2976
3022
  const trimmed = tailwindVersion.trim();
@@ -3001,6 +3047,7 @@ const isTailwindAtLeast = (detected, required) => {
3001
3047
  return detected.minor >= required.minor;
3002
3048
  };
3003
3049
  const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
3050
+ const MILLISECONDS_PER_SECOND = 1e3;
3004
3051
  const SCORE_API_URL = "https://www.react.doctor/api/score";
3005
3052
  const FETCH_TIMEOUT_MS = 1e4;
3006
3053
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
@@ -4522,6 +4569,49 @@ const collectIgnorePatterns = (rootDirectory) => {
4522
4569
  return patterns;
4523
4570
  };
4524
4571
  const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
4572
+ const DEAD_CODE_WORKER_SCRIPT = `
4573
+ const { parentPort, workerData } = require("node:worker_threads");
4574
+
4575
+ const normalizeResult = (result) => ({
4576
+ unusedFiles: result.unusedFiles.map((unusedFile) => ({
4577
+ path: unusedFile.path,
4578
+ })),
4579
+ unusedExports: result.unusedExports.map((unusedExport) => ({
4580
+ path: unusedExport.path,
4581
+ name: unusedExport.name,
4582
+ line: unusedExport.line,
4583
+ column: unusedExport.column,
4584
+ isTypeOnly: unusedExport.isTypeOnly,
4585
+ })),
4586
+ unusedDependencies: result.unusedDependencies.map((unusedDependency) => ({
4587
+ name: unusedDependency.name,
4588
+ isDevDependency: unusedDependency.isDevDependency,
4589
+ })),
4590
+ circularDependencies: result.circularDependencies.map((cycle) => ({
4591
+ files: cycle.files,
4592
+ })),
4593
+ });
4594
+
4595
+ const serializeError = (error) =>
4596
+ error instanceof Error
4597
+ ? { name: error.name, message: error.message, stack: error.stack }
4598
+ : { message: String(error) };
4599
+
4600
+ (async () => {
4601
+ try {
4602
+ const { analyze, defineConfig } = await import(workerData.deslopJsModuleSpecifier);
4603
+ const config = {
4604
+ rootDir: workerData.rootDirectory,
4605
+ ...(workerData.tsConfigPath ? { tsConfigPath: workerData.tsConfigPath } : {}),
4606
+ ...(workerData.ignorePatterns.length > 0 ? { ignorePatterns: workerData.ignorePatterns } : {}),
4607
+ };
4608
+ const result = await analyze(defineConfig(config));
4609
+ parentPort.postMessage({ ok: true, result: normalizeResult(result) });
4610
+ } catch (error) {
4611
+ parentPort.postMessage({ ok: false, error: serializeError(error) });
4612
+ }
4613
+ })();
4614
+ `;
4525
4615
  const resolveTsConfigPath = (rootDirectory) => {
4526
4616
  for (const filename of TSCONFIG_FILENAMES$1) {
4527
4617
  const candidate = path.join(rootDirectory, filename);
@@ -4542,16 +4632,180 @@ const toRelativeFilePath = (rootDirectory, filePath) => {
4542
4632
  const relative = toRelativePath(filePath, rootDirectory);
4543
4633
  return relative.length > 0 ? relative : filePath.replace(/\\/g, "/");
4544
4634
  };
4635
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4636
+ const parseArray = (value, label) => {
4637
+ if (!Array.isArray(value)) throw new Error(`Dead-code worker returned invalid ${label}.`);
4638
+ return value;
4639
+ };
4640
+ const parseString = (value, label) => {
4641
+ if (typeof value !== "string") throw new Error(`Dead-code worker returned invalid ${label}.`);
4642
+ return value;
4643
+ };
4644
+ const parseNumber = (value, label) => {
4645
+ if (typeof value !== "number") throw new Error(`Dead-code worker returned invalid ${label}.`);
4646
+ return value;
4647
+ };
4648
+ const parseBoolean = (value, label) => {
4649
+ if (typeof value !== "boolean") throw new Error(`Dead-code worker returned invalid ${label}.`);
4650
+ return value;
4651
+ };
4652
+ const parseStringArray = (value, label) => {
4653
+ return parseArray(value, label).map((entry, index) => parseString(entry, `${label}[${index}]`));
4654
+ };
4655
+ const parseUnusedFiles = (value) => {
4656
+ const values = parseArray(value, "unusedFiles");
4657
+ const unusedFiles = [];
4658
+ for (const [index, entry] of values.entries()) {
4659
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedFiles[${index}].`);
4660
+ unusedFiles.push({ path: parseString(entry.path, `unusedFiles[${index}].path`) });
4661
+ }
4662
+ return unusedFiles;
4663
+ };
4664
+ const parseUnusedExports = (value) => {
4665
+ const values = parseArray(value, "unusedExports");
4666
+ const unusedExports = [];
4667
+ for (const [index, entry] of values.entries()) {
4668
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedExports[${index}].`);
4669
+ unusedExports.push({
4670
+ path: parseString(entry.path, `unusedExports[${index}].path`),
4671
+ name: parseString(entry.name, `unusedExports[${index}].name`),
4672
+ line: parseNumber(entry.line, `unusedExports[${index}].line`),
4673
+ column: parseNumber(entry.column, `unusedExports[${index}].column`),
4674
+ isTypeOnly: parseBoolean(entry.isTypeOnly, `unusedExports[${index}].isTypeOnly`)
4675
+ });
4676
+ }
4677
+ return unusedExports;
4678
+ };
4679
+ const parseUnusedDependencies = (value) => {
4680
+ const values = parseArray(value, "unusedDependencies");
4681
+ const unusedDependencies = [];
4682
+ for (const [index, entry] of values.entries()) {
4683
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid unusedDependencies[${index}].`);
4684
+ unusedDependencies.push({
4685
+ name: parseString(entry.name, `unusedDependencies[${index}].name`),
4686
+ isDevDependency: parseBoolean(entry.isDevDependency, `unusedDependencies[${index}].isDevDependency`)
4687
+ });
4688
+ }
4689
+ return unusedDependencies;
4690
+ };
4691
+ const parseCircularDependencies = (value) => {
4692
+ const values = parseArray(value, "circularDependencies");
4693
+ const circularDependencies = [];
4694
+ for (const [index, entry] of values.entries()) {
4695
+ if (!isRecord(entry)) throw new Error(`Dead-code worker returned invalid circularDependencies[${index}].`);
4696
+ circularDependencies.push({ files: parseStringArray(entry.files, `circularDependencies[${index}].files`) });
4697
+ }
4698
+ return circularDependencies;
4699
+ };
4700
+ const parseDeadCodeWorkerResult = (value) => {
4701
+ if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid result.");
4702
+ return {
4703
+ unusedFiles: parseUnusedFiles(value.unusedFiles),
4704
+ unusedExports: parseUnusedExports(value.unusedExports),
4705
+ unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
4706
+ circularDependencies: parseCircularDependencies(value.circularDependencies)
4707
+ };
4708
+ };
4709
+ const parseDeadCodeWorkerError = (value) => {
4710
+ if (!isRecord(value) || typeof value.message !== "string") return { message: "Dead-code worker failed." };
4711
+ return {
4712
+ ...typeof value.name === "string" ? { name: value.name } : {},
4713
+ message: value.message,
4714
+ ...typeof value.stack === "string" ? { stack: value.stack } : {}
4715
+ };
4716
+ };
4717
+ const parseDeadCodeWorkerMessage = (value) => {
4718
+ if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid message.");
4719
+ if (value.ok === true) return {
4720
+ ok: true,
4721
+ result: value.result
4722
+ };
4723
+ if (value.ok === false) return {
4724
+ ok: false,
4725
+ error: parseDeadCodeWorkerError(value.error)
4726
+ };
4727
+ throw new Error("Dead-code worker returned an invalid status.");
4728
+ };
4729
+ const buildDeadCodeWorkerError = (workerError) => {
4730
+ const error = new Error(workerError.message);
4731
+ if (workerError.name !== void 0) error.name = workerError.name;
4732
+ if (workerError.stack !== void 0) error.stack = workerError.stack;
4733
+ return error;
4734
+ };
4735
+ const createDeadCodeWorker = (input) => {
4736
+ const worker = new Worker(DEAD_CODE_WORKER_SCRIPT, {
4737
+ eval: true,
4738
+ workerData: input
4739
+ });
4740
+ let didSettle = false;
4741
+ return {
4742
+ result: new Promise((resolve, reject) => {
4743
+ const settle = (callback) => {
4744
+ if (didSettle) return;
4745
+ didSettle = true;
4746
+ worker.removeAllListeners();
4747
+ callback();
4748
+ };
4749
+ worker.once("message", (message) => {
4750
+ try {
4751
+ const parsedMessage = parseDeadCodeWorkerMessage(message);
4752
+ if (parsedMessage.ok) {
4753
+ settle(() => resolve(parsedMessage.result));
4754
+ return;
4755
+ }
4756
+ settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
4757
+ } catch (error) {
4758
+ settle(() => reject(error));
4759
+ }
4760
+ });
4761
+ worker.once("error", (error) => {
4762
+ settle(() => reject(error));
4763
+ });
4764
+ worker.once("exit", (exitCode) => {
4765
+ if (exitCode === 0) return;
4766
+ settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker exited with code ${exitCode}.`)));
4767
+ });
4768
+ }),
4769
+ terminate: () => {
4770
+ didSettle = true;
4771
+ worker.removeAllListeners();
4772
+ return worker.terminate();
4773
+ }
4774
+ };
4775
+ };
4776
+ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
4777
+ let didSettle = false;
4778
+ const timeoutHandle = setTimeout(() => {
4779
+ if (didSettle) return;
4780
+ didSettle = true;
4781
+ handle.terminate?.();
4782
+ reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
4783
+ }, timeoutMs);
4784
+ timeoutHandle.unref?.();
4785
+ handle.result.then((value) => {
4786
+ if (didSettle) return;
4787
+ didSettle = true;
4788
+ clearTimeout(timeoutHandle);
4789
+ handle.terminate?.();
4790
+ resolve(value);
4791
+ }, (error) => {
4792
+ if (didSettle) return;
4793
+ didSettle = true;
4794
+ clearTimeout(timeoutHandle);
4795
+ handle.terminate?.();
4796
+ reject(error);
4797
+ });
4798
+ });
4545
4799
  const checkDeadCode = async (options) => {
4546
4800
  const { rootDirectory, userConfig } = options;
4547
4801
  if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
4548
- const { analyze, defineConfig } = await import("deslop-js");
4549
4802
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
4550
- const result = await analyze(defineConfig({
4551
- rootDir: rootDirectory,
4803
+ const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
4804
+ rootDirectory,
4552
4805
  tsConfigPath: resolveTsConfigPath(rootDirectory),
4553
- ...ignorePatterns.length > 0 ? { ignorePatterns } : {}
4554
- }));
4806
+ ignorePatterns,
4807
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
4808
+ }), options.workerTimeoutMs ?? 12e4));
4555
4809
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
4556
4810
  const diagnostics = [];
4557
4811
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -5175,7 +5429,15 @@ const buildCapabilities = (project) => {
5175
5429
  capabilities.add(project.framework);
5176
5430
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
5177
5431
  const reactMajor = project.reactMajorVersion;
5178
- if (reactMajor !== null) for (let major = 17; major <= reactMajor; major++) capabilities.add(`react:${major}`);
5432
+ if (reactMajor !== null) {
5433
+ for (let major = 17; major <= reactMajor; major++) capabilities.add(`react:${major}`);
5434
+ if (reactMajor >= 19) {
5435
+ if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
5436
+ major: 19,
5437
+ minor: 2
5438
+ })) capabilities.add("react:19.2");
5439
+ }
5440
+ }
5179
5441
  if (project.tailwindVersion !== null) {
5180
5442
  capabilities.add("tailwind");
5181
5443
  if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
@@ -5186,6 +5448,10 @@ const buildCapabilities = (project) => {
5186
5448
  if (project.hasReactCompiler) capabilities.add("react-compiler");
5187
5449
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
5188
5450
  if (project.hasTypeScript) capabilities.add("typescript");
5451
+ if (project.hasPreact) {
5452
+ capabilities.add("preact");
5453
+ if (project.reactVersion === null) capabilities.add("pure-preact");
5454
+ }
5189
5455
  return capabilities;
5190
5456
  };
5191
5457
  const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
@@ -6515,17 +6781,6 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6515
6781
  didFail: false,
6516
6782
  reason: null
6517
6783
  });
6518
- const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
6519
- const deadCodeFiber = yield* Effect.forkChild(shouldRunDeadCode ? Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
6520
- rootDirectory: scanDirectory,
6521
- userConfig: resolvedConfig.config
6522
- }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6523
- yield* Ref.set(deadCodeFailure, {
6524
- didFail: true,
6525
- reason: error.message
6526
- });
6527
- return Stream.empty;
6528
- })))))) : Effect.succeed([]));
6529
6784
  const scanProgress = yield* progressService.start("Scanning...");
6530
6785
  const scanStartTime = Date.now();
6531
6786
  let lastReportedTotalFileCount = 0;
@@ -6555,11 +6810,18 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6555
6810
  const lintCollected = yield* Stream.runCollect(applyPerElementPipeline(rawLintStream));
6556
6811
  const lintFailureState = yield* Ref.get(lintFailure);
6557
6812
  yield* afterLint(lintFailureState.didFail);
6558
- if (lintFailureState.didFail) {
6559
- yield* Fiber.interrupt(deadCodeFiber);
6560
- yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
6561
- }
6562
- const deadCodeCollected = lintFailureState.didFail ? [] : yield* Fiber.join(deadCodeFiber);
6813
+ if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
6814
+ const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
6815
+ const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update("Analyzing dead code...").pipe(Effect.andThen(Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
6816
+ rootDirectory: scanDirectory,
6817
+ userConfig: resolvedConfig.config
6818
+ }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6819
+ yield* Ref.set(deadCodeFailure, {
6820
+ didFail: true,
6821
+ reason: error.message
6822
+ });
6823
+ return Stream.empty;
6824
+ }))))))));
6563
6825
  const deadCodeFailureState = yield* Ref.get(deadCodeFailure);
6564
6826
  const scanElapsedSeconds = ((Date.now() - scanStartTime) / 1e3).toFixed(1);
6565
6827
  const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
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,14 +58,14 @@
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.9"
61
+ "oxlint-plugin-react-doctor": "0.2.10"
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.9",
68
- "@react-doctor/core": "0.2.9"
67
+ "@react-doctor/api": "0.2.10",
68
+ "@react-doctor/core": "0.2.10"
69
69
  },
70
70
  "engines": {
71
71
  "node": "^20.19.0 || >=22.12.0"