react-doctor 0.5.7-dev.9f733f7 → 0.5.8-dev.0c19858

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,7 +8,7 @@ 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";
@@ -6021,7 +6021,7 @@ const composePassthrough = /* @__PURE__ */ dual(2, (left, right) => (input) => {
6021
6021
  * @since 2.0.0
6022
6022
  */
6023
6023
  const Scheduler = /* @__PURE__ */ Reference("effect/Scheduler", { defaultValue: () => new MixedScheduler() });
6024
- const setImmediate = "setImmediate" in globalThis ? (f) => {
6024
+ const setImmediate$1 = "setImmediate" in globalThis ? (f) => {
6025
6025
  const timer = globalThis.setImmediate(f);
6026
6026
  return () => globalThis.clearImmediate(timer);
6027
6027
  } : (f) => {
@@ -6065,7 +6065,7 @@ var PriorityBuckets = class {
6065
6065
  var MixedScheduler = class {
6066
6066
  executionMode;
6067
6067
  setImmediate;
6068
- constructor(executionMode = "async", setImmediateFn = setImmediate) {
6068
+ constructor(executionMode = "async", setImmediateFn = setImmediate$1) {
6069
6069
  this.executionMode = executionMode;
6070
6070
  this.setImmediate = setImmediateFn;
6071
6071
  }
@@ -6090,7 +6090,7 @@ var MixedSchedulerDispatcher = class {
6090
6090
  tasks = /* @__PURE__ */ new PriorityBuckets();
6091
6091
  running = void 0;
6092
6092
  setImmediate;
6093
- constructor(setImmediateFn = setImmediate) {
6093
+ constructor(setImmediateFn = setImmediate$1) {
6094
6094
  this.setImmediate = setImmediateFn;
6095
6095
  }
6096
6096
  /**
@@ -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
@@ -25206,6 +25254,14 @@ const runWith = (self, f, onHalt) => suspend$2(() => {
25206
25254
  return catchDone(flatMap$2(toTransform(self)(done$1(), scope), f), onHalt ? onHalt : succeed$2).pipe(onExit$1((exit) => close(scope, exit)));
25207
25255
  });
25208
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
+ /**
25209
25265
  * Runs a channel and applies an effect to each output element.
25210
25266
  *
25211
25267
  * **Example** (Running effects for each output)
@@ -26594,6 +26650,44 @@ const splitLines = (self) => self.channel.pipe(pipeTo(splitLines$1()), fromChann
26594
26650
  */
26595
26651
  const ensuring = /* @__PURE__ */ dual(2, (self, finalizer) => fromChannel(ensuring$1(self.channel, finalizer)));
26596
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
+ /**
26597
26691
  * Runs a stream with a sink and returns the sink result.
26598
26692
  *
26599
26693
  * **Example** (Running a stream with a sink)
@@ -30023,7 +30117,7 @@ const make$8 = /* @__PURE__ */ fnUntraced(function* (options) {
30023
30117
  const runFork = runForkWith(services);
30024
30118
  const exportInterval = max(fromInputUnsafe(options.exportInterval), zero);
30025
30119
  let disabledUntil = void 0;
30026
- 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({
30027
30121
  schedule: policy,
30028
30122
  times: 3
30029
30123
  }));
@@ -32753,15 +32847,24 @@ const isMinifiedSource = (absolutePath) => {
32753
32847
  if (fileDescriptor !== void 0) NFS.closeSync(fileDescriptor);
32754
32848
  }
32755
32849
  };
32756
- const isLargeMinifiedFile = (absolutePath) => {
32757
- let sizeBytes;
32850
+ const cachedIsLargeMinifiedByPath = /* @__PURE__ */ new Map();
32851
+ const clearMinifiedFileCache = () => {
32852
+ cachedIsLargeMinifiedByPath.clear();
32853
+ };
32854
+ const statSourceFileSize = (absolutePath) => {
32758
32855
  try {
32759
- sizeBytes = NFS.statSync(absolutePath).size;
32856
+ return NFS.statSync(absolutePath).size;
32760
32857
  } catch {
32761
- return false;
32858
+ return null;
32762
32859
  }
32763
- if (sizeBytes < 2e4) return false;
32764
- return isMinifiedSource(absolutePath);
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;
32765
32868
  };
32766
32869
  const isErrnoException = (error) => error instanceof Error && "code" in error;
32767
32870
  const IGNORABLE_READDIR_ERROR_CODES = new Set([
@@ -33627,6 +33730,8 @@ const MILLISECONDS_PER_SECOND = 1e3;
33627
33730
  const SCORE_API_URL = "https://www.react.doctor/api/score";
33628
33731
  const FETCH_TIMEOUT_MS = 1e4;
33629
33732
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
33733
+ const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
33734
+ const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
33630
33735
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
33631
33736
  const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
33632
33737
  const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
@@ -33699,7 +33804,17 @@ const CONFIG_FINGERPRINT_FILENAMES = [
33699
33804
  ];
33700
33805
  const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
33701
33806
  const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
33807
+ const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
33808
+ const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
33809
+ const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
33810
+ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
33811
+ const LINT_PHASE_TIMEOUT_MS = 3e5;
33812
+ const SCAN_TOTAL_DEADLINE_MS = 9e5;
33702
33813
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
33814
+ const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
33815
+ const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
33816
+ const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
33817
+ const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
33703
33818
  const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
33704
33819
  const REACT_SERVER_DOM_PACKAGES = [
33705
33820
  "react-server-dom-webpack",
@@ -33734,9 +33849,13 @@ const CONFIG_CACHE_TTL_MS = 300 * 1e3;
33734
33849
  const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
33735
33850
  const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
33736
33851
  const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
33852
+ const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
33853
+ const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
33737
33854
  const SUPPLY_CHAIN_PLUGIN = "socket";
33738
33855
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
33739
33856
  const SUPPLY_CHAIN_CATEGORY = "Security";
33857
+ const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
33858
+ const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
33740
33859
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
33741
33860
  const TSCONFIG_FILENAME = "tsconfig.json";
33742
33861
  const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
@@ -34429,7 +34548,10 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
34429
34548
  NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
34430
34549
  }
34431
34550
  const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
34432
- const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
34551
+ const canonicalizeRuleKey = (ruleKey) => {
34552
+ const nativeRuleKey = LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey];
34553
+ return typeof nativeRuleKey === "string" ? nativeRuleKey : ruleKey;
34554
+ };
34433
34555
  const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
34434
34556
  const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
34435
34557
  const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
@@ -35091,6 +35213,11 @@ var OxlintBatchExceeded = class extends TaggedErrorClass()("OxlintBatchExceeded"
35091
35213
  }
35092
35214
  }
35093
35215
  };
35216
+ var ScanDeadlineExceeded = class extends TaggedErrorClass()("ScanDeadlineExceeded", { detail: String$1 }) {
35217
+ get message() {
35218
+ return `Scan exceeded its overall time budget: ${this.detail}`;
35219
+ }
35220
+ };
35094
35221
  var OxlintSpawnFailed = class extends TaggedErrorClass()("OxlintSpawnFailed", { cause: Unknown }) {
35095
35222
  get message() {
35096
35223
  return `Failed to run oxlint: ${pretty(fail$6(this.cause))}`;
@@ -35154,6 +35281,7 @@ var GitBaseBranchInvalid = class extends TaggedErrorClass()("GitBaseBranchInvali
35154
35281
  const ReactDoctorErrorReason = Union([
35155
35282
  OxlintUnavailable,
35156
35283
  OxlintBatchExceeded,
35284
+ ScanDeadlineExceeded,
35157
35285
  OxlintSpawnFailed,
35158
35286
  OxlintOutputUnparseable,
35159
35287
  ConfigParseFailed,
@@ -35204,15 +35332,105 @@ const layerOtlp = unwrap$3(gen(function* () {
35204
35332
  }).pipe(provide$2(layer$8));
35205
35333
  }).pipe(orDie));
35206
35334
  /**
35207
- * Resolves a requested lint worker count to a clamped integer within
35208
- * `[MIN_SCAN_CONCURRENCY, MAX_SCAN_CONCURRENCY]`. `"auto"` uses the
35209
- * machine's CPU cores; out-of-range or non-finite requests degrade to
35335
+ * Read a positive-millisecond timeout from an env var, falling back to
35336
+ * `defaultMs` when the var is unset, non-finite, or not strictly positive.
35337
+ */
35338
+ const readPositiveEnvMs = (envVarName, defaultMs) => {
35339
+ const rawValue = process.env[envVarName];
35340
+ if (rawValue === void 0) return defaultMs;
35341
+ const parsedValue = Number(rawValue);
35342
+ if (!Number.isFinite(parsedValue) || parsedValue <= 0) return defaultMs;
35343
+ return parsedValue;
35344
+ };
35345
+ const CGROUP_V2_MEMORY_MAX_PATH = "/sys/fs/cgroup/memory.max";
35346
+ const CGROUP_V1_MEMORY_LIMIT_PATH = "/sys/fs/cgroup/memory/memory.limit_in_bytes";
35347
+ const CGROUP_UNLIMITED_SENTINEL_BYTES = Number.MAX_SAFE_INTEGER;
35348
+ /**
35349
+ * Parses one raw cgroup memory-limit file value into a positive byte count, or
35350
+ * `undefined` when it represents "no limit" (the v2 `"max"` literal, an empty
35351
+ * read, a non-positive / non-finite value, or v1's near-2^63 unlimited
35352
+ * sentinel). Pure and exported so the classification is unit-testable without
35353
+ * touching the filesystem.
35354
+ */
35355
+ const parseCgroupMemoryLimitBytes = (raw) => {
35356
+ if (raw === void 0) return void 0;
35357
+ const trimmed = raw.trim();
35358
+ if (trimmed === "" || trimmed === "max") return void 0;
35359
+ const parsed = Number(trimmed);
35360
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed >= CGROUP_UNLIMITED_SENTINEL_BYTES) return;
35361
+ return parsed;
35362
+ };
35363
+ const CGROUP_MEMORY_LIMIT_PATHS = [CGROUP_V2_MEMORY_MAX_PATH, CGROUP_V1_MEMORY_LIMIT_PATH];
35364
+ /**
35365
+ * Reads this process's cgroup memory limit in bytes from the first candidate
35366
+ * path that yields a real limit, or `undefined` when none does — no cgroup, no
35367
+ * limit, or the files are unreadable (e.g. macOS / Windows dev machines).
35368
+ * `os.totalmem()` reports the HOST total and ignores cgroup memory limits, so a
35369
+ * memory-constrained container over-reports total memory; `resolveAutoScan-
35370
+ * Concurrency` takes `min(totalmem, this)` to honor the limit.
35371
+ *
35372
+ * The cgroup v2 read is the mount-root `memory.max`, which IS the container's
35373
+ * limit under the standard cgroup-namespace setup CI runners use (the
35374
+ * container's own cgroup is the root of its namespaced view). A process in a
35375
+ * non-namespaced nested/delegated cgroup whose root reads `"max"` is not
35376
+ * detected here and falls back to the host total; the EAGAIN/ENOMEM serial
35377
+ * replay in `spawnLintBatches` remains the runtime backstop for that case.
35378
+ *
35379
+ * `candidatePaths` is injectable so tests exercise the v2-wins-over-v1
35380
+ * precedence, the skip-unreadable fallback, and the all-missing case without a
35381
+ * real `/sys/fs/cgroup`.
35382
+ */
35383
+ const readCgroupMemoryLimitBytes = (candidatePaths = CGROUP_MEMORY_LIMIT_PATHS) => {
35384
+ for (const limitPath of candidatePaths) {
35385
+ let raw;
35386
+ try {
35387
+ raw = fs.readFileSync(limitPath, "utf8");
35388
+ } catch {
35389
+ continue;
35390
+ }
35391
+ const limitBytes = parseCgroupMemoryLimitBytes(raw);
35392
+ if (limitBytes !== void 0) return limitBytes;
35393
+ }
35394
+ };
35395
+ /**
35396
+ * Clamps a requested lint worker count to `[MIN_SCAN_CONCURRENCY,
35397
+ * HARD_MAX_SCAN_CONCURRENCY]` as a finite integer. This is the explicit-pin and
35398
+ * spawn-boundary clamp — the memory-and-core-budgeted auto count comes from
35399
+ * `resolveAutoScanConcurrency`. Out-of-range or non-finite requests degrade to
35210
35400
  * `MIN_SCAN_CONCURRENCY` rather than oversubscribing or running zero workers.
35211
35401
  */
35212
35402
  const resolveScanConcurrency = (requested) => {
35213
- const desired = requested === "auto" ? os.availableParallelism() : requested;
35214
- if (!Number.isFinite(desired) || desired < 1) return 1;
35215
- return Math.max(1, Math.min(Math.floor(desired), 16));
35403
+ if (!Number.isFinite(requested) || requested < 1) return 1;
35404
+ return Math.min(Math.floor(requested), 32);
35405
+ };
35406
+ const readSystemFacts$1 = () => ({
35407
+ availableCores: os.availableParallelism(),
35408
+ totalMemoryBytes: os.totalmem(),
35409
+ cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
35410
+ });
35411
+ /**
35412
+ * Auto lint-worker count: the smaller of the (cgroup-CPU-aware) core count and
35413
+ * the number of `PER_WORKER_MEM_BUDGET_BYTES` workers that fit in available
35414
+ * memory, then clamped to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
35415
+ *
35416
+ * `os.availableParallelism()` already respects cgroup CPU quotas, so the core
35417
+ * term needs no help. Available memory is `os.totalmem()` floored by the cgroup
35418
+ * memory limit — `os.freemem()` is deliberately NOT used: it excludes
35419
+ * reclaimable page cache and reads near-zero on macOS / cache-heavy Linux, which
35420
+ * would collapse the auto path to a single worker. `os.totalmem()` reports the
35421
+ * host total even inside a container, so the cgroup limit (read directly,
35422
+ * because Node doesn't fold it into `totalmem()`) is the real ceiling there.
35423
+ *
35424
+ * `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
35425
+ * limited, and ceiling cases without mocking `os` or the filesystem.
35426
+ */
35427
+ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
35428
+ const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
35429
+ const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
35430
+ return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
35431
+ };
35432
+ const resolveLintBatchOrdering = () => {
35433
+ return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "cost" ? "cost" : "arrival";
35216
35434
  };
35217
35435
  /**
35218
35436
  * Per-batch oxlint wall-clock budget. Reads from the env var on
@@ -35220,11 +35438,38 @@ const resolveScanConcurrency = (requested) => {
35220
35438
  * microVMs without recompiling react-doctor. Tests override via
35221
35439
  * `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
35222
35440
  */
35223
- var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
35224
- const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
35225
- if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
35441
+ var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS", OXLINT_SPAWN_TIMEOUT_MS) }) {};
35442
+ /**
35443
+ * Effect-side cap on the lint phase. The env var lets CI / eval runners
35444
+ * raise the phase budget for slow large repos without recompiling.
35445
+ * Tests override via `Layer.succeed(LintPhaseTimeoutMs, ...)`.
35446
+ */
35447
+ var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", LINT_PHASE_TIMEOUT_MS) }) {};
35448
+ /**
35449
+ * Effect-side cap on the dead-code phase, sitting above the in-worker
35450
+ * timeout as a runtime-independent backstop. The env var raises it for
35451
+ * type-heavy projects; tests override via
35452
+ * `Layer.succeed(DeadCodePhaseTimeoutMs, ...)`.
35453
+ */
35454
+ var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", DEAD_CODE_PHASE_TIMEOUT_MS) }) {};
35455
+ /**
35456
+ * Overall scan deadline backstop, bounding everything the per-phase
35457
+ * timeouts don't (wedged git / IO). The env var raises it for very
35458
+ * large repos; tests override via `Layer.succeed(ScanDeadlineMs, ...)`.
35459
+ */
35460
+ var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", SCAN_TOTAL_DEADLINE_MS) }) {};
35461
+ /**
35462
+ * Wall-clock budget for the supply-chain check when it runs on a background
35463
+ * fiber overlapping the lint pass. Reads from the env var on startup so the
35464
+ * eval harness can raise the budget under sandbox microVMs (slower network)
35465
+ * without recompiling react-doctor. Tests override via
35466
+ * `Layer.succeed(SupplyChainOverlapTimeoutMs, ...)`.
35467
+ */
35468
+ var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => {
35469
+ const raw = process.env["REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS"];
35470
+ if (raw === void 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35226
35471
  const parsed = Number(raw);
35227
- if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
35472
+ if (!Number.isFinite(parsed) || parsed <= 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35228
35473
  return parsed;
35229
35474
  } }) {};
35230
35475
  /**
@@ -35235,31 +35480,93 @@ var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTime
35235
35480
  */
35236
35481
  var OxlintOutputMaxBytes = class extends Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
35237
35482
  /**
35238
- * Number of oxlint subprocesses the lint pass runs in parallel. Defaults
35239
- * to auto-detected CPU cores (parallel) so large repos scan fast out of
35240
- * the box; `spawnLintBatches` transparently falls back to a single worker
35241
- * if a parallel run exhausts system resources. The CLI's `--no-parallel`
35242
- * flag forces serial via `Layer.succeed`; the `REACT_DOCTOR_PARALLEL` env
35243
- * var seeds the default for programmatic / CI callers that never touch the
35244
- * flag parallelism is opt-OUT, so only the explicit serial values pin
35245
- * one worker:
35246
- *
35247
- * - unset / `auto` / `true` / `on` → available CPU cores (clamped)
35483
+ * Number of oxlint subprocesses the lint pass runs in parallel. Defaults to a
35484
+ * memory-and-core-budgeted auto count (`resolveAutoScanConcurrency`) so large
35485
+ * repos scan fast out of the box without OOMing the native binding on a
35486
+ * high-core / low-memory box; `spawnLintBatches` transparently falls back to a
35487
+ * single worker if a parallel run still exhausts system resources. The CLI's
35488
+ * `--no-parallel` flag forces serial via `Layer.succeed`; the
35489
+ * `REACT_DOCTOR_PARALLEL` env var seeds the default for programmatic / CI
35490
+ * callers that never touch the flag — parallelism is opt-OUT, so only the
35491
+ * explicit serial values pin one worker:
35492
+ *
35493
+ * - unset / `auto` / `true` / `on` → memory-and-core-budgeted auto count
35248
35494
  * - `0` / `false` / `off` → `1` (serial)
35249
35495
  * - a positive integer → that many workers (clamped)
35250
- * - any other value → available CPU cores (clamped)
35496
+ * - any other value → memory-and-core-budgeted auto count
35251
35497
  *
35252
35498
  * The resolved value is always within
35253
- * `[MIN_SCAN_CONCURRENCY, MAX_SCAN_CONCURRENCY]`.
35499
+ * `[MIN_SCAN_CONCURRENCY, HARD_MAX_SCAN_CONCURRENCY]`.
35254
35500
  */
35255
35501
  var OxlintConcurrency = class extends Reference("react-doctor/OxlintConcurrency", { defaultValue: () => {
35256
35502
  const raw = process.env["REACT_DOCTOR_PARALLEL"];
35257
- if (raw === void 0) return resolveScanConcurrency("auto");
35503
+ if (raw === void 0) return resolveAutoScanConcurrency();
35258
35504
  const normalized = raw.trim().toLowerCase();
35259
35505
  if (normalized === "0" || normalized === "false" || normalized === "off") return 1;
35260
35506
  const parsed = Number.parseInt(normalized, 10);
35261
35507
  if (Number.isInteger(parsed) && parsed > 0) return resolveScanConcurrency(parsed);
35262
- return resolveScanConcurrency("auto");
35508
+ return resolveAutoScanConcurrency();
35509
+ } }) {};
35510
+ /**
35511
+ * Three-state control for overlapping the dead-code pass with the lint pass —
35512
+ * forking dead-code as a child fiber that runs DURING lint instead of strictly
35513
+ * after it.
35514
+ *
35515
+ * - `"auto"` (default) / `"off"` → strictly SEQUENTIAL: dead-code runs after
35516
+ * lint with the full core budget. Both deslop's parse pool and the oxlint
35517
+ * pool are CPU-bound and each size themselves to all cores, so overlapping
35518
+ * them only oversubscribes (~2x the cores) and starves the parse pass past
35519
+ * its timeout — for no wall-clock win, since there are no spare cores to
35520
+ * absorb the second pass. Sequential is both faster per-phase and safe.
35521
+ * - `"on"` → force the overlap anyway. The orchestrator then SPLITS the core
35522
+ * budget (`DEAD_CODE_OVERLAP_PARSE_SHARE`): deslop's parse pool is capped
35523
+ * and lint shrinks to the remainder, so the two sum to the cores instead of
35524
+ * doubling them, and the dead-code timeout scales up for the reduced share.
35525
+ *
35526
+ * Seeded from `REACT_DOCTOR_DEAD_CODE_OVERLAP` so operators get a redeploy-free
35527
+ * switch; tests pin it via `Layer.succeed(DeadCodeOverlap, ...)`.
35528
+ */
35529
+ var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", { defaultValue: () => {
35530
+ const raw = process.env["REACT_DOCTOR_DEAD_CODE_OVERLAP"]?.trim().toLowerCase();
35531
+ if (raw === "on" || raw === "true" || raw === "1") return "on";
35532
+ if (raw === "off" || raw === "false" || raw === "0") return "off";
35533
+ return "auto";
35534
+ } }) {};
35535
+ /**
35536
+ * How the full-scan lint pass orders its file batches. `"arrival"` (the
35537
+ * default) keeps `git ls-files` discovery order. `"cost"` opts into LPT (feed
35538
+ * the largest files first); set `REACT_DOCTOR_LINT_BATCH_ORDERING=cost`. NOTE:
35539
+ * `cost` is OFF by default because the current sort-desc-then-chunk-100 packs
35540
+ * the heaviest files into one wave-1 batch — on size-skewed repos that mega-
35541
+ * batch is a straggler (and can trip the per-batch timeout + split), measurably
35542
+ * regressing the common full-scan case. LPT needs the heavy files SPREAD across
35543
+ * batches before `cost` earns the default. Tests override via
35544
+ * `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans never reach this
35545
+ * — they pass user-scoped `includePaths` that skip discovery and stay in
35546
+ * arrival order; only the full-scan branch reads it.
35547
+ */
35548
+ var LintBatchOrdering = class extends Reference("react-doctor/LintBatchOrdering", { defaultValue: resolveLintBatchOrdering }) {};
35549
+ const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
35550
+ /**
35551
+ * Whether the per-file lint cache (`runners/oxlint/file-lint-cache.ts`) is
35552
+ * active. Defaults ON — repeat scans re-lint only the files whose content
35553
+ * changed, and correctness is guaranteed byte-identical to a cold scan by the
35554
+ * always-fresh cross-file sidecar. Opt-OUT, two knobs (matching the whole-repo
35555
+ * scan cache's `REACT_DOCTOR_NO_CACHE`):
35556
+ *
35557
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch; disables BOTH the
35558
+ * whole-repo scan cache and this per-file cache.
35559
+ * - `REACT_DOCTOR_NO_FILE_CACHE` — granular: bust only the per-file cache
35560
+ * while keeping the whole-repo short-circuit.
35561
+ *
35562
+ * Tests override via `Layer.succeed(PerFileLintCacheEnabled, false)`.
35563
+ */
35564
+ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintCacheEnabled", { defaultValue: () => {
35565
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
35566
+ const noFileCache = process.env["REACT_DOCTOR_NO_FILE_CACHE"]?.toLowerCase() ?? "";
35567
+ if (CACHE_DISABLED_VALUES.has(noCache)) return false;
35568
+ if (CACHE_DISABLED_VALUES.has(noFileCache)) return false;
35569
+ return true;
35263
35570
  } }) {};
35264
35571
  const DIAGNOSTIC_SURFACES = [
35265
35572
  "cli",
@@ -35308,6 +35615,12 @@ const BOOLEAN_FIELD_NAMES = [
35308
35615
  "adoptExistingLintConfig"
35309
35616
  ];
35310
35617
  const STRING_FIELD_NAMES = ["rootDir"];
35618
+ const STRING_ARRAY_FIELD_NAMES = [
35619
+ "projects",
35620
+ "textComponents",
35621
+ "rawTextWrapperComponents",
35622
+ "serverAuthFunctionNames"
35623
+ ];
35311
35624
  const SURFACE_CONTROL_FIELD_NAMES = [
35312
35625
  "includeTags",
35313
35626
  "excludeTags",
@@ -35409,6 +35722,7 @@ const validateConfigTypes = (config) => {
35409
35722
  const validated = { ...config };
35410
35723
  for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
35411
35724
  for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
35725
+ for (const fieldName of STRING_ARRAY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateStringArrayField(fieldName, value));
35412
35726
  applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
35413
35727
  for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
35414
35728
  applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
@@ -35617,6 +35931,8 @@ const assignFixGroups = (diagnostics) => {
35617
35931
  };
35618
35932
  });
35619
35933
  };
35934
+ const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
35935
+ 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));
35620
35936
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
35621
35937
  const buildExpoCheckContext = (rootDirectory, expoVersion) => {
35622
35938
  const packageJson = readPackageJson(Path.join(rootDirectory, "package.json"));
@@ -36593,7 +36909,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
36593
36909
  }
36594
36910
  return true;
36595
36911
  };
36596
- const checkSecurityScan = (rootDirectory, options = {}) => {
36912
+ const yieldToEventLoop = () => new Promise((resolve) => {
36913
+ setImmediate(resolve);
36914
+ });
36915
+ const createSecurityScanSession = (rootDirectory, options) => {
36597
36916
  const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
36598
36917
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
36599
36918
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
@@ -36608,7 +36927,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36608
36927
  committedFilesOnly: rule.committedFilesOnly === true
36609
36928
  }];
36610
36929
  });
36611
- if (enabledScanRules.length === 0) return [];
36930
+ if (enabledScanRules.length === 0) return null;
36612
36931
  const diagnostics = [];
36613
36932
  const seen = /* @__PURE__ */ new Set();
36614
36933
  const gitIgnoredCache = /* @__PURE__ */ new Map();
@@ -36620,15 +36939,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36620
36939
  }
36621
36940
  return status === true;
36622
36941
  };
36623
- for (const file of collectSecurityScanFiles(rootDirectory)) for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36624
- if (committedFilesOnly && isFileGitIgnored(file)) continue;
36625
- const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36626
- const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36627
- if (seen.has(key)) continue;
36628
- seen.add(key);
36629
- diagnostics.push(diagnostic);
36942
+ const scanFile = (file) => {
36943
+ for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36944
+ if (committedFilesOnly && isFileGitIgnored(file)) continue;
36945
+ const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36946
+ const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36947
+ if (seen.has(key)) continue;
36948
+ seen.add(key);
36949
+ diagnostics.push(diagnostic);
36950
+ }
36951
+ };
36952
+ return {
36953
+ scanFile,
36954
+ diagnostics
36955
+ };
36956
+ };
36957
+ const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
36958
+ const session = createSecurityScanSession(rootDirectory, options);
36959
+ if (session === null) return [];
36960
+ let filesSinceYield = 0;
36961
+ for (const file of collectSecurityScanFiles(rootDirectory)) {
36962
+ session.scanFile(file);
36963
+ filesSinceYield += 1;
36964
+ if (filesSinceYield >= 16) {
36965
+ filesSinceYield = 0;
36966
+ await yieldToEventLoop();
36967
+ }
36630
36968
  }
36631
- return diagnostics;
36969
+ return session.diagnostics;
36632
36970
  };
36633
36971
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
36634
36972
  let p = process || {}, argv = p.argv || [], env = p.env || {};
@@ -36845,6 +37183,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
36845
37183
  return [...seen].filter((pattern) => pattern.length > 0);
36846
37184
  };
36847
37185
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
37186
+ const readSystemFacts = () => ({
37187
+ availableCores: os.availableParallelism(),
37188
+ totalMemoryBytes: os.totalmem(),
37189
+ cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
37190
+ });
37191
+ /**
37192
+ * How many real deslop dead-code child processes may run at once, across the
37193
+ * concurrent per-project `runInspect` fibers of one CLI run. The cap is the
37194
+ * smaller of the core count and the number of `DEAD_CODE_WORKER_MEM_BUDGET_BYTES`
37195
+ * workers that fit in available memory, floored at 1.
37196
+ *
37197
+ * On a roomy dev box / CI runner this resolves high enough that every
37198
+ * concurrently-scanned project still spawns its own worker (no serialization vs
37199
+ * the prior uncapped behavior); on a memory-constrained runner it collapses
37200
+ * toward 1, so the `withDeadCodeWorkerSlot` semaphore serializes the spawns
37201
+ * instead of oversubscribing memory with N simultaneous children — the global
37202
+ * cap the per-project spawn path lacked.
37203
+ *
37204
+ * Mirrors `resolveAutoScanConcurrency` (lint), but budgets memory per the
37205
+ * heavier dead-code worker. `facts` is injectable for tests.
37206
+ */
37207
+ const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
37208
+ const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
37209
+ const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
37210
+ return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
37211
+ };
37212
+ let availableSlots = -1;
37213
+ const waiters = [];
37214
+ const releaseSlot = () => {
37215
+ const nextWaiter = waiters.shift();
37216
+ if (nextWaiter !== void 0) nextWaiter();
37217
+ else availableSlots += 1;
37218
+ };
37219
+ /**
37220
+ * Runs `task` once a dead-code worker slot is free, releasing the slot when the
37221
+ * task settles (success or failure). With a high cap (roomy machine) every
37222
+ * caller proceeds immediately; with a low cap (constrained runner) callers
37223
+ * queue and run as slots free.
37224
+ *
37225
+ * `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
37226
+ * this caller is queued, the call rejects without acquiring a slot or running
37227
+ * `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
37228
+ * then spawn a child only to tear it down. A queued caller that aborts removes
37229
+ * its own waiter so a later release never hands a slot to a dead request.
37230
+ */
37231
+ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
37232
+ if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
37233
+ if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
37234
+ if (availableSlots > 0) availableSlots -= 1;
37235
+ else await new Promise((resolve, reject) => {
37236
+ const waiter = () => {
37237
+ abortSignal?.removeEventListener("abort", onAbort);
37238
+ resolve();
37239
+ };
37240
+ const onAbort = () => {
37241
+ const queuedIndex = waiters.indexOf(waiter);
37242
+ if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
37243
+ reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
37244
+ };
37245
+ waiters.push(waiter);
37246
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
37247
+ });
37248
+ try {
37249
+ return await task();
37250
+ } finally {
37251
+ releaseSlot();
37252
+ }
37253
+ };
36848
37254
  /**
36849
37255
  * Resolves a path to its canonical, symlink-free form, falling back to
36850
37256
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -36916,6 +37322,22 @@ process.stdin.on("end", () => {
36916
37322
  ...(workerInput.ignorePatterns.length > 0
36917
37323
  ? { ignorePatterns: workerInput.ignorePatterns }
36918
37324
  : {}),
37325
+ // We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
37326
+ // unusedDependencies, circularDependencies). Everything else deslop can compute
37327
+ // is pure wasted work for us, and it's the bulk of the runtime:
37328
+ // - semantic: a full TS Program for unusedTypes/enum/class-members/
37329
+ // misclassifiedDependencies (~37-45% of the phase).
37330
+ // - reportCodeQuality: the duplicate-block, complexity, feature-flag,
37331
+ // TypeScript-smell, private-type-leak and re-export-cycle detectors. These
37332
+ // are the single most expensive pass — duplicate-block detection alone was
37333
+ // ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
37334
+ // speedup on a large repo.
37335
+ // Both are provably safe: the consumed graph findings are computed by their own
37336
+ // detectors, independent of these passes (confirmed byte-identical on
37337
+ // excalidraw + mui-material + sentry). tsConfigPath stays — the module resolver
37338
+ // needs it for path-alias resolution in the import graph.
37339
+ semantic: { enabled: false },
37340
+ reportCodeQuality: false,
36919
37341
  };
36920
37342
  const result = await analyze(defineConfig(config));
36921
37343
  emit({ ok: true, result: normalizeResult(result) });
@@ -37045,7 +37467,11 @@ const createDeadCodeWorker = (input) => {
37045
37467
  "pipe",
37046
37468
  "pipe"
37047
37469
  ],
37048
- windowsHide: true
37470
+ windowsHide: true,
37471
+ env: input.parseConcurrency === void 0 ? process.env : {
37472
+ ...process.env,
37473
+ DESLOP_PARSE_CONCURRENCY: String(input.parseConcurrency)
37474
+ }
37049
37475
  });
37050
37476
  const stdoutChunks = [];
37051
37477
  const stderrChunks = [];
@@ -37090,41 +37516,42 @@ const createDeadCodeWorker = (input) => {
37090
37516
  }
37091
37517
  };
37092
37518
  };
37093
- const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
37519
+ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs, abortSignal) => new Promise((resolve, reject) => {
37094
37520
  let didSettle = false;
37095
- const timeoutHandle = setTimeout(() => {
37096
- if (didSettle) return;
37097
- didSettle = true;
37098
- handle.terminate?.();
37099
- reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
37100
- }, timeoutMs);
37101
- timeoutHandle.unref?.();
37102
- handle.result.then((value) => {
37521
+ const settle = (finish) => {
37103
37522
  if (didSettle) return;
37104
37523
  didSettle = true;
37105
37524
  clearTimeout(timeoutHandle);
37525
+ abortSignal?.removeEventListener("abort", onAbort);
37106
37526
  handle.terminate?.();
37107
- resolve(value);
37108
- }, (error) => {
37109
- if (didSettle) return;
37110
- didSettle = true;
37111
- clearTimeout(timeoutHandle);
37112
- handle.terminate?.();
37113
- reject(error);
37114
- });
37527
+ finish();
37528
+ };
37529
+ const onAbort = () => settle(() => reject(/* @__PURE__ */ new Error("Dead-code worker aborted.")));
37530
+ const timeoutHandle = setTimeout(() => settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`))), timeoutMs);
37531
+ timeoutHandle.unref?.();
37532
+ if (abortSignal?.aborted) {
37533
+ onAbort();
37534
+ return;
37535
+ }
37536
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
37537
+ handle.result.then((value) => settle(() => resolve(value)), (error) => settle(() => reject(error)));
37115
37538
  });
37116
37539
  const checkDeadCode = async (options) => {
37117
37540
  const rootDirectory = toCanonicalPath(options.rootDirectory);
37118
37541
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37119
37542
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37120
37543
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37121
- const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37122
- rootDirectory,
37123
- entryPatterns,
37124
- tsConfigPath: resolveTsConfigPath(rootDirectory),
37125
- ignorePatterns,
37126
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
37127
- }), options.workerTimeoutMs ?? 12e4));
37544
+ const spawnAndRun = () => {
37545
+ return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37546
+ rootDirectory,
37547
+ entryPatterns,
37548
+ tsConfigPath: resolveTsConfigPath(rootDirectory),
37549
+ ignorePatterns,
37550
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37551
+ parseConcurrency: options.parseConcurrency
37552
+ }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
37553
+ };
37554
+ const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
37128
37555
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
37129
37556
  const diagnostics = [];
37130
37557
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -37222,7 +37649,37 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
37222
37649
  return true;
37223
37650
  };
37224
37651
  const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
37225
- const excludeMinifiedFiles = (rootDirectory, relativePaths) => relativePaths.filter((relativePath) => !isLargeMinifiedFile(Path.resolve(rootDirectory, relativePath)));
37652
+ /**
37653
+ * Budget for the dead-code phase, scaled to the work. deslop's graph build is
37654
+ * CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
37655
+ * for a large repo (where the pass legitimately runs that long) and is then
37656
+ * tipped over by any concurrent load — silently dropping every dead-code
37657
+ * finding. Scaling the budget with file count (and inversely with the core
37658
+ * share when overlapped) lets the pass complete, while the ceiling still
37659
+ * reclaims a genuinely wedged worker. Returns the in-worker SIGKILL deadline
37660
+ * and the Effect-side phase backstop that sits a margin above it.
37661
+ */
37662
+ const resolveDeadCodeTimeout = (input) => {
37663
+ const coreShareFactor = Math.max(1, input.fullConcurrency / Math.max(1, input.deadCodeConcurrency));
37664
+ const workerTimeoutMs = Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor)));
37665
+ return {
37666
+ workerTimeoutMs,
37667
+ phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
37668
+ };
37669
+ };
37670
+ const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
37671
+ const entries = [];
37672
+ for (const relativePath of relativePaths) {
37673
+ const absolutePath = Path.resolve(rootDirectory, relativePath);
37674
+ const sizeBytes = statSourceFileSize(absolutePath);
37675
+ if (isLargeMinifiedFile(absolutePath, sizeBytes)) continue;
37676
+ entries.push({
37677
+ path: relativePath,
37678
+ sizeBytes: sizeBytes ?? 0
37679
+ });
37680
+ }
37681
+ return entries;
37682
+ };
37226
37683
  const listSourceFilesViaGit = (rootDirectory) => {
37227
37684
  const result = spawnSync("git", [
37228
37685
  "ls-files",
@@ -37255,7 +37712,8 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
37255
37712
  }
37256
37713
  return filePaths;
37257
37714
  };
37258
- const listSourceFiles = (rootDirectory) => excludeMinifiedFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
37715
+ const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
37716
+ const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
37259
37717
  const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
37260
37718
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
37261
37719
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
@@ -37298,9 +37756,12 @@ var Config = class Config extends Service()("react-doctor/Config") {
37298
37756
  var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37299
37757
  static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
37300
37758
  return yield* tryPromise({
37301
- try: () => checkDeadCode({
37759
+ try: (signal) => checkDeadCode({
37302
37760
  rootDirectory: input.rootDirectory,
37303
- userConfig: input.userConfig
37761
+ userConfig: input.userConfig,
37762
+ parseConcurrency: input.parseConcurrency,
37763
+ workerTimeoutMs: input.workerTimeoutMs,
37764
+ abortSignal: signal
37304
37765
  }),
37305
37766
  catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
37306
37767
  }).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
@@ -37491,43 +37952,46 @@ var Git = class Git extends Service()("react-doctor/Git") {
37491
37952
  * reason: GitInvocationFailed })` so the rest of the codebase
37492
37953
  * sees a single failure channel.
37493
37954
  */
37494
- const runCommand = (input) => scoped(gen(function* () {
37495
- const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37496
- cwd: input.directory,
37497
- env: input.env,
37498
- extendEnv: true
37499
- }));
37500
- const maxStdoutBytes = input.maxStdoutBytes;
37501
- const stdoutByteCount = yield* make$13(0);
37502
- const [stdout, stderr, status] = yield* all([
37503
- mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37504
- args: [...input.args],
37505
- directory: input.directory,
37506
- cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
37507
- }) })) : void_)))))),
37508
- mkString(decodeText(handle.stderr)),
37509
- handle.exitCode
37510
- ], { concurrency: 3 });
37511
- return {
37512
- status,
37513
- stdout,
37514
- stderr
37515
- };
37516
- })).pipe(catchTag$1("PlatformError", (cause) => {
37517
- if (input.command !== "git") return succeed$2({
37955
+ const runCommand = (input) => {
37956
+ const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
37518
37957
  status: 127,
37519
37958
  stdout: "",
37520
37959
  stderr: String(cause)
37521
- });
37522
- return new ReactDoctorError({ reason: new GitInvocationFailed({
37960
+ }) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37523
37961
  args: [...input.args],
37524
37962
  directory: input.directory,
37525
37963
  cause
37526
- }) });
37527
- }), withSpan("git.exec", { attributes: {
37528
- "git.command": input.command,
37529
- "git.subcommand": input.args[0] ?? ""
37530
- } }));
37964
+ }) }));
37965
+ return scoped(gen(function* () {
37966
+ if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
37967
+ const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
37968
+ if (argvLengthChars > 24e3) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${SPAWN_ARGS_MAX_LENGTH_CHARS})`);
37969
+ const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37970
+ cwd: input.directory,
37971
+ env: input.env,
37972
+ extendEnv: true
37973
+ }));
37974
+ const maxStdoutBytes = input.maxStdoutBytes;
37975
+ const stdoutByteCount = yield* make$13(0);
37976
+ const [stdout, stderr, status] = yield* all([
37977
+ mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37978
+ args: [...input.args],
37979
+ directory: input.directory,
37980
+ cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
37981
+ }) })) : void_)))))),
37982
+ mkString(decodeText(handle.stderr)),
37983
+ handle.exitCode
37984
+ ], { concurrency: 3 });
37985
+ return {
37986
+ status,
37987
+ stdout,
37988
+ stderr
37989
+ };
37990
+ })).pipe(catchTag$1("PlatformError", foldSpawnFailure), withSpan("git.exec", { attributes: {
37991
+ "git.command": input.command,
37992
+ "git.subcommand": input.args[0] ?? ""
37993
+ } }));
37994
+ };
37531
37995
  const runGit = (directory, args) => runCommand({
37532
37996
  command: "git",
37533
37997
  args,
@@ -37560,7 +38024,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37560
38024
  "rev-parse",
37561
38025
  "--verify",
37562
38026
  branch
37563
- ]).pipe(map$3((result) => result.status === 0));
38027
+ ]).pipe(map$3((result) => result.status === 0), catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(false) : fail$4(error)));
37564
38028
  const headSha = (directory) => runGit(directory, ["rev-parse", "HEAD"]).pipe(map$3((result) => result.status === 0 ? trimOrNull(result.stdout) : null));
37565
38029
  const mergeBase = (input) => isSafeGitRevision(input.ref) ? runGit(input.directory, [
37566
38030
  "merge-base",
@@ -37774,7 +38238,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37774
38238
  ]);
37775
38239
  if (result.status !== 0) return null;
37776
38240
  return parseChangedLineRanges(result.stdout);
37777
- }).pipe(withSpan("Git.changedLineRanges"))
38241
+ }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
37778
38242
  });
37779
38243
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
37780
38244
  /**
@@ -38013,6 +38477,14 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
38013
38477
  process.removeListener("exit", onExit);
38014
38478
  };
38015
38479
  };
38480
+ const ROOT_DIRECTORY_PLACEHOLDER = "<root>";
38481
+ const normalizeConfigForHash = (config) => {
38482
+ const clone = JSON.parse(JSON.stringify(config));
38483
+ if (clone?.settings?.["react-doctor"]) clone.settings["react-doctor"].rootDirectory = ROOT_DIRECTORY_PLACEHOLDER;
38484
+ if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
38485
+ return clone;
38486
+ };
38487
+ 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");
38016
38488
  /**
38017
38489
  * Loads a plugin module via the local require resolver and extracts
38018
38490
  * `(name, ruleNames)` from either `module.exports.meta + rules` or
@@ -38039,16 +38511,16 @@ const readPluginShape = (pluginSpecifier, loadModule) => {
38039
38511
  ruleNames: new Set(Object.keys(rules))
38040
38512
  };
38041
38513
  };
38042
- const bundledRequire = createRequire(import.meta.url);
38514
+ const bundledRequire$1 = createRequire(import.meta.url);
38043
38515
  const resolveReactHooksJsPlugin = (hasReactCompiler, customRulesOnly) => {
38044
38516
  if (!hasReactCompiler || customRulesOnly) return null;
38045
38517
  let pluginSpecifier;
38046
38518
  try {
38047
- pluginSpecifier = bundledRequire.resolve("eslint-plugin-react-hooks");
38519
+ pluginSpecifier = bundledRequire$1.resolve("eslint-plugin-react-hooks");
38048
38520
  } catch {
38049
38521
  return null;
38050
38522
  }
38051
- const { ruleNames } = readPluginShape(pluginSpecifier, (spec) => bundledRequire(spec));
38523
+ const { ruleNames } = readPluginShape(pluginSpecifier, (spec) => bundledRequire$1(spec));
38052
38524
  return {
38053
38525
  entry: {
38054
38526
  name: "react-hooks-js",
@@ -38167,8 +38639,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
38167
38639
  }
38168
38640
  return enabled;
38169
38641
  };
38170
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
38171
- const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38642
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
38643
+ const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38172
38644
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
38173
38645
  const jsPlugins = [];
38174
38646
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -38177,6 +38649,8 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38177
38649
  for (const registryEntry of REACT_DOCTOR_RULES) {
38178
38650
  const rule = reactDoctorPlugin.rules[registryEntry.id];
38179
38651
  if (!rule) continue;
38652
+ if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
38653
+ if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
38180
38654
  if (rule.scan !== void 0) continue;
38181
38655
  if (customRulesOnly && registryEntry.originallyExternal) continue;
38182
38656
  if (rule.framework !== "global" && !rule.requires) continue;
@@ -38191,7 +38665,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38191
38665
  enabledReactDoctorRules[registryEntry.key] = severity;
38192
38666
  }
38193
38667
  const userPluginRules = {};
38194
- for (const userPlugin of userPlugins) {
38668
+ if (ruleSelection !== "sidecar") for (const userPlugin of userPlugins) {
38195
38669
  Object.assign(userPluginRules, buildUserPluginRules(userPlugin, severityControls));
38196
38670
  jsPlugins.push(userPlugin.entry);
38197
38671
  }
@@ -38221,6 +38695,100 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38221
38695
  }
38222
38696
  };
38223
38697
  };
38698
+ const atomicWriteJson = (filePath, value) => {
38699
+ try {
38700
+ NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
38701
+ const temporaryPath = `${filePath}.${process.pid}.tmp`;
38702
+ NFS.writeFileSync(temporaryPath, JSON.stringify(value));
38703
+ NFS.renameSync(temporaryPath, filePath);
38704
+ } catch {
38705
+ return;
38706
+ }
38707
+ };
38708
+ const failOpenReadJson = (filePath, fallback) => {
38709
+ try {
38710
+ return JSON.parse(NFS.readFileSync(filePath, "utf8"));
38711
+ } catch {
38712
+ return fallback;
38713
+ }
38714
+ };
38715
+ const validateDiagnostic = decodeUnknownSync(Diagnostic);
38716
+ const decodeFileDiagnostics = (raw) => {
38717
+ if (!Array.isArray(raw)) return null;
38718
+ try {
38719
+ for (const entry of raw) validateDiagnostic(entry);
38720
+ return raw;
38721
+ } catch {
38722
+ return null;
38723
+ }
38724
+ };
38725
+ const emptyCache = () => ({
38726
+ version: 1,
38727
+ rulesets: {}
38728
+ });
38729
+ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
38730
+ const entries = /* @__PURE__ */ new Map();
38731
+ const persisted = failOpenReadJson(cacheFilePath, emptyCache());
38732
+ if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
38733
+ const bucket = persisted.rulesets[rulesetHash];
38734
+ if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
38735
+ for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
38736
+ const decoded = decodeFileDiagnostics(rawDiagnostics);
38737
+ if (decoded !== null) entries.set(fileKey, decoded);
38738
+ }
38739
+ return entries;
38740
+ };
38741
+ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38742
+ const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
38743
+ const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
38744
+ return {
38745
+ lookup: (fileKey) => entries.get(fileKey) ?? null,
38746
+ store: (fileKey, diagnostics) => {
38747
+ entries.delete(fileKey);
38748
+ entries.set(fileKey, diagnostics);
38749
+ },
38750
+ persist: () => {
38751
+ const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
38752
+ const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
38753
+ const existingBucket = rulesets[rulesetHash];
38754
+ const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
38755
+ const ourFiles = {};
38756
+ for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
38757
+ const cappedEntries = Object.entries({
38758
+ ...existingFiles,
38759
+ ...ourFiles
38760
+ }).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
38761
+ rulesets[rulesetHash] = {
38762
+ updatedAtMs: Date.now(),
38763
+ files: Object.fromEntries(cappedEntries)
38764
+ };
38765
+ const keptHashes = Object.entries(rulesets).sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8).map(([hash]) => hash);
38766
+ const prunedRulesets = {};
38767
+ for (const hash of keptHashes) prunedRulesets[hash] = rulesets[hash];
38768
+ atomicWriteJson(cacheFilePath, {
38769
+ version: 1,
38770
+ rulesets: prunedRulesets
38771
+ });
38772
+ }
38773
+ };
38774
+ };
38775
+ const bundledRequire = createRequire(import.meta.url);
38776
+ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
38777
+ "oxlint/package.json",
38778
+ "oxlint-plugin-react-doctor/package.json",
38779
+ "eslint-plugin-react-hooks/package.json"
38780
+ ];
38781
+ const resolveOxlintToolchainVersions = () => {
38782
+ const versions = [`node=${process.version}`];
38783
+ for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
38784
+ const packageJson = bundledRequire(specifier);
38785
+ const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
38786
+ versions.push(`${specifier}=${version}`);
38787
+ } catch {
38788
+ versions.push(`${specifier}=missing`);
38789
+ }
38790
+ return versions;
38791
+ };
38224
38792
  const esmRequire = createRequire(import.meta.url);
38225
38793
  const resolveOxlintBinary = () => {
38226
38794
  const oxlintMainPath = esmRequire.resolve("oxlint");
@@ -38902,15 +39470,19 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
38902
39470
  };
38903
39471
  });
38904
39472
  };
38905
- const SANITIZED_ENV = (() => {
38906
- const sanitized = {};
38907
- for (const [name, value] of Object.entries(process.env)) {
39473
+ const buildOxlintChildEnv = (sourceEnv) => {
39474
+ const childEnv = {};
39475
+ for (const [name, value] of Object.entries(sourceEnv)) {
38908
39476
  if (name === "NODE_OPTIONS" || name === "NODE_DEBUG") continue;
38909
39477
  if (name.startsWith("npm_config_")) continue;
38910
- sanitized[name] = value;
39478
+ childEnv[name] = value;
38911
39479
  }
38912
- return sanitized;
38913
- })();
39480
+ const isCompileCacheDisabled = Boolean(sourceEnv.NODE_DISABLE_COMPILE_CACHE);
39481
+ const isCompileCacheAlreadySet = childEnv.NODE_COMPILE_CACHE !== void 0;
39482
+ if (!isCompileCacheDisabled && !isCompileCacheAlreadySet) childEnv.NODE_COMPILE_CACHE = Path.join(os.tmpdir(), NODE_COMPILE_CACHE_DIR_NAME);
39483
+ return childEnv;
39484
+ };
39485
+ const SANITIZED_ENV = buildOxlintChildEnv(process.env);
38914
39486
  /**
38915
39487
  * Spawn one oxlint subprocess with hard ceilings on wall time and
38916
39488
  * output size. Returns stdout on success; raises a tagged
@@ -38927,7 +39499,11 @@ const SANITIZED_ENV = (() => {
38927
39499
  * The first three are splittable (the caller's binary-split retry
38928
39500
  * shrinks the batch and re-spawns); the fourth isn't.
38929
39501
  */
38930
- const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
39502
+ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES, abortSignal) => new Promise((resolve, reject) => {
39503
+ if (abortSignal?.aborted) {
39504
+ reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
39505
+ return;
39506
+ }
38931
39507
  const child = spawn(nodeBinaryPath, args, {
38932
39508
  cwd: rootDirectory,
38933
39509
  env: SANITIZED_ENV,
@@ -38937,7 +39513,14 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
38937
39513
  "pipe"
38938
39514
  ]
38939
39515
  });
39516
+ const onAbort = () => {
39517
+ child.kill("SIGKILL");
39518
+ reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
39519
+ };
39520
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
39521
+ const clearAbortListener = () => abortSignal?.removeEventListener("abort", onAbort);
38940
39522
  const timeoutHandle = setTimeout(() => {
39523
+ clearAbortListener();
38941
39524
  child.kill("SIGKILL");
38942
39525
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
38943
39526
  kind: "timeout",
@@ -38972,10 +39555,12 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
38972
39555
  });
38973
39556
  child.on("error", (error) => {
38974
39557
  clearTimeout(timeoutHandle);
39558
+ clearAbortListener();
38975
39559
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: error }) }));
38976
39560
  });
38977
39561
  child.on("close", (_code, signal) => {
38978
39562
  clearTimeout(timeoutHandle);
39563
+ clearAbortListener();
38979
39564
  if (didKillForSize) {
38980
39565
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
38981
39566
  kind: "output-too-large",
@@ -39042,26 +39627,28 @@ const isParallelismRelatedSpawnError = (error) => {
39042
39627
  * loop with a slimmer config in that case.
39043
39628
  */
39044
39629
  const spawnLintBatches = async (input) => {
39045
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
39630
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal } = input;
39046
39631
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
39047
39632
  const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
39048
39633
  const runBatchPass = async (concurrency) => {
39049
39634
  const allDiagnostics = [];
39050
39635
  const droppedFiles = [];
39051
39636
  let firstDropReason = null;
39052
- const spawnLintBatch = async (batch) => {
39637
+ const splitDeadlineMs = Date.now() + splitTotalBudgetMs;
39638
+ const spawnLintBatch = async (batch, depth) => {
39053
39639
  const batchArgs = [...baseArgs, ...batch];
39054
39640
  try {
39055
- return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
39641
+ return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
39056
39642
  } catch (error) {
39057
39643
  if (!isSplittableReactDoctorError(error)) throw error;
39058
- if (batch.length <= 1) {
39644
+ const splitBudgetExhausted = Date.now() >= splitDeadlineMs || depth >= splitMaxDepth;
39645
+ if (batch.length <= 1 || splitBudgetExhausted) {
39059
39646
  droppedFiles.push(...batch);
39060
- if (firstDropReason === null) firstDropReason = error.message;
39647
+ if (firstDropReason === null) firstDropReason = splitBudgetExhausted && batch.length > 1 ? `${error.message} (split budget exhausted after ${splitMaxDepth} levels / ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s)` : error.message;
39061
39648
  return [];
39062
39649
  }
39063
39650
  const splitIndex = Math.ceil(batch.length / 2);
39064
- return [...await spawnLintBatch(batch.slice(0, splitIndex)), ...await spawnLintBatch(batch.slice(splitIndex))];
39651
+ return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1)];
39065
39652
  }
39066
39653
  };
39067
39654
  let startedFileCount = 0;
@@ -39078,7 +39665,7 @@ const spawnLintBatches = async (input) => {
39078
39665
  try {
39079
39666
  const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
39080
39667
  startedFileCount += batch.length;
39081
- const batchDiagnostics = await spawnLintBatch(batch);
39668
+ const batchDiagnostics = await spawnLintBatch(batch, 0);
39082
39669
  scannedFileCount += batch.length;
39083
39670
  if (onFileProgress) {
39084
39671
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
@@ -39139,6 +39726,22 @@ const validateRuleRegistration = () => {
39139
39726
  ].filter((entry) => entry !== null).join("; ");
39140
39727
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
39141
39728
  };
39729
+ const hashFileContents = (filePath) => {
39730
+ try {
39731
+ return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
39732
+ } catch {
39733
+ return null;
39734
+ }
39735
+ };
39736
+ const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
39737
+ const resolveReactDoctorCacheDir = (projectDirectory) => {
39738
+ const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
39739
+ if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
39740
+ const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
39741
+ if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
39742
+ return Path.join(os.tmpdir(), "react-doctor-cache", projectCacheSubdir(projectDirectory));
39743
+ };
39744
+ const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
39142
39745
  /**
39143
39746
  * Atomically (re)writes the generated oxlintrc.json. Used twice in
39144
39747
  * the runner: once for the primary scan, once for the
@@ -39197,7 +39800,7 @@ const reactHooksJsPluginDropNote = (error) => {
39197
39800
  * 6. always restore disable directives + clean up the temp dir
39198
39801
  */
39199
39802
  const runOxlint = async (options) => {
39200
- const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, spawnTimeoutMs, outputMaxBytes } = options;
39803
+ 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;
39201
39804
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
39202
39805
  const severityControls = buildRuleSeverityControls(userConfig);
39203
39806
  validateRuleRegistration();
@@ -39214,30 +39817,156 @@ const runOxlint = async (options) => {
39214
39817
  serverAuthFunctionNames,
39215
39818
  severityControls,
39216
39819
  userPlugins,
39217
- disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
39820
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
39821
+ ruleSelection: overrides.ruleSelection
39218
39822
  });
39219
39823
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
39220
39824
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
39221
39825
  const configPath = Path.join(configDirectory, "oxlintrc.json");
39222
39826
  try {
39223
- const baseArgs = [
39224
- resolveOxlintBinary(),
39225
- "-c",
39226
- configPath,
39227
- "--format",
39228
- "json"
39229
- ];
39827
+ const oxlintBinary = resolveOxlintBinary();
39828
+ const sharedArgs = [];
39829
+ let tsconfigContent = null;
39230
39830
  if (project.hasTypeScript) {
39231
39831
  const tsconfigRelativePath = resolveTsConfigRelativePath(rootDirectory);
39232
- if (tsconfigRelativePath) baseArgs.push("--tsconfig", tsconfigRelativePath);
39832
+ if (tsconfigRelativePath) {
39833
+ sharedArgs.push("--tsconfig", tsconfigRelativePath);
39834
+ try {
39835
+ tsconfigContent = NFS.readFileSync(Path.resolve(rootDirectory, tsconfigRelativePath), "utf8");
39836
+ } catch {
39837
+ tsconfigContent = null;
39838
+ }
39839
+ }
39233
39840
  }
39234
39841
  const combinedPatterns = collectIgnorePatterns(rootDirectory);
39235
39842
  if (combinedPatterns.length > 0) {
39236
39843
  const combinedIgnorePath = Path.join(configDirectory, "combined.ignore");
39237
39844
  NFS.writeFileSync(combinedIgnorePath, `${combinedPatterns.join("\n")}\n`);
39238
- baseArgs.push("--ignore-path", combinedIgnorePath);
39845
+ sharedArgs.push("--ignore-path", combinedIgnorePath);
39239
39846
  }
39240
- const fileBatches = batchIncludePaths(baseArgs, includePaths !== void 0 ? includePaths : listSourceFiles(rootDirectory));
39847
+ const makeBaseArgs = (oxlintConfigPath) => [
39848
+ oxlintBinary,
39849
+ "-c",
39850
+ oxlintConfigPath,
39851
+ "--format",
39852
+ "json",
39853
+ ...sharedArgs
39854
+ ];
39855
+ const discoverScanFiles = () => lintBatchOrdering === "cost" ? sortSourceFilesByCost(listSourceFilesWithSize(rootDirectory)) : listSourceFiles(rootDirectory);
39856
+ const candidateFiles = includePaths !== void 0 ? includePaths : discoverScanFiles();
39857
+ const runConfigOverFiles = async (buildConfigForPass, configFileName, files, fileProgress) => {
39858
+ if (files.length === 0) return {
39859
+ diagnostics: [],
39860
+ didDropReactHooksJsPlugin: false,
39861
+ hadPartialFailure: false
39862
+ };
39863
+ let hadPartialFailure = false;
39864
+ const reportPartialFailure = (reason) => {
39865
+ hadPartialFailure = true;
39866
+ onPartialFailure?.(reason);
39867
+ };
39868
+ const passConfigPath = Path.join(configDirectory, configFileName);
39869
+ const passBaseArgs = makeBaseArgs(passConfigPath);
39870
+ const passFileBatches = batchIncludePaths(passBaseArgs, files);
39871
+ const spawnPass = () => spawnLintBatches({
39872
+ baseArgs: passBaseArgs,
39873
+ fileBatches: passFileBatches,
39874
+ rootDirectory,
39875
+ nodeBinaryPath,
39876
+ project,
39877
+ onPartialFailure: reportPartialFailure,
39878
+ onFileProgress: fileProgress,
39879
+ spawnTimeoutMs,
39880
+ outputMaxBytes,
39881
+ concurrency: options.concurrency,
39882
+ signal: options.signal
39883
+ });
39884
+ writeOxlintConfig(passConfigPath, buildConfigForPass({}));
39885
+ try {
39886
+ return {
39887
+ diagnostics: await spawnPass(),
39888
+ didDropReactHooksJsPlugin: false,
39889
+ hadPartialFailure
39890
+ };
39891
+ } catch (error) {
39892
+ const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
39893
+ if (reactHooksJsDropNote === null) throw error;
39894
+ writeOxlintConfig(passConfigPath, buildConfigForPass({ disableReactHooksJsPlugin: true }));
39895
+ const diagnostics = await spawnPass();
39896
+ reportPartialFailure(reactHooksJsDropNote);
39897
+ return {
39898
+ diagnostics,
39899
+ didDropReactHooksJsPlugin: true,
39900
+ hadPartialFailure
39901
+ };
39902
+ }
39903
+ };
39904
+ if (perFileLintCacheEnabled && respectInlineDisables && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
39905
+ const rulesetHash = computeRulesetHash({
39906
+ config: buildConfig({
39907
+ extendsPaths: [],
39908
+ ruleSelection: "cacheable"
39909
+ }),
39910
+ toolchainVersions: resolveOxlintToolchainVersions(),
39911
+ ignorePatterns: combinedPatterns,
39912
+ tsconfigContent
39913
+ });
39914
+ const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
39915
+ const cacheKeyByFile = /* @__PURE__ */ new Map();
39916
+ const missFiles = [];
39917
+ const replayedDiagnostics = [];
39918
+ for (const candidateFile of candidateFiles) {
39919
+ const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
39920
+ if (contentHash === null) {
39921
+ missFiles.push(candidateFile);
39922
+ continue;
39923
+ }
39924
+ const cacheKey = `${candidateFile.replaceAll("\\", "/")}${contentHash}`;
39925
+ cacheKeyByFile.set(candidateFile, cacheKey);
39926
+ const cachedDiagnostics = cache.lookup(cacheKey);
39927
+ if (cachedDiagnostics === null) missFiles.push(candidateFile);
39928
+ else replayedDiagnostics.push(...cachedDiagnostics);
39929
+ }
39930
+ const cacheHitFileCount = candidateFiles.length - missFiles.length;
39931
+ const cacheableResult = await runConfigOverFiles((overrides) => buildConfig({
39932
+ extendsPaths: [],
39933
+ ruleSelection: "cacheable",
39934
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
39935
+ }), "oxlintrc.cacheable.json", missFiles, void 0);
39936
+ const sidecarResult = await runConfigOverFiles(() => buildConfig({
39937
+ extendsPaths: [],
39938
+ ruleSelection: "sidecar"
39939
+ }), "oxlintrc.sidecar.json", candidateFiles, options.onFileProgress);
39940
+ onCacheStats?.(cacheHitFileCount, candidateFiles.length);
39941
+ const missFileByNormalizedPath = /* @__PURE__ */ new Map();
39942
+ for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
39943
+ const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
39944
+ let isAttributionSound = true;
39945
+ for (const diagnostic of cacheableResult.diagnostics) {
39946
+ const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
39947
+ if (missFile === void 0) {
39948
+ isAttributionSound = false;
39949
+ break;
39950
+ }
39951
+ const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
39952
+ fileDiagnostics.push(diagnostic);
39953
+ freshDiagnosticsByFile.set(missFile, fileDiagnostics);
39954
+ }
39955
+ if (!cacheableResult.didDropReactHooksJsPlugin && !cacheableResult.hadPartialFailure && isAttributionSound) {
39956
+ for (const missFile of missFiles) {
39957
+ const cacheKey = cacheKeyByFile.get(missFile);
39958
+ if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
39959
+ }
39960
+ cache.persist();
39961
+ }
39962
+ return dedupeDiagnostics([
39963
+ ...replayedDiagnostics,
39964
+ ...cacheableResult.diagnostics,
39965
+ ...sidecarResult.diagnostics
39966
+ ]);
39967
+ }
39968
+ const baseArgs = makeBaseArgs(configPath);
39969
+ const fileBatches = batchIncludePaths(baseArgs, candidateFiles);
39241
39970
  const runBatches = () => spawnLintBatches({
39242
39971
  baseArgs,
39243
39972
  fileBatches,
@@ -39248,7 +39977,8 @@ const runOxlint = async (options) => {
39248
39977
  onFileProgress: options.onFileProgress,
39249
39978
  spawnTimeoutMs,
39250
39979
  outputMaxBytes,
39251
- concurrency: options.concurrency
39980
+ concurrency: options.concurrency,
39981
+ signal: options.signal
39252
39982
  });
39253
39983
  writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
39254
39984
  try {
@@ -39327,9 +40057,11 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
39327
40057
  const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
39328
40058
  const outputMaxBytes = yield* OxlintOutputMaxBytes;
39329
40059
  const concurrency = yield* OxlintConcurrency;
40060
+ const lintBatchOrdering = yield* LintBatchOrdering;
40061
+ const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
39330
40062
  const collectedFailures = [];
39331
40063
  const diagnostics = yield* tryPromise({
39332
- try: () => runOxlint({
40064
+ try: (signal) => runOxlint({
39333
40065
  rootDirectory: input.rootDirectory,
39334
40066
  project: input.project,
39335
40067
  includePaths: input.includePaths ? [...input.includePaths] : void 0,
@@ -39344,9 +40076,13 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
39344
40076
  collectedFailures.push(reason);
39345
40077
  },
39346
40078
  onFileProgress: input.onFileProgress,
40079
+ perFileLintCacheEnabled,
40080
+ onCacheStats: input.onCacheStats,
39347
40081
  spawnTimeoutMs,
39348
40082
  outputMaxBytes,
39349
- concurrency
40083
+ concurrency,
40084
+ signal,
40085
+ lintBatchOrdering
39350
40086
  }),
39351
40087
  catch: ensureReactDoctorError
39352
40088
  });
@@ -39738,14 +40474,49 @@ const parseArtifactFromBody = (body) => {
39738
40474
  }
39739
40475
  return null;
39740
40476
  };
39741
- const fetchSocketArtifact = (dependency) => tryPromise(async (signal) => {
40477
+ const isSupplyChainCacheDisabled = () => {
40478
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
40479
+ return noCache === "1" || noCache === "true";
40480
+ };
40481
+ const supplyChainCacheFile = (cacheDirectory, dependency) => {
40482
+ const purlHash = crypto.createHash("sha256").update(toPurl(dependency)).digest("hex").slice(0, 16);
40483
+ return Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR, `${purlHash}.json`);
40484
+ };
40485
+ const readCachedSocketBody = (cacheFile) => {
40486
+ try {
40487
+ const entry = JSON.parse(NFS.readFileSync(cacheFile, "utf-8"));
40488
+ 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;
40489
+ } catch {}
40490
+ return null;
40491
+ };
40492
+ const writeCachedSocketBody = (cacheFile, body) => {
40493
+ try {
40494
+ NFS.mkdirSync(Path.dirname(cacheFile), { recursive: true });
40495
+ NFS.writeFileSync(cacheFile, JSON.stringify({
40496
+ fetchedAtMs: Date.now(),
40497
+ body
40498
+ }));
40499
+ } catch {}
40500
+ };
40501
+ const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
40502
+ const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
40503
+ if (cacheFile !== null) {
40504
+ const cachedBody = readCachedSocketBody(cacheFile);
40505
+ if (cachedBody !== null) {
40506
+ const cachedArtifact = parseArtifactFromBody(cachedBody);
40507
+ if (cachedArtifact !== null) return cachedArtifact;
40508
+ }
40509
+ }
39742
40510
  const requestUrl = `${SOCKET_FREE_PURL_API_BASE}/${encodeURIComponent(toPurl(dependency))}`;
39743
40511
  const response = await fetch(requestUrl, {
39744
40512
  headers: { "User-Agent": SOCKET_FREE_USER_AGENT },
39745
40513
  signal
39746
40514
  });
39747
40515
  if (!response.ok) return null;
39748
- return parseArtifactFromBody(await response.text());
40516
+ const body = await response.text();
40517
+ const artifact = parseArtifactFromBody(body);
40518
+ if (artifact !== null && cacheFile !== null) writeCachedSocketBody(cacheFile, body);
40519
+ return artifact;
39749
40520
  }).pipe(timeout(FETCH_TIMEOUT_MS), orElseSucceed(() => null), tap$1((artifact) => {
39750
40521
  const scoreAttributes = {};
39751
40522
  if (artifact !== null) {
@@ -39850,7 +40621,8 @@ const checkSupplyChain = (input) => gen(function* () {
39850
40621
  const packageJsonPath = Path.join(input.rootDirectory, "package.json");
39851
40622
  const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
39852
40623
  if (dependencies.length === 0) return [];
39853
- const artifacts = yield* forEach$1(dependencies, fetchSocketArtifact, { concurrency: 8 });
40624
+ const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
40625
+ const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
39854
40626
  const diagnostics = [];
39855
40627
  for (let index = 0; index < dependencies.length; index += 1) {
39856
40628
  const artifact = artifacts[index];
@@ -39875,6 +40647,10 @@ const checkSupplyChain = (input) => gen(function* () {
39875
40647
  * The underlying `checkSupplyChain` Effect is total/fail-open — per-package
39876
40648
  * timeouts and network failures recover to "skip" — so the stream never
39877
40649
  * fails, mirroring `DeadCode`'s stream shape so the two compose the same way.
40650
+ * The orchestrator (`run-inspect.ts`) consumes this stream on a background
40651
+ * fiber whose network time overlaps the lint pass, joined under a generous
40652
+ * wall-clock budget; a budget expiry is the same fail-open outcome as a Socket
40653
+ * outage.
39878
40654
  */
39879
40655
  var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
39880
40656
  static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(checkSupplyChain(input).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
@@ -39933,18 +40709,45 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
39933
40709
  *
39934
40710
  * Phases:
39935
40711
  *
39936
- * 1. Config.resolve(directory) → Project.discover → Git metadata
40712
+ * 1. Config.resolve(directory) → Project.discover → Git metadata.
40713
+ * The GitHub viewer-permission lookup is forked onto a background
40714
+ * fiber here and joined late (it feeds score metadata, not
40715
+ * diagnostics).
39937
40716
  * 2. beforeLint hook (e.g. CLI renders the project-detection block)
39938
40717
  * 3. environment checks (reduced-motion + pnpm hardening +
39939
- * expo/react-native + security scan)
39940
- * 4. Linter.run + DeadCode.run forked as concurrent fibers so
39941
- * their wall-clock times overlap. Progress spinners stay
39942
- * sequential (lint first, then dead-code) for clean terminal
39943
- * output. GitHub viewer permission also runs as a background
39944
- * fiber during this phase.
39945
- * 5. afterLint hook
39946
- * 6. Reporter.finalize
39947
- * 7. Score.compute against the surface-filtered diagnostic set
40718
+ * expo/react-native), collected synchronously. The heavier
40719
+ * content-regex security scan is forked instead (like supply-chain
40720
+ * below) and joined before the concat, so its CPU overlaps lint
40721
+ * rather than blocking the event loop before it.
40722
+ * 4. The supply-chain check (Socket.dev) is forked onto a background
40723
+ * fiber so its ~100% network-bound time overlaps the ~100%
40724
+ * CPU/subprocess-bound lint pass below, collapsing two serial
40725
+ * phases into roughly `max(supplyChain, lint)`. It is capped by
40726
+ * `SupplyChainOverlapTimeoutMs` (measured from fork) so a hung
40727
+ * socket can't drag out its join; on timeout it fails open to no
40728
+ * diagnostics — the same outcome class as a Socket outage.
40729
+ * 5. Linter.run runs; DeadCode.run runs concurrently (forked child
40730
+ * fiber) ONLY when the memory gate has headroom to run the 8 GB
40731
+ * dead-code child alongside the oxlint workers — or when overlap is
40732
+ * forced via REACT_DOCTOR_DEAD_CODE_OVERLAP. Otherwise dead-code
40733
+ * runs sequentially after lint, exactly as it did pre-overlap. The
40734
+ * fiber is joined (or interrupted, SIGKILLing its worker, on lint
40735
+ * failure) before diagnostics are concatenated. The afterLint hook
40736
+ * fires between lint and dead-code. Progress spinner labels AND the
40737
+ * final diagnostic / score order stay independent of execution
40738
+ * order, so terminal output is identical either way; supply-chain
40739
+ * rides alongside without a spinner.
40740
+ * 6. Join the supply-chain fiber, then assemble the diagnostics in a
40741
+ * FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
40742
+ * byte-identical regardless of which fiber settled first. The
40743
+ * viewer-permission fiber is joined later, during score-metadata
40744
+ * assembly (it feeds score metadata, not diagnostics). The per-element
40745
+ * `Reporter.emit` side-channel now interleaves supply-chain with lint
40746
+ * emits, so capture-order assertions must target the deterministic
40747
+ * concat below, not emit order (production `Reporter.layerNoop` makes
40748
+ * emit a no-op).
40749
+ * 7. Reporter.finalize
40750
+ * 8. Score.compute against the surface-filtered diagnostic set
39948
40751
  *
39949
40752
  * The orchestrator owns spinner lifecycle via `Progress`; callers
39950
40753
  * choose `Progress.layerOra(...)` for CLI feedback or
@@ -39996,16 +40799,27 @@ const runInspect = (input, hooks = {}) => gen(function* () {
39996
40799
  ...checkPnpmHardening(scanDirectory),
39997
40800
  ...checkReactServerComponentsAdvisory(scanDirectory, project),
39998
40801
  ...checkExpoProject(scanDirectory, project),
39999
- ...checkReactNativeProject(scanDirectory, project),
40000
- ...checkSecurityScan(scanDirectory, {
40001
- project,
40002
- ignoredTags: input.ignoredTags
40003
- })
40802
+ ...checkReactNativeProject(scanDirectory, project)
40004
40803
  ])));
40005
- const supplyChainCollected = !isDiffMode || (input.supplyChainManifestChanged ?? false) ? yield* runCollect(applyPerElementPipeline(supplyChainService.run({
40804
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(promise(() => checkSecurityScanCooperative(scanDirectory, {
40805
+ project,
40806
+ ignoredTags: input.ignoredTags
40807
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)))))).pipe(withSpan("SecurityScan.run")));
40808
+ const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
40809
+ const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
40810
+ const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
40006
40811
  rootDirectory: scanDirectory,
40007
40812
  userConfig: resolvedConfig.config
40008
- }))) : [];
40813
+ }))).pipe(map$3((diagnostics) => ({
40814
+ diagnostics,
40815
+ timedOut: false
40816
+ })), timeout(supplyChainOverlapTimeout), orElseSucceed(() => ({
40817
+ diagnostics: [],
40818
+ timedOut: true
40819
+ }))) : succeed$2({
40820
+ diagnostics: [],
40821
+ timedOut: false
40822
+ }));
40009
40823
  const lintFailure = yield* make$13({
40010
40824
  didFail: false,
40011
40825
  reason: null,
@@ -40016,12 +40830,49 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40016
40830
  didFail: false,
40017
40831
  reason: null
40018
40832
  });
40019
- const scanConcurrency = yield* OxlintConcurrency;
40833
+ const scanConcurrency = resolveScanConcurrency(yield* OxlintConcurrency);
40834
+ const lintPhaseTimeoutMs = yield* LintPhaseTimeoutMs;
40835
+ const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
40836
+ const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
40020
40837
  const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
40838
+ const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
40839
+ const deadCodeOverlapMode = yield* DeadCodeOverlap;
40840
+ const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
40841
+ const deadCodeParseConcurrency = shouldOverlapDeadCode ? Math.max(1, Math.floor(scanConcurrency * DEAD_CODE_OVERLAP_PARSE_SHARE)) : void 0;
40842
+ const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
40843
+ const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
40844
+ rootDirectory: scanDirectory,
40845
+ userConfig: resolvedConfig.config,
40846
+ parseConcurrency: deadCodeParseConcurrency,
40847
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
40848
+ }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
40849
+ yield* set(deadCodeFailure, {
40850
+ didFail: true,
40851
+ reason: error.message
40852
+ });
40853
+ return empty$4;
40854
+ })))))).pipe(timeoutOption(deadCodeTimeout.phaseTimeoutMs), flatMap$2(match$3({
40855
+ onNone: () => set(deadCodeFailure, {
40856
+ didFail: true,
40857
+ reason: `Dead-code analysis exceeded ${Math.round(deadCodeTimeout.phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
40858
+ }).pipe(as([])),
40859
+ onSome: succeed$2
40860
+ })));
40861
+ const overlapDeadCodeTimeout = resolveDeadCodeTimeout({
40862
+ sourceFileCount: project.sourceFileCount,
40863
+ deadCodeConcurrency: deadCodeParseConcurrency ?? scanConcurrency,
40864
+ fullConcurrency: scanConcurrency
40865
+ });
40866
+ const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
40867
+ workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
40868
+ phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs)
40869
+ })) : null;
40021
40870
  const scanProgress = yield* progressService.start("Scanning...");
40022
40871
  const scanStartTime = Date.now();
40023
40872
  let lastReportedTotalFileCount = 0;
40024
- const lintCollected = yield* runCollect(applyPerElementPipeline(linterService.run({
40873
+ let lintCacheHitFileCount = null;
40874
+ let lintCacheTotalFileCount = null;
40875
+ const baseLintStream = linterService.run({
40025
40876
  rootDirectory: scanDirectory,
40026
40877
  project,
40027
40878
  includePaths: lintIncludePaths ?? void 0,
@@ -40035,6 +40886,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40035
40886
  onFileProgress: (scannedFileCount, totalFileCount) => {
40036
40887
  lastReportedTotalFileCount = totalFileCount;
40037
40888
  runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})${workerCountSuffix}...`));
40889
+ },
40890
+ onCacheStats: (cacheHitFileCount, totalConsideredFileCount) => {
40891
+ lintCacheHitFileCount = cacheHitFileCount;
40892
+ lintCacheTotalFileCount = totalConsideredFileCount;
40038
40893
  }
40039
40894
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
40040
40895
  yield* set(lintFailure, {
@@ -40044,36 +40899,56 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40044
40899
  reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
40045
40900
  });
40046
40901
  return empty$4;
40047
- }))))));
40902
+ }))));
40903
+ const lintCollected = yield* runCollect(applyPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
40904
+ onNone: () => set(lintFailure, {
40905
+ didFail: true,
40906
+ reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
40907
+ reasonTag: "OxlintBatchExceeded",
40908
+ reasonKind: null
40909
+ }).pipe(as([])),
40910
+ onSome: succeed$2
40911
+ })));
40048
40912
  const lintFailureState = yield* get$2(lintFailure);
40049
40913
  yield* afterLint(lintFailureState.didFail);
40050
40914
  if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
40051
40915
  const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
40052
40916
  const scannedFilesLabel = `${totalFileCount} ${totalFileCount === 1 ? "file" : "files"}`;
40053
- const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
40054
- const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`).pipe(andThen(runCollect(applyPerElementPipeline(deadCodeService.run({
40055
- rootDirectory: scanDirectory,
40056
- userConfig: resolvedConfig.config
40057
- }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
40058
- yield* set(deadCodeFailure, {
40059
- didFail: true,
40060
- reason: error.message
40917
+ let deadCodeCollected = [];
40918
+ if (lintFailureState.didFail) {
40919
+ if (deadCodeFiber !== null) yield* interrupt(deadCodeFiber);
40920
+ } else if (shouldRunDeadCode) {
40921
+ yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
40922
+ const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
40923
+ sourceFileCount: totalFileCount,
40924
+ deadCodeConcurrency: scanConcurrency,
40925
+ fullConcurrency: scanConcurrency
40061
40926
  });
40062
- return empty$4;
40063
- }))))))));
40064
- const deadCodeFailureState = yield* get$2(deadCodeFailure);
40927
+ deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
40928
+ workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
40929
+ phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs)
40930
+ });
40931
+ }
40932
+ const deadCodeFailureState = lintFailureState.didFail ? {
40933
+ didFail: false,
40934
+ reason: null
40935
+ } : yield* get$2(deadCodeFailure);
40065
40936
  const scanElapsedMilliseconds = Date.now() - scanStartTime;
40066
40937
  const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
40067
40938
  if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
40068
40939
  else if (input.suppressScanSummary) yield* scanProgress.stop();
40069
40940
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
40941
+ const supplyChainResult = yield* join(supplyChainFiber);
40942
+ const supplyChainCollected = supplyChainResult.diagnostics;
40943
+ const securityScanCollected = yield* join(securityScanFiber);
40070
40944
  yield* reporterService.finalize;
40071
- const finalDiagnostics = assignFixGroups([
40945
+ const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
40072
40946
  ...envCollected,
40947
+ ...securityScanCollected,
40073
40948
  ...supplyChainCollected,
40074
40949
  ...lintCollected,
40075
40950
  ...deadCodeCollected
40076
- ]);
40951
+ ]));
40077
40952
  const githubViewerPermission = yield* join(githubViewerPermissionFiber);
40078
40953
  const scoreMetadata = {
40079
40954
  ...repo !== null ? { repo } : {},
@@ -40109,9 +40984,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40109
40984
  lintPartialFailures,
40110
40985
  didDeadCodeFail: deadCodeFailureState.didFail,
40111
40986
  deadCodeFailureReason: deadCodeFailureState.reason,
40987
+ deadCodeOverlapped: shouldOverlapDeadCode,
40112
40988
  scannedFileCount: totalFileCount,
40113
40989
  scannedFilePaths,
40114
- scanElapsedMilliseconds
40990
+ scanElapsedMilliseconds,
40991
+ scanConcurrency,
40992
+ supplyChainOverlapTimedOut: supplyChainResult.timedOut,
40993
+ lintCacheHitFileCount,
40994
+ lintCacheTotalFileCount
40115
40995
  };
40116
40996
  }).pipe(withSpan("runInspect", { attributes: {
40117
40997
  "inspect.directory": input.directory,
@@ -40119,7 +40999,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40119
40999
  "inspect.runDeadCode": input.runDeadCode,
40120
41000
  "inspect.isCi": input.isCi,
40121
41001
  "inspect.scoreSurface": input.scoreSurface ?? "score"
40122
- } }));
41002
+ } }), (scanProgram) => flatMap$2(ScanDeadlineMs, (scanDeadlineMs) => scanProgram.pipe(timeout(scanDeadlineMs), catchTag$1("TimeoutError", () => new ReactDoctorError({ reason: new ScanDeadlineExceeded({ detail: `${scanDeadlineMs / MILLISECONDS_PER_SECOND}s elapsed` }) })))));
40123
41003
  const parseNodeVersion = (versionString) => {
40124
41004
  const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
40125
41005
  return {
@@ -40440,7 +41320,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
40440
41320
  /** Display name used in client-facing messages and progress titles. */
40441
41321
  const SERVER_DISPLAY_NAME = "React Doctor";
40442
41322
  /** Server version reported in `serverInfo`; injected at build, `dev` from source. */
40443
- const SERVER_VERSION = "0.5.7";
41323
+ const SERVER_VERSION = "0.5.8";
40444
41324
  /** `Diagnostic.source` shown next to every published diagnostic. */
40445
41325
  const DIAGNOSTIC_SOURCE = "react-doctor";
40446
41326
  /**
@@ -41095,6 +41975,7 @@ const createProjectGraph = (options) => {
41095
41975
  clearPackageJsonCache();
41096
41976
  clearIgnorePatternsCache();
41097
41977
  clearAutoSuppressionCaches();
41978
+ clearMinifiedFileCache();
41098
41979
  projects = null;
41099
41980
  }
41100
41981
  };
@@ -42135,6 +43016,7 @@ const SENTRY_FLUSH_TIMEOUT_MS = 2e3;
42135
43016
  const METRIC = {
42136
43017
  cliInvoked: "cli.invoked",
42137
43018
  cliError: "cli.error",
43019
+ cliEnvironmentError: "cli.env_error",
42138
43020
  projectDetected: "project.detected",
42139
43021
  projectPathSelected: "project.path_selected",
42140
43022
  projectConfigSelected: "project.config_selected",
@@ -42481,5 +43363,5 @@ const startLanguageServer = () => {
42481
43363
  };
42482
43364
  //#endregion
42483
43365
  export { startLanguageServer };
42484
- !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]="d139c7ae-128c-5b80-90a7-3ae52c407bf7")}catch(e){}}();
42485
- //# debugId=d139c7ae-128c-5b80-90a7-3ae52c407bf7
43366
+ !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]="bb5309e5-ce90-551d-bf0a-7db0ea302f24")}catch(e){}}();
43367
+ //# debugId=bb5309e5-ce90-551d-bf0a-7db0ea302f24