react-doctor 0.5.6-dev.f45cb29 → 0.5.7-dev.0b4f4f4

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/lsp.js CHANGED
@@ -8,13 +8,13 @@ import path from "node:path";
8
8
  import * as NodeChildProcess from "node:child_process";
9
9
  import { spawn, spawnSync } from "node:child_process";
10
10
  import * as ts from "typescript";
11
- import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
11
+ import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
12
12
  import { parseJSON5 } from "confbox";
13
13
  import * as NodeUrl from "node:url";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { createJiti } from "jiti";
16
16
  import * as Crypto from "node:crypto";
17
- import crypto from "node:crypto";
17
+ import crypto, { createHash } from "node:crypto";
18
18
  import { gzipSync } from "node:zlib";
19
19
  import { CodeActionKind, CodeActionTriggerKind, DidChangeWatchedFilesNotification, DocumentDiagnosticReportKind, FileChangeType, TextDocumentSyncKind, TextDocuments, createConnection } from "vscode-languageserver/node.js";
20
20
  import { TextDocument } from "vscode-languageserver-textdocument";
@@ -7224,7 +7224,7 @@ const provideContext$1 = /* @__PURE__ */ dual(2, (self, context) => {
7224
7224
  return updateContext$1(self, merge$3(context));
7225
7225
  });
7226
7226
  /** @internal */
7227
- const provideService$1 = function() {
7227
+ const provideService$3 = function() {
7228
7228
  if (arguments.length === 1) return dual(2, (self, impl) => provideServiceImpl(self, arguments[0], impl));
7229
7229
  return dual(3, (self, service, impl) => provideServiceImpl(self, service, impl)).apply(this, arguments);
7230
7230
  };
@@ -7445,7 +7445,7 @@ const constScopeEmpty = { _tag: "Empty" };
7445
7445
  /** @internal */
7446
7446
  const scope = scopeTag;
7447
7447
  /** @internal */
7448
- const provideScope = /* @__PURE__ */ provideService$1(scopeTag);
7448
+ const provideScope = /* @__PURE__ */ provideService$3(scopeTag);
7449
7449
  /** @internal */
7450
7450
  const scoped$1 = (self) => withFiber$1((fiber) => {
7451
7451
  const prev = fiber.context;
@@ -7888,7 +7888,7 @@ const makeLatchUnsafe = (open) => new Latch(open ?? false);
7888
7888
  /** @internal */
7889
7889
  const makeLatch = (open) => sync$2(() => makeLatchUnsafe(open));
7890
7890
  /** @internal */
7891
- const withTracerEnabled$1 = /* @__PURE__ */ provideService$1(TracerEnabled);
7891
+ const withTracerEnabled$1 = /* @__PURE__ */ provideService$3(TracerEnabled);
7892
7892
  const bigint0 = /* @__PURE__ */ BigInt(0);
7893
7893
  const NoopSpanProto = {
7894
7894
  _tag: "Span",
@@ -7969,7 +7969,7 @@ const useSpan$1 = (name, ...args) => {
7969
7969
  }));
7970
7970
  });
7971
7971
  };
7972
- const provideParentSpan = /* @__PURE__ */ provideService$1(ParentSpan);
7972
+ const provideParentSpan = /* @__PURE__ */ provideService$3(ParentSpan);
7973
7973
  /** @internal */
7974
7974
  const withParentSpan$1 = function() {
7975
7975
  const dataFirst = isEffect$1(arguments[0]);
@@ -9536,7 +9536,7 @@ var CurrentMemoMap = class extends Service()("effect/Layer/CurrentMemoMap") {
9536
9536
  * @category memo map
9537
9537
  * @since 2.0.0
9538
9538
  */
9539
- const buildWithMemoMap = /* @__PURE__ */ dual(3, (self, memoMap, scope) => provideService$1(map$4(self.build(memoMap, scope), add(CurrentMemoMap, memoMap)), CurrentMemoMap, memoMap));
9539
+ const buildWithMemoMap = /* @__PURE__ */ dual(3, (self, memoMap, scope) => provideService$3(map$4(self.build(memoMap, scope), add(CurrentMemoMap, memoMap)), CurrentMemoMap, memoMap));
9540
9540
  /**
9541
9541
  * Builds a layer into an `Effect` value. Any resources associated with this
9542
9542
  * layer will be released when the specified scope is closed unless their scope
@@ -10889,7 +10889,7 @@ const provide$1 = /* @__PURE__ */ dual((args) => isEffect$1(args[0]), (self, sou
10889
10889
  /** @internal */
10890
10890
  const repeatOrElse = /* @__PURE__ */ dual(3, (self, schedule, orElse) => flatMap$4(toStepWithMetadata(schedule), (step) => {
10891
10891
  let meta = CurrentMetadata.defaultValue();
10892
- return catch_$2(forever$2(tap$2(flatMap$4(suspend$3(() => provideService$1(self, CurrentMetadata, meta)), step), (meta_) => sync$2(() => {
10892
+ return catch_$2(forever$2(tap$2(flatMap$4(suspend$3(() => provideService$3(self, CurrentMetadata, meta)), step), (meta_) => sync$2(() => {
10893
10893
  meta = meta_;
10894
10894
  })), { disableYield: true }), (error) => isDone$2(error) ? succeed$5(error.value) : orElse(error, meta.attempt === 0 ? none() : some(meta)));
10895
10895
  }));
@@ -10897,7 +10897,7 @@ const repeatOrElse = /* @__PURE__ */ dual(3, (self, schedule, orElse) => flatMap
10897
10897
  const retryOrElse = /* @__PURE__ */ dual(3, (self, policy, orElse) => flatMap$4(toStepWithMetadata(policy), (step) => {
10898
10898
  let meta = CurrentMetadata.defaultValue();
10899
10899
  let lastError;
10900
- const loop = catch_$2(suspend$3(() => provideService$1(self, CurrentMetadata, meta)), (error) => {
10900
+ const loop = catch_$2(suspend$3(() => provideService$3(self, CurrentMetadata, meta)), (error) => {
10901
10901
  lastError = error;
10902
10902
  return flatMap$4(step(error), (meta_) => {
10903
10903
  meta = meta_;
@@ -12977,7 +12977,7 @@ const updateContext = updateContext$1;
12977
12977
  * @category Context
12978
12978
  * @since 2.0.0
12979
12979
  */
12980
- const provideService = provideService$1;
12980
+ const provideService$2 = provideService$3;
12981
12981
  /**
12982
12982
  * Scopes all resources used in this workflow to the lifetime of the workflow,
12983
12983
  * ensuring that their finalizers are run as soon as this workflow completes
@@ -18019,6 +18019,20 @@ function decodeUnknownOption$1(schema, options) {
18019
18019
  return asOption(decodeUnknownEffect(schema, options));
18020
18020
  }
18021
18021
  /**
18022
+ * Creates a synchronous decoder for `unknown` input.
18023
+ *
18024
+ * **Details**
18025
+ *
18026
+ * The returned function returns the decoded `Type` on success and throws an
18027
+ * `Error` with the `SchemaIssue.Issue` in its `cause` on decoding failure.
18028
+ *
18029
+ * @category decoding
18030
+ * @since 3.10.0
18031
+ */
18032
+ function decodeUnknownSync$1(schema, options) {
18033
+ return asSync(decodeUnknownEffect(schema, options));
18034
+ }
18035
+ /**
18022
18036
  * Creates an effectful encoder for `unknown` input.
18023
18037
  *
18024
18038
  * **Details**
@@ -18320,6 +18334,40 @@ function isSchemaError(u) {
18320
18334
  */
18321
18335
  const decodeUnknownOption = decodeUnknownOption$1;
18322
18336
  /**
18337
+ * Decodes an `unknown` input against a schema synchronously, returning the
18338
+ * decoded value or throwing an `Error` whose cause contains the schema issue.
18339
+ * Use this when you want to validate data at a boundary and treat a schema
18340
+ * mismatch as an exception. For typed input use `decodeSync`.
18341
+ *
18342
+ * **Details**
18343
+ *
18344
+ * Only service-free schemas can be decoded synchronously. For non-throwing
18345
+ * alternatives see `decodeUnknownOption`, `decodeUnknownExit`, or
18346
+ * `decodeUnknownEffect`. Options may be provided either when creating the
18347
+ * decoder or when applying it; application options override creation options.
18348
+ *
18349
+ * **Example** (Decoding with a transformation schema)
18350
+ *
18351
+ * ```ts
18352
+ * import { Schema } from "effect"
18353
+ *
18354
+ * const NumberFromString = Schema.NumberFromString
18355
+ *
18356
+ * console.log(Schema.decodeUnknownSync(NumberFromString)("42"))
18357
+ * // Output: 42
18358
+ *
18359
+ * Schema.decodeUnknownSync(NumberFromString)("not a number")
18360
+ * // throws SchemaError: NumberFromString
18361
+ * // └─ Encoded side transformation failure
18362
+ * // └─ NumberFromString
18363
+ * // └─ Expected a numeric string, actual "not a number"
18364
+ * ```
18365
+ *
18366
+ * @category decoding
18367
+ * @since 4.0.0
18368
+ */
18369
+ const decodeUnknownSync = decodeUnknownSync$1;
18370
+ /**
18323
18371
  * Encodes an `unknown` input against a schema synchronously, throwing a
18324
18372
  * {@link SchemaError} on failure. Use this when you want to serialize data at a
18325
18373
  * boundary and treat a schema mismatch as an unrecoverable error. For
@@ -19286,7 +19334,8 @@ var Diagnostic = class extends Class("Diagnostic")({
19286
19334
  category: String$1,
19287
19335
  fileContext: optional(Literals(["test", "story"])),
19288
19336
  suppressionHint: optional(String$1),
19289
- relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation))
19337
+ relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation)),
19338
+ fixGroupId: optional(String$1)
19290
19339
  }) {};
19291
19340
  /**
19292
19341
  * Deterministic identity string for a diagnostic. Same diagnostic
@@ -19335,6 +19384,7 @@ var JsonReportProjectEntry = class extends Class("JsonReportProjectEntry")({
19335
19384
  score: Unknown,
19336
19385
  skippedChecks: ArraySchema(String$1),
19337
19386
  skippedCheckReasons: optional(Record$1(String$1, String$1)),
19387
+ scannedFileCount: optional(Number$1),
19338
19388
  elapsedMilliseconds: Number$1
19339
19389
  }) {};
19340
19390
  /**
@@ -25204,6 +25254,14 @@ const runWith = (self, f, onHalt) => suspend$2(() => {
25204
25254
  return catchDone(flatMap$2(toTransform(self)(done$1(), scope), f), onHalt ? onHalt : succeed$2).pipe(onExit$1((exit) => close(scope, exit)));
25205
25255
  });
25206
25256
  /**
25257
+ * Provides a concrete service for a context key, removing that service
25258
+ * requirement from the returned channel.
25259
+ *
25260
+ * @category services
25261
+ * @since 2.0.0
25262
+ */
25263
+ const provideService$1 = /* @__PURE__ */ dual(3, (self, key, service) => fromTransform$1((upstream, scope) => map$3(provideService$2(toTransform(self)(upstream, scope), key, service), provideService$2(key, service))));
25264
+ /**
25207
25265
  * Runs a channel and applies an effect to each output element.
25208
25266
  *
25209
25267
  * **Example** (Running effects for each output)
@@ -26592,6 +26650,44 @@ const splitLines = (self) => self.channel.pipe(pipeTo(splitLines$1()), fromChann
26592
26650
  */
26593
26651
  const ensuring = /* @__PURE__ */ dual(2, (self, finalizer) => fromChannel(ensuring$1(self.channel, finalizer)));
26594
26652
  /**
26653
+ * Provides the stream with a single required service, eliminating that
26654
+ * requirement from its environment.
26655
+ *
26656
+ * **Example** (Providing a stream service)
26657
+ *
26658
+ * ```ts
26659
+ * import { Console, Context, Effect, Stream } from "effect"
26660
+ *
26661
+ * class Greeter extends Context.Service<Greeter, {
26662
+ * greet: (name: string) => string
26663
+ * }>()("Greeter") {}
26664
+ *
26665
+ * const stream = Stream.fromEffect(
26666
+ * Effect.service(Greeter).pipe(
26667
+ * Effect.map((greeter) => greeter.greet("Ada"))
26668
+ * )
26669
+ * )
26670
+ *
26671
+ * const program = Effect.gen(function*() {
26672
+ * const collected = yield* Stream.runCollect(
26673
+ * stream.pipe(
26674
+ * Stream.provideService(Greeter, {
26675
+ * greet: (name) => `Hello, ${name}`
26676
+ * })
26677
+ * )
26678
+ * )
26679
+ * yield* Console.log(collected)
26680
+ * })
26681
+ *
26682
+ * Effect.runPromise(program)
26683
+ * //=> ["Hello, Ada"]
26684
+ * ```
26685
+ *
26686
+ * @category services
26687
+ * @since 2.0.0
26688
+ */
26689
+ const provideService = /* @__PURE__ */ dual(3, (self, key, service) => fromChannel(provideService$1(self.channel, key, service)));
26690
+ /**
26595
26691
  * Runs a stream with a sink and returns the sink result.
26596
26692
  *
26597
26693
  * **Example** (Running a stream with a sink)
@@ -30021,7 +30117,7 @@ const make$8 = /* @__PURE__ */ fnUntraced(function* (options) {
30021
30117
  const runFork = runForkWith(services);
30022
30118
  const exportInterval = max(fromInputUnsafe(options.exportInterval), zero);
30023
30119
  let disabledUntil = void 0;
30024
- const client = filterStatusOk(get$4(services, HttpClient)).pipe(transformResponse(provideService(TracerPropagationEnabled, false)), retryTransient({
30120
+ const client = filterStatusOk(get$4(services, HttpClient)).pipe(transformResponse(provideService$2(TracerPropagationEnabled, false)), retryTransient({
30025
30121
  schedule: policy,
30026
30122
  times: 3
30027
30123
  }));
@@ -32751,16 +32847,26 @@ const isMinifiedSource = (absolutePath) => {
32751
32847
  if (fileDescriptor !== void 0) NFS.closeSync(fileDescriptor);
32752
32848
  }
32753
32849
  };
32754
- const isLargeMinifiedFile = (absolutePath) => {
32755
- let sizeBytes;
32850
+ const cachedIsLargeMinifiedByPath = /* @__PURE__ */ new Map();
32851
+ const clearMinifiedFileCache = () => {
32852
+ cachedIsLargeMinifiedByPath.clear();
32853
+ };
32854
+ const statSourceFileSize = (absolutePath) => {
32756
32855
  try {
32757
- sizeBytes = NFS.statSync(absolutePath).size;
32856
+ return NFS.statSync(absolutePath).size;
32758
32857
  } catch {
32759
- return false;
32858
+ return null;
32760
32859
  }
32761
- if (sizeBytes < 2e4) return false;
32762
- return isMinifiedSource(absolutePath);
32763
32860
  };
32861
+ const isLargeMinifiedFile = (absolutePath, knownSizeBytes) => {
32862
+ const cached = cachedIsLargeMinifiedByPath.get(absolutePath);
32863
+ if (cached !== void 0) return cached;
32864
+ const sizeBytes = knownSizeBytes === void 0 ? statSourceFileSize(absolutePath) : knownSizeBytes;
32865
+ const result = sizeBytes !== null && sizeBytes >= 2e4 && isMinifiedSource(absolutePath);
32866
+ cachedIsLargeMinifiedByPath.set(absolutePath, result);
32867
+ return result;
32868
+ };
32869
+ const isErrnoException = (error) => error instanceof Error && "code" in error;
32764
32870
  const IGNORABLE_READDIR_ERROR_CODES = new Set([
32765
32871
  "EACCES",
32766
32872
  "EPERM",
@@ -32770,11 +32876,7 @@ const IGNORABLE_READDIR_ERROR_CODES = new Set([
32770
32876
  "ELOOP",
32771
32877
  "ENAMETOOLONG"
32772
32878
  ]);
32773
- const isIgnorableReaddirError = (error) => {
32774
- if (typeof error !== "object" || error === null) return false;
32775
- const errorCode = error.code;
32776
- return typeof errorCode === "string" && IGNORABLE_READDIR_ERROR_CODES.has(errorCode);
32777
- };
32879
+ const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
32778
32880
  const readDirectoryEntries = (directoryPath) => {
32779
32881
  try {
32780
32882
  return NFS.readdirSync(directoryPath, { withFileTypes: true });
@@ -32824,7 +32926,7 @@ const readPackageJsonUncached = (packageJsonPath) => {
32824
32926
  return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
32825
32927
  } catch (error) {
32826
32928
  if (error instanceof SyntaxError) return {};
32827
- if (error instanceof Error && "code" in error) {
32929
+ if (isErrnoException(error)) {
32828
32930
  const { code } = error;
32829
32931
  if (code === "EISDIR" || code === "EACCES" || code === "EPERM" || code === "ENOENT") return {};
32830
32932
  }
@@ -33549,17 +33651,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
33549
33651
  return false;
33550
33652
  };
33551
33653
  const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
33552
- const getExpoDependencySpec = (packageJson) => {
33553
- const spec = packageJson.dependencies?.expo ?? packageJson.devDependencies?.expo ?? packageJson.peerDependencies?.expo ?? packageJson.optionalDependencies?.expo;
33654
+ const getDependencySpec = (packageJson, packageName) => {
33655
+ const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
33554
33656
  return typeof spec === "string" ? spec : null;
33555
33657
  };
33556
- const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getExpoDependencySpec);
33658
+ const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
33557
33659
  const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
33558
- const getShopifyFlashListDependencySpec = (packageJson) => {
33559
- const spec = packageJson.dependencies?.["@shopify/flash-list"] ?? packageJson.devDependencies?.["@shopify/flash-list"] ?? packageJson.peerDependencies?.["@shopify/flash-list"] ?? packageJson.optionalDependencies?.["@shopify/flash-list"];
33560
- return typeof spec === "string" ? spec : null;
33561
- };
33562
- const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getShopifyFlashListDependencySpec);
33660
+ const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
33563
33661
  const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
33564
33662
  if (version === null || !isCatalogReference(version)) return version;
33565
33663
  const catalogName = extractCatalogName(version);
@@ -33571,11 +33669,7 @@ const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson,
33571
33669
  if (!isFile(monorepoPackageJsonPath)) return version;
33572
33670
  return resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
33573
33671
  };
33574
- const getNextjsDependencySpec = (packageJson) => {
33575
- const spec = packageJson.dependencies?.next ?? packageJson.devDependencies?.next ?? packageJson.peerDependencies?.next ?? packageJson.optionalDependencies?.next;
33576
- return typeof spec === "string" ? spec : null;
33577
- };
33578
- const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getNextjsDependencySpec);
33672
+ const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
33579
33673
  const getPreactVersion = (packageJson) => {
33580
33674
  return {
33581
33675
  ...packageJson.peerDependencies,
@@ -33636,6 +33730,7 @@ const MILLISECONDS_PER_SECOND = 1e3;
33636
33730
  const SCORE_API_URL = "https://www.react.doctor/api/score";
33637
33731
  const FETCH_TIMEOUT_MS = 1e4;
33638
33732
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
33733
+ const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
33639
33734
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
33640
33735
  const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
33641
33736
  const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
@@ -33657,6 +33752,11 @@ const ES_TARGET_YEAR_BY_NAME = {
33657
33752
  esnext: 9999
33658
33753
  };
33659
33754
  /**
33755
+ * tsconfig filenames probed when resolving a project's TypeScript
33756
+ * compiler options — the root config first, then a monorepo base config.
33757
+ */
33758
+ const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
33759
+ /**
33660
33760
  * Project-config files that `StagedFiles.materialize` copies into
33661
33761
  * the temp directory alongside staged sources so oxlint resolves
33662
33762
  * `tsconfig` / `package.json` / lint configs the same way it would
@@ -33703,7 +33803,16 @@ const CONFIG_FINGERPRINT_FILENAMES = [
33703
33803
  ];
33704
33804
  const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
33705
33805
  const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
33806
+ const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
33807
+ const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
33808
+ const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
33809
+ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
33810
+ const LINT_PHASE_TIMEOUT_MS = 3e5;
33811
+ const SCAN_TOTAL_DEADLINE_MS = 9e5;
33706
33812
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
33813
+ const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
33814
+ const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
33815
+ const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
33707
33816
  const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
33708
33817
  const REACT_SERVER_DOM_PACKAGES = [
33709
33818
  "react-server-dom-webpack",
@@ -33726,14 +33835,25 @@ const APP_ONLY_RULE_KEYS = new Set([
33726
33835
  ]);
33727
33836
  const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
33728
33837
  const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
33838
+ const ROOT_CAUSE_GROUPABLE_RULE_KEYS = new Set([
33839
+ "react-doctor/no-derived-state",
33840
+ "react-doctor/no-derived-state-effect",
33841
+ "react-doctor/no-derived-useState",
33842
+ "react-doctor/no-adjust-state-on-prop-change",
33843
+ "react-doctor/no-reset-all-state-on-prop-change"
33844
+ ]);
33729
33845
  const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
33730
33846
  const CONFIG_CACHE_TTL_MS = 300 * 1e3;
33731
33847
  const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
33732
33848
  const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
33733
33849
  const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
33850
+ const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
33851
+ const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
33734
33852
  const SUPPLY_CHAIN_PLUGIN = "socket";
33735
33853
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
33736
33854
  const SUPPLY_CHAIN_CATEGORY = "Security";
33855
+ const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
33856
+ const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
33737
33857
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
33738
33858
  const TSCONFIG_FILENAME = "tsconfig.json";
33739
33859
  const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
@@ -34178,6 +34298,7 @@ const isTailwindAtLeast = (detected, required) => {
34178
34298
  if (detected.major !== required.major) return detected.major > required.major;
34179
34299
  return detected.minor >= required.minor;
34180
34300
  };
34301
+ const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
34181
34302
  var InvalidGlobPatternError = class extends Error {
34182
34303
  pattern;
34183
34304
  reason;
@@ -34206,7 +34327,7 @@ const compileGlobPattern = (rawPattern) => {
34206
34327
  try {
34207
34328
  return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
34208
34329
  } catch (caughtError) {
34209
- throw new InvalidGlobPatternError(rawPattern, caughtError instanceof Error ? caughtError.message : String(caughtError));
34330
+ throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
34210
34331
  }
34211
34332
  };
34212
34333
  const compileGlobPatternsLenient = (patterns, onInvalid) => {
@@ -34302,115 +34423,6 @@ const buildRuleSeverityControls = (config) => {
34302
34423
  ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
34303
34424
  };
34304
34425
  };
34305
- const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
34306
- const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
34307
- const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
34308
- let stringDelimiter = null;
34309
- for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
34310
- const character = line[charIndex];
34311
- if (stringDelimiter !== null) {
34312
- if (character === "\\") {
34313
- charIndex++;
34314
- continue;
34315
- }
34316
- if (character === stringDelimiter) stringDelimiter = null;
34317
- continue;
34318
- }
34319
- if (character === "\"" || character === "'" || character === "`") {
34320
- stringDelimiter = character;
34321
- continue;
34322
- }
34323
- if (character === "/" && line[charIndex + 1] === "/") return true;
34324
- }
34325
- return false;
34326
- };
34327
- const findOpenerTagOnLine = (line) => {
34328
- for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
34329
- if (match.index === void 0) continue;
34330
- if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
34331
- }
34332
- return null;
34333
- };
34334
- const findJsxOpenerSpan = (lines, openerLineIndex) => {
34335
- const openerLine = lines[openerLineIndex];
34336
- if (openerLine === void 0) return null;
34337
- const opener = findOpenerTagOnLine(openerLine);
34338
- if (!opener) return null;
34339
- const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
34340
- let braceDepth = 0;
34341
- let innerAngleDepth = 0;
34342
- let stringDelimiter = null;
34343
- for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
34344
- const currentLine = lines[lineIndex];
34345
- const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
34346
- for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
34347
- const character = currentLine[charIndex];
34348
- if (stringDelimiter !== null) {
34349
- if (character === "\\") {
34350
- charIndex++;
34351
- continue;
34352
- }
34353
- if (character === stringDelimiter) stringDelimiter = null;
34354
- continue;
34355
- }
34356
- if (character === "\"" || character === "'" || character === "`") {
34357
- stringDelimiter = character;
34358
- continue;
34359
- }
34360
- if (character === "{") {
34361
- braceDepth++;
34362
- continue;
34363
- }
34364
- if (character === "}") {
34365
- braceDepth--;
34366
- continue;
34367
- }
34368
- if (braceDepth !== 0) continue;
34369
- if (character === "<") {
34370
- const followCharacter = currentLine[charIndex + 1];
34371
- if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
34372
- continue;
34373
- }
34374
- if (character !== ">") continue;
34375
- const previousCharacter = currentLine[charIndex - 1];
34376
- const nextCharacter = currentLine[charIndex + 1];
34377
- if (previousCharacter === "=" || nextCharacter === "=") continue;
34378
- if (innerAngleDepth > 0) {
34379
- innerAngleDepth--;
34380
- continue;
34381
- }
34382
- return lineIndex;
34383
- }
34384
- }
34385
- return null;
34386
- };
34387
- const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
34388
- for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
34389
- const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
34390
- if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
34391
- }
34392
- return null;
34393
- };
34394
- const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
34395
- const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
34396
- const collected = [];
34397
- let isStillInChain = true;
34398
- for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
34399
- const candidateLine = lines[candidateIndex];
34400
- if (candidateLine === void 0) break;
34401
- const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
34402
- if (match) {
34403
- collected.push({
34404
- commentLineIndex: candidateIndex,
34405
- ruleList: match[1],
34406
- isInChain: isStillInChain
34407
- });
34408
- continue;
34409
- }
34410
- isStillInChain = false;
34411
- }
34412
- return collected;
34413
- };
34414
34426
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
34415
34427
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
34416
34428
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -34535,7 +34547,13 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
34535
34547
  }
34536
34548
  const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
34537
34549
  const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
34538
- const isSameRuleKey = (candidateRuleKey, targetRuleKey) => canonicalizeRuleKey(candidateRuleKey) === canonicalizeRuleKey(targetRuleKey);
34550
+ const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
34551
+ const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
34552
+ const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
34553
+ const canonicalTarget = canonicalizeRuleKey(targetRuleKey);
34554
+ if (canonicalCandidate === canonicalTarget) return true;
34555
+ return isReactDoctorShortIdOf(canonicalCandidate, canonicalTarget) || isReactDoctorShortIdOf(canonicalTarget, canonicalCandidate);
34556
+ };
34539
34557
  const getEquivalentRuleKeys = (ruleKey) => {
34540
34558
  const nativeRuleKey = canonicalizeRuleKey(ruleKey);
34541
34559
  return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
@@ -34545,12 +34563,182 @@ const stripDescriptionTail = (ruleList) => {
34545
34563
  if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
34546
34564
  return ruleList.slice(0, descriptionMatch.index);
34547
34565
  };
34548
- const isRuleListedInComment = (ruleList, ruleId) => {
34566
+ const tokenizeRuleList = (ruleList) => {
34549
34567
  const trimmed = ruleList?.trim();
34550
- if (!trimmed) return true;
34568
+ if (!trimmed) return [];
34551
34569
  const ruleSection = stripDescriptionTail(trimmed).trim();
34552
- if (!ruleSection) return true;
34553
- return ruleSection.split(/[,\s]+/).some((token) => isSameRuleKey(token.trim(), ruleId));
34570
+ if (!ruleSection) return [];
34571
+ return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
34572
+ };
34573
+ const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34574
+ const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34575
+ const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34576
+ const buildHint = (tool, token, ruleId) => `oxlint matches plugin rules only by their full name, so \`${token}\` in your ${tool}-disable comment does not silence \`${ruleId}\` — change it to \`${ruleId}\`.`;
34577
+ const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
34578
+ const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34579
+ const candidates = [{
34580
+ line: lines[diagnosticLineIndex],
34581
+ requiredScope: "line"
34582
+ }, {
34583
+ line: lines[diagnosticLineIndex - 1],
34584
+ requiredScope: "next-line"
34585
+ }];
34586
+ for (const { line, requiredScope } of candidates) {
34587
+ const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
34588
+ if (!match) continue;
34589
+ const [, tool, scope, ruleList] = match;
34590
+ if (scope !== requiredScope) continue;
34591
+ const tokens = tokenizeRuleList(ruleList);
34592
+ if (tokens.includes(ruleId)) continue;
34593
+ for (const token of tokens) if (tokenMisnamesRule(token, ruleId)) return buildHint(tool, token, ruleId);
34594
+ }
34595
+ return null;
34596
+ };
34597
+ const detectBlockNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34598
+ let openMisname = null;
34599
+ const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
34600
+ for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
34601
+ const line = lines[lineIndex];
34602
+ if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
34603
+ const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
34604
+ if (disableMatch) {
34605
+ const [, tool, ruleList] = disableMatch;
34606
+ const tokens = tokenizeRuleList(ruleList);
34607
+ if (tokens.includes(ruleId)) openMisname = null;
34608
+ else {
34609
+ const misnamed = tokens.find((token) => tokenMisnamesRule(token, ruleId));
34610
+ if (misnamed) openMisname = {
34611
+ tool,
34612
+ token: misnamed
34613
+ };
34614
+ }
34615
+ continue;
34616
+ }
34617
+ const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
34618
+ if (enableMatch) {
34619
+ const enabledRules = tokenizeRuleList(enableMatch[1]);
34620
+ if (enabledRules.length === 0 || enabledRules.some((rule) => isSameRuleKey(rule, ruleId))) openMisname = null;
34621
+ }
34622
+ }
34623
+ return openMisname ? buildHint(openMisname.tool, openMisname.token, ruleId) : null;
34624
+ };
34625
+ const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34626
+ if (!ruleId.startsWith("react-doctor/")) return null;
34627
+ return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
34628
+ };
34629
+ const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
34630
+ const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
34631
+ const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
34632
+ let stringDelimiter = null;
34633
+ for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
34634
+ const character = line[charIndex];
34635
+ if (stringDelimiter !== null) {
34636
+ if (character === "\\") {
34637
+ charIndex++;
34638
+ continue;
34639
+ }
34640
+ if (character === stringDelimiter) stringDelimiter = null;
34641
+ continue;
34642
+ }
34643
+ if (character === "\"" || character === "'" || character === "`") {
34644
+ stringDelimiter = character;
34645
+ continue;
34646
+ }
34647
+ if (character === "/" && line[charIndex + 1] === "/") return true;
34648
+ }
34649
+ return false;
34650
+ };
34651
+ const findOpenerTagOnLine = (line) => {
34652
+ for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
34653
+ if (match.index === void 0) continue;
34654
+ if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
34655
+ }
34656
+ return null;
34657
+ };
34658
+ const findJsxOpenerSpan = (lines, openerLineIndex) => {
34659
+ const openerLine = lines[openerLineIndex];
34660
+ if (openerLine === void 0) return null;
34661
+ const opener = findOpenerTagOnLine(openerLine);
34662
+ if (!opener) return null;
34663
+ const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
34664
+ let braceDepth = 0;
34665
+ let innerAngleDepth = 0;
34666
+ let stringDelimiter = null;
34667
+ for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
34668
+ const currentLine = lines[lineIndex];
34669
+ const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
34670
+ for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
34671
+ const character = currentLine[charIndex];
34672
+ if (stringDelimiter !== null) {
34673
+ if (character === "\\") {
34674
+ charIndex++;
34675
+ continue;
34676
+ }
34677
+ if (character === stringDelimiter) stringDelimiter = null;
34678
+ continue;
34679
+ }
34680
+ if (character === "\"" || character === "'" || character === "`") {
34681
+ stringDelimiter = character;
34682
+ continue;
34683
+ }
34684
+ if (character === "{") {
34685
+ braceDepth++;
34686
+ continue;
34687
+ }
34688
+ if (character === "}") {
34689
+ braceDepth--;
34690
+ continue;
34691
+ }
34692
+ if (braceDepth !== 0) continue;
34693
+ if (character === "<") {
34694
+ const followCharacter = currentLine[charIndex + 1];
34695
+ if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
34696
+ continue;
34697
+ }
34698
+ if (character !== ">") continue;
34699
+ const previousCharacter = currentLine[charIndex - 1];
34700
+ const nextCharacter = currentLine[charIndex + 1];
34701
+ if (previousCharacter === "=" || nextCharacter === "=") continue;
34702
+ if (innerAngleDepth > 0) {
34703
+ innerAngleDepth--;
34704
+ continue;
34705
+ }
34706
+ return lineIndex;
34707
+ }
34708
+ }
34709
+ return null;
34710
+ };
34711
+ const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
34712
+ for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
34713
+ const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
34714
+ if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
34715
+ }
34716
+ return null;
34717
+ };
34718
+ const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
34719
+ const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
34720
+ const collected = [];
34721
+ let isStillInChain = true;
34722
+ for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
34723
+ const candidateLine = lines[candidateIndex];
34724
+ if (candidateLine === void 0) break;
34725
+ const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
34726
+ if (match) {
34727
+ collected.push({
34728
+ commentLineIndex: candidateIndex,
34729
+ ruleList: match[1],
34730
+ isInChain: isStillInChain
34731
+ });
34732
+ continue;
34733
+ }
34734
+ isStillInChain = false;
34735
+ }
34736
+ return collected;
34737
+ };
34738
+ const isRuleListedInComment = (ruleList, ruleId) => {
34739
+ const tokens = tokenizeRuleList(ruleList);
34740
+ if (tokens.length === 0) return true;
34741
+ return tokens.some((token) => isSameRuleKey(token, ruleId));
34554
34742
  };
34555
34743
  const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
34556
34744
  const formatLineGap = (gapLineCount) => `${gapLineCount} line${gapLineCount === 1 ? "" : "s"}`;
@@ -34594,7 +34782,7 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
34594
34782
  };
34595
34783
  return {
34596
34784
  isSuppressed: false,
34597
- nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId)
34785
+ nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId) ?? detectForeignDisableNearMiss(lines, diagnosticLineIndex, ruleId)
34598
34786
  };
34599
34787
  };
34600
34788
  /**
@@ -35020,6 +35208,11 @@ var OxlintBatchExceeded = class extends TaggedErrorClass()("OxlintBatchExceeded"
35020
35208
  }
35021
35209
  }
35022
35210
  };
35211
+ var ScanDeadlineExceeded = class extends TaggedErrorClass()("ScanDeadlineExceeded", { detail: String$1 }) {
35212
+ get message() {
35213
+ return `Scan exceeded its overall time budget: ${this.detail}`;
35214
+ }
35215
+ };
35023
35216
  var OxlintSpawnFailed = class extends TaggedErrorClass()("OxlintSpawnFailed", { cause: Unknown }) {
35024
35217
  get message() {
35025
35218
  return `Failed to run oxlint: ${pretty(fail$6(this.cause))}`;
@@ -35083,6 +35276,7 @@ var GitBaseBranchInvalid = class extends TaggedErrorClass()("GitBaseBranchInvali
35083
35276
  const ReactDoctorErrorReason = Union([
35084
35277
  OxlintUnavailable,
35085
35278
  OxlintBatchExceeded,
35279
+ ScanDeadlineExceeded,
35086
35280
  OxlintSpawnFailed,
35087
35281
  OxlintOutputUnparseable,
35088
35282
  ConfigParseFailed,
@@ -35133,15 +35327,105 @@ const layerOtlp = unwrap$3(gen(function* () {
35133
35327
  }).pipe(provide$2(layer$8));
35134
35328
  }).pipe(orDie));
35135
35329
  /**
35136
- * Resolves a requested lint worker count to a clamped integer within
35137
- * `[MIN_SCAN_CONCURRENCY, MAX_SCAN_CONCURRENCY]`. `"auto"` uses the
35138
- * machine's CPU cores; out-of-range or non-finite requests degrade to
35330
+ * Read a positive-millisecond timeout from an env var, falling back to
35331
+ * `defaultMs` when the var is unset, non-finite, or not strictly positive.
35332
+ */
35333
+ const readPositiveEnvMs = (envVarName, defaultMs) => {
35334
+ const rawValue = process.env[envVarName];
35335
+ if (rawValue === void 0) return defaultMs;
35336
+ const parsedValue = Number(rawValue);
35337
+ if (!Number.isFinite(parsedValue) || parsedValue <= 0) return defaultMs;
35338
+ return parsedValue;
35339
+ };
35340
+ const CGROUP_V2_MEMORY_MAX_PATH = "/sys/fs/cgroup/memory.max";
35341
+ const CGROUP_V1_MEMORY_LIMIT_PATH = "/sys/fs/cgroup/memory/memory.limit_in_bytes";
35342
+ const CGROUP_UNLIMITED_SENTINEL_BYTES = Number.MAX_SAFE_INTEGER;
35343
+ /**
35344
+ * Parses one raw cgroup memory-limit file value into a positive byte count, or
35345
+ * `undefined` when it represents "no limit" (the v2 `"max"` literal, an empty
35346
+ * read, a non-positive / non-finite value, or v1's near-2^63 unlimited
35347
+ * sentinel). Pure and exported so the classification is unit-testable without
35348
+ * touching the filesystem.
35349
+ */
35350
+ const parseCgroupMemoryLimitBytes = (raw) => {
35351
+ if (raw === void 0) return void 0;
35352
+ const trimmed = raw.trim();
35353
+ if (trimmed === "" || trimmed === "max") return void 0;
35354
+ const parsed = Number(trimmed);
35355
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed >= CGROUP_UNLIMITED_SENTINEL_BYTES) return;
35356
+ return parsed;
35357
+ };
35358
+ const CGROUP_MEMORY_LIMIT_PATHS = [CGROUP_V2_MEMORY_MAX_PATH, CGROUP_V1_MEMORY_LIMIT_PATH];
35359
+ /**
35360
+ * Reads this process's cgroup memory limit in bytes from the first candidate
35361
+ * path that yields a real limit, or `undefined` when none does — no cgroup, no
35362
+ * limit, or the files are unreadable (e.g. macOS / Windows dev machines).
35363
+ * `os.totalmem()` reports the HOST total and ignores cgroup memory limits, so a
35364
+ * memory-constrained container over-reports total memory; `resolveAutoScan-
35365
+ * Concurrency` takes `min(totalmem, this)` to honor the limit.
35366
+ *
35367
+ * The cgroup v2 read is the mount-root `memory.max`, which IS the container's
35368
+ * limit under the standard cgroup-namespace setup CI runners use (the
35369
+ * container's own cgroup is the root of its namespaced view). A process in a
35370
+ * non-namespaced nested/delegated cgroup whose root reads `"max"` is not
35371
+ * detected here and falls back to the host total; the EAGAIN/ENOMEM serial
35372
+ * replay in `spawnLintBatches` remains the runtime backstop for that case.
35373
+ *
35374
+ * `candidatePaths` is injectable so tests exercise the v2-wins-over-v1
35375
+ * precedence, the skip-unreadable fallback, and the all-missing case without a
35376
+ * real `/sys/fs/cgroup`.
35377
+ */
35378
+ const readCgroupMemoryLimitBytes = (candidatePaths = CGROUP_MEMORY_LIMIT_PATHS) => {
35379
+ for (const limitPath of candidatePaths) {
35380
+ let raw;
35381
+ try {
35382
+ raw = fs.readFileSync(limitPath, "utf8");
35383
+ } catch {
35384
+ continue;
35385
+ }
35386
+ const limitBytes = parseCgroupMemoryLimitBytes(raw);
35387
+ if (limitBytes !== void 0) return limitBytes;
35388
+ }
35389
+ };
35390
+ /**
35391
+ * Clamps a requested lint worker count to `[MIN_SCAN_CONCURRENCY,
35392
+ * HARD_MAX_SCAN_CONCURRENCY]` as a finite integer. This is the explicit-pin and
35393
+ * spawn-boundary clamp — the memory-and-core-budgeted auto count comes from
35394
+ * `resolveAutoScanConcurrency`. Out-of-range or non-finite requests degrade to
35139
35395
  * `MIN_SCAN_CONCURRENCY` rather than oversubscribing or running zero workers.
35140
35396
  */
35141
35397
  const resolveScanConcurrency = (requested) => {
35142
- const desired = requested === "auto" ? os.availableParallelism() : requested;
35143
- if (!Number.isFinite(desired) || desired < 1) return 1;
35144
- return Math.max(1, Math.min(Math.floor(desired), 16));
35398
+ if (!Number.isFinite(requested) || requested < 1) return 1;
35399
+ return Math.min(Math.floor(requested), 32);
35400
+ };
35401
+ const readSystemFacts = () => ({
35402
+ availableCores: os.availableParallelism(),
35403
+ totalMemoryBytes: os.totalmem(),
35404
+ cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
35405
+ });
35406
+ /**
35407
+ * Auto lint-worker count: the smaller of the (cgroup-CPU-aware) core count and
35408
+ * the number of `PER_WORKER_MEM_BUDGET_BYTES` workers that fit in available
35409
+ * memory, then clamped to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
35410
+ *
35411
+ * `os.availableParallelism()` already respects cgroup CPU quotas, so the core
35412
+ * term needs no help. Available memory is `os.totalmem()` floored by the cgroup
35413
+ * memory limit — `os.freemem()` is deliberately NOT used: it excludes
35414
+ * reclaimable page cache and reads near-zero on macOS / cache-heavy Linux, which
35415
+ * would collapse the auto path to a single worker. `os.totalmem()` reports the
35416
+ * host total even inside a container, so the cgroup limit (read directly,
35417
+ * because Node doesn't fold it into `totalmem()`) is the real ceiling there.
35418
+ *
35419
+ * `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
35420
+ * limited, and ceiling cases without mocking `os` or the filesystem.
35421
+ */
35422
+ const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
35423
+ const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
35424
+ const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
35425
+ return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
35426
+ };
35427
+ const resolveLintBatchOrdering = () => {
35428
+ return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "cost" ? "cost" : "arrival";
35145
35429
  };
35146
35430
  /**
35147
35431
  * Per-batch oxlint wall-clock budget. Reads from the env var on
@@ -35149,11 +35433,38 @@ const resolveScanConcurrency = (requested) => {
35149
35433
  * microVMs without recompiling react-doctor. Tests override via
35150
35434
  * `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
35151
35435
  */
35152
- var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
35153
- const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
35154
- if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
35436
+ var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS", OXLINT_SPAWN_TIMEOUT_MS) }) {};
35437
+ /**
35438
+ * Effect-side cap on the lint phase. The env var lets CI / eval runners
35439
+ * raise the phase budget for slow large repos without recompiling.
35440
+ * Tests override via `Layer.succeed(LintPhaseTimeoutMs, ...)`.
35441
+ */
35442
+ var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", LINT_PHASE_TIMEOUT_MS) }) {};
35443
+ /**
35444
+ * Effect-side cap on the dead-code phase, sitting above the in-worker
35445
+ * timeout as a runtime-independent backstop. The env var raises it for
35446
+ * type-heavy projects; tests override via
35447
+ * `Layer.succeed(DeadCodePhaseTimeoutMs, ...)`.
35448
+ */
35449
+ var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", DEAD_CODE_PHASE_TIMEOUT_MS) }) {};
35450
+ /**
35451
+ * Overall scan deadline backstop, bounding everything the per-phase
35452
+ * timeouts don't (wedged git / IO). The env var raises it for very
35453
+ * large repos; tests override via `Layer.succeed(ScanDeadlineMs, ...)`.
35454
+ */
35455
+ var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", SCAN_TOTAL_DEADLINE_MS) }) {};
35456
+ /**
35457
+ * Wall-clock budget for the supply-chain check when it runs on a background
35458
+ * fiber overlapping the lint pass. Reads from the env var on startup so the
35459
+ * eval harness can raise the budget under sandbox microVMs (slower network)
35460
+ * without recompiling react-doctor. Tests override via
35461
+ * `Layer.succeed(SupplyChainOverlapTimeoutMs, ...)`.
35462
+ */
35463
+ var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => {
35464
+ const raw = process.env["REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS"];
35465
+ if (raw === void 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35155
35466
  const parsed = Number(raw);
35156
- if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
35467
+ if (!Number.isFinite(parsed) || parsed <= 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35157
35468
  return parsed;
35158
35469
  } }) {};
35159
35470
  /**
@@ -35164,31 +35475,93 @@ var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTime
35164
35475
  */
35165
35476
  var OxlintOutputMaxBytes = class extends Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
35166
35477
  /**
35167
- * Number of oxlint subprocesses the lint pass runs in parallel. Defaults
35168
- * to auto-detected CPU cores (parallel) so large repos scan fast out of
35169
- * the box; `spawnLintBatches` transparently falls back to a single worker
35170
- * if a parallel run exhausts system resources. The CLI's `--no-parallel`
35171
- * flag forces serial via `Layer.succeed`; the `REACT_DOCTOR_PARALLEL` env
35172
- * var seeds the default for programmatic / CI callers that never touch the
35173
- * flag parallelism is opt-OUT, so only the explicit serial values pin
35174
- * one worker:
35175
- *
35176
- * - unset / `auto` / `true` / `on` → available CPU cores (clamped)
35478
+ * Number of oxlint subprocesses the lint pass runs in parallel. Defaults to a
35479
+ * memory-and-core-budgeted auto count (`resolveAutoScanConcurrency`) so large
35480
+ * repos scan fast out of the box without OOMing the native binding on a
35481
+ * high-core / low-memory box; `spawnLintBatches` transparently falls back to a
35482
+ * single worker if a parallel run still exhausts system resources. The CLI's
35483
+ * `--no-parallel` flag forces serial via `Layer.succeed`; the
35484
+ * `REACT_DOCTOR_PARALLEL` env var seeds the default for programmatic / CI
35485
+ * callers that never touch the flag — parallelism is opt-OUT, so only the
35486
+ * explicit serial values pin one worker:
35487
+ *
35488
+ * - unset / `auto` / `true` / `on` → memory-and-core-budgeted auto count
35177
35489
  * - `0` / `false` / `off` → `1` (serial)
35178
35490
  * - a positive integer → that many workers (clamped)
35179
- * - any other value → available CPU cores (clamped)
35491
+ * - any other value → memory-and-core-budgeted auto count
35180
35492
  *
35181
35493
  * The resolved value is always within
35182
- * `[MIN_SCAN_CONCURRENCY, MAX_SCAN_CONCURRENCY]`.
35494
+ * `[MIN_SCAN_CONCURRENCY, HARD_MAX_SCAN_CONCURRENCY]`.
35183
35495
  */
35184
35496
  var OxlintConcurrency = class extends Reference("react-doctor/OxlintConcurrency", { defaultValue: () => {
35185
35497
  const raw = process.env["REACT_DOCTOR_PARALLEL"];
35186
- if (raw === void 0) return resolveScanConcurrency("auto");
35498
+ if (raw === void 0) return resolveAutoScanConcurrency();
35187
35499
  const normalized = raw.trim().toLowerCase();
35188
35500
  if (normalized === "0" || normalized === "false" || normalized === "off") return 1;
35189
35501
  const parsed = Number.parseInt(normalized, 10);
35190
35502
  if (Number.isInteger(parsed) && parsed > 0) return resolveScanConcurrency(parsed);
35191
- return resolveScanConcurrency("auto");
35503
+ return resolveAutoScanConcurrency();
35504
+ } }) {};
35505
+ /**
35506
+ * Three-state control for overlapping the dead-code pass with the lint pass —
35507
+ * forking dead-code as a child fiber that runs DURING lint instead of strictly
35508
+ * after it.
35509
+ *
35510
+ * - `"auto"` (default) / `"off"` → strictly SEQUENTIAL: dead-code runs after
35511
+ * lint with the full core budget. Both deslop's parse pool and the oxlint
35512
+ * pool are CPU-bound and each size themselves to all cores, so overlapping
35513
+ * them only oversubscribes (~2x the cores) and starves the parse pass past
35514
+ * its timeout — for no wall-clock win, since there are no spare cores to
35515
+ * absorb the second pass. Sequential is both faster per-phase and safe.
35516
+ * - `"on"` → force the overlap anyway. The orchestrator then SPLITS the core
35517
+ * budget (`DEAD_CODE_OVERLAP_PARSE_SHARE`): deslop's parse pool is capped
35518
+ * and lint shrinks to the remainder, so the two sum to the cores instead of
35519
+ * doubling them, and the dead-code timeout scales up for the reduced share.
35520
+ *
35521
+ * Seeded from `REACT_DOCTOR_DEAD_CODE_OVERLAP` so operators get a redeploy-free
35522
+ * switch; tests pin it via `Layer.succeed(DeadCodeOverlap, ...)`.
35523
+ */
35524
+ var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", { defaultValue: () => {
35525
+ const raw = process.env["REACT_DOCTOR_DEAD_CODE_OVERLAP"]?.trim().toLowerCase();
35526
+ if (raw === "on" || raw === "true" || raw === "1") return "on";
35527
+ if (raw === "off" || raw === "false" || raw === "0") return "off";
35528
+ return "auto";
35529
+ } }) {};
35530
+ /**
35531
+ * How the full-scan lint pass orders its file batches. `"arrival"` (the
35532
+ * default) keeps `git ls-files` discovery order. `"cost"` opts into LPT (feed
35533
+ * the largest files first); set `REACT_DOCTOR_LINT_BATCH_ORDERING=cost`. NOTE:
35534
+ * `cost` is OFF by default because the current sort-desc-then-chunk-100 packs
35535
+ * the heaviest files into one wave-1 batch — on size-skewed repos that mega-
35536
+ * batch is a straggler (and can trip the per-batch timeout + split), measurably
35537
+ * regressing the common full-scan case. LPT needs the heavy files SPREAD across
35538
+ * batches before `cost` earns the default. Tests override via
35539
+ * `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans never reach this
35540
+ * — they pass user-scoped `includePaths` that skip discovery and stay in
35541
+ * arrival order; only the full-scan branch reads it.
35542
+ */
35543
+ var LintBatchOrdering = class extends Reference("react-doctor/LintBatchOrdering", { defaultValue: resolveLintBatchOrdering }) {};
35544
+ const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
35545
+ /**
35546
+ * Whether the per-file lint cache (`runners/oxlint/file-lint-cache.ts`) is
35547
+ * active. Defaults ON — repeat scans re-lint only the files whose content
35548
+ * changed, and correctness is guaranteed byte-identical to a cold scan by the
35549
+ * always-fresh cross-file sidecar. Opt-OUT, two knobs (matching the whole-repo
35550
+ * scan cache's `REACT_DOCTOR_NO_CACHE`):
35551
+ *
35552
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch; disables BOTH the
35553
+ * whole-repo scan cache and this per-file cache.
35554
+ * - `REACT_DOCTOR_NO_FILE_CACHE` — granular: bust only the per-file cache
35555
+ * while keeping the whole-repo short-circuit.
35556
+ *
35557
+ * Tests override via `Layer.succeed(PerFileLintCacheEnabled, false)`.
35558
+ */
35559
+ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintCacheEnabled", { defaultValue: () => {
35560
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
35561
+ const noFileCache = process.env["REACT_DOCTOR_NO_FILE_CACHE"]?.toLowerCase() ?? "";
35562
+ if (CACHE_DISABLED_VALUES.has(noCache)) return false;
35563
+ if (CACHE_DISABLED_VALUES.has(noFileCache)) return false;
35564
+ return true;
35192
35565
  } }) {};
35193
35566
  const DIAGNOSTIC_SURFACES = [
35194
35567
  "cli",
@@ -35362,7 +35735,6 @@ const PACKAGE_JSON_FILENAME = "package.json";
35362
35735
  const PACKAGE_JSON_CONFIG_KEY = "reactDoctor";
35363
35736
  const LEGACY_CONFIG_FILENAME = "react-doctor.config.json";
35364
35737
  const jiti = createJiti(import.meta.url);
35365
- const formatError = (error) => error instanceof Error ? error.message : String(error);
35366
35738
  const importDefaultExport = async (jitiInstance, filePath) => {
35367
35739
  const imported = await jitiInstance.import(filePath);
35368
35740
  return imported?.default ?? imported;
@@ -35394,7 +35766,7 @@ const loadModuleConfig = async (filePath) => {
35394
35766
  try {
35395
35767
  return await importDefaultExport(aliasJiti, filePath);
35396
35768
  } catch (retryError) {
35397
- throw new Error(`${formatError(error)} (retry with ${SELF_PACKAGE_IMPORT_SPECIFIER} aliased to the running react-doctor package also failed: ${formatError(retryError)})`, { cause: retryError });
35769
+ throw new Error(`${messageFromUnknown(error)} (retry with ${SELF_PACKAGE_IMPORT_SPECIFIER} aliased to the running react-doctor package also failed: ${messageFromUnknown(retryError)})`, { cause: retryError });
35398
35770
  }
35399
35771
  }
35400
35772
  };
@@ -35443,7 +35815,7 @@ const loadLegacyConfig = (directory) => {
35443
35815
  }
35444
35816
  warn(`${LEGACY_CONFIG_FILENAME} must contain an object, ignoring.`);
35445
35817
  } catch (error) {
35446
- warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${formatError(error)}`);
35818
+ warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${messageFromUnknown(error)}`);
35447
35819
  }
35448
35820
  return {
35449
35821
  status: "invalid",
@@ -35470,7 +35842,7 @@ const loadConfigFromDirectory = async (directory) => {
35470
35842
  warn(`${CONFIG_BASENAME}.${extension} must export an object, ignoring.`);
35471
35843
  sawBrokenConfigFile = true;
35472
35844
  } catch (error) {
35473
- warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${formatError(error)}`);
35845
+ warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${messageFromUnknown(error)}`);
35474
35846
  sawBrokenConfigFile = true;
35475
35847
  }
35476
35848
  }
@@ -35524,6 +35896,31 @@ const resolveConfigRootDir = (config, configSourceDirectory) => {
35524
35896
  }
35525
35897
  return resolvedRootDir;
35526
35898
  };
35899
+ const buildFixGroupId = (diagnostic) => createHash("sha1").update(JSON.stringify([
35900
+ diagnostic.filePath,
35901
+ `${diagnostic.plugin}/${diagnostic.rule}`,
35902
+ diagnostic.message
35903
+ ])).digest("hex").slice(0, 16);
35904
+ const isGroupableRule = (diagnostic) => ROOT_CAUSE_GROUPABLE_RULE_KEYS.has(`${diagnostic.plugin}/${diagnostic.rule}`);
35905
+ const assignFixGroups = (diagnostics) => {
35906
+ const siteCountByGroupId = /* @__PURE__ */ new Map();
35907
+ for (const diagnostic of diagnostics) {
35908
+ if (!isGroupableRule(diagnostic)) continue;
35909
+ const groupId = buildFixGroupId(diagnostic);
35910
+ siteCountByGroupId.set(groupId, (siteCountByGroupId.get(groupId) ?? 0) + 1);
35911
+ }
35912
+ return diagnostics.map((diagnostic) => {
35913
+ if (!isGroupableRule(diagnostic)) return diagnostic;
35914
+ const groupId = buildFixGroupId(diagnostic);
35915
+ if ((siteCountByGroupId.get(groupId) ?? 0) < 2) return diagnostic;
35916
+ return {
35917
+ ...diagnostic,
35918
+ fixGroupId: groupId
35919
+ };
35920
+ });
35921
+ };
35922
+ const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
35923
+ const sortDiagnosticsStable = (diagnostics) => [...diagnostics].sort((left, right) => compareStrings(left.filePath, right.filePath) || left.line - right.line || left.column - right.column || compareStrings(left.plugin, right.plugin) || compareStrings(left.rule, right.rule) || compareStrings(left.severity, right.severity) || compareStrings(left.message, right.message));
35527
35924
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
35528
35925
  const buildExpoCheckContext = (rootDirectory, expoVersion) => {
35529
35926
  const packageJson = readPackageJson(Path.join(rootDirectory, "package.json"));
@@ -36030,10 +36427,15 @@ const buildHardeningDiagnostic = (input) => ({
36030
36427
  column: input.column ?? 0,
36031
36428
  category: "Security"
36032
36429
  });
36033
- const checkPnpmHardening = (rootDirectory) => {
36034
- if (!isPnpmManagedProject(rootDirectory)) return [];
36035
- const workspacePath = Path.join(rootDirectory, PNPM_WORKSPACE_FILE);
36036
- const settings = parseHardeningSettings(isFile(workspacePath) ? NFS.readFileSync(workspacePath, "utf-8") : "");
36430
+ const checkPnpmHardening = (scanDirectory) => {
36431
+ if (!isPnpmManagedProject(scanDirectory)) return [];
36432
+ const workspacePath = Path.join(scanDirectory, PNPM_WORKSPACE_FILE);
36433
+ const hasWorkspaceFile = isFile(workspacePath);
36434
+ if (!hasWorkspaceFile) {
36435
+ const monorepoRoot = findMonorepoRoot(scanDirectory);
36436
+ if (monorepoRoot !== null && isFile(Path.join(monorepoRoot, PNPM_WORKSPACE_FILE))) return [];
36437
+ }
36438
+ const settings = parseHardeningSettings(hasWorkspaceFile ? NFS.readFileSync(workspacePath, "utf-8") : "");
36037
36439
  const diagnostics = [];
36038
36440
  if (settings.minimumReleaseAge === null) diagnostics.push(buildHardeningDiagnostic({
36039
36441
  message: "pnpm-workspace.yaml is missing `minimumReleaseAge` — newly published versions can ship malware that gets caught and unpublished within hours",
@@ -36650,7 +37052,7 @@ const readIgnoreFile = (filePath) => {
36650
37052
  try {
36651
37053
  content = NFS.readFileSync(filePath, "utf-8");
36652
37054
  } catch (error) {
36653
- const errnoCode = error?.code;
37055
+ const errnoCode = isErrnoException(error) ? error.code : void 0;
36654
37056
  if (errnoCode && errnoCode !== "ENOENT") runSync(warn$1(`Could not read ignore file ${filePath}: ${errnoCode}`));
36655
37057
  return [];
36656
37058
  }
@@ -36691,8 +37093,8 @@ const collectIgnorePatterns = (rootDirectory) => {
36691
37093
  cachedPatternsByRoot.set(rootDirectory, patterns);
36692
37094
  return patterns;
36693
37095
  };
37096
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
36694
37097
  const KNIP_JSON_FILENAME = "knip.json";
36695
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
36696
37098
  const readJsonFileSafe = (filePath) => {
36697
37099
  let rawContents;
36698
37100
  try {
@@ -36708,10 +37110,10 @@ const readJsonFileSafe = (filePath) => {
36708
37110
  };
36709
37111
  const readKnipConfig = (rootDirectory) => {
36710
37112
  const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
36711
- if (isRecord$1(knipJson)) return knipJson;
37113
+ if (isRecord(knipJson)) return knipJson;
36712
37114
  const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
36713
- const packageKnipConfig = isRecord$1(packageJson) ? packageJson.knip : null;
36714
- return isRecord$1(packageKnipConfig) ? packageKnipConfig : null;
37115
+ const packageKnipConfig = isRecord(packageJson) ? packageJson.knip : null;
37116
+ return isRecord(packageKnipConfig) ? packageKnipConfig : null;
36715
37117
  };
36716
37118
  const normalizePatternList = (value) => {
36717
37119
  if (typeof value === "string" && value.length > 0) return [value];
@@ -36723,10 +37125,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
36723
37125
  return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
36724
37126
  };
36725
37127
  const collectKnipWorkspacePatterns = (workspaces, settingName) => {
36726
- if (!isRecord$1(workspaces)) return [];
37128
+ if (!isRecord(workspaces)) return [];
36727
37129
  const patterns = [];
36728
37130
  for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
36729
- if (!isRecord$1(workspaceConfig)) continue;
37131
+ if (!isRecord(workspaceConfig)) continue;
36730
37132
  patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
36731
37133
  }
36732
37134
  return patterns;
@@ -36771,8 +37173,6 @@ const toCanonicalPath = (filePath) => {
36771
37173
  };
36772
37174
  const DEAD_CODE_PLUGIN = "deslop";
36773
37175
  const DEAD_CODE_CATEGORY = "Maintainability";
36774
- const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
36775
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
36776
37176
  const DEAD_CODE_WORKER_SCRIPT = `
36777
37177
  const inputChunks = [];
36778
37178
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -36820,6 +37220,22 @@ process.stdin.on("end", () => {
36820
37220
  ...(workerInput.ignorePatterns.length > 0
36821
37221
  ? { ignorePatterns: workerInput.ignorePatterns }
36822
37222
  : {}),
37223
+ // We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
37224
+ // unusedDependencies, circularDependencies). Everything else deslop can compute
37225
+ // is pure wasted work for us, and it's the bulk of the runtime:
37226
+ // - semantic: a full TS Program for unusedTypes/enum/class-members/
37227
+ // misclassifiedDependencies (~37-45% of the phase).
37228
+ // - reportCodeQuality: the duplicate-block, complexity, feature-flag,
37229
+ // TypeScript-smell, private-type-leak and re-export-cycle detectors. These
37230
+ // are the single most expensive pass — duplicate-block detection alone was
37231
+ // ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
37232
+ // speedup on a large repo.
37233
+ // Both are provably safe: the consumed graph findings are computed by their own
37234
+ // detectors, independent of these passes (confirmed byte-identical on
37235
+ // excalidraw + mui-material + sentry). tsConfigPath stays — the module resolver
37236
+ // needs it for path-alias resolution in the import graph.
37237
+ semantic: { enabled: false },
37238
+ reportCodeQuality: false,
36823
37239
  };
36824
37240
  const result = await analyze(defineConfig(config));
36825
37241
  emit({ ok: true, result: normalizeResult(result) });
@@ -36830,7 +37246,7 @@ process.stdin.on("end", () => {
36830
37246
  });
36831
37247
  `;
36832
37248
  const resolveTsConfigPath = (rootDirectory) => {
36833
- for (const filename of TSCONFIG_FILENAMES$1) {
37249
+ for (const filename of TSCONFIG_FILENAMES) {
36834
37250
  const candidate = Path.join(rootDirectory, filename);
36835
37251
  if (NFS.existsSync(candidate)) return candidate;
36836
37252
  }
@@ -36949,7 +37365,11 @@ const createDeadCodeWorker = (input) => {
36949
37365
  "pipe",
36950
37366
  "pipe"
36951
37367
  ],
36952
- windowsHide: true
37368
+ windowsHide: true,
37369
+ env: input.parseConcurrency === void 0 ? process.env : {
37370
+ ...process.env,
37371
+ DESLOP_PARSE_CONCURRENCY: String(input.parseConcurrency)
37372
+ }
36953
37373
  });
36954
37374
  const stdoutChunks = [];
36955
37375
  const stderrChunks = [];
@@ -36994,28 +37414,25 @@ const createDeadCodeWorker = (input) => {
36994
37414
  }
36995
37415
  };
36996
37416
  };
36997
- const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
37417
+ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs, abortSignal) => new Promise((resolve, reject) => {
36998
37418
  let didSettle = false;
36999
- const timeoutHandle = setTimeout(() => {
37000
- if (didSettle) return;
37001
- didSettle = true;
37002
- handle.terminate?.();
37003
- reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
37004
- }, timeoutMs);
37005
- timeoutHandle.unref?.();
37006
- handle.result.then((value) => {
37007
- if (didSettle) return;
37008
- didSettle = true;
37009
- clearTimeout(timeoutHandle);
37010
- handle.terminate?.();
37011
- resolve(value);
37012
- }, (error) => {
37419
+ const settle = (finish) => {
37013
37420
  if (didSettle) return;
37014
37421
  didSettle = true;
37015
37422
  clearTimeout(timeoutHandle);
37423
+ abortSignal?.removeEventListener("abort", onAbort);
37016
37424
  handle.terminate?.();
37017
- reject(error);
37018
- });
37425
+ finish();
37426
+ };
37427
+ const onAbort = () => settle(() => reject(/* @__PURE__ */ new Error("Dead-code worker aborted.")));
37428
+ const timeoutHandle = setTimeout(() => settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`))), timeoutMs);
37429
+ timeoutHandle.unref?.();
37430
+ if (abortSignal?.aborted) {
37431
+ onAbort();
37432
+ return;
37433
+ }
37434
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
37435
+ handle.result.then((value) => settle(() => resolve(value)), (error) => settle(() => reject(error)));
37019
37436
  });
37020
37437
  const checkDeadCode = async (options) => {
37021
37438
  const rootDirectory = toCanonicalPath(options.rootDirectory);
@@ -37027,8 +37444,9 @@ const checkDeadCode = async (options) => {
37027
37444
  entryPatterns,
37028
37445
  tsConfigPath: resolveTsConfigPath(rootDirectory),
37029
37446
  ignorePatterns,
37030
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
37031
- }), options.workerTimeoutMs ?? 12e4));
37447
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37448
+ parseConcurrency: options.parseConcurrency
37449
+ }), options.workerTimeoutMs ?? 12e4, options.abortSignal));
37032
37450
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
37033
37451
  const diagnostics = [];
37034
37452
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -37126,7 +37544,37 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
37126
37544
  return true;
37127
37545
  };
37128
37546
  const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
37129
- const excludeMinifiedFiles = (rootDirectory, relativePaths) => relativePaths.filter((relativePath) => !isLargeMinifiedFile(Path.resolve(rootDirectory, relativePath)));
37547
+ /**
37548
+ * Budget for the dead-code phase, scaled to the work. deslop's graph build is
37549
+ * CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
37550
+ * for a large repo (where the pass legitimately runs that long) and is then
37551
+ * tipped over by any concurrent load — silently dropping every dead-code
37552
+ * finding. Scaling the budget with file count (and inversely with the core
37553
+ * share when overlapped) lets the pass complete, while the ceiling still
37554
+ * reclaims a genuinely wedged worker. Returns the in-worker SIGKILL deadline
37555
+ * and the Effect-side phase backstop that sits a margin above it.
37556
+ */
37557
+ const resolveDeadCodeTimeout = (input) => {
37558
+ const coreShareFactor = Math.max(1, input.fullConcurrency / Math.max(1, input.deadCodeConcurrency));
37559
+ const workerTimeoutMs = Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor)));
37560
+ return {
37561
+ workerTimeoutMs,
37562
+ phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
37563
+ };
37564
+ };
37565
+ const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
37566
+ const entries = [];
37567
+ for (const relativePath of relativePaths) {
37568
+ const absolutePath = Path.resolve(rootDirectory, relativePath);
37569
+ const sizeBytes = statSourceFileSize(absolutePath);
37570
+ if (isLargeMinifiedFile(absolutePath, sizeBytes)) continue;
37571
+ entries.push({
37572
+ path: relativePath,
37573
+ sizeBytes: sizeBytes ?? 0
37574
+ });
37575
+ }
37576
+ return entries;
37577
+ };
37130
37578
  const listSourceFilesViaGit = (rootDirectory) => {
37131
37579
  const result = spawnSync("git", [
37132
37580
  "ls-files",
@@ -37159,7 +37607,8 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
37159
37607
  }
37160
37608
  return filePaths;
37161
37609
  };
37162
- const listSourceFiles = (rootDirectory) => excludeMinifiedFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
37610
+ const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
37611
+ const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
37163
37612
  const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
37164
37613
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
37165
37614
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
@@ -37202,24 +37651,25 @@ var Config = class Config extends Service()("react-doctor/Config") {
37202
37651
  var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37203
37652
  static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
37204
37653
  return yield* tryPromise({
37205
- try: () => checkDeadCode({
37654
+ try: (signal) => checkDeadCode({
37206
37655
  rootDirectory: input.rootDirectory,
37207
- userConfig: input.userConfig
37656
+ userConfig: input.userConfig,
37657
+ parseConcurrency: input.parseConcurrency,
37658
+ workerTimeoutMs: input.workerTimeoutMs,
37659
+ abortSignal: signal
37208
37660
  }),
37209
37661
  catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
37210
37662
  }).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
37211
37663
  })()) }));
37212
37664
  static layerOf = (diagnostics) => succeed$3(DeadCode, DeadCode.of({ run: () => fromIterable$1(diagnostics) }));
37213
37665
  };
37214
- const createNodeReadFileLinesSync = (rootDirectory) => {
37215
- return (filePath) => {
37216
- const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
37217
- try {
37218
- return NFS.readFileSync(absolutePath, "utf-8").split("\n");
37219
- } catch {
37220
- return null;
37221
- }
37222
- };
37666
+ const createNodeReadFileLinesSync = (rootDirectory) => (filePath) => {
37667
+ const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
37668
+ try {
37669
+ return NFS.readFileSync(absolutePath, "utf-8").split("\n");
37670
+ } catch {
37671
+ return null;
37672
+ }
37223
37673
  };
37224
37674
  var Files = class Files extends Service()("react-doctor/Files") {
37225
37675
  static layerNode = succeed$3(Files, Files.of({
@@ -37430,7 +37880,10 @@ var Git = class Git extends Service()("react-doctor/Git") {
37430
37880
  directory: input.directory,
37431
37881
  cause
37432
37882
  }) });
37433
- }));
37883
+ }), withSpan("git.exec", { attributes: {
37884
+ "git.command": input.command,
37885
+ "git.subcommand": input.args[0] ?? ""
37886
+ } }));
37434
37887
  const runGit = (directory, args) => runCommand({
37435
37888
  command: "git",
37436
37889
  args,
@@ -37458,7 +37911,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37458
37911
  ]);
37459
37912
  if (candidates.status !== 0) return null;
37460
37913
  return trimOrNull(candidates.stdout.split("\n")[0] ?? "");
37461
- });
37914
+ }).pipe(withSpan("Git.defaultBranch"));
37462
37915
  const branchExists = (directory, branch) => runGit(directory, [
37463
37916
  "rev-parse",
37464
37917
  "--verify",
@@ -37505,7 +37958,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37505
37958
  const result = resultOption.value;
37506
37959
  if (result.status !== 0) return null;
37507
37960
  return parseGithubViewerPermission(result.stdout);
37508
- }).pipe(catch_$1(() => succeed$2(null)));
37961
+ }).pipe(catch_$1(() => succeed$2(null)), withSpan("Git.githubViewerPermission"));
37509
37962
  /**
37510
37963
  * Resolves a `--diff A..B` / `A...B` commit range into a changed-file
37511
37964
  * selection. Each endpoint is validated with `isSafeGitRevision`
@@ -37619,7 +38072,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37619
38072
  changedFiles: splitNullSeparated(diff.stdout),
37620
38073
  isCurrentChanges: false
37621
38074
  };
37622
- }),
38075
+ }).pipe(withSpan("Git.diffSelection")),
37623
38076
  stagedFilePaths: (directory) => runGit(directory, [
37624
38077
  "diff",
37625
38078
  "--cached",
@@ -37661,7 +38114,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37661
38114
  status: result.status,
37662
38115
  stdout: result.stdout
37663
38116
  };
37664
- }),
38117
+ }).pipe(withSpan("Git.grep")),
37665
38118
  changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
37666
38119
  if (files.length === 0) return [];
37667
38120
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
@@ -37677,7 +38130,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37677
38130
  ]);
37678
38131
  if (result.status !== 0) return null;
37679
38132
  return parseChangedLineRanges(result.stdout);
37680
- })
38133
+ }).pipe(withSpan("Git.changedLineRanges"))
37681
38134
  });
37682
38135
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
37683
38136
  /**
@@ -37892,7 +38345,7 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
37892
38345
  for (const [absolutePath, originalContent] of originalContents) try {
37893
38346
  NFS.writeFileSync(absolutePath, originalContent);
37894
38347
  } catch (error) {
37895
- process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${error instanceof Error ? error.message : String(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
38348
+ process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${messageFromUnknown(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
37896
38349
  }
37897
38350
  };
37898
38351
  const onExit = () => restore();
@@ -37916,6 +38369,14 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
37916
38369
  process.removeListener("exit", onExit);
37917
38370
  };
37918
38371
  };
38372
+ const ROOT_DIRECTORY_PLACEHOLDER = "<root>";
38373
+ const normalizeConfigForHash = (config) => {
38374
+ const clone = JSON.parse(JSON.stringify(config));
38375
+ if (clone?.settings?.["react-doctor"]) clone.settings["react-doctor"].rootDirectory = ROOT_DIRECTORY_PLACEHOLDER;
38376
+ if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
38377
+ return clone;
38378
+ };
38379
+ const computeRulesetHash = (input) => crypto.createHash("sha1").update(JSON.stringify(normalizeConfigForHash(input.config))).update("\0").update([...input.toolchainVersions].join("\0")).update("\0").update([...input.ignorePatterns].join("\n")).update(" ").update(input.tsconfigContent ?? "").digest("hex");
37919
38380
  /**
37920
38381
  * Loads a plugin module via the local require resolver and extracts
37921
38382
  * `(name, ruleNames)` from either `module.exports.meta + rules` or
@@ -37942,16 +38403,16 @@ const readPluginShape = (pluginSpecifier, loadModule) => {
37942
38403
  ruleNames: new Set(Object.keys(rules))
37943
38404
  };
37944
38405
  };
37945
- const bundledRequire = createRequire(import.meta.url);
38406
+ const bundledRequire$1 = createRequire(import.meta.url);
37946
38407
  const resolveReactHooksJsPlugin = (hasReactCompiler, customRulesOnly) => {
37947
38408
  if (!hasReactCompiler || customRulesOnly) return null;
37948
38409
  let pluginSpecifier;
37949
38410
  try {
37950
- pluginSpecifier = bundledRequire.resolve("eslint-plugin-react-hooks");
38411
+ pluginSpecifier = bundledRequire$1.resolve("eslint-plugin-react-hooks");
37951
38412
  } catch {
37952
38413
  return null;
37953
38414
  }
37954
- const { ruleNames } = readPluginShape(pluginSpecifier, (spec) => bundledRequire(spec));
38415
+ const { ruleNames } = readPluginShape(pluginSpecifier, (spec) => bundledRequire$1(spec));
37955
38416
  return {
37956
38417
  entry: {
37957
38418
  name: "react-hooks-js",
@@ -37998,7 +38459,7 @@ const resolveUserPlugin = (spec, configSourceDirectory) => {
37998
38459
  try {
37999
38460
  resolvedSpecifier = isRelative ? Path.resolve(configSourceDirectory, spec) : candidateRequire.resolve(spec);
38000
38461
  } catch (error) {
38001
- warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${error instanceof Error ? error.message : String(error)}`);
38462
+ warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${messageFromUnknown(error)}`);
38002
38463
  return null;
38003
38464
  }
38004
38465
  const { name, ruleNames } = readPluginShape(resolvedSpecifier, (target) => candidateRequire(target));
@@ -38070,8 +38531,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
38070
38531
  }
38071
38532
  return enabled;
38072
38533
  };
38073
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
38074
- const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38534
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
38535
+ const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38075
38536
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
38076
38537
  const jsPlugins = [];
38077
38538
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -38080,6 +38541,8 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38080
38541
  for (const registryEntry of REACT_DOCTOR_RULES) {
38081
38542
  const rule = reactDoctorPlugin.rules[registryEntry.id];
38082
38543
  if (!rule) continue;
38544
+ if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
38545
+ if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
38083
38546
  if (rule.scan !== void 0) continue;
38084
38547
  if (customRulesOnly && registryEntry.originallyExternal) continue;
38085
38548
  if (rule.framework !== "global" && !rule.requires) continue;
@@ -38094,7 +38557,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38094
38557
  enabledReactDoctorRules[registryEntry.key] = severity;
38095
38558
  }
38096
38559
  const userPluginRules = {};
38097
- for (const userPlugin of userPlugins) {
38560
+ if (ruleSelection !== "sidecar") for (const userPlugin of userPlugins) {
38098
38561
  Object.assign(userPluginRules, buildUserPluginRules(userPlugin, severityControls));
38099
38562
  jsPlugins.push(userPlugin.entry);
38100
38563
  }
@@ -38124,6 +38587,100 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38124
38587
  }
38125
38588
  };
38126
38589
  };
38590
+ const atomicWriteJson = (filePath, value) => {
38591
+ try {
38592
+ NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
38593
+ const temporaryPath = `${filePath}.${process.pid}.tmp`;
38594
+ NFS.writeFileSync(temporaryPath, JSON.stringify(value));
38595
+ NFS.renameSync(temporaryPath, filePath);
38596
+ } catch {
38597
+ return;
38598
+ }
38599
+ };
38600
+ const failOpenReadJson = (filePath, fallback) => {
38601
+ try {
38602
+ return JSON.parse(NFS.readFileSync(filePath, "utf8"));
38603
+ } catch {
38604
+ return fallback;
38605
+ }
38606
+ };
38607
+ const validateDiagnostic = decodeUnknownSync(Diagnostic);
38608
+ const decodeFileDiagnostics = (raw) => {
38609
+ if (!Array.isArray(raw)) return null;
38610
+ try {
38611
+ for (const entry of raw) validateDiagnostic(entry);
38612
+ return raw;
38613
+ } catch {
38614
+ return null;
38615
+ }
38616
+ };
38617
+ const emptyCache = () => ({
38618
+ version: 1,
38619
+ rulesets: {}
38620
+ });
38621
+ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
38622
+ const entries = /* @__PURE__ */ new Map();
38623
+ const persisted = failOpenReadJson(cacheFilePath, emptyCache());
38624
+ if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
38625
+ const bucket = persisted.rulesets[rulesetHash];
38626
+ if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
38627
+ for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
38628
+ const decoded = decodeFileDiagnostics(rawDiagnostics);
38629
+ if (decoded !== null) entries.set(fileKey, decoded);
38630
+ }
38631
+ return entries;
38632
+ };
38633
+ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38634
+ const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
38635
+ const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
38636
+ return {
38637
+ lookup: (fileKey) => entries.get(fileKey) ?? null,
38638
+ store: (fileKey, diagnostics) => {
38639
+ entries.delete(fileKey);
38640
+ entries.set(fileKey, diagnostics);
38641
+ },
38642
+ persist: () => {
38643
+ const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
38644
+ const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
38645
+ const existingBucket = rulesets[rulesetHash];
38646
+ const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
38647
+ const ourFiles = {};
38648
+ for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
38649
+ const cappedEntries = Object.entries({
38650
+ ...existingFiles,
38651
+ ...ourFiles
38652
+ }).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
38653
+ rulesets[rulesetHash] = {
38654
+ updatedAtMs: Date.now(),
38655
+ files: Object.fromEntries(cappedEntries)
38656
+ };
38657
+ const keptHashes = Object.entries(rulesets).sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8).map(([hash]) => hash);
38658
+ const prunedRulesets = {};
38659
+ for (const hash of keptHashes) prunedRulesets[hash] = rulesets[hash];
38660
+ atomicWriteJson(cacheFilePath, {
38661
+ version: 1,
38662
+ rulesets: prunedRulesets
38663
+ });
38664
+ }
38665
+ };
38666
+ };
38667
+ const bundledRequire = createRequire(import.meta.url);
38668
+ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
38669
+ "oxlint/package.json",
38670
+ "oxlint-plugin-react-doctor/package.json",
38671
+ "eslint-plugin-react-hooks/package.json"
38672
+ ];
38673
+ const resolveOxlintToolchainVersions = () => {
38674
+ const versions = [`node=${process.version}`];
38675
+ for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
38676
+ const packageJson = bundledRequire(specifier);
38677
+ const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
38678
+ versions.push(`${specifier}=${version}`);
38679
+ } catch {
38680
+ versions.push(`${specifier}=missing`);
38681
+ }
38682
+ return versions;
38683
+ };
38127
38684
  const esmRequire = createRequire(import.meta.url);
38128
38685
  const resolveOxlintBinary = () => {
38129
38686
  const oxlintMainPath = esmRequire.resolve("oxlint");
@@ -38131,7 +38688,6 @@ const resolveOxlintBinary = () => {
38131
38688
  return Path.join(oxlintPackageDirectory, "bin", "oxlint");
38132
38689
  };
38133
38690
  const resolvePluginPath = () => esmRequire.resolve("oxlint-plugin-react-doctor");
38134
- const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
38135
38691
  const resolveTsConfigRelativePath = (rootDirectory) => {
38136
38692
  for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
38137
38693
  return null;
@@ -38503,7 +39059,7 @@ const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
38503
39059
  const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
38504
39060
  let currentNode = identifier.parent;
38505
39061
  while (currentNode) {
38506
- if (isScopeNode(currentNode)) {
39062
+ if (isScopeBoundary(currentNode)) {
38507
39063
  if (scopeContainsNonImportBinding(currentNode, currentNode, identifier.text)) return true;
38508
39064
  }
38509
39065
  if (currentNode === sourceFile) return false;
@@ -38594,11 +39150,10 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
38594
39150
  });
38595
39151
  return resolution;
38596
39152
  };
38597
- const isScopeNode = isScopeBoundary;
38598
39153
  const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, visitedDeclarations = /* @__PURE__ */ new Set()) => {
38599
39154
  let currentNode = identifier.parent;
38600
39155
  while (currentNode) {
38601
- if (isScopeNode(currentNode)) {
39156
+ if (isScopeBoundary(currentNode)) {
38602
39157
  const resolution = findResolutionInScope(currentNode, identifier.text, reactImportBindings, sourceFile, visitedDeclarations);
38603
39158
  if (resolution) return resolution;
38604
39159
  }
@@ -38768,9 +39323,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
38768
39323
  try {
38769
39324
  parsed = JSON.parse(sanitizedStdout);
38770
39325
  } catch {
38771
- throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
39326
+ throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
38772
39327
  }
38773
- if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
39328
+ if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
38774
39329
  const minifiedFileCache = /* @__PURE__ */ new Map();
38775
39330
  const isMinifiedDiagnosticFile = (filename) => {
38776
39331
  const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
@@ -38807,15 +39362,19 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
38807
39362
  };
38808
39363
  });
38809
39364
  };
38810
- const SANITIZED_ENV = (() => {
38811
- const sanitized = {};
38812
- for (const [name, value] of Object.entries(process.env)) {
39365
+ const buildOxlintChildEnv = (sourceEnv) => {
39366
+ const childEnv = {};
39367
+ for (const [name, value] of Object.entries(sourceEnv)) {
38813
39368
  if (name === "NODE_OPTIONS" || name === "NODE_DEBUG") continue;
38814
39369
  if (name.startsWith("npm_config_")) continue;
38815
- sanitized[name] = value;
39370
+ childEnv[name] = value;
38816
39371
  }
38817
- return sanitized;
38818
- })();
39372
+ const isCompileCacheDisabled = Boolean(sourceEnv.NODE_DISABLE_COMPILE_CACHE);
39373
+ const isCompileCacheAlreadySet = childEnv.NODE_COMPILE_CACHE !== void 0;
39374
+ if (!isCompileCacheDisabled && !isCompileCacheAlreadySet) childEnv.NODE_COMPILE_CACHE = Path.join(os.tmpdir(), NODE_COMPILE_CACHE_DIR_NAME);
39375
+ return childEnv;
39376
+ };
39377
+ const SANITIZED_ENV = buildOxlintChildEnv(process.env);
38819
39378
  /**
38820
39379
  * Spawn one oxlint subprocess with hard ceilings on wall time and
38821
39380
  * output size. Returns stdout on success; raises a tagged
@@ -38832,7 +39391,11 @@ const SANITIZED_ENV = (() => {
38832
39391
  * The first three are splittable (the caller's binary-split retry
38833
39392
  * shrinks the batch and re-spawns); the fourth isn't.
38834
39393
  */
38835
- const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
39394
+ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES, abortSignal) => new Promise((resolve, reject) => {
39395
+ if (abortSignal?.aborted) {
39396
+ reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
39397
+ return;
39398
+ }
38836
39399
  const child = spawn(nodeBinaryPath, args, {
38837
39400
  cwd: rootDirectory,
38838
39401
  env: SANITIZED_ENV,
@@ -38842,11 +39405,18 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
38842
39405
  "pipe"
38843
39406
  ]
38844
39407
  });
39408
+ const onAbort = () => {
39409
+ child.kill("SIGKILL");
39410
+ reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
39411
+ };
39412
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
39413
+ const clearAbortListener = () => abortSignal?.removeEventListener("abort", onAbort);
38845
39414
  const timeoutHandle = setTimeout(() => {
39415
+ clearAbortListener();
38846
39416
  child.kill("SIGKILL");
38847
39417
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
38848
39418
  kind: "timeout",
38849
- detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
39419
+ detail: `${spawnTimeoutMs / MILLISECONDS_PER_SECOND}s budget exceeded`
38850
39420
  }) }));
38851
39421
  }, spawnTimeoutMs);
38852
39422
  timeoutHandle.unref?.();
@@ -38877,10 +39447,12 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
38877
39447
  });
38878
39448
  child.on("error", (error) => {
38879
39449
  clearTimeout(timeoutHandle);
39450
+ clearAbortListener();
38880
39451
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: error }) }));
38881
39452
  });
38882
39453
  child.on("close", (_code, signal) => {
38883
39454
  clearTimeout(timeoutHandle);
39455
+ clearAbortListener();
38884
39456
  if (didKillForSize) {
38885
39457
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
38886
39458
  kind: "output-too-large",
@@ -38947,26 +39519,28 @@ const isParallelismRelatedSpawnError = (error) => {
38947
39519
  * loop with a slimmer config in that case.
38948
39520
  */
38949
39521
  const spawnLintBatches = async (input) => {
38950
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
39522
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal } = input;
38951
39523
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
38952
39524
  const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
38953
39525
  const runBatchPass = async (concurrency) => {
38954
39526
  const allDiagnostics = [];
38955
39527
  const droppedFiles = [];
38956
39528
  let firstDropReason = null;
38957
- const spawnLintBatch = async (batch) => {
39529
+ const splitDeadlineMs = Date.now() + splitTotalBudgetMs;
39530
+ const spawnLintBatch = async (batch, depth) => {
38958
39531
  const batchArgs = [...baseArgs, ...batch];
38959
39532
  try {
38960
- return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
39533
+ return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
38961
39534
  } catch (error) {
38962
39535
  if (!isSplittableReactDoctorError(error)) throw error;
38963
- if (batch.length <= 1) {
39536
+ const splitBudgetExhausted = Date.now() >= splitDeadlineMs || depth >= splitMaxDepth;
39537
+ if (batch.length <= 1 || splitBudgetExhausted) {
38964
39538
  droppedFiles.push(...batch);
38965
- if (firstDropReason === null) firstDropReason = error.message;
39539
+ if (firstDropReason === null) firstDropReason = splitBudgetExhausted && batch.length > 1 ? `${error.message} (split budget exhausted after ${splitMaxDepth} levels / ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s)` : error.message;
38966
39540
  return [];
38967
39541
  }
38968
39542
  const splitIndex = Math.ceil(batch.length / 2);
38969
- return [...await spawnLintBatch(batch.slice(0, splitIndex)), ...await spawnLintBatch(batch.slice(splitIndex))];
39543
+ return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1)];
38970
39544
  }
38971
39545
  };
38972
39546
  let startedFileCount = 0;
@@ -38983,7 +39557,7 @@ const spawnLintBatches = async (input) => {
38983
39557
  try {
38984
39558
  const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
38985
39559
  startedFileCount += batch.length;
38986
- const batchDiagnostics = await spawnLintBatch(batch);
39560
+ const batchDiagnostics = await spawnLintBatch(batch, 0);
38987
39561
  scannedFileCount += batch.length;
38988
39562
  if (onFileProgress) {
38989
39563
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
@@ -39044,6 +39618,22 @@ const validateRuleRegistration = () => {
39044
39618
  ].filter((entry) => entry !== null).join("; ");
39045
39619
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
39046
39620
  };
39621
+ const hashFileContents = (filePath) => {
39622
+ try {
39623
+ return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
39624
+ } catch {
39625
+ return null;
39626
+ }
39627
+ };
39628
+ const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
39629
+ const resolveReactDoctorCacheDir = (projectDirectory) => {
39630
+ const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
39631
+ if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
39632
+ const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
39633
+ if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
39634
+ return Path.join(os.tmpdir(), "react-doctor-cache", projectCacheSubdir(projectDirectory));
39635
+ };
39636
+ const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
39047
39637
  /**
39048
39638
  * Atomically (re)writes the generated oxlintrc.json. Used twice in
39049
39639
  * the runner: once for the primary scan, once for the
@@ -39061,6 +39651,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
39061
39651
  NFS.closeSync(fileHandle);
39062
39652
  }
39063
39653
  };
39654
+ const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
39655
+ /**
39656
+ * Detects an oxlint config-load crash caused by the optional
39657
+ * `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
39658
+ * builds the partial-failure note for it; returns `null` when the failure
39659
+ * was anything else.
39660
+ *
39661
+ * oxlint prints a framed error to stdout (not stderr) and exits non-zero
39662
+ * when a `jsPlugins` entry can't be imported; that non-JSON stdout
39663
+ * surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
39664
+ * config load on it, leaving the plugin in would drop every curated
39665
+ * react-doctor diagnostic too — so the caller retries with the plugin
39666
+ * stripped (issue #833). Both markers sit at the start of oxlint's
39667
+ * message, so they survive the `preview` slice even for deep pnpm paths.
39668
+ */
39669
+ const reactHooksJsPluginDropNote = (error) => {
39670
+ if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
39671
+ const { preview } = error.reason;
39672
+ if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
39673
+ const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
39674
+ return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
39675
+ };
39064
39676
  /**
39065
39677
  * The oxlint runner. Composed of three pieces in `runners/oxlint/`:
39066
39678
  *
@@ -39080,7 +39692,7 @@ const writeOxlintConfig = (configPath, configToWrite) => {
39080
39692
  * 6. always restore disable directives + clean up the temp dir
39081
39693
  */
39082
39694
  const runOxlint = async (options) => {
39083
- const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, spawnTimeoutMs, outputMaxBytes } = options;
39695
+ const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, perFileLintCacheEnabled = false, onCacheStats, spawnTimeoutMs, outputMaxBytes, lintBatchOrdering = "arrival" } = options;
39084
39696
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
39085
39697
  const severityControls = buildRuleSeverityControls(userConfig);
39086
39698
  validateRuleRegistration();
@@ -39088,38 +39700,165 @@ const runOxlint = async (options) => {
39088
39700
  const pluginPath = resolvePluginPath();
39089
39701
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
39090
39702
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
39091
- const buildConfig = (extendsForThisAttempt) => createOxlintConfig({
39703
+ const buildConfig = (overrides) => createOxlintConfig({
39092
39704
  pluginPath,
39093
39705
  project,
39094
39706
  customRulesOnly,
39095
- extendsPaths: extendsForThisAttempt,
39707
+ extendsPaths: overrides.extendsPaths,
39096
39708
  ignoredTags,
39097
39709
  serverAuthFunctionNames,
39098
39710
  severityControls,
39099
- userPlugins
39711
+ userPlugins,
39712
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
39713
+ ruleSelection: overrides.ruleSelection
39100
39714
  });
39101
39715
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
39102
39716
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
39103
39717
  const configPath = Path.join(configDirectory, "oxlintrc.json");
39104
39718
  try {
39105
- const baseArgs = [
39106
- resolveOxlintBinary(),
39107
- "-c",
39108
- configPath,
39109
- "--format",
39110
- "json"
39111
- ];
39719
+ const oxlintBinary = resolveOxlintBinary();
39720
+ const sharedArgs = [];
39721
+ let tsconfigContent = null;
39112
39722
  if (project.hasTypeScript) {
39113
39723
  const tsconfigRelativePath = resolveTsConfigRelativePath(rootDirectory);
39114
- if (tsconfigRelativePath) baseArgs.push("--tsconfig", tsconfigRelativePath);
39724
+ if (tsconfigRelativePath) {
39725
+ sharedArgs.push("--tsconfig", tsconfigRelativePath);
39726
+ try {
39727
+ tsconfigContent = NFS.readFileSync(Path.resolve(rootDirectory, tsconfigRelativePath), "utf8");
39728
+ } catch {
39729
+ tsconfigContent = null;
39730
+ }
39731
+ }
39115
39732
  }
39116
39733
  const combinedPatterns = collectIgnorePatterns(rootDirectory);
39117
39734
  if (combinedPatterns.length > 0) {
39118
39735
  const combinedIgnorePath = Path.join(configDirectory, "combined.ignore");
39119
39736
  NFS.writeFileSync(combinedIgnorePath, `${combinedPatterns.join("\n")}\n`);
39120
- baseArgs.push("--ignore-path", combinedIgnorePath);
39737
+ sharedArgs.push("--ignore-path", combinedIgnorePath);
39121
39738
  }
39122
- const fileBatches = batchIncludePaths(baseArgs, includePaths !== void 0 ? includePaths : listSourceFiles(rootDirectory));
39739
+ const makeBaseArgs = (oxlintConfigPath) => [
39740
+ oxlintBinary,
39741
+ "-c",
39742
+ oxlintConfigPath,
39743
+ "--format",
39744
+ "json",
39745
+ ...sharedArgs
39746
+ ];
39747
+ const discoverScanFiles = () => lintBatchOrdering === "cost" ? sortSourceFilesByCost(listSourceFilesWithSize(rootDirectory)) : listSourceFiles(rootDirectory);
39748
+ const candidateFiles = includePaths !== void 0 ? includePaths : discoverScanFiles();
39749
+ const runConfigOverFiles = async (buildConfigForPass, configFileName, files, fileProgress) => {
39750
+ if (files.length === 0) return {
39751
+ diagnostics: [],
39752
+ didDropReactHooksJsPlugin: false,
39753
+ hadPartialFailure: false
39754
+ };
39755
+ let hadPartialFailure = false;
39756
+ const reportPartialFailure = (reason) => {
39757
+ hadPartialFailure = true;
39758
+ onPartialFailure?.(reason);
39759
+ };
39760
+ const passConfigPath = Path.join(configDirectory, configFileName);
39761
+ const passBaseArgs = makeBaseArgs(passConfigPath);
39762
+ const passFileBatches = batchIncludePaths(passBaseArgs, files);
39763
+ const spawnPass = () => spawnLintBatches({
39764
+ baseArgs: passBaseArgs,
39765
+ fileBatches: passFileBatches,
39766
+ rootDirectory,
39767
+ nodeBinaryPath,
39768
+ project,
39769
+ onPartialFailure: reportPartialFailure,
39770
+ onFileProgress: fileProgress,
39771
+ spawnTimeoutMs,
39772
+ outputMaxBytes,
39773
+ concurrency: options.concurrency,
39774
+ signal: options.signal
39775
+ });
39776
+ writeOxlintConfig(passConfigPath, buildConfigForPass({}));
39777
+ try {
39778
+ return {
39779
+ diagnostics: await spawnPass(),
39780
+ didDropReactHooksJsPlugin: false,
39781
+ hadPartialFailure
39782
+ };
39783
+ } catch (error) {
39784
+ const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
39785
+ if (reactHooksJsDropNote === null) throw error;
39786
+ writeOxlintConfig(passConfigPath, buildConfigForPass({ disableReactHooksJsPlugin: true }));
39787
+ const diagnostics = await spawnPass();
39788
+ reportPartialFailure(reactHooksJsDropNote);
39789
+ return {
39790
+ diagnostics,
39791
+ didDropReactHooksJsPlugin: true,
39792
+ hadPartialFailure
39793
+ };
39794
+ }
39795
+ };
39796
+ if (perFileLintCacheEnabled && respectInlineDisables && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
39797
+ const rulesetHash = computeRulesetHash({
39798
+ config: buildConfig({
39799
+ extendsPaths: [],
39800
+ ruleSelection: "cacheable"
39801
+ }),
39802
+ toolchainVersions: resolveOxlintToolchainVersions(),
39803
+ ignorePatterns: combinedPatterns,
39804
+ tsconfigContent
39805
+ });
39806
+ const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
39807
+ const cacheKeyByFile = /* @__PURE__ */ new Map();
39808
+ const missFiles = [];
39809
+ const replayedDiagnostics = [];
39810
+ for (const candidateFile of candidateFiles) {
39811
+ const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
39812
+ if (contentHash === null) {
39813
+ missFiles.push(candidateFile);
39814
+ continue;
39815
+ }
39816
+ const cacheKey = `${candidateFile.replaceAll("\\", "/")}${contentHash}`;
39817
+ cacheKeyByFile.set(candidateFile, cacheKey);
39818
+ const cachedDiagnostics = cache.lookup(cacheKey);
39819
+ if (cachedDiagnostics === null) missFiles.push(candidateFile);
39820
+ else replayedDiagnostics.push(...cachedDiagnostics);
39821
+ }
39822
+ const cacheHitFileCount = candidateFiles.length - missFiles.length;
39823
+ const cacheableResult = await runConfigOverFiles((overrides) => buildConfig({
39824
+ extendsPaths: [],
39825
+ ruleSelection: "cacheable",
39826
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
39827
+ }), "oxlintrc.cacheable.json", missFiles, void 0);
39828
+ const sidecarResult = await runConfigOverFiles(() => buildConfig({
39829
+ extendsPaths: [],
39830
+ ruleSelection: "sidecar"
39831
+ }), "oxlintrc.sidecar.json", candidateFiles, options.onFileProgress);
39832
+ onCacheStats?.(cacheHitFileCount, candidateFiles.length);
39833
+ const missFileByNormalizedPath = /* @__PURE__ */ new Map();
39834
+ for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
39835
+ const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
39836
+ let isAttributionSound = true;
39837
+ for (const diagnostic of cacheableResult.diagnostics) {
39838
+ const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
39839
+ if (missFile === void 0) {
39840
+ isAttributionSound = false;
39841
+ break;
39842
+ }
39843
+ const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
39844
+ fileDiagnostics.push(diagnostic);
39845
+ freshDiagnosticsByFile.set(missFile, fileDiagnostics);
39846
+ }
39847
+ if (!cacheableResult.didDropReactHooksJsPlugin && !cacheableResult.hadPartialFailure && isAttributionSound) {
39848
+ for (const missFile of missFiles) {
39849
+ const cacheKey = cacheKeyByFile.get(missFile);
39850
+ if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
39851
+ }
39852
+ cache.persist();
39853
+ }
39854
+ return dedupeDiagnostics([
39855
+ ...replayedDiagnostics,
39856
+ ...cacheableResult.diagnostics,
39857
+ ...sidecarResult.diagnostics
39858
+ ]);
39859
+ }
39860
+ const baseArgs = makeBaseArgs(configPath);
39861
+ const fileBatches = batchIncludePaths(baseArgs, candidateFiles);
39123
39862
  const runBatches = () => spawnLintBatches({
39124
39863
  baseArgs,
39125
39864
  fileBatches,
@@ -39130,14 +39869,25 @@ const runOxlint = async (options) => {
39130
39869
  onFileProgress: options.onFileProgress,
39131
39870
  spawnTimeoutMs,
39132
39871
  outputMaxBytes,
39133
- concurrency: options.concurrency
39872
+ concurrency: options.concurrency,
39873
+ signal: options.signal
39134
39874
  });
39135
- writeOxlintConfig(configPath, buildConfig(extendsPaths));
39875
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
39136
39876
  try {
39137
39877
  return await runBatches();
39138
39878
  } catch (error) {
39879
+ const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
39880
+ if (reactHooksJsDropNote !== null) {
39881
+ writeOxlintConfig(configPath, buildConfig({
39882
+ extendsPaths,
39883
+ disableReactHooksJsPlugin: true
39884
+ }));
39885
+ const diagnostics = await runBatches();
39886
+ onPartialFailure?.(reactHooksJsDropNote);
39887
+ return diagnostics;
39888
+ }
39139
39889
  if (extendsPaths.length === 0) throw error;
39140
- writeOxlintConfig(configPath, buildConfig([]));
39890
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
39141
39891
  return await runBatches();
39142
39892
  }
39143
39893
  } finally {
@@ -39199,9 +39949,11 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
39199
39949
  const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
39200
39950
  const outputMaxBytes = yield* OxlintOutputMaxBytes;
39201
39951
  const concurrency = yield* OxlintConcurrency;
39952
+ const lintBatchOrdering = yield* LintBatchOrdering;
39953
+ const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
39202
39954
  const collectedFailures = [];
39203
39955
  const diagnostics = yield* tryPromise({
39204
- try: () => runOxlint({
39956
+ try: (signal) => runOxlint({
39205
39957
  rootDirectory: input.rootDirectory,
39206
39958
  project: input.project,
39207
39959
  includePaths: input.includePaths ? [...input.includePaths] : void 0,
@@ -39216,9 +39968,13 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
39216
39968
  collectedFailures.push(reason);
39217
39969
  },
39218
39970
  onFileProgress: input.onFileProgress,
39971
+ perFileLintCacheEnabled,
39972
+ onCacheStats: input.onCacheStats,
39219
39973
  spawnTimeoutMs,
39220
39974
  outputMaxBytes,
39221
- concurrency
39975
+ concurrency,
39976
+ signal,
39977
+ lintBatchOrdering
39222
39978
  }),
39223
39979
  catch: ensureReactDoctorError
39224
39980
  });
@@ -39610,14 +40366,49 @@ const parseArtifactFromBody = (body) => {
39610
40366
  }
39611
40367
  return null;
39612
40368
  };
39613
- const fetchSocketArtifact = (dependency) => tryPromise(async (signal) => {
40369
+ const isSupplyChainCacheDisabled = () => {
40370
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
40371
+ return noCache === "1" || noCache === "true";
40372
+ };
40373
+ const supplyChainCacheFile = (cacheDirectory, dependency) => {
40374
+ const purlHash = crypto.createHash("sha256").update(toPurl(dependency)).digest("hex").slice(0, 16);
40375
+ return Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR, `${purlHash}.json`);
40376
+ };
40377
+ const readCachedSocketBody = (cacheFile) => {
40378
+ try {
40379
+ const entry = JSON.parse(NFS.readFileSync(cacheFile, "utf-8"));
40380
+ if (typeof entry === "object" && entry !== null && "fetchedAtMs" in entry && "body" in entry && typeof entry.fetchedAtMs === "number" && typeof entry.body === "string" && Date.now() - entry.fetchedAtMs <= 864e5) return entry.body;
40381
+ } catch {}
40382
+ return null;
40383
+ };
40384
+ const writeCachedSocketBody = (cacheFile, body) => {
40385
+ try {
40386
+ NFS.mkdirSync(Path.dirname(cacheFile), { recursive: true });
40387
+ NFS.writeFileSync(cacheFile, JSON.stringify({
40388
+ fetchedAtMs: Date.now(),
40389
+ body
40390
+ }));
40391
+ } catch {}
40392
+ };
40393
+ const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
40394
+ const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
40395
+ if (cacheFile !== null) {
40396
+ const cachedBody = readCachedSocketBody(cacheFile);
40397
+ if (cachedBody !== null) {
40398
+ const cachedArtifact = parseArtifactFromBody(cachedBody);
40399
+ if (cachedArtifact !== null) return cachedArtifact;
40400
+ }
40401
+ }
39614
40402
  const requestUrl = `${SOCKET_FREE_PURL_API_BASE}/${encodeURIComponent(toPurl(dependency))}`;
39615
40403
  const response = await fetch(requestUrl, {
39616
40404
  headers: { "User-Agent": SOCKET_FREE_USER_AGENT },
39617
40405
  signal
39618
40406
  });
39619
40407
  if (!response.ok) return null;
39620
- return parseArtifactFromBody(await response.text());
40408
+ const body = await response.text();
40409
+ const artifact = parseArtifactFromBody(body);
40410
+ if (artifact !== null && cacheFile !== null) writeCachedSocketBody(cacheFile, body);
40411
+ return artifact;
39621
40412
  }).pipe(timeout(FETCH_TIMEOUT_MS), orElseSucceed(() => null), tap$1((artifact) => {
39622
40413
  const scoreAttributes = {};
39623
40414
  if (artifact !== null) {
@@ -39722,7 +40513,8 @@ const checkSupplyChain = (input) => gen(function* () {
39722
40513
  const packageJsonPath = Path.join(input.rootDirectory, "package.json");
39723
40514
  const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
39724
40515
  if (dependencies.length === 0) return [];
39725
- const artifacts = yield* forEach$1(dependencies, fetchSocketArtifact, { concurrency: 8 });
40516
+ const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
40517
+ const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
39726
40518
  const diagnostics = [];
39727
40519
  for (let index = 0; index < dependencies.length; index += 1) {
39728
40520
  const artifact = artifacts[index];
@@ -39747,6 +40539,10 @@ const checkSupplyChain = (input) => gen(function* () {
39747
40539
  * The underlying `checkSupplyChain` Effect is total/fail-open — per-package
39748
40540
  * timeouts and network failures recover to "skip" — so the stream never
39749
40541
  * fails, mirroring `DeadCode`'s stream shape so the two compose the same way.
40542
+ * The orchestrator (`run-inspect.ts`) consumes this stream on a background
40543
+ * fiber whose network time overlaps the lint pass, joined under a generous
40544
+ * wall-clock budget; a budget expiry is the same fail-open outcome as a Socket
40545
+ * outage.
39750
40546
  */
39751
40547
  var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
39752
40548
  static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(checkSupplyChain(input).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
@@ -39805,18 +40601,42 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
39805
40601
  *
39806
40602
  * Phases:
39807
40603
  *
39808
- * 1. Config.resolve(directory) → Project.discover → Git metadata
40604
+ * 1. Config.resolve(directory) → Project.discover → Git metadata.
40605
+ * The GitHub viewer-permission lookup is forked onto a background
40606
+ * fiber here and joined late (it feeds score metadata, not
40607
+ * diagnostics).
39809
40608
  * 2. beforeLint hook (e.g. CLI renders the project-detection block)
39810
40609
  * 3. environment checks (reduced-motion + pnpm hardening +
39811
- * expo/react-native + security scan)
39812
- * 4. Linter.run + DeadCode.run forked as concurrent fibers so
39813
- * their wall-clock times overlap. Progress spinners stay
39814
- * sequential (lint first, then dead-code) for clean terminal
39815
- * output. GitHub viewer permission also runs as a background
39816
- * fiber during this phase.
39817
- * 5. afterLint hook
39818
- * 6. Reporter.finalize
39819
- * 7. Score.compute against the surface-filtered diagnostic set
40610
+ * expo/react-native + security scan), collected synchronously
40611
+ * 4. The supply-chain check (Socket.dev) is forked onto a background
40612
+ * fiber so its ~100% network-bound time overlaps the ~100%
40613
+ * CPU/subprocess-bound lint pass below, collapsing two serial
40614
+ * phases into roughly `max(supplyChain, lint)`. It is capped by
40615
+ * `SupplyChainOverlapTimeoutMs` (measured from fork) so a hung
40616
+ * socket can't drag out its join; on timeout it fails open to no
40617
+ * diagnostics — the same outcome class as a Socket outage.
40618
+ * 5. Linter.run runs; DeadCode.run runs concurrently (forked child
40619
+ * fiber) ONLY when the memory gate has headroom to run the 8 GB
40620
+ * dead-code child alongside the oxlint workers — or when overlap is
40621
+ * forced via REACT_DOCTOR_DEAD_CODE_OVERLAP. Otherwise dead-code
40622
+ * runs sequentially after lint, exactly as it did pre-overlap. The
40623
+ * fiber is joined (or interrupted, SIGKILLing its worker, on lint
40624
+ * failure) before diagnostics are concatenated. The afterLint hook
40625
+ * fires between lint and dead-code. Progress spinner labels AND the
40626
+ * final diagnostic / score order stay independent of execution
40627
+ * order, so terminal output is identical either way; supply-chain
40628
+ * rides alongside without a spinner.
40629
+ * 6. Join the supply-chain fiber, then assemble the diagnostics in a
40630
+ * FIXED order (env, supply-chain, lint, dead-code) so the output is
40631
+ * byte-identical regardless of which fiber settled first. The
40632
+ * viewer-permission fiber is joined later, during score-metadata
40633
+ * assembly (it feeds score metadata, not diagnostics). The per-element
40634
+ * `Reporter.emit` side-channel now interleaves supply-chain with lint
40635
+ * emits, so capture-order assertions must target the deterministic
40636
+ * concat below, not emit order (production `Reporter.layerNoop` makes
40637
+ * emit a no-op).
40638
+ * 7. Reporter.finalize
40639
+ * 8. Score.compute against the surface-filtered diagnostic set
39820
40640
  *
39821
40641
  * The orchestrator owns spinner lifecycle via `Progress`; callers
39822
40642
  * choose `Progress.layerOra(...)` for CLI feedback or
@@ -39874,10 +40694,21 @@ const runInspect = (input, hooks = {}) => gen(function* () {
39874
40694
  ignoredTags: input.ignoredTags
39875
40695
  })
39876
40696
  ])));
39877
- const supplyChainCollected = !isDiffMode || (input.supplyChainManifestChanged ?? false) ? yield* runCollect(applyPerElementPipeline(supplyChainService.run({
40697
+ const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
40698
+ const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
40699
+ const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
39878
40700
  rootDirectory: scanDirectory,
39879
40701
  userConfig: resolvedConfig.config
39880
- }))) : [];
40702
+ }))).pipe(map$3((diagnostics) => ({
40703
+ diagnostics,
40704
+ timedOut: false
40705
+ })), timeout(supplyChainOverlapTimeout), orElseSucceed(() => ({
40706
+ diagnostics: [],
40707
+ timedOut: true
40708
+ }))) : succeed$2({
40709
+ diagnostics: [],
40710
+ timedOut: false
40711
+ }));
39881
40712
  const lintFailure = yield* make$13({
39882
40713
  didFail: false,
39883
40714
  reason: null,
@@ -39888,12 +40719,49 @@ const runInspect = (input, hooks = {}) => gen(function* () {
39888
40719
  didFail: false,
39889
40720
  reason: null
39890
40721
  });
39891
- const scanConcurrency = yield* OxlintConcurrency;
40722
+ const scanConcurrency = resolveScanConcurrency(yield* OxlintConcurrency);
40723
+ const lintPhaseTimeoutMs = yield* LintPhaseTimeoutMs;
40724
+ const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
40725
+ const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
39892
40726
  const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
40727
+ const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
40728
+ const deadCodeOverlapMode = yield* DeadCodeOverlap;
40729
+ const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
40730
+ const deadCodeParseConcurrency = shouldOverlapDeadCode ? Math.max(1, Math.floor(scanConcurrency * DEAD_CODE_OVERLAP_PARSE_SHARE)) : void 0;
40731
+ const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
40732
+ const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
40733
+ rootDirectory: scanDirectory,
40734
+ userConfig: resolvedConfig.config,
40735
+ parseConcurrency: deadCodeParseConcurrency,
40736
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
40737
+ }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
40738
+ yield* set(deadCodeFailure, {
40739
+ didFail: true,
40740
+ reason: error.message
40741
+ });
40742
+ return empty$4;
40743
+ })))))).pipe(timeoutOption(deadCodeTimeout.phaseTimeoutMs), flatMap$2(match$3({
40744
+ onNone: () => set(deadCodeFailure, {
40745
+ didFail: true,
40746
+ reason: `Dead-code analysis exceeded ${Math.round(deadCodeTimeout.phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
40747
+ }).pipe(as([])),
40748
+ onSome: succeed$2
40749
+ })));
40750
+ const overlapDeadCodeTimeout = resolveDeadCodeTimeout({
40751
+ sourceFileCount: project.sourceFileCount,
40752
+ deadCodeConcurrency: deadCodeParseConcurrency ?? scanConcurrency,
40753
+ fullConcurrency: scanConcurrency
40754
+ });
40755
+ const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
40756
+ workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
40757
+ phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs)
40758
+ })) : null;
39893
40759
  const scanProgress = yield* progressService.start("Scanning...");
39894
40760
  const scanStartTime = Date.now();
39895
40761
  let lastReportedTotalFileCount = 0;
39896
- const lintCollected = yield* runCollect(applyPerElementPipeline(linterService.run({
40762
+ let lintCacheHitFileCount = null;
40763
+ let lintCacheTotalFileCount = null;
40764
+ const baseLintStream = linterService.run({
39897
40765
  rootDirectory: scanDirectory,
39898
40766
  project,
39899
40767
  includePaths: lintIncludePaths ?? void 0,
@@ -39907,6 +40775,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
39907
40775
  onFileProgress: (scannedFileCount, totalFileCount) => {
39908
40776
  lastReportedTotalFileCount = totalFileCount;
39909
40777
  runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})${workerCountSuffix}...`));
40778
+ },
40779
+ onCacheStats: (cacheHitFileCount, totalConsideredFileCount) => {
40780
+ lintCacheHitFileCount = cacheHitFileCount;
40781
+ lintCacheTotalFileCount = totalConsideredFileCount;
39910
40782
  }
39911
40783
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
39912
40784
  yield* set(lintFailure, {
@@ -39916,36 +40788,54 @@ const runInspect = (input, hooks = {}) => gen(function* () {
39916
40788
  reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
39917
40789
  });
39918
40790
  return empty$4;
39919
- }))))));
40791
+ }))));
40792
+ const lintCollected = yield* runCollect(applyPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
40793
+ onNone: () => set(lintFailure, {
40794
+ didFail: true,
40795
+ reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
40796
+ reasonTag: "OxlintBatchExceeded",
40797
+ reasonKind: null
40798
+ }).pipe(as([])),
40799
+ onSome: succeed$2
40800
+ })));
39920
40801
  const lintFailureState = yield* get$2(lintFailure);
39921
40802
  yield* afterLint(lintFailureState.didFail);
39922
40803
  if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
39923
40804
  const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
39924
40805
  const scannedFilesLabel = `${totalFileCount} ${totalFileCount === 1 ? "file" : "files"}`;
39925
- const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
39926
- const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`).pipe(andThen(runCollect(applyPerElementPipeline(deadCodeService.run({
39927
- rootDirectory: scanDirectory,
39928
- userConfig: resolvedConfig.config
39929
- }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
39930
- yield* set(deadCodeFailure, {
39931
- didFail: true,
39932
- reason: error.message
40806
+ let deadCodeCollected = [];
40807
+ if (lintFailureState.didFail) {
40808
+ if (deadCodeFiber !== null) yield* interrupt(deadCodeFiber);
40809
+ } else if (shouldRunDeadCode) {
40810
+ yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
40811
+ const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
40812
+ sourceFileCount: totalFileCount,
40813
+ deadCodeConcurrency: scanConcurrency,
40814
+ fullConcurrency: scanConcurrency
39933
40815
  });
39934
- return empty$4;
39935
- }))))))));
39936
- const deadCodeFailureState = yield* get$2(deadCodeFailure);
40816
+ deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
40817
+ workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
40818
+ phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs)
40819
+ });
40820
+ }
40821
+ const deadCodeFailureState = lintFailureState.didFail ? {
40822
+ didFail: false,
40823
+ reason: null
40824
+ } : yield* get$2(deadCodeFailure);
39937
40825
  const scanElapsedMilliseconds = Date.now() - scanStartTime;
39938
- const scanElapsedSeconds = (scanElapsedMilliseconds / 1e3).toFixed(1);
40826
+ const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
39939
40827
  if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
39940
40828
  else if (input.suppressScanSummary) yield* scanProgress.stop();
39941
40829
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
40830
+ const supplyChainResult = yield* join(supplyChainFiber);
40831
+ const supplyChainCollected = supplyChainResult.diagnostics;
39942
40832
  yield* reporterService.finalize;
39943
- const finalDiagnostics = [
40833
+ const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
39944
40834
  ...envCollected,
39945
40835
  ...supplyChainCollected,
39946
40836
  ...lintCollected,
39947
40837
  ...deadCodeCollected
39948
- ];
40838
+ ]));
39949
40839
  const githubViewerPermission = yield* join(githubViewerPermissionFiber);
39950
40840
  const scoreMetadata = {
39951
40841
  ...repo !== null ? { repo } : {},
@@ -39981,9 +40871,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
39981
40871
  lintPartialFailures,
39982
40872
  didDeadCodeFail: deadCodeFailureState.didFail,
39983
40873
  deadCodeFailureReason: deadCodeFailureState.reason,
40874
+ deadCodeOverlapped: shouldOverlapDeadCode,
39984
40875
  scannedFileCount: totalFileCount,
39985
40876
  scannedFilePaths,
39986
- scanElapsedMilliseconds
40877
+ scanElapsedMilliseconds,
40878
+ scanConcurrency,
40879
+ supplyChainOverlapTimedOut: supplyChainResult.timedOut,
40880
+ lintCacheHitFileCount,
40881
+ lintCacheTotalFileCount
39987
40882
  };
39988
40883
  }).pipe(withSpan("runInspect", { attributes: {
39989
40884
  "inspect.directory": input.directory,
@@ -39991,7 +40886,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
39991
40886
  "inspect.runDeadCode": input.runDeadCode,
39992
40887
  "inspect.isCi": input.isCi,
39993
40888
  "inspect.scoreSurface": input.scoreSurface ?? "score"
39994
- } }));
40889
+ } }), (scanProgram) => flatMap$2(ScanDeadlineMs, (scanDeadlineMs) => scanProgram.pipe(timeout(scanDeadlineMs), catchTag$1("TimeoutError", () => new ReactDoctorError({ reason: new ScanDeadlineExceeded({ detail: `${scanDeadlineMs / MILLISECONDS_PER_SECOND}s elapsed` }) })))));
39995
40890
  const parseNodeVersion = (versionString) => {
39996
40891
  const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
39997
40892
  return {
@@ -40149,7 +41044,7 @@ const materializeSourceTree = (input) => gen(function* () {
40149
41044
  static layerNode = effect(StagedFiles, gen(function* () {
40150
41045
  const git = yield* Git;
40151
41046
  return StagedFiles.of({
40152
- discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile))),
41047
+ discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile)), withSpan("StagedFiles.discoverSourceFiles")),
40153
41048
  materialize: ({ directory, stagedFiles, tempDirectory }) => materializeSourceTree({
40154
41049
  directory,
40155
41050
  files: stagedFiles,
@@ -40159,7 +41054,7 @@ const materializeSourceTree = (input) => gen(function* () {
40159
41054
  tempDirectory: tree.tempDirectory,
40160
41055
  stagedFiles: tree.materializedFiles,
40161
41056
  cleanup: tree.cleanup
40162
- })))
41057
+ })), withSpan("StagedFiles.materialize"))
40163
41058
  });
40164
41059
  }));
40165
41060
  /**
@@ -40227,7 +41122,10 @@ const runEditorScan = async (input) => {
40227
41122
  isCi: false,
40228
41123
  resolveLocalGithubViewerPermission: false,
40229
41124
  skipJsxIncludeFilter: true
40230
- }).pipe(provide(layers), provide(layerOtlp)));
41125
+ }).pipe(withSpan("runEditorScan", { attributes: {
41126
+ "editor.lint": lint,
41127
+ "editor.runDeadCode": runDeadCode
41128
+ } }), provide(layers), provide(layerOtlp)));
40231
41129
  if (isSuccess(exit)) {
40232
41130
  const output = exit.value;
40233
41131
  return {
@@ -40257,7 +41155,7 @@ const runEditorScan = async (input) => {
40257
41155
  didDeadCodeFail: false,
40258
41156
  deadCodeFailureReason: null,
40259
41157
  lintPartialFailures: [],
40260
- error: error instanceof Error ? error.message : String(error)
41158
+ error: messageFromUnknown(error)
40261
41159
  };
40262
41160
  };
40263
41161
  /**
@@ -40309,7 +41207,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
40309
41207
  /** Display name used in client-facing messages and progress titles. */
40310
41208
  const SERVER_DISPLAY_NAME = "React Doctor";
40311
41209
  /** Server version reported in `serverInfo`; injected at build, `dev` from source. */
40312
- const SERVER_VERSION = "0.5.6";
41210
+ const SERVER_VERSION = "0.5.7";
40313
41211
  /** `Diagnostic.source` shown next to every published diagnostic. */
40314
41212
  const DIAGNOSTIC_SOURCE = "react-doctor";
40315
41213
  /**
@@ -40539,7 +41437,6 @@ const toLspDiagnostic = (input) => {
40539
41437
  data
40540
41438
  };
40541
41439
  };
40542
- const toUri = (absoluteFilePath) => fsPathToUri(absoluteFilePath);
40543
41440
  /**
40544
41441
  * Owns the published-diagnostic state. Maps scan outcomes to LSP
40545
41442
  * diagnostics, publishes complete per-URI replacement sets (so the
@@ -40569,7 +41466,7 @@ var DiagnosticsManager = class {
40569
41466
  const isProtectedPath = (fsPath) => protectOpen && this.isOpen(fsPath);
40570
41467
  for (const [fsPath, coreDiagnostics] of outcome.byFile) {
40571
41468
  if (isProtectedPath(fsPath)) continue;
40572
- const uri = toUri(fsPath);
41469
+ const uri = fsPathToUri(fsPath);
40573
41470
  const text = this.textProvider(fsPath);
40574
41471
  const lspDiagnostics = coreDiagnostics.map((diagnostic) => toLspDiagnostic({
40575
41472
  diagnostic,
@@ -40591,7 +41488,7 @@ var DiagnosticsManager = class {
40591
41488
  for (const fsPath of outcome.requestedPaths) {
40592
41489
  if (isProtectedPath(fsPath)) continue;
40593
41490
  if (outcome.byFile.has(fsPath)) continue;
40594
- const uri = toUri(fsPath);
41491
+ const uri = fsPathToUri(fsPath);
40595
41492
  if (this.byUri.has(uri)) this.byUri.delete(uri);
40596
41493
  this.publish(uri, []);
40597
41494
  }
@@ -40616,7 +41513,7 @@ var DiagnosticsManager = class {
40616
41513
  const set = this.projectUris.get(project) ?? /* @__PURE__ */ new Set();
40617
41514
  for (const uri of liveUris) set.add(uri);
40618
41515
  for (const fsPath of outcome.requestedPaths) {
40619
- const uri = toUri(fsPath);
41516
+ const uri = fsPathToUri(fsPath);
40620
41517
  if (!liveUris.has(uri)) set.delete(uri);
40621
41518
  }
40622
41519
  this.projectUris.set(project, set);
@@ -40644,7 +41541,7 @@ var DiagnosticsManager = class {
40644
41541
  const tracked = this.projectUris.get(project);
40645
41542
  if (!tracked) return;
40646
41543
  const liveUris = /* @__PURE__ */ new Set();
40647
- for (const fsPath of liveFsPaths) liveUris.add(toUri(fsPath));
41544
+ for (const fsPath of liveFsPaths) liveUris.add(fsPathToUri(fsPath));
40648
41545
  for (const uri of [...tracked]) {
40649
41546
  if (liveUris.has(uri)) continue;
40650
41547
  this.byUri.delete(uri);
@@ -40941,7 +41838,7 @@ const createProjectGraph = (options) => {
40941
41838
  });
40942
41839
  }
40943
41840
  } catch (error) {
40944
- logger.warn(`Project discovery failed for ${root}: ${error instanceof Error ? error.message : String(error)}`);
41841
+ logger.warn(`Project discovery failed for ${root}: ${messageFromUnknown(error)}`);
40945
41842
  }
40946
41843
  return [...seen.values()].sort((first, second) => second.directory.length - first.directory.length);
40947
41844
  };
@@ -40965,10 +41862,16 @@ const createProjectGraph = (options) => {
40965
41862
  clearPackageJsonCache();
40966
41863
  clearIgnorePatternsCache();
40967
41864
  clearAutoSuppressionCaches();
41865
+ clearMinifiedFileCache();
40968
41866
  projects = null;
40969
41867
  }
40970
41868
  };
40971
41869
  };
41870
+ const toProjectRelative = (projectDirectory, filePath) => {
41871
+ const relative = Path.relative(projectDirectory, filePath).replace(/\\/g, "/");
41872
+ if (relative.length === 0 || relative.startsWith("../") || Path.isAbsolute(relative)) return null;
41873
+ return relative;
41874
+ };
40972
41875
  const resolveCacheFilePath = (projectDirectory) => {
40973
41876
  const nodeModules = path.join(projectDirectory, "node_modules");
40974
41877
  if (fs.existsSync(nodeModules)) return path.join(nodeModules, ".cache", "react-doctor", "lint-cache.json");
@@ -41003,7 +41906,7 @@ const createLintCache = (input) => {
41003
41906
  fs.writeFileSync(tempPath, JSON.stringify(payload));
41004
41907
  fs.renameSync(tempPath, cacheFilePath);
41005
41908
  } catch (error) {
41006
- logger.warn(`Failed to persist lint cache: ${error instanceof Error ? error.message : String(error)}`);
41909
+ logger.warn(`Failed to persist lint cache: ${messageFromUnknown(error)}`);
41007
41910
  }
41008
41911
  };
41009
41912
  return {
@@ -41029,11 +41932,6 @@ const createLintCache = (input) => {
41029
41932
  };
41030
41933
  const OVERLAY_TEMP_PREFIX = "react-doctor-lsp-";
41031
41934
  const OVERLAY_CONFIG_FILENAMES = [...new Set([...STAGED_FILES_PROJECT_CONFIG_FILENAMES, ...ADOPTABLE_LINT_CONFIG_FILENAMES])];
41032
- const toProjectRelative$1 = (projectDirectory, filePath) => {
41033
- const relative = path.relative(projectDirectory, filePath).replace(/\\/g, "/");
41034
- if (relative.length === 0 || relative.startsWith("../") || path.isAbsolute(relative)) return null;
41035
- return relative;
41036
- };
41037
41935
  /**
41038
41936
  * Writes the live (possibly unsaved) content of the target files into a
41039
41937
  * throwaway temp tree that mirrors the project, alongside the well-known
@@ -41047,7 +41945,7 @@ const materializeOverlay = (input) => {
41047
41945
  const relativePaths = [];
41048
41946
  try {
41049
41947
  for (const filePath of input.files) {
41050
- const relative = toProjectRelative$1(input.projectDirectory, filePath);
41948
+ const relative = toProjectRelative(input.projectDirectory, filePath);
41051
41949
  if (relative === null) continue;
41052
41950
  const content = input.readText(filePath);
41053
41951
  if (content === null) continue;
@@ -41095,11 +41993,6 @@ const materializeOverlay = (input) => {
41095
41993
  throw error;
41096
41994
  }
41097
41995
  };
41098
- const toProjectRelative = (projectDirectory, filePath) => {
41099
- const relative = path.relative(projectDirectory, filePath).replace(/\\/g, "/");
41100
- if (relative.length === 0 || relative.startsWith("../") || path.isAbsolute(relative)) return null;
41101
- return relative;
41102
- };
41103
41996
  /**
41104
41997
  * Resolves a diagnostic's (possibly relative, possibly overlay-temp)
41105
41998
  * file path back to the canonical absolute path inside the real project.
@@ -41321,7 +42214,7 @@ const createScheduler = (options) => {
41321
42214
  if (outcome && !token.isCancelled) options.onResult(outcome);
41322
42215
  }).catch((error) => {
41323
42216
  if (options.onError) options.onError(error, request);
41324
- else logger.error(`Scan failed: ${error instanceof Error ? error.message : String(error)}`);
42217
+ else logger.error(`Scan failed: ${messageFromUnknown(error)}`);
41325
42218
  }).finally(() => {
41326
42219
  running -= 1;
41327
42220
  if (isBackground) runningBackground -= 1;
@@ -41708,7 +42601,7 @@ const createServer = (connection, options = {}) => {
41708
42601
  maybeWarnLintUnavailable(outcome);
41709
42602
  if (outcome.request.priority === "background") scanTelemetry.accumulate(outcome);
41710
42603
  },
41711
- onError: (error, request) => logger.error(`Scan of ${request.projectDirectory} threw: ${error instanceof Error ? error.message : String(error)}`),
42604
+ onError: (error, request) => logger.error(`Scan of ${request.projectDirectory} threw: ${messageFromUnknown(error)}`),
41712
42605
  onIdleChange: (idle) => {
41713
42606
  setBusy(!idle);
41714
42607
  if (idle) scanTelemetry.finish();
@@ -42356,5 +43249,5 @@ const startLanguageServer = () => {
42356
43249
  };
42357
43250
  //#endregion
42358
43251
  export { startLanguageServer };
42359
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="03349093-b1b2-5f21-bad3-7e212e5a7f91")}catch(e){}}();
42360
- //# debugId=03349093-b1b2-5f21-bad3-7e212e5a7f91
43252
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="267b748a-e815-5749-9203-b2a80cf11a2f")}catch(e){}}();
43253
+ //# debugId=267b748a-e815-5749-9203-b2a80cf11a2f