react-doctor 0.2.8 → 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.
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";
@@ -2067,6 +2068,14 @@ var PackageJsonNotFoundError = class extends Error {
2067
2068
  this.directory = directory;
2068
2069
  }
2069
2070
  };
2071
+ var NotADirectoryError = class extends Error {
2072
+ name = "NotADirectoryError";
2073
+ resolvedPath;
2074
+ constructor(resolvedPath, options) {
2075
+ super(`Resolved scan target "${resolvedPath}" is not a directory. Ensure the path exists and points to a project directory, not a file.`, options);
2076
+ this.resolvedPath = resolvedPath;
2077
+ }
2078
+ };
2070
2079
  var AmbiguousProjectError = class extends Error {
2071
2080
  name = "AmbiguousProjectError";
2072
2081
  directory;
@@ -2077,7 +2086,7 @@ var AmbiguousProjectError = class extends Error {
2077
2086
  this.candidates = candidates;
2078
2087
  }
2079
2088
  };
2080
- const isProjectDiscoveryError = (value) => value instanceof ProjectNotFoundError || value instanceof NoReactDependencyError || value instanceof PackageJsonNotFoundError || value instanceof AmbiguousProjectError;
2089
+ const isProjectDiscoveryError = (value) => value instanceof ProjectNotFoundError || value instanceof NoReactDependencyError || value instanceof PackageJsonNotFoundError || value instanceof NotADirectoryError || value instanceof AmbiguousProjectError;
2081
2090
  const isFile = (filePath) => {
2082
2091
  try {
2083
2092
  return fs.statSync(filePath).isFile();
@@ -2290,11 +2299,13 @@ const FRAMEWORK_DISPLAY_NAMES = {
2290
2299
  gatsby: "Gatsby",
2291
2300
  expo: "Expo",
2292
2301
  "react-native": "React Native",
2302
+ preact: "Preact",
2293
2303
  unknown: "React"
2294
2304
  };
2295
2305
  const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
2296
2306
  const detectFramework = (dependencies) => {
2297
2307
  for (const [packageName, frameworkName] of Object.entries(FRAMEWORK_PACKAGES)) if (dependencies[packageName]) return frameworkName;
2308
+ if (dependencies.preact && !dependencies.react) return "preact";
2298
2309
  return "unknown";
2299
2310
  };
2300
2311
  const UPPER_BOUND_COMPARATOR = /<\s*=?\s*\d+(?:\.\d+){0,2}(?:-[^\s,|]+)?/g;
@@ -2758,6 +2769,13 @@ const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => {
2758
2769
  }
2759
2770
  return false;
2760
2771
  };
2772
+ const hasPreact = (packageJson) => {
2773
+ return "preact" in {
2774
+ ...packageJson.peerDependencies,
2775
+ ...packageJson.dependencies,
2776
+ ...packageJson.devDependencies
2777
+ };
2778
+ };
2761
2779
  const TANSTACK_QUERY_PACKAGES = new Set([
2762
2780
  "@tanstack/react-query",
2763
2781
  "@tanstack/query-core",
@@ -2795,7 +2813,8 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
2795
2813
  const REACT_DEPENDENCY_NAMES = new Set([
2796
2814
  "react",
2797
2815
  "react-native",
2798
- "next"
2816
+ "next",
2817
+ "preact"
2799
2818
  ]);
2800
2819
  const hasReactDependency = (packageJson) => {
2801
2820
  const allDependencies = {
@@ -2957,12 +2976,47 @@ const discoverProject = (directory) => {
2957
2976
  hasTypeScript,
2958
2977
  hasReactCompiler: detectReactCompiler(directory, packageJson),
2959
2978
  hasTanStackQuery: hasTanStackQuery(packageJson),
2979
+ hasPreact: hasPreact(packageJson),
2960
2980
  hasReactNativeWorkspace,
2961
2981
  sourceFileCount
2962
2982
  };
2963
2983
  cachedProjectInfos.set(directory, projectInfo);
2964
2984
  return projectInfo;
2965
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
+ };
2966
3020
  const parseTailwindMajorMinor = (tailwindVersion) => {
2967
3021
  if (typeof tailwindVersion !== "string") return null;
2968
3022
  const trimmed = tailwindVersion.trim();
@@ -2993,6 +3047,7 @@ const isTailwindAtLeast = (detected, required) => {
2993
3047
  return detected.minor >= required.minor;
2994
3048
  };
2995
3049
  const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
3050
+ const MILLISECONDS_PER_SECOND = 1e3;
2996
3051
  const SCORE_API_URL = "https://www.react.doctor/api/score";
2997
3052
  const FETCH_TIMEOUT_MS = 1e4;
2998
3053
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
@@ -4160,8 +4215,10 @@ const resolveScanTarget = (requestedDirectory) => {
4160
4215
  const configSourceDirectory = loadedConfig?.sourceDirectory ?? null;
4161
4216
  const redirectedDirectory = resolveConfigRootDir(userConfig, configSourceDirectory);
4162
4217
  const directoryAfterRedirect = redirectedDirectory ?? absoluteRequested;
4218
+ const resolvedDirectory = resolveDiagnoseTarget(directoryAfterRedirect) ?? directoryAfterRedirect;
4219
+ if (!isDirectory(resolvedDirectory)) throw existsSync(resolvedDirectory) ? new NotADirectoryError(resolvedDirectory) : new ProjectNotFoundError(resolvedDirectory);
4163
4220
  return {
4164
- resolvedDirectory: resolveDiagnoseTarget(directoryAfterRedirect) ?? directoryAfterRedirect,
4221
+ resolvedDirectory,
4165
4222
  requestedDirectory: absoluteRequested,
4166
4223
  userConfig,
4167
4224
  configSourceDirectory,
@@ -4512,6 +4569,49 @@ const collectIgnorePatterns = (rootDirectory) => {
4512
4569
  return patterns;
4513
4570
  };
4514
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
+ `;
4515
4615
  const resolveTsConfigPath = (rootDirectory) => {
4516
4616
  for (const filename of TSCONFIG_FILENAMES$1) {
4517
4617
  const candidate = path.join(rootDirectory, filename);
@@ -4532,16 +4632,180 @@ const toRelativeFilePath = (rootDirectory, filePath) => {
4532
4632
  const relative = toRelativePath(filePath, rootDirectory);
4533
4633
  return relative.length > 0 ? relative : filePath.replace(/\\/g, "/");
4534
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
+ });
4535
4799
  const checkDeadCode = async (options) => {
4536
4800
  const { rootDirectory, userConfig } = options;
4537
4801
  if (!fs.existsSync(path.join(rootDirectory, "package.json"))) return [];
4538
- const { analyze, defineConfig } = await import("deslop-js");
4539
4802
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory, userConfig);
4540
- const result = await analyze(defineConfig({
4541
- rootDir: rootDirectory,
4803
+ const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
4804
+ rootDirectory,
4542
4805
  tsConfigPath: resolveTsConfigPath(rootDirectory),
4543
- ...ignorePatterns.length > 0 ? { ignorePatterns } : {}
4544
- }));
4806
+ ignorePatterns,
4807
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
4808
+ }), options.workerTimeoutMs ?? 12e4));
4545
4809
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
4546
4810
  const diagnostics = [];
4547
4811
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -4645,7 +4909,7 @@ var Files = class Files extends Context.Service()("react-doctor/Files") {
4645
4909
  * pattern in react-doctor-evals' test layers.
4646
4910
  */
4647
4911
  static layerInMemory = (tree) => {
4648
- const resolveAbsolute = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
4912
+ const resolveAbsolute = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : `${rootDirectory}/${filePath}`;
4649
4913
  return Layer.succeed(Files, Files.of({
4650
4914
  readLines: (input) => Effect.sync(() => {
4651
4915
  const absolute = resolveAbsolute(input.filePath, input.rootDirectory);
@@ -4653,17 +4917,17 @@ var Files = class Files extends Context.Service()("react-doctor/Files") {
4653
4917
  return content === void 0 ? null : content.split("\n");
4654
4918
  }),
4655
4919
  listSourceFiles: (rootDirectory) => Effect.sync(() => {
4656
- const prefix = rootDirectory.endsWith(Path.sep) ? rootDirectory : `${rootDirectory}${Path.sep}`;
4920
+ const prefix = rootDirectory.endsWith("/") ? rootDirectory : `${rootDirectory}/`;
4657
4921
  const files = [];
4658
4922
  for (const absolute of tree.keys()) {
4659
4923
  if (!absolute.startsWith(prefix)) continue;
4660
- files.push(absolute.slice(prefix.length).split(Path.sep).join("/"));
4924
+ files.push(absolute.slice(prefix.length));
4661
4925
  }
4662
4926
  return files;
4663
4927
  }),
4664
4928
  isFile: (filePath) => Effect.sync(() => tree.has(filePath)),
4665
4929
  isDirectory: (filePath) => Effect.sync(() => {
4666
- const prefix = filePath.endsWith(Path.sep) ? filePath : `${filePath}${Path.sep}`;
4930
+ const prefix = filePath.endsWith("/") ? filePath : `${filePath}/`;
4667
4931
  for (const absolute of tree.keys()) if (absolute.startsWith(prefix)) return true;
4668
4932
  return false;
4669
4933
  })
@@ -5165,7 +5429,15 @@ const buildCapabilities = (project) => {
5165
5429
  capabilities.add(project.framework);
5166
5430
  if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
5167
5431
  const reactMajor = project.reactMajorVersion;
5168
- 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
+ }
5169
5441
  if (project.tailwindVersion !== null) {
5170
5442
  capabilities.add("tailwind");
5171
5443
  if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
@@ -5176,6 +5448,10 @@ const buildCapabilities = (project) => {
5176
5448
  if (project.hasReactCompiler) capabilities.add("react-compiler");
5177
5449
  if (project.hasTanStackQuery) capabilities.add("tanstack-query");
5178
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
+ }
5179
5455
  return capabilities;
5180
5456
  };
5181
5457
  const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy) => {
@@ -5800,7 +6076,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
5800
6076
  const primaryLabel = diagnostic.labels[0];
5801
6077
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
5802
6078
  return {
5803
- filePath: diagnostic.filename,
6079
+ filePath: diagnostic.filename.replaceAll("\\", "/"),
5804
6080
  plugin,
5805
6081
  rule,
5806
6082
  severity: diagnostic.severity,
@@ -6505,17 +6781,6 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6505
6781
  didFail: false,
6506
6782
  reason: null
6507
6783
  });
6508
- const shouldRunDeadCode = input.runDeadCode && !isDiffMode;
6509
- const deadCodeFiber = yield* Effect.forkChild(shouldRunDeadCode ? Stream.runCollect(applyPerElementPipeline(deadCodeService.run({
6510
- rootDirectory: scanDirectory,
6511
- userConfig: resolvedConfig.config
6512
- }).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
6513
- yield* Ref.set(deadCodeFailure, {
6514
- didFail: true,
6515
- reason: error.message
6516
- });
6517
- return Stream.empty;
6518
- })))))) : Effect.succeed([]));
6519
6784
  const scanProgress = yield* progressService.start("Scanning...");
6520
6785
  const scanStartTime = Date.now();
6521
6786
  let lastReportedTotalFileCount = 0;
@@ -6545,11 +6810,18 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6545
6810
  const lintCollected = yield* Stream.runCollect(applyPerElementPipeline(rawLintStream));
6546
6811
  const lintFailureState = yield* Ref.get(lintFailure);
6547
6812
  yield* afterLint(lintFailureState.didFail);
6548
- if (lintFailureState.didFail) {
6549
- yield* Fiber.interrupt(deadCodeFiber);
6550
- yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
6551
- }
6552
- 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
+ }))))))));
6553
6825
  const deadCodeFailureState = yield* Ref.get(deadCodeFailure);
6554
6826
  const scanElapsedSeconds = ((Date.now() - scanStartTime) / 1e3).toFixed(1);
6555
6827
  const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
@@ -6602,22 +6874,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6602
6874
  "inspect.isCi": input.isCi,
6603
6875
  "inspect.scoreSurface": input.scoreSurface ?? "score"
6604
6876
  } }));
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);
6877
+ Layer.mergeAll(Project.layerNode, Config.layerNode, DeadCode.layerNode, Files.layerNode, Git.layerNode, Linter.layerOxlint, LintPartialFailures.layerLive, Progress.layerNoop, Reporter.layerNoop, Score.layerHttp);
6621
6878
  const parseNodeVersion = (versionString) => {
6622
6879
  const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
6623
6880
  return {
@@ -7021,22 +7278,23 @@ import_picocolors.default.red, import_picocolors.default.yellow, import_picocolo
7021
7278
  const clearAutoSuppressionCaches = () => {};
7022
7279
  //#endregion
7023
7280
  //#region ../api/dist/index.js
7024
- const diagnose = async (directory, options = {}) => {
7025
- const startTime = globalThis.performance.now();
7026
- const scanTarget = resolveScanTarget(directory);
7281
+ 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);
7282
+ const buildInspectProgram = (scanTarget, options, configOverride) => {
7283
+ const effectiveConfig = configOverride ?? scanTarget.userConfig;
7027
7284
  const includePaths = options.includePaths ?? [];
7028
- const program = runInspect({
7285
+ return runInspect({
7029
7286
  directory: scanTarget.resolvedDirectory,
7030
7287
  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,
7288
+ customRulesOnly: effectiveConfig?.customRulesOnly ?? false,
7289
+ respectInlineDisables: options.respectInlineDisables ?? effectiveConfig?.respectInlineDisables ?? true,
7290
+ adoptExistingLintConfig: effectiveConfig?.adoptExistingLintConfig ?? true,
7291
+ ignoredTags: new Set(effectiveConfig?.ignore?.tags ?? []),
7292
+ runDeadCode: options.deadCode ?? effectiveConfig?.deadCode ?? true,
7036
7293
  isCi: false,
7037
7294
  resolveLocalGithubViewerPermission: true
7038
7295
  });
7039
- const output = await Effect.runPromise(restoreLegacyThrow(program.pipe(Effect.provide(layerInspectLive), Effect.provide(layerOtlp))));
7296
+ };
7297
+ const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
7040
7298
  if (output.didLintFail && output.lintFailureReason !== null) console.error("Lint failed:", output.lintFailureReason);
7041
7299
  const skippedChecks = [];
7042
7300
  const skippedCheckReasons = {};
@@ -7050,9 +7308,14 @@ const diagnose = async (directory, options = {}) => {
7050
7308
  skippedChecks,
7051
7309
  ...Object.keys(skippedCheckReasons).length > 0 ? { skippedCheckReasons } : {},
7052
7310
  project: output.project,
7053
- elapsedMilliseconds: globalThis.performance.now() - startTime
7311
+ elapsedMilliseconds
7054
7312
  };
7055
7313
  };
7314
+ const diagnose = async (directory, options = {}) => {
7315
+ const startTime = globalThis.performance.now();
7316
+ const program = buildInspectProgram(resolveScanTarget(directory), options);
7317
+ return outputToDiagnoseResult(await Effect.runPromise(restoreLegacyThrow(program.pipe(Effect.provide(DEFAULT_LAYER), Effect.provide(layerOtlp)))), globalThis.performance.now() - startTime);
7318
+ };
7056
7319
  //#endregion
7057
7320
  //#region src/index.ts
7058
7321
  const clearCaches = () => {
@@ -7081,6 +7344,6 @@ const toJsonReport = (result, options) => buildJsonReport({
7081
7344
  totalElapsedMilliseconds: result.elapsedMilliseconds
7082
7345
  });
7083
7346
  //#endregion
7084
- export { AmbiguousProjectError, NoReactDependencyError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
7347
+ export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
7085
7348
 
7086
7349
  //# 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.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,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.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.8",
68
- "@react-doctor/core": "0.2.8"
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"
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
  }