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/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !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]="0a49b285-88be-5b0e-803b-22151c5001c2")}catch(e){}}();
2
+ !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]="b9d1a98e-f5ec-5357-a08b-f84864fdfa20")}catch(e){}}();
3
3
  import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
4
4
  import { createRequire } from "node:module";
5
5
  import * as NFS from "node:fs";
@@ -9,7 +9,7 @@ import path from "node:path";
9
9
  import * as NodeChildProcess from "node:child_process";
10
10
  import { spawn, spawnSync } from "node:child_process";
11
11
  import * as ts from "typescript";
12
- 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";
12
+ 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";
13
13
  import * as OS from "node:os";
14
14
  import os from "node:os";
15
15
  import { parseJSON5 } from "confbox";
@@ -17,7 +17,7 @@ import * as NodeUrl from "node:url";
17
17
  import { fileURLToPath } from "node:url";
18
18
  import { createJiti } from "jiti";
19
19
  import * as Crypto from "node:crypto";
20
- import { createHash } from "node:crypto";
20
+ import crypto, { createHash } from "node:crypto";
21
21
  import { gzipSync } from "node:zlib";
22
22
  //#region ../../node_modules/.pnpm/effect@4.0.0-beta.70/node_modules/effect/dist/Pipeable.js
23
23
  /**
@@ -5996,7 +5996,7 @@ const composePassthrough = /* @__PURE__ */ dual(2, (left, right) => (input) => {
5996
5996
  * @since 2.0.0
5997
5997
  */
5998
5998
  const Scheduler = /* @__PURE__ */ Reference("effect/Scheduler", { defaultValue: () => new MixedScheduler() });
5999
- const setImmediate = "setImmediate" in globalThis ? (f) => {
5999
+ const setImmediate$1 = "setImmediate" in globalThis ? (f) => {
6000
6000
  const timer = globalThis.setImmediate(f);
6001
6001
  return () => globalThis.clearImmediate(timer);
6002
6002
  } : (f) => {
@@ -6040,7 +6040,7 @@ var PriorityBuckets = class {
6040
6040
  var MixedScheduler = class {
6041
6041
  executionMode;
6042
6042
  setImmediate;
6043
- constructor(executionMode = "async", setImmediateFn = setImmediate) {
6043
+ constructor(executionMode = "async", setImmediateFn = setImmediate$1) {
6044
6044
  this.executionMode = executionMode;
6045
6045
  this.setImmediate = setImmediateFn;
6046
6046
  }
@@ -6065,7 +6065,7 @@ var MixedSchedulerDispatcher = class {
6065
6065
  tasks = /* @__PURE__ */ new PriorityBuckets();
6066
6066
  running = void 0;
6067
6067
  setImmediate;
6068
- constructor(setImmediateFn = setImmediate) {
6068
+ constructor(setImmediateFn = setImmediate$1) {
6069
6069
  this.setImmediate = setImmediateFn;
6070
6070
  }
6071
6071
  /**
@@ -7199,7 +7199,7 @@ const provideContext$1 = /* @__PURE__ */ dual(2, (self, context) => {
7199
7199
  return updateContext$1(self, merge$3(context));
7200
7200
  });
7201
7201
  /** @internal */
7202
- const provideService$1 = function() {
7202
+ const provideService$3 = function() {
7203
7203
  if (arguments.length === 1) return dual(2, (self, impl) => provideServiceImpl(self, arguments[0], impl));
7204
7204
  return dual(3, (self, service, impl) => provideServiceImpl(self, service, impl)).apply(this, arguments);
7205
7205
  };
@@ -7430,7 +7430,7 @@ const constScopeEmpty = { _tag: "Empty" };
7430
7430
  /** @internal */
7431
7431
  const scope = scopeTag;
7432
7432
  /** @internal */
7433
- const provideScope = /* @__PURE__ */ provideService$1(scopeTag);
7433
+ const provideScope = /* @__PURE__ */ provideService$3(scopeTag);
7434
7434
  /** @internal */
7435
7435
  const scoped$1 = (self) => withFiber$1((fiber) => {
7436
7436
  const prev = fiber.context;
@@ -7871,7 +7871,7 @@ const makeLatchUnsafe = (open) => new Latch(open ?? false);
7871
7871
  /** @internal */
7872
7872
  const makeLatch = (open) => sync$2(() => makeLatchUnsafe(open));
7873
7873
  /** @internal */
7874
- const withTracerEnabled$1 = /* @__PURE__ */ provideService$1(TracerEnabled);
7874
+ const withTracerEnabled$1 = /* @__PURE__ */ provideService$3(TracerEnabled);
7875
7875
  const bigint0 = /* @__PURE__ */ BigInt(0);
7876
7876
  const NoopSpanProto = {
7877
7877
  _tag: "Span",
@@ -7952,7 +7952,7 @@ const useSpan$1 = (name, ...args) => {
7952
7952
  }));
7953
7953
  });
7954
7954
  };
7955
- const provideParentSpan = /* @__PURE__ */ provideService$1(ParentSpan);
7955
+ const provideParentSpan = /* @__PURE__ */ provideService$3(ParentSpan);
7956
7956
  /** @internal */
7957
7957
  const withParentSpan$1 = function() {
7958
7958
  const dataFirst = isEffect$1(arguments[0]);
@@ -9519,7 +9519,7 @@ var CurrentMemoMap = class extends Service()("effect/Layer/CurrentMemoMap") {
9519
9519
  * @category memo map
9520
9520
  * @since 2.0.0
9521
9521
  */
9522
- const buildWithMemoMap = /* @__PURE__ */ dual(3, (self, memoMap, scope) => provideService$1(map$4(self.build(memoMap, scope), add(CurrentMemoMap, memoMap)), CurrentMemoMap, memoMap));
9522
+ const buildWithMemoMap = /* @__PURE__ */ dual(3, (self, memoMap, scope) => provideService$3(map$4(self.build(memoMap, scope), add(CurrentMemoMap, memoMap)), CurrentMemoMap, memoMap));
9523
9523
  /**
9524
9524
  * Builds a layer into an `Effect` value. Any resources associated with this
9525
9525
  * layer will be released when the specified scope is closed unless their scope
@@ -10872,7 +10872,7 @@ const provide$1 = /* @__PURE__ */ dual((args) => isEffect$1(args[0]), (self, sou
10872
10872
  /** @internal */
10873
10873
  const repeatOrElse = /* @__PURE__ */ dual(3, (self, schedule, orElse) => flatMap$4(toStepWithMetadata(schedule), (step) => {
10874
10874
  let meta = CurrentMetadata.defaultValue();
10875
- return catch_$2(forever$2(tap$2(flatMap$4(suspend$3(() => provideService$1(self, CurrentMetadata, meta)), step), (meta_) => sync$2(() => {
10875
+ return catch_$2(forever$2(tap$2(flatMap$4(suspend$3(() => provideService$3(self, CurrentMetadata, meta)), step), (meta_) => sync$2(() => {
10876
10876
  meta = meta_;
10877
10877
  })), { disableYield: true }), (error) => isDone$2(error) ? succeed$5(error.value) : orElse(error, meta.attempt === 0 ? none() : some(meta)));
10878
10878
  }));
@@ -10880,7 +10880,7 @@ const repeatOrElse = /* @__PURE__ */ dual(3, (self, schedule, orElse) => flatMap
10880
10880
  const retryOrElse = /* @__PURE__ */ dual(3, (self, policy, orElse) => flatMap$4(toStepWithMetadata(policy), (step) => {
10881
10881
  let meta = CurrentMetadata.defaultValue();
10882
10882
  let lastError;
10883
- const loop = catch_$2(suspend$3(() => provideService$1(self, CurrentMetadata, meta)), (error) => {
10883
+ const loop = catch_$2(suspend$3(() => provideService$3(self, CurrentMetadata, meta)), (error) => {
10884
10884
  lastError = error;
10885
10885
  return flatMap$4(step(error), (meta_) => {
10886
10886
  meta = meta_;
@@ -12996,7 +12996,7 @@ const updateContext = updateContext$1;
12996
12996
  * @category Context
12997
12997
  * @since 2.0.0
12998
12998
  */
12999
- const provideService = provideService$1;
12999
+ const provideService$2 = provideService$3;
13000
13000
  /**
13001
13001
  * Scopes all resources used in this workflow to the lifetime of the workflow,
13002
13002
  * ensuring that their finalizers are run as soon as this workflow completes
@@ -17990,6 +17990,20 @@ function decodeUnknownOption$1(schema, options) {
17990
17990
  return asOption(decodeUnknownEffect(schema, options));
17991
17991
  }
17992
17992
  /**
17993
+ * Creates a synchronous decoder for `unknown` input.
17994
+ *
17995
+ * **Details**
17996
+ *
17997
+ * The returned function returns the decoded `Type` on success and throws an
17998
+ * `Error` with the `SchemaIssue.Issue` in its `cause` on decoding failure.
17999
+ *
18000
+ * @category decoding
18001
+ * @since 3.10.0
18002
+ */
18003
+ function decodeUnknownSync$1(schema, options) {
18004
+ return asSync(decodeUnknownEffect(schema, options));
18005
+ }
18006
+ /**
17993
18007
  * Creates an effectful encoder for `unknown` input.
17994
18008
  *
17995
18009
  * **Details**
@@ -18291,6 +18305,40 @@ function isSchemaError(u) {
18291
18305
  */
18292
18306
  const decodeUnknownOption = decodeUnknownOption$1;
18293
18307
  /**
18308
+ * Decodes an `unknown` input against a schema synchronously, returning the
18309
+ * decoded value or throwing an `Error` whose cause contains the schema issue.
18310
+ * Use this when you want to validate data at a boundary and treat a schema
18311
+ * mismatch as an exception. For typed input use `decodeSync`.
18312
+ *
18313
+ * **Details**
18314
+ *
18315
+ * Only service-free schemas can be decoded synchronously. For non-throwing
18316
+ * alternatives see `decodeUnknownOption`, `decodeUnknownExit`, or
18317
+ * `decodeUnknownEffect`. Options may be provided either when creating the
18318
+ * decoder or when applying it; application options override creation options.
18319
+ *
18320
+ * **Example** (Decoding with a transformation schema)
18321
+ *
18322
+ * ```ts
18323
+ * import { Schema } from "effect"
18324
+ *
18325
+ * const NumberFromString = Schema.NumberFromString
18326
+ *
18327
+ * console.log(Schema.decodeUnknownSync(NumberFromString)("42"))
18328
+ * // Output: 42
18329
+ *
18330
+ * Schema.decodeUnknownSync(NumberFromString)("not a number")
18331
+ * // throws SchemaError: NumberFromString
18332
+ * // └─ Encoded side transformation failure
18333
+ * // └─ NumberFromString
18334
+ * // └─ Expected a numeric string, actual "not a number"
18335
+ * ```
18336
+ *
18337
+ * @category decoding
18338
+ * @since 4.0.0
18339
+ */
18340
+ const decodeUnknownSync = decodeUnknownSync$1;
18341
+ /**
18294
18342
  * Encodes an `unknown` input against a schema synchronously, throwing a
18295
18343
  * {@link SchemaError} on failure. Use this when you want to serialize data at a
18296
18344
  * boundary and treat a schema mismatch as an unrecoverable error. For
@@ -25170,6 +25218,14 @@ const runWith = (self, f, onHalt) => suspend$2(() => {
25170
25218
  return catchDone(flatMap$2(toTransform(self)(done$1(), scope), f), onHalt ? onHalt : succeed$2).pipe(onExit$1((exit) => close(scope, exit)));
25171
25219
  });
25172
25220
  /**
25221
+ * Provides a concrete service for a context key, removing that service
25222
+ * requirement from the returned channel.
25223
+ *
25224
+ * @category services
25225
+ * @since 2.0.0
25226
+ */
25227
+ 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))));
25228
+ /**
25173
25229
  * Runs a channel and applies an effect to each output element.
25174
25230
  *
25175
25231
  * **Example** (Running effects for each output)
@@ -26558,6 +26614,44 @@ const splitLines = (self) => self.channel.pipe(pipeTo(splitLines$1()), fromChann
26558
26614
  */
26559
26615
  const ensuring = /* @__PURE__ */ dual(2, (self, finalizer) => fromChannel(ensuring$1(self.channel, finalizer)));
26560
26616
  /**
26617
+ * Provides the stream with a single required service, eliminating that
26618
+ * requirement from its environment.
26619
+ *
26620
+ * **Example** (Providing a stream service)
26621
+ *
26622
+ * ```ts
26623
+ * import { Console, Context, Effect, Stream } from "effect"
26624
+ *
26625
+ * class Greeter extends Context.Service<Greeter, {
26626
+ * greet: (name: string) => string
26627
+ * }>()("Greeter") {}
26628
+ *
26629
+ * const stream = Stream.fromEffect(
26630
+ * Effect.service(Greeter).pipe(
26631
+ * Effect.map((greeter) => greeter.greet("Ada"))
26632
+ * )
26633
+ * )
26634
+ *
26635
+ * const program = Effect.gen(function*() {
26636
+ * const collected = yield* Stream.runCollect(
26637
+ * stream.pipe(
26638
+ * Stream.provideService(Greeter, {
26639
+ * greet: (name) => `Hello, ${name}`
26640
+ * })
26641
+ * )
26642
+ * )
26643
+ * yield* Console.log(collected)
26644
+ * })
26645
+ *
26646
+ * Effect.runPromise(program)
26647
+ * //=> ["Hello, Ada"]
26648
+ * ```
26649
+ *
26650
+ * @category services
26651
+ * @since 2.0.0
26652
+ */
26653
+ const provideService = /* @__PURE__ */ dual(3, (self, key, service) => fromChannel(provideService$1(self.channel, key, service)));
26654
+ /**
26561
26655
  * Runs a stream with a sink and returns the sink result.
26562
26656
  *
26563
26657
  * **Example** (Running a stream with a sink)
@@ -29987,7 +30081,7 @@ const make$8 = /* @__PURE__ */ fnUntraced(function* (options) {
29987
30081
  const runFork = runForkWith(services);
29988
30082
  const exportInterval = max(fromInputUnsafe(options.exportInterval), zero);
29989
30083
  let disabledUntil = void 0;
29990
- const client = filterStatusOk(get$4(services, HttpClient)).pipe(transformResponse(provideService(TracerPropagationEnabled, false)), retryTransient({
30084
+ const client = filterStatusOk(get$4(services, HttpClient)).pipe(transformResponse(provideService$2(TracerPropagationEnabled, false)), retryTransient({
29991
30085
  schedule: policy,
29992
30086
  times: 3
29993
30087
  }));
@@ -32717,15 +32811,24 @@ const isMinifiedSource = (absolutePath) => {
32717
32811
  if (fileDescriptor !== void 0) NFS.closeSync(fileDescriptor);
32718
32812
  }
32719
32813
  };
32720
- const isLargeMinifiedFile = (absolutePath) => {
32721
- let sizeBytes;
32814
+ const cachedIsLargeMinifiedByPath = /* @__PURE__ */ new Map();
32815
+ const clearMinifiedFileCache = () => {
32816
+ cachedIsLargeMinifiedByPath.clear();
32817
+ };
32818
+ const statSourceFileSize = (absolutePath) => {
32722
32819
  try {
32723
- sizeBytes = NFS.statSync(absolutePath).size;
32820
+ return NFS.statSync(absolutePath).size;
32724
32821
  } catch {
32725
- return false;
32822
+ return null;
32726
32823
  }
32727
- if (sizeBytes < 2e4) return false;
32728
- return isMinifiedSource(absolutePath);
32824
+ };
32825
+ const isLargeMinifiedFile = (absolutePath, knownSizeBytes) => {
32826
+ const cached = cachedIsLargeMinifiedByPath.get(absolutePath);
32827
+ if (cached !== void 0) return cached;
32828
+ const sizeBytes = knownSizeBytes === void 0 ? statSourceFileSize(absolutePath) : knownSizeBytes;
32829
+ const result = sizeBytes !== null && sizeBytes >= 2e4 && isMinifiedSource(absolutePath);
32830
+ cachedIsLargeMinifiedByPath.set(absolutePath, result);
32831
+ return result;
32729
32832
  };
32730
32833
  const isErrnoException = (error) => error instanceof Error && "code" in error;
32731
32834
  const IGNORABLE_READDIR_ERROR_CODES = new Set([
@@ -33591,6 +33694,8 @@ const MILLISECONDS_PER_SECOND = 1e3;
33591
33694
  const SCORE_API_URL = "https://www.react.doctor/api/score";
33592
33695
  const FETCH_TIMEOUT_MS = 1e4;
33593
33696
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
33697
+ const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
33698
+ const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
33594
33699
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
33595
33700
  const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
33596
33701
  const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
@@ -33640,7 +33745,17 @@ const STAGED_FILES_PROJECT_CONFIG_FILENAMES = [
33640
33745
  ];
33641
33746
  const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
33642
33747
  const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
33748
+ const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
33749
+ const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
33750
+ const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
33751
+ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
33752
+ const LINT_PHASE_TIMEOUT_MS = 3e5;
33753
+ const SCAN_TOTAL_DEADLINE_MS = 9e5;
33643
33754
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
33755
+ const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
33756
+ const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
33757
+ const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
33758
+ const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
33644
33759
  const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
33645
33760
  const REACT_SERVER_DOM_PACKAGES = [
33646
33761
  "react-server-dom-webpack",
@@ -33675,9 +33790,13 @@ const CONFIG_CACHE_TTL_MS = 300 * 1e3;
33675
33790
  const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
33676
33791
  const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
33677
33792
  const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
33793
+ const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
33794
+ const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
33678
33795
  const SUPPLY_CHAIN_PLUGIN = "socket";
33679
33796
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
33680
33797
  const SUPPLY_CHAIN_CATEGORY = "Security";
33798
+ const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
33799
+ const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
33681
33800
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
33682
33801
  const TSCONFIG_FILENAME = "tsconfig.json";
33683
33802
  const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
@@ -34370,7 +34489,10 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
34370
34489
  NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
34371
34490
  }
34372
34491
  const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
34373
- const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
34492
+ const canonicalizeRuleKey = (ruleKey) => {
34493
+ const nativeRuleKey = LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey];
34494
+ return typeof nativeRuleKey === "string" ? nativeRuleKey : ruleKey;
34495
+ };
34374
34496
  const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
34375
34497
  const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
34376
34498
  const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
@@ -35036,6 +35158,11 @@ var OxlintBatchExceeded = class extends TaggedErrorClass()("OxlintBatchExceeded"
35036
35158
  }
35037
35159
  }
35038
35160
  };
35161
+ var ScanDeadlineExceeded = class extends TaggedErrorClass()("ScanDeadlineExceeded", { detail: String$1 }) {
35162
+ get message() {
35163
+ return `Scan exceeded its overall time budget: ${this.detail}`;
35164
+ }
35165
+ };
35039
35166
  var OxlintSpawnFailed = class extends TaggedErrorClass()("OxlintSpawnFailed", { cause: Unknown }) {
35040
35167
  get message() {
35041
35168
  return `Failed to run oxlint: ${pretty(fail$6(this.cause))}`;
@@ -35099,6 +35226,7 @@ var GitBaseBranchInvalid = class extends TaggedErrorClass()("GitBaseBranchInvali
35099
35226
  const ReactDoctorErrorReason = Union([
35100
35227
  OxlintUnavailable,
35101
35228
  OxlintBatchExceeded,
35229
+ ScanDeadlineExceeded,
35102
35230
  OxlintSpawnFailed,
35103
35231
  OxlintOutputUnparseable,
35104
35232
  ConfigParseFailed,
@@ -35171,15 +35299,105 @@ const layerOtlp = unwrap$3(gen(function* () {
35171
35299
  }).pipe(provide$2(layer$8));
35172
35300
  }).pipe(orDie));
35173
35301
  /**
35174
- * Resolves a requested lint worker count to a clamped integer within
35175
- * `[MIN_SCAN_CONCURRENCY, MAX_SCAN_CONCURRENCY]`. `"auto"` uses the
35176
- * machine's CPU cores; out-of-range or non-finite requests degrade to
35302
+ * Read a positive-millisecond timeout from an env var, falling back to
35303
+ * `defaultMs` when the var is unset, non-finite, or not strictly positive.
35304
+ */
35305
+ const readPositiveEnvMs = (envVarName, defaultMs) => {
35306
+ const rawValue = process.env[envVarName];
35307
+ if (rawValue === void 0) return defaultMs;
35308
+ const parsedValue = Number(rawValue);
35309
+ if (!Number.isFinite(parsedValue) || parsedValue <= 0) return defaultMs;
35310
+ return parsedValue;
35311
+ };
35312
+ const CGROUP_V2_MEMORY_MAX_PATH = "/sys/fs/cgroup/memory.max";
35313
+ const CGROUP_V1_MEMORY_LIMIT_PATH = "/sys/fs/cgroup/memory/memory.limit_in_bytes";
35314
+ const CGROUP_UNLIMITED_SENTINEL_BYTES = Number.MAX_SAFE_INTEGER;
35315
+ /**
35316
+ * Parses one raw cgroup memory-limit file value into a positive byte count, or
35317
+ * `undefined` when it represents "no limit" (the v2 `"max"` literal, an empty
35318
+ * read, a non-positive / non-finite value, or v1's near-2^63 unlimited
35319
+ * sentinel). Pure and exported so the classification is unit-testable without
35320
+ * touching the filesystem.
35321
+ */
35322
+ const parseCgroupMemoryLimitBytes = (raw) => {
35323
+ if (raw === void 0) return void 0;
35324
+ const trimmed = raw.trim();
35325
+ if (trimmed === "" || trimmed === "max") return void 0;
35326
+ const parsed = Number(trimmed);
35327
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed >= CGROUP_UNLIMITED_SENTINEL_BYTES) return;
35328
+ return parsed;
35329
+ };
35330
+ const CGROUP_MEMORY_LIMIT_PATHS = [CGROUP_V2_MEMORY_MAX_PATH, CGROUP_V1_MEMORY_LIMIT_PATH];
35331
+ /**
35332
+ * Reads this process's cgroup memory limit in bytes from the first candidate
35333
+ * path that yields a real limit, or `undefined` when none does — no cgroup, no
35334
+ * limit, or the files are unreadable (e.g. macOS / Windows dev machines).
35335
+ * `os.totalmem()` reports the HOST total and ignores cgroup memory limits, so a
35336
+ * memory-constrained container over-reports total memory; `resolveAutoScan-
35337
+ * Concurrency` takes `min(totalmem, this)` to honor the limit.
35338
+ *
35339
+ * The cgroup v2 read is the mount-root `memory.max`, which IS the container's
35340
+ * limit under the standard cgroup-namespace setup CI runners use (the
35341
+ * container's own cgroup is the root of its namespaced view). A process in a
35342
+ * non-namespaced nested/delegated cgroup whose root reads `"max"` is not
35343
+ * detected here and falls back to the host total; the EAGAIN/ENOMEM serial
35344
+ * replay in `spawnLintBatches` remains the runtime backstop for that case.
35345
+ *
35346
+ * `candidatePaths` is injectable so tests exercise the v2-wins-over-v1
35347
+ * precedence, the skip-unreadable fallback, and the all-missing case without a
35348
+ * real `/sys/fs/cgroup`.
35349
+ */
35350
+ const readCgroupMemoryLimitBytes = (candidatePaths = CGROUP_MEMORY_LIMIT_PATHS) => {
35351
+ for (const limitPath of candidatePaths) {
35352
+ let raw;
35353
+ try {
35354
+ raw = fs.readFileSync(limitPath, "utf8");
35355
+ } catch {
35356
+ continue;
35357
+ }
35358
+ const limitBytes = parseCgroupMemoryLimitBytes(raw);
35359
+ if (limitBytes !== void 0) return limitBytes;
35360
+ }
35361
+ };
35362
+ /**
35363
+ * Clamps a requested lint worker count to `[MIN_SCAN_CONCURRENCY,
35364
+ * HARD_MAX_SCAN_CONCURRENCY]` as a finite integer. This is the explicit-pin and
35365
+ * spawn-boundary clamp — the memory-and-core-budgeted auto count comes from
35366
+ * `resolveAutoScanConcurrency`. Out-of-range or non-finite requests degrade to
35177
35367
  * `MIN_SCAN_CONCURRENCY` rather than oversubscribing or running zero workers.
35178
35368
  */
35179
35369
  const resolveScanConcurrency = (requested) => {
35180
- const desired = requested === "auto" ? os.availableParallelism() : requested;
35181
- if (!Number.isFinite(desired) || desired < 1) return 1;
35182
- return Math.max(1, Math.min(Math.floor(desired), 16));
35370
+ if (!Number.isFinite(requested) || requested < 1) return 1;
35371
+ return Math.min(Math.floor(requested), 32);
35372
+ };
35373
+ const readSystemFacts$1 = () => ({
35374
+ availableCores: os.availableParallelism(),
35375
+ totalMemoryBytes: os.totalmem(),
35376
+ cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
35377
+ });
35378
+ /**
35379
+ * Auto lint-worker count: the smaller of the (cgroup-CPU-aware) core count and
35380
+ * the number of `PER_WORKER_MEM_BUDGET_BYTES` workers that fit in available
35381
+ * memory, then clamped to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
35382
+ *
35383
+ * `os.availableParallelism()` already respects cgroup CPU quotas, so the core
35384
+ * term needs no help. Available memory is `os.totalmem()` floored by the cgroup
35385
+ * memory limit — `os.freemem()` is deliberately NOT used: it excludes
35386
+ * reclaimable page cache and reads near-zero on macOS / cache-heavy Linux, which
35387
+ * would collapse the auto path to a single worker. `os.totalmem()` reports the
35388
+ * host total even inside a container, so the cgroup limit (read directly,
35389
+ * because Node doesn't fold it into `totalmem()`) is the real ceiling there.
35390
+ *
35391
+ * `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
35392
+ * limited, and ceiling cases without mocking `os` or the filesystem.
35393
+ */
35394
+ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
35395
+ const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
35396
+ const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
35397
+ return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
35398
+ };
35399
+ const resolveLintBatchOrdering = () => {
35400
+ return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "cost" ? "cost" : "arrival";
35183
35401
  };
35184
35402
  /**
35185
35403
  * Per-batch oxlint wall-clock budget. Reads from the env var on
@@ -35187,11 +35405,38 @@ const resolveScanConcurrency = (requested) => {
35187
35405
  * microVMs without recompiling react-doctor. Tests override via
35188
35406
  * `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
35189
35407
  */
35190
- var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
35191
- const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
35192
- if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
35408
+ var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS", OXLINT_SPAWN_TIMEOUT_MS) }) {};
35409
+ /**
35410
+ * Effect-side cap on the lint phase. The env var lets CI / eval runners
35411
+ * raise the phase budget for slow large repos without recompiling.
35412
+ * Tests override via `Layer.succeed(LintPhaseTimeoutMs, ...)`.
35413
+ */
35414
+ var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", LINT_PHASE_TIMEOUT_MS) }) {};
35415
+ /**
35416
+ * Effect-side cap on the dead-code phase, sitting above the in-worker
35417
+ * timeout as a runtime-independent backstop. The env var raises it for
35418
+ * type-heavy projects; tests override via
35419
+ * `Layer.succeed(DeadCodePhaseTimeoutMs, ...)`.
35420
+ */
35421
+ var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", DEAD_CODE_PHASE_TIMEOUT_MS) }) {};
35422
+ /**
35423
+ * Overall scan deadline backstop, bounding everything the per-phase
35424
+ * timeouts don't (wedged git / IO). The env var raises it for very
35425
+ * large repos; tests override via `Layer.succeed(ScanDeadlineMs, ...)`.
35426
+ */
35427
+ var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", SCAN_TOTAL_DEADLINE_MS) }) {};
35428
+ /**
35429
+ * Wall-clock budget for the supply-chain check when it runs on a background
35430
+ * fiber overlapping the lint pass. Reads from the env var on startup so the
35431
+ * eval harness can raise the budget under sandbox microVMs (slower network)
35432
+ * without recompiling react-doctor. Tests override via
35433
+ * `Layer.succeed(SupplyChainOverlapTimeoutMs, ...)`.
35434
+ */
35435
+ var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => {
35436
+ const raw = process.env["REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS"];
35437
+ if (raw === void 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35193
35438
  const parsed = Number(raw);
35194
- if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
35439
+ if (!Number.isFinite(parsed) || parsed <= 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35195
35440
  return parsed;
35196
35441
  } }) {};
35197
35442
  /**
@@ -35202,31 +35447,93 @@ var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTime
35202
35447
  */
35203
35448
  var OxlintOutputMaxBytes = class extends Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
35204
35449
  /**
35205
- * Number of oxlint subprocesses the lint pass runs in parallel. Defaults
35206
- * to auto-detected CPU cores (parallel) so large repos scan fast out of
35207
- * the box; `spawnLintBatches` transparently falls back to a single worker
35208
- * if a parallel run exhausts system resources. The CLI's `--no-parallel`
35209
- * flag forces serial via `Layer.succeed`; the `REACT_DOCTOR_PARALLEL` env
35210
- * var seeds the default for programmatic / CI callers that never touch the
35211
- * flag parallelism is opt-OUT, so only the explicit serial values pin
35212
- * one worker:
35213
- *
35214
- * - unset / `auto` / `true` / `on` → available CPU cores (clamped)
35450
+ * Number of oxlint subprocesses the lint pass runs in parallel. Defaults to a
35451
+ * memory-and-core-budgeted auto count (`resolveAutoScanConcurrency`) so large
35452
+ * repos scan fast out of the box without OOMing the native binding on a
35453
+ * high-core / low-memory box; `spawnLintBatches` transparently falls back to a
35454
+ * single worker if a parallel run still exhausts system resources. The CLI's
35455
+ * `--no-parallel` flag forces serial via `Layer.succeed`; the
35456
+ * `REACT_DOCTOR_PARALLEL` env var seeds the default for programmatic / CI
35457
+ * callers that never touch the flag — parallelism is opt-OUT, so only the
35458
+ * explicit serial values pin one worker:
35459
+ *
35460
+ * - unset / `auto` / `true` / `on` → memory-and-core-budgeted auto count
35215
35461
  * - `0` / `false` / `off` → `1` (serial)
35216
35462
  * - a positive integer → that many workers (clamped)
35217
- * - any other value → available CPU cores (clamped)
35463
+ * - any other value → memory-and-core-budgeted auto count
35218
35464
  *
35219
35465
  * The resolved value is always within
35220
- * `[MIN_SCAN_CONCURRENCY, MAX_SCAN_CONCURRENCY]`.
35466
+ * `[MIN_SCAN_CONCURRENCY, HARD_MAX_SCAN_CONCURRENCY]`.
35221
35467
  */
35222
35468
  var OxlintConcurrency = class extends Reference("react-doctor/OxlintConcurrency", { defaultValue: () => {
35223
35469
  const raw = process.env["REACT_DOCTOR_PARALLEL"];
35224
- if (raw === void 0) return resolveScanConcurrency("auto");
35470
+ if (raw === void 0) return resolveAutoScanConcurrency();
35225
35471
  const normalized = raw.trim().toLowerCase();
35226
35472
  if (normalized === "0" || normalized === "false" || normalized === "off") return 1;
35227
35473
  const parsed = Number.parseInt(normalized, 10);
35228
35474
  if (Number.isInteger(parsed) && parsed > 0) return resolveScanConcurrency(parsed);
35229
- return resolveScanConcurrency("auto");
35475
+ return resolveAutoScanConcurrency();
35476
+ } }) {};
35477
+ /**
35478
+ * Three-state control for overlapping the dead-code pass with the lint pass —
35479
+ * forking dead-code as a child fiber that runs DURING lint instead of strictly
35480
+ * after it.
35481
+ *
35482
+ * - `"auto"` (default) / `"off"` → strictly SEQUENTIAL: dead-code runs after
35483
+ * lint with the full core budget. Both deslop's parse pool and the oxlint
35484
+ * pool are CPU-bound and each size themselves to all cores, so overlapping
35485
+ * them only oversubscribes (~2x the cores) and starves the parse pass past
35486
+ * its timeout — for no wall-clock win, since there are no spare cores to
35487
+ * absorb the second pass. Sequential is both faster per-phase and safe.
35488
+ * - `"on"` → force the overlap anyway. The orchestrator then SPLITS the core
35489
+ * budget (`DEAD_CODE_OVERLAP_PARSE_SHARE`): deslop's parse pool is capped
35490
+ * and lint shrinks to the remainder, so the two sum to the cores instead of
35491
+ * doubling them, and the dead-code timeout scales up for the reduced share.
35492
+ *
35493
+ * Seeded from `REACT_DOCTOR_DEAD_CODE_OVERLAP` so operators get a redeploy-free
35494
+ * switch; tests pin it via `Layer.succeed(DeadCodeOverlap, ...)`.
35495
+ */
35496
+ var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", { defaultValue: () => {
35497
+ const raw = process.env["REACT_DOCTOR_DEAD_CODE_OVERLAP"]?.trim().toLowerCase();
35498
+ if (raw === "on" || raw === "true" || raw === "1") return "on";
35499
+ if (raw === "off" || raw === "false" || raw === "0") return "off";
35500
+ return "auto";
35501
+ } }) {};
35502
+ /**
35503
+ * How the full-scan lint pass orders its file batches. `"arrival"` (the
35504
+ * default) keeps `git ls-files` discovery order. `"cost"` opts into LPT (feed
35505
+ * the largest files first); set `REACT_DOCTOR_LINT_BATCH_ORDERING=cost`. NOTE:
35506
+ * `cost` is OFF by default because the current sort-desc-then-chunk-100 packs
35507
+ * the heaviest files into one wave-1 batch — on size-skewed repos that mega-
35508
+ * batch is a straggler (and can trip the per-batch timeout + split), measurably
35509
+ * regressing the common full-scan case. LPT needs the heavy files SPREAD across
35510
+ * batches before `cost` earns the default. Tests override via
35511
+ * `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans never reach this
35512
+ * — they pass user-scoped `includePaths` that skip discovery and stay in
35513
+ * arrival order; only the full-scan branch reads it.
35514
+ */
35515
+ var LintBatchOrdering = class extends Reference("react-doctor/LintBatchOrdering", { defaultValue: resolveLintBatchOrdering }) {};
35516
+ const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
35517
+ /**
35518
+ * Whether the per-file lint cache (`runners/oxlint/file-lint-cache.ts`) is
35519
+ * active. Defaults ON — repeat scans re-lint only the files whose content
35520
+ * changed, and correctness is guaranteed byte-identical to a cold scan by the
35521
+ * always-fresh cross-file sidecar. Opt-OUT, two knobs (matching the whole-repo
35522
+ * scan cache's `REACT_DOCTOR_NO_CACHE`):
35523
+ *
35524
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch; disables BOTH the
35525
+ * whole-repo scan cache and this per-file cache.
35526
+ * - `REACT_DOCTOR_NO_FILE_CACHE` — granular: bust only the per-file cache
35527
+ * while keeping the whole-repo short-circuit.
35528
+ *
35529
+ * Tests override via `Layer.succeed(PerFileLintCacheEnabled, false)`.
35530
+ */
35531
+ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintCacheEnabled", { defaultValue: () => {
35532
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
35533
+ const noFileCache = process.env["REACT_DOCTOR_NO_FILE_CACHE"]?.toLowerCase() ?? "";
35534
+ if (CACHE_DISABLED_VALUES.has(noCache)) return false;
35535
+ if (CACHE_DISABLED_VALUES.has(noFileCache)) return false;
35536
+ return true;
35230
35537
  } }) {};
35231
35538
  const DIAGNOSTIC_SURFACES = [
35232
35539
  "cli",
@@ -35275,6 +35582,12 @@ const BOOLEAN_FIELD_NAMES = [
35275
35582
  "adoptExistingLintConfig"
35276
35583
  ];
35277
35584
  const STRING_FIELD_NAMES = ["rootDir"];
35585
+ const STRING_ARRAY_FIELD_NAMES = [
35586
+ "projects",
35587
+ "textComponents",
35588
+ "rawTextWrapperComponents",
35589
+ "serverAuthFunctionNames"
35590
+ ];
35278
35591
  const SURFACE_CONTROL_FIELD_NAMES = [
35279
35592
  "includeTags",
35280
35593
  "excludeTags",
@@ -35376,6 +35689,7 @@ const validateConfigTypes = (config) => {
35376
35689
  const validated = { ...config };
35377
35690
  for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
35378
35691
  for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
35692
+ for (const fieldName of STRING_ARRAY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateStringArrayField(fieldName, value));
35379
35693
  applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
35380
35694
  for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
35381
35695
  applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
@@ -35632,6 +35946,8 @@ const assignFixGroups = (diagnostics) => {
35632
35946
  };
35633
35947
  });
35634
35948
  };
35949
+ const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
35950
+ 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));
35635
35951
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
35636
35952
  const buildExpoCheckContext = (rootDirectory, expoVersion) => {
35637
35953
  const packageJson = readPackageJson(Path.join(rootDirectory, "package.json"));
@@ -36608,7 +36924,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
36608
36924
  }
36609
36925
  return true;
36610
36926
  };
36611
- const checkSecurityScan = (rootDirectory, options = {}) => {
36927
+ const yieldToEventLoop = () => new Promise((resolve) => {
36928
+ setImmediate(resolve);
36929
+ });
36930
+ const createSecurityScanSession = (rootDirectory, options) => {
36612
36931
  const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
36613
36932
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
36614
36933
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
@@ -36623,7 +36942,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36623
36942
  committedFilesOnly: rule.committedFilesOnly === true
36624
36943
  }];
36625
36944
  });
36626
- if (enabledScanRules.length === 0) return [];
36945
+ if (enabledScanRules.length === 0) return null;
36627
36946
  const diagnostics = [];
36628
36947
  const seen = /* @__PURE__ */ new Set();
36629
36948
  const gitIgnoredCache = /* @__PURE__ */ new Map();
@@ -36635,15 +36954,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36635
36954
  }
36636
36955
  return status === true;
36637
36956
  };
36638
- for (const file of collectSecurityScanFiles(rootDirectory)) for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36639
- if (committedFilesOnly && isFileGitIgnored(file)) continue;
36640
- const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36641
- const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36642
- if (seen.has(key)) continue;
36643
- seen.add(key);
36644
- diagnostics.push(diagnostic);
36957
+ const scanFile = (file) => {
36958
+ for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36959
+ if (committedFilesOnly && isFileGitIgnored(file)) continue;
36960
+ const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36961
+ const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36962
+ if (seen.has(key)) continue;
36963
+ seen.add(key);
36964
+ diagnostics.push(diagnostic);
36965
+ }
36966
+ };
36967
+ return {
36968
+ scanFile,
36969
+ diagnostics
36970
+ };
36971
+ };
36972
+ const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
36973
+ const session = createSecurityScanSession(rootDirectory, options);
36974
+ if (session === null) return [];
36975
+ let filesSinceYield = 0;
36976
+ for (const file of collectSecurityScanFiles(rootDirectory)) {
36977
+ session.scanFile(file);
36978
+ filesSinceYield += 1;
36979
+ if (filesSinceYield >= 16) {
36980
+ filesSinceYield = 0;
36981
+ await yieldToEventLoop();
36982
+ }
36645
36983
  }
36646
- return diagnostics;
36984
+ return session.diagnostics;
36647
36985
  };
36648
36986
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
36649
36987
  let p = process || {}, argv = p.argv || [], env = p.env || {};
@@ -36860,6 +37198,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
36860
37198
  return [...seen].filter((pattern) => pattern.length > 0);
36861
37199
  };
36862
37200
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
37201
+ const readSystemFacts = () => ({
37202
+ availableCores: os.availableParallelism(),
37203
+ totalMemoryBytes: os.totalmem(),
37204
+ cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
37205
+ });
37206
+ /**
37207
+ * How many real deslop dead-code child processes may run at once, across the
37208
+ * concurrent per-project `runInspect` fibers of one CLI run. The cap is the
37209
+ * smaller of the core count and the number of `DEAD_CODE_WORKER_MEM_BUDGET_BYTES`
37210
+ * workers that fit in available memory, floored at 1.
37211
+ *
37212
+ * On a roomy dev box / CI runner this resolves high enough that every
37213
+ * concurrently-scanned project still spawns its own worker (no serialization vs
37214
+ * the prior uncapped behavior); on a memory-constrained runner it collapses
37215
+ * toward 1, so the `withDeadCodeWorkerSlot` semaphore serializes the spawns
37216
+ * instead of oversubscribing memory with N simultaneous children — the global
37217
+ * cap the per-project spawn path lacked.
37218
+ *
37219
+ * Mirrors `resolveAutoScanConcurrency` (lint), but budgets memory per the
37220
+ * heavier dead-code worker. `facts` is injectable for tests.
37221
+ */
37222
+ const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
37223
+ const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
37224
+ const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
37225
+ return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
37226
+ };
37227
+ let availableSlots = -1;
37228
+ const waiters = [];
37229
+ const releaseSlot = () => {
37230
+ const nextWaiter = waiters.shift();
37231
+ if (nextWaiter !== void 0) nextWaiter();
37232
+ else availableSlots += 1;
37233
+ };
37234
+ /**
37235
+ * Runs `task` once a dead-code worker slot is free, releasing the slot when the
37236
+ * task settles (success or failure). With a high cap (roomy machine) every
37237
+ * caller proceeds immediately; with a low cap (constrained runner) callers
37238
+ * queue and run as slots free.
37239
+ *
37240
+ * `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
37241
+ * this caller is queued, the call rejects without acquiring a slot or running
37242
+ * `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
37243
+ * then spawn a child only to tear it down. A queued caller that aborts removes
37244
+ * its own waiter so a later release never hands a slot to a dead request.
37245
+ */
37246
+ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
37247
+ if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
37248
+ if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
37249
+ if (availableSlots > 0) availableSlots -= 1;
37250
+ else await new Promise((resolve, reject) => {
37251
+ const waiter = () => {
37252
+ abortSignal?.removeEventListener("abort", onAbort);
37253
+ resolve();
37254
+ };
37255
+ const onAbort = () => {
37256
+ const queuedIndex = waiters.indexOf(waiter);
37257
+ if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
37258
+ reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
37259
+ };
37260
+ waiters.push(waiter);
37261
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
37262
+ });
37263
+ try {
37264
+ return await task();
37265
+ } finally {
37266
+ releaseSlot();
37267
+ }
37268
+ };
36863
37269
  /**
36864
37270
  * Resolves a path to its canonical, symlink-free form, falling back to
36865
37271
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -36931,6 +37337,22 @@ process.stdin.on("end", () => {
36931
37337
  ...(workerInput.ignorePatterns.length > 0
36932
37338
  ? { ignorePatterns: workerInput.ignorePatterns }
36933
37339
  : {}),
37340
+ // We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
37341
+ // unusedDependencies, circularDependencies). Everything else deslop can compute
37342
+ // is pure wasted work for us, and it's the bulk of the runtime:
37343
+ // - semantic: a full TS Program for unusedTypes/enum/class-members/
37344
+ // misclassifiedDependencies (~37-45% of the phase).
37345
+ // - reportCodeQuality: the duplicate-block, complexity, feature-flag,
37346
+ // TypeScript-smell, private-type-leak and re-export-cycle detectors. These
37347
+ // are the single most expensive pass — duplicate-block detection alone was
37348
+ // ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
37349
+ // speedup on a large repo.
37350
+ // Both are provably safe: the consumed graph findings are computed by their own
37351
+ // detectors, independent of these passes (confirmed byte-identical on
37352
+ // excalidraw + mui-material + sentry). tsConfigPath stays — the module resolver
37353
+ // needs it for path-alias resolution in the import graph.
37354
+ semantic: { enabled: false },
37355
+ reportCodeQuality: false,
36934
37356
  };
36935
37357
  const result = await analyze(defineConfig(config));
36936
37358
  emit({ ok: true, result: normalizeResult(result) });
@@ -37060,7 +37482,11 @@ const createDeadCodeWorker = (input) => {
37060
37482
  "pipe",
37061
37483
  "pipe"
37062
37484
  ],
37063
- windowsHide: true
37485
+ windowsHide: true,
37486
+ env: input.parseConcurrency === void 0 ? process.env : {
37487
+ ...process.env,
37488
+ DESLOP_PARSE_CONCURRENCY: String(input.parseConcurrency)
37489
+ }
37064
37490
  });
37065
37491
  const stdoutChunks = [];
37066
37492
  const stderrChunks = [];
@@ -37105,41 +37531,42 @@ const createDeadCodeWorker = (input) => {
37105
37531
  }
37106
37532
  };
37107
37533
  };
37108
- const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
37534
+ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs, abortSignal) => new Promise((resolve, reject) => {
37109
37535
  let didSettle = false;
37110
- const timeoutHandle = setTimeout(() => {
37111
- if (didSettle) return;
37112
- didSettle = true;
37113
- handle.terminate?.();
37114
- reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
37115
- }, timeoutMs);
37116
- timeoutHandle.unref?.();
37117
- handle.result.then((value) => {
37536
+ const settle = (finish) => {
37118
37537
  if (didSettle) return;
37119
37538
  didSettle = true;
37120
37539
  clearTimeout(timeoutHandle);
37540
+ abortSignal?.removeEventListener("abort", onAbort);
37121
37541
  handle.terminate?.();
37122
- resolve(value);
37123
- }, (error) => {
37124
- if (didSettle) return;
37125
- didSettle = true;
37126
- clearTimeout(timeoutHandle);
37127
- handle.terminate?.();
37128
- reject(error);
37129
- });
37542
+ finish();
37543
+ };
37544
+ const onAbort = () => settle(() => reject(/* @__PURE__ */ new Error("Dead-code worker aborted.")));
37545
+ const timeoutHandle = setTimeout(() => settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`))), timeoutMs);
37546
+ timeoutHandle.unref?.();
37547
+ if (abortSignal?.aborted) {
37548
+ onAbort();
37549
+ return;
37550
+ }
37551
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
37552
+ handle.result.then((value) => settle(() => resolve(value)), (error) => settle(() => reject(error)));
37130
37553
  });
37131
37554
  const checkDeadCode = async (options) => {
37132
37555
  const rootDirectory = toCanonicalPath(options.rootDirectory);
37133
37556
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37134
37557
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37135
37558
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37136
- const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37137
- rootDirectory,
37138
- entryPatterns,
37139
- tsConfigPath: resolveTsConfigPath(rootDirectory),
37140
- ignorePatterns,
37141
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
37142
- }), options.workerTimeoutMs ?? 12e4));
37559
+ const spawnAndRun = () => {
37560
+ return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37561
+ rootDirectory,
37562
+ entryPatterns,
37563
+ tsConfigPath: resolveTsConfigPath(rootDirectory),
37564
+ ignorePatterns,
37565
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37566
+ parseConcurrency: options.parseConcurrency
37567
+ }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
37568
+ };
37569
+ const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
37143
37570
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
37144
37571
  const diagnostics = [];
37145
37572
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -37237,7 +37664,37 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
37237
37664
  return true;
37238
37665
  };
37239
37666
  const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
37240
- const excludeMinifiedFiles = (rootDirectory, relativePaths) => relativePaths.filter((relativePath) => !isLargeMinifiedFile(Path.resolve(rootDirectory, relativePath)));
37667
+ /**
37668
+ * Budget for the dead-code phase, scaled to the work. deslop's graph build is
37669
+ * CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
37670
+ * for a large repo (where the pass legitimately runs that long) and is then
37671
+ * tipped over by any concurrent load — silently dropping every dead-code
37672
+ * finding. Scaling the budget with file count (and inversely with the core
37673
+ * share when overlapped) lets the pass complete, while the ceiling still
37674
+ * reclaims a genuinely wedged worker. Returns the in-worker SIGKILL deadline
37675
+ * and the Effect-side phase backstop that sits a margin above it.
37676
+ */
37677
+ const resolveDeadCodeTimeout = (input) => {
37678
+ const coreShareFactor = Math.max(1, input.fullConcurrency / Math.max(1, input.deadCodeConcurrency));
37679
+ const workerTimeoutMs = Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor)));
37680
+ return {
37681
+ workerTimeoutMs,
37682
+ phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
37683
+ };
37684
+ };
37685
+ const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
37686
+ const entries = [];
37687
+ for (const relativePath of relativePaths) {
37688
+ const absolutePath = Path.resolve(rootDirectory, relativePath);
37689
+ const sizeBytes = statSourceFileSize(absolutePath);
37690
+ if (isLargeMinifiedFile(absolutePath, sizeBytes)) continue;
37691
+ entries.push({
37692
+ path: relativePath,
37693
+ sizeBytes: sizeBytes ?? 0
37694
+ });
37695
+ }
37696
+ return entries;
37697
+ };
37241
37698
  const listSourceFilesViaGit = (rootDirectory) => {
37242
37699
  const result = spawnSync("git", [
37243
37700
  "ls-files",
@@ -37270,7 +37727,8 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
37270
37727
  }
37271
37728
  return filePaths;
37272
37729
  };
37273
- const listSourceFiles = (rootDirectory) => excludeMinifiedFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
37730
+ const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
37731
+ const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
37274
37732
  const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
37275
37733
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
37276
37734
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
@@ -37313,9 +37771,12 @@ var Config = class Config extends Service()("react-doctor/Config") {
37313
37771
  var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37314
37772
  static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
37315
37773
  return yield* tryPromise({
37316
- try: () => checkDeadCode({
37774
+ try: (signal) => checkDeadCode({
37317
37775
  rootDirectory: input.rootDirectory,
37318
- userConfig: input.userConfig
37776
+ userConfig: input.userConfig,
37777
+ parseConcurrency: input.parseConcurrency,
37778
+ workerTimeoutMs: input.workerTimeoutMs,
37779
+ abortSignal: signal
37319
37780
  }),
37320
37781
  catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
37321
37782
  }).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
@@ -37506,43 +37967,46 @@ var Git = class Git extends Service()("react-doctor/Git") {
37506
37967
  * reason: GitInvocationFailed })` so the rest of the codebase
37507
37968
  * sees a single failure channel.
37508
37969
  */
37509
- const runCommand = (input) => scoped(gen(function* () {
37510
- const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37511
- cwd: input.directory,
37512
- env: input.env,
37513
- extendEnv: true
37514
- }));
37515
- const maxStdoutBytes = input.maxStdoutBytes;
37516
- const stdoutByteCount = yield* make$13(0);
37517
- const [stdout, stderr, status] = yield* all([
37518
- 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({
37519
- args: [...input.args],
37520
- directory: input.directory,
37521
- cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
37522
- }) })) : void_)))))),
37523
- mkString(decodeText(handle.stderr)),
37524
- handle.exitCode
37525
- ], { concurrency: 3 });
37526
- return {
37527
- status,
37528
- stdout,
37529
- stderr
37530
- };
37531
- })).pipe(catchTag$1("PlatformError", (cause) => {
37532
- if (input.command !== "git") return succeed$2({
37970
+ const runCommand = (input) => {
37971
+ const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
37533
37972
  status: 127,
37534
37973
  stdout: "",
37535
37974
  stderr: String(cause)
37536
- });
37537
- return new ReactDoctorError({ reason: new GitInvocationFailed({
37975
+ }) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37538
37976
  args: [...input.args],
37539
37977
  directory: input.directory,
37540
37978
  cause
37541
- }) });
37542
- }), withSpan("git.exec", { attributes: {
37543
- "git.command": input.command,
37544
- "git.subcommand": input.args[0] ?? ""
37545
- } }));
37979
+ }) }));
37980
+ return scoped(gen(function* () {
37981
+ if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
37982
+ const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
37983
+ if (argvLengthChars > 24e3) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${SPAWN_ARGS_MAX_LENGTH_CHARS})`);
37984
+ const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37985
+ cwd: input.directory,
37986
+ env: input.env,
37987
+ extendEnv: true
37988
+ }));
37989
+ const maxStdoutBytes = input.maxStdoutBytes;
37990
+ const stdoutByteCount = yield* make$13(0);
37991
+ const [stdout, stderr, status] = yield* all([
37992
+ 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({
37993
+ args: [...input.args],
37994
+ directory: input.directory,
37995
+ cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
37996
+ }) })) : void_)))))),
37997
+ mkString(decodeText(handle.stderr)),
37998
+ handle.exitCode
37999
+ ], { concurrency: 3 });
38000
+ return {
38001
+ status,
38002
+ stdout,
38003
+ stderr
38004
+ };
38005
+ })).pipe(catchTag$1("PlatformError", foldSpawnFailure), withSpan("git.exec", { attributes: {
38006
+ "git.command": input.command,
38007
+ "git.subcommand": input.args[0] ?? ""
38008
+ } }));
38009
+ };
37546
38010
  const runGit = (directory, args) => runCommand({
37547
38011
  command: "git",
37548
38012
  args,
@@ -37575,7 +38039,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37575
38039
  "rev-parse",
37576
38040
  "--verify",
37577
38041
  branch
37578
- ]).pipe(map$3((result) => result.status === 0));
38042
+ ]).pipe(map$3((result) => result.status === 0), catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(false) : fail$4(error)));
37579
38043
  const headSha = (directory) => runGit(directory, ["rev-parse", "HEAD"]).pipe(map$3((result) => result.status === 0 ? trimOrNull(result.stdout) : null));
37580
38044
  const mergeBase = (input) => isSafeGitRevision(input.ref) ? runGit(input.directory, [
37581
38045
  "merge-base",
@@ -37789,7 +38253,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37789
38253
  ]);
37790
38254
  if (result.status !== 0) return null;
37791
38255
  return parseChangedLineRanges(result.stdout);
37792
- }).pipe(withSpan("Git.changedLineRanges"))
38256
+ }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
37793
38257
  });
37794
38258
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
37795
38259
  /**
@@ -38028,6 +38492,14 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
38028
38492
  process.removeListener("exit", onExit);
38029
38493
  };
38030
38494
  };
38495
+ const ROOT_DIRECTORY_PLACEHOLDER = "<root>";
38496
+ const normalizeConfigForHash = (config) => {
38497
+ const clone = JSON.parse(JSON.stringify(config));
38498
+ if (clone?.settings?.["react-doctor"]) clone.settings["react-doctor"].rootDirectory = ROOT_DIRECTORY_PLACEHOLDER;
38499
+ if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
38500
+ return clone;
38501
+ };
38502
+ 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");
38031
38503
  /**
38032
38504
  * Loads a plugin module via the local require resolver and extracts
38033
38505
  * `(name, ruleNames)` from either `module.exports.meta + rules` or
@@ -38054,16 +38526,16 @@ const readPluginShape = (pluginSpecifier, loadModule) => {
38054
38526
  ruleNames: new Set(Object.keys(rules))
38055
38527
  };
38056
38528
  };
38057
- const bundledRequire = createRequire(import.meta.url);
38529
+ const bundledRequire$1 = createRequire(import.meta.url);
38058
38530
  const resolveReactHooksJsPlugin = (hasReactCompiler, customRulesOnly) => {
38059
38531
  if (!hasReactCompiler || customRulesOnly) return null;
38060
38532
  let pluginSpecifier;
38061
38533
  try {
38062
- pluginSpecifier = bundledRequire.resolve("eslint-plugin-react-hooks");
38534
+ pluginSpecifier = bundledRequire$1.resolve("eslint-plugin-react-hooks");
38063
38535
  } catch {
38064
38536
  return null;
38065
38537
  }
38066
- const { ruleNames } = readPluginShape(pluginSpecifier, (spec) => bundledRequire(spec));
38538
+ const { ruleNames } = readPluginShape(pluginSpecifier, (spec) => bundledRequire$1(spec));
38067
38539
  return {
38068
38540
  entry: {
38069
38541
  name: "react-hooks-js",
@@ -38182,8 +38654,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
38182
38654
  }
38183
38655
  return enabled;
38184
38656
  };
38185
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
38186
- const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38657
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
38658
+ const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38187
38659
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
38188
38660
  const jsPlugins = [];
38189
38661
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -38192,6 +38664,8 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38192
38664
  for (const registryEntry of REACT_DOCTOR_RULES) {
38193
38665
  const rule = reactDoctorPlugin.rules[registryEntry.id];
38194
38666
  if (!rule) continue;
38667
+ if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
38668
+ if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
38195
38669
  if (rule.scan !== void 0) continue;
38196
38670
  if (customRulesOnly && registryEntry.originallyExternal) continue;
38197
38671
  if (rule.framework !== "global" && !rule.requires) continue;
@@ -38206,7 +38680,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38206
38680
  enabledReactDoctorRules[registryEntry.key] = severity;
38207
38681
  }
38208
38682
  const userPluginRules = {};
38209
- for (const userPlugin of userPlugins) {
38683
+ if (ruleSelection !== "sidecar") for (const userPlugin of userPlugins) {
38210
38684
  Object.assign(userPluginRules, buildUserPluginRules(userPlugin, severityControls));
38211
38685
  jsPlugins.push(userPlugin.entry);
38212
38686
  }
@@ -38236,6 +38710,100 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38236
38710
  }
38237
38711
  };
38238
38712
  };
38713
+ const atomicWriteJson = (filePath, value) => {
38714
+ try {
38715
+ NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
38716
+ const temporaryPath = `${filePath}.${process.pid}.tmp`;
38717
+ NFS.writeFileSync(temporaryPath, JSON.stringify(value));
38718
+ NFS.renameSync(temporaryPath, filePath);
38719
+ } catch {
38720
+ return;
38721
+ }
38722
+ };
38723
+ const failOpenReadJson = (filePath, fallback) => {
38724
+ try {
38725
+ return JSON.parse(NFS.readFileSync(filePath, "utf8"));
38726
+ } catch {
38727
+ return fallback;
38728
+ }
38729
+ };
38730
+ const validateDiagnostic = decodeUnknownSync(Diagnostic);
38731
+ const decodeFileDiagnostics = (raw) => {
38732
+ if (!Array.isArray(raw)) return null;
38733
+ try {
38734
+ for (const entry of raw) validateDiagnostic(entry);
38735
+ return raw;
38736
+ } catch {
38737
+ return null;
38738
+ }
38739
+ };
38740
+ const emptyCache = () => ({
38741
+ version: 1,
38742
+ rulesets: {}
38743
+ });
38744
+ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
38745
+ const entries = /* @__PURE__ */ new Map();
38746
+ const persisted = failOpenReadJson(cacheFilePath, emptyCache());
38747
+ if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
38748
+ const bucket = persisted.rulesets[rulesetHash];
38749
+ if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
38750
+ for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
38751
+ const decoded = decodeFileDiagnostics(rawDiagnostics);
38752
+ if (decoded !== null) entries.set(fileKey, decoded);
38753
+ }
38754
+ return entries;
38755
+ };
38756
+ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38757
+ const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
38758
+ const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
38759
+ return {
38760
+ lookup: (fileKey) => entries.get(fileKey) ?? null,
38761
+ store: (fileKey, diagnostics) => {
38762
+ entries.delete(fileKey);
38763
+ entries.set(fileKey, diagnostics);
38764
+ },
38765
+ persist: () => {
38766
+ const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
38767
+ const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
38768
+ const existingBucket = rulesets[rulesetHash];
38769
+ const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
38770
+ const ourFiles = {};
38771
+ for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
38772
+ const cappedEntries = Object.entries({
38773
+ ...existingFiles,
38774
+ ...ourFiles
38775
+ }).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
38776
+ rulesets[rulesetHash] = {
38777
+ updatedAtMs: Date.now(),
38778
+ files: Object.fromEntries(cappedEntries)
38779
+ };
38780
+ const keptHashes = Object.entries(rulesets).sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8).map(([hash]) => hash);
38781
+ const prunedRulesets = {};
38782
+ for (const hash of keptHashes) prunedRulesets[hash] = rulesets[hash];
38783
+ atomicWriteJson(cacheFilePath, {
38784
+ version: 1,
38785
+ rulesets: prunedRulesets
38786
+ });
38787
+ }
38788
+ };
38789
+ };
38790
+ const bundledRequire = createRequire(import.meta.url);
38791
+ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
38792
+ "oxlint/package.json",
38793
+ "oxlint-plugin-react-doctor/package.json",
38794
+ "eslint-plugin-react-hooks/package.json"
38795
+ ];
38796
+ const resolveOxlintToolchainVersions = () => {
38797
+ const versions = [`node=${process.version}`];
38798
+ for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
38799
+ const packageJson = bundledRequire(specifier);
38800
+ const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
38801
+ versions.push(`${specifier}=${version}`);
38802
+ } catch {
38803
+ versions.push(`${specifier}=missing`);
38804
+ }
38805
+ return versions;
38806
+ };
38239
38807
  const esmRequire = createRequire(import.meta.url);
38240
38808
  const resolveOxlintBinary = () => {
38241
38809
  const oxlintMainPath = esmRequire.resolve("oxlint");
@@ -38917,15 +39485,19 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
38917
39485
  };
38918
39486
  });
38919
39487
  };
38920
- const SANITIZED_ENV = (() => {
38921
- const sanitized = {};
38922
- for (const [name, value] of Object.entries(process.env)) {
39488
+ const buildOxlintChildEnv = (sourceEnv) => {
39489
+ const childEnv = {};
39490
+ for (const [name, value] of Object.entries(sourceEnv)) {
38923
39491
  if (name === "NODE_OPTIONS" || name === "NODE_DEBUG") continue;
38924
39492
  if (name.startsWith("npm_config_")) continue;
38925
- sanitized[name] = value;
39493
+ childEnv[name] = value;
38926
39494
  }
38927
- return sanitized;
38928
- })();
39495
+ const isCompileCacheDisabled = Boolean(sourceEnv.NODE_DISABLE_COMPILE_CACHE);
39496
+ const isCompileCacheAlreadySet = childEnv.NODE_COMPILE_CACHE !== void 0;
39497
+ if (!isCompileCacheDisabled && !isCompileCacheAlreadySet) childEnv.NODE_COMPILE_CACHE = Path.join(os.tmpdir(), NODE_COMPILE_CACHE_DIR_NAME);
39498
+ return childEnv;
39499
+ };
39500
+ const SANITIZED_ENV = buildOxlintChildEnv(process.env);
38929
39501
  /**
38930
39502
  * Spawn one oxlint subprocess with hard ceilings on wall time and
38931
39503
  * output size. Returns stdout on success; raises a tagged
@@ -38942,7 +39514,11 @@ const SANITIZED_ENV = (() => {
38942
39514
  * The first three are splittable (the caller's binary-split retry
38943
39515
  * shrinks the batch and re-spawns); the fourth isn't.
38944
39516
  */
38945
- const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
39517
+ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES, abortSignal) => new Promise((resolve, reject) => {
39518
+ if (abortSignal?.aborted) {
39519
+ reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
39520
+ return;
39521
+ }
38946
39522
  const child = spawn(nodeBinaryPath, args, {
38947
39523
  cwd: rootDirectory,
38948
39524
  env: SANITIZED_ENV,
@@ -38952,7 +39528,14 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
38952
39528
  "pipe"
38953
39529
  ]
38954
39530
  });
39531
+ const onAbort = () => {
39532
+ child.kill("SIGKILL");
39533
+ reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
39534
+ };
39535
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
39536
+ const clearAbortListener = () => abortSignal?.removeEventListener("abort", onAbort);
38955
39537
  const timeoutHandle = setTimeout(() => {
39538
+ clearAbortListener();
38956
39539
  child.kill("SIGKILL");
38957
39540
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
38958
39541
  kind: "timeout",
@@ -38987,10 +39570,12 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
38987
39570
  });
38988
39571
  child.on("error", (error) => {
38989
39572
  clearTimeout(timeoutHandle);
39573
+ clearAbortListener();
38990
39574
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: error }) }));
38991
39575
  });
38992
39576
  child.on("close", (_code, signal) => {
38993
39577
  clearTimeout(timeoutHandle);
39578
+ clearAbortListener();
38994
39579
  if (didKillForSize) {
38995
39580
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
38996
39581
  kind: "output-too-large",
@@ -39057,26 +39642,28 @@ const isParallelismRelatedSpawnError = (error) => {
39057
39642
  * loop with a slimmer config in that case.
39058
39643
  */
39059
39644
  const spawnLintBatches = async (input) => {
39060
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
39645
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal } = input;
39061
39646
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
39062
39647
  const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
39063
39648
  const runBatchPass = async (concurrency) => {
39064
39649
  const allDiagnostics = [];
39065
39650
  const droppedFiles = [];
39066
39651
  let firstDropReason = null;
39067
- const spawnLintBatch = async (batch) => {
39652
+ const splitDeadlineMs = Date.now() + splitTotalBudgetMs;
39653
+ const spawnLintBatch = async (batch, depth) => {
39068
39654
  const batchArgs = [...baseArgs, ...batch];
39069
39655
  try {
39070
- return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
39656
+ return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
39071
39657
  } catch (error) {
39072
39658
  if (!isSplittableReactDoctorError(error)) throw error;
39073
- if (batch.length <= 1) {
39659
+ const splitBudgetExhausted = Date.now() >= splitDeadlineMs || depth >= splitMaxDepth;
39660
+ if (batch.length <= 1 || splitBudgetExhausted) {
39074
39661
  droppedFiles.push(...batch);
39075
- if (firstDropReason === null) firstDropReason = error.message;
39662
+ if (firstDropReason === null) firstDropReason = splitBudgetExhausted && batch.length > 1 ? `${error.message} (split budget exhausted after ${splitMaxDepth} levels / ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s)` : error.message;
39076
39663
  return [];
39077
39664
  }
39078
39665
  const splitIndex = Math.ceil(batch.length / 2);
39079
- return [...await spawnLintBatch(batch.slice(0, splitIndex)), ...await spawnLintBatch(batch.slice(splitIndex))];
39666
+ return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1)];
39080
39667
  }
39081
39668
  };
39082
39669
  let startedFileCount = 0;
@@ -39093,7 +39680,7 @@ const spawnLintBatches = async (input) => {
39093
39680
  try {
39094
39681
  const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
39095
39682
  startedFileCount += batch.length;
39096
- const batchDiagnostics = await spawnLintBatch(batch);
39683
+ const batchDiagnostics = await spawnLintBatch(batch, 0);
39097
39684
  scannedFileCount += batch.length;
39098
39685
  if (onFileProgress) {
39099
39686
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
@@ -39154,6 +39741,22 @@ const validateRuleRegistration = () => {
39154
39741
  ].filter((entry) => entry !== null).join("; ");
39155
39742
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
39156
39743
  };
39744
+ const hashFileContents = (filePath) => {
39745
+ try {
39746
+ return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
39747
+ } catch {
39748
+ return null;
39749
+ }
39750
+ };
39751
+ const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
39752
+ const resolveReactDoctorCacheDir = (projectDirectory) => {
39753
+ const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
39754
+ if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
39755
+ const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
39756
+ if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
39757
+ return Path.join(os.tmpdir(), "react-doctor-cache", projectCacheSubdir(projectDirectory));
39758
+ };
39759
+ const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
39157
39760
  /**
39158
39761
  * Atomically (re)writes the generated oxlintrc.json. Used twice in
39159
39762
  * the runner: once for the primary scan, once for the
@@ -39212,7 +39815,7 @@ const reactHooksJsPluginDropNote = (error) => {
39212
39815
  * 6. always restore disable directives + clean up the temp dir
39213
39816
  */
39214
39817
  const runOxlint = async (options) => {
39215
- const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, spawnTimeoutMs, outputMaxBytes } = options;
39818
+ 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;
39216
39819
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
39217
39820
  const severityControls = buildRuleSeverityControls(userConfig);
39218
39821
  validateRuleRegistration();
@@ -39229,30 +39832,156 @@ const runOxlint = async (options) => {
39229
39832
  serverAuthFunctionNames,
39230
39833
  severityControls,
39231
39834
  userPlugins,
39232
- disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
39835
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
39836
+ ruleSelection: overrides.ruleSelection
39233
39837
  });
39234
39838
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
39235
39839
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
39236
39840
  const configPath = Path.join(configDirectory, "oxlintrc.json");
39237
39841
  try {
39238
- const baseArgs = [
39239
- resolveOxlintBinary(),
39240
- "-c",
39241
- configPath,
39242
- "--format",
39243
- "json"
39244
- ];
39842
+ const oxlintBinary = resolveOxlintBinary();
39843
+ const sharedArgs = [];
39844
+ let tsconfigContent = null;
39245
39845
  if (project.hasTypeScript) {
39246
39846
  const tsconfigRelativePath = resolveTsConfigRelativePath(rootDirectory);
39247
- if (tsconfigRelativePath) baseArgs.push("--tsconfig", tsconfigRelativePath);
39847
+ if (tsconfigRelativePath) {
39848
+ sharedArgs.push("--tsconfig", tsconfigRelativePath);
39849
+ try {
39850
+ tsconfigContent = NFS.readFileSync(Path.resolve(rootDirectory, tsconfigRelativePath), "utf8");
39851
+ } catch {
39852
+ tsconfigContent = null;
39853
+ }
39854
+ }
39248
39855
  }
39249
39856
  const combinedPatterns = collectIgnorePatterns(rootDirectory);
39250
39857
  if (combinedPatterns.length > 0) {
39251
39858
  const combinedIgnorePath = Path.join(configDirectory, "combined.ignore");
39252
39859
  NFS.writeFileSync(combinedIgnorePath, `${combinedPatterns.join("\n")}\n`);
39253
- baseArgs.push("--ignore-path", combinedIgnorePath);
39860
+ sharedArgs.push("--ignore-path", combinedIgnorePath);
39254
39861
  }
39255
- const fileBatches = batchIncludePaths(baseArgs, includePaths !== void 0 ? includePaths : listSourceFiles(rootDirectory));
39862
+ const makeBaseArgs = (oxlintConfigPath) => [
39863
+ oxlintBinary,
39864
+ "-c",
39865
+ oxlintConfigPath,
39866
+ "--format",
39867
+ "json",
39868
+ ...sharedArgs
39869
+ ];
39870
+ const discoverScanFiles = () => lintBatchOrdering === "cost" ? sortSourceFilesByCost(listSourceFilesWithSize(rootDirectory)) : listSourceFiles(rootDirectory);
39871
+ const candidateFiles = includePaths !== void 0 ? includePaths : discoverScanFiles();
39872
+ const runConfigOverFiles = async (buildConfigForPass, configFileName, files, fileProgress) => {
39873
+ if (files.length === 0) return {
39874
+ diagnostics: [],
39875
+ didDropReactHooksJsPlugin: false,
39876
+ hadPartialFailure: false
39877
+ };
39878
+ let hadPartialFailure = false;
39879
+ const reportPartialFailure = (reason) => {
39880
+ hadPartialFailure = true;
39881
+ onPartialFailure?.(reason);
39882
+ };
39883
+ const passConfigPath = Path.join(configDirectory, configFileName);
39884
+ const passBaseArgs = makeBaseArgs(passConfigPath);
39885
+ const passFileBatches = batchIncludePaths(passBaseArgs, files);
39886
+ const spawnPass = () => spawnLintBatches({
39887
+ baseArgs: passBaseArgs,
39888
+ fileBatches: passFileBatches,
39889
+ rootDirectory,
39890
+ nodeBinaryPath,
39891
+ project,
39892
+ onPartialFailure: reportPartialFailure,
39893
+ onFileProgress: fileProgress,
39894
+ spawnTimeoutMs,
39895
+ outputMaxBytes,
39896
+ concurrency: options.concurrency,
39897
+ signal: options.signal
39898
+ });
39899
+ writeOxlintConfig(passConfigPath, buildConfigForPass({}));
39900
+ try {
39901
+ return {
39902
+ diagnostics: await spawnPass(),
39903
+ didDropReactHooksJsPlugin: false,
39904
+ hadPartialFailure
39905
+ };
39906
+ } catch (error) {
39907
+ const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
39908
+ if (reactHooksJsDropNote === null) throw error;
39909
+ writeOxlintConfig(passConfigPath, buildConfigForPass({ disableReactHooksJsPlugin: true }));
39910
+ const diagnostics = await spawnPass();
39911
+ reportPartialFailure(reactHooksJsDropNote);
39912
+ return {
39913
+ diagnostics,
39914
+ didDropReactHooksJsPlugin: true,
39915
+ hadPartialFailure
39916
+ };
39917
+ }
39918
+ };
39919
+ if (perFileLintCacheEnabled && respectInlineDisables && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
39920
+ const rulesetHash = computeRulesetHash({
39921
+ config: buildConfig({
39922
+ extendsPaths: [],
39923
+ ruleSelection: "cacheable"
39924
+ }),
39925
+ toolchainVersions: resolveOxlintToolchainVersions(),
39926
+ ignorePatterns: combinedPatterns,
39927
+ tsconfigContent
39928
+ });
39929
+ const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
39930
+ const cacheKeyByFile = /* @__PURE__ */ new Map();
39931
+ const missFiles = [];
39932
+ const replayedDiagnostics = [];
39933
+ for (const candidateFile of candidateFiles) {
39934
+ const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
39935
+ if (contentHash === null) {
39936
+ missFiles.push(candidateFile);
39937
+ continue;
39938
+ }
39939
+ const cacheKey = `${candidateFile.replaceAll("\\", "/")}${contentHash}`;
39940
+ cacheKeyByFile.set(candidateFile, cacheKey);
39941
+ const cachedDiagnostics = cache.lookup(cacheKey);
39942
+ if (cachedDiagnostics === null) missFiles.push(candidateFile);
39943
+ else replayedDiagnostics.push(...cachedDiagnostics);
39944
+ }
39945
+ const cacheHitFileCount = candidateFiles.length - missFiles.length;
39946
+ const cacheableResult = await runConfigOverFiles((overrides) => buildConfig({
39947
+ extendsPaths: [],
39948
+ ruleSelection: "cacheable",
39949
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
39950
+ }), "oxlintrc.cacheable.json", missFiles, void 0);
39951
+ const sidecarResult = await runConfigOverFiles(() => buildConfig({
39952
+ extendsPaths: [],
39953
+ ruleSelection: "sidecar"
39954
+ }), "oxlintrc.sidecar.json", candidateFiles, options.onFileProgress);
39955
+ onCacheStats?.(cacheHitFileCount, candidateFiles.length);
39956
+ const missFileByNormalizedPath = /* @__PURE__ */ new Map();
39957
+ for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
39958
+ const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
39959
+ let isAttributionSound = true;
39960
+ for (const diagnostic of cacheableResult.diagnostics) {
39961
+ const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
39962
+ if (missFile === void 0) {
39963
+ isAttributionSound = false;
39964
+ break;
39965
+ }
39966
+ const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
39967
+ fileDiagnostics.push(diagnostic);
39968
+ freshDiagnosticsByFile.set(missFile, fileDiagnostics);
39969
+ }
39970
+ if (!cacheableResult.didDropReactHooksJsPlugin && !cacheableResult.hadPartialFailure && isAttributionSound) {
39971
+ for (const missFile of missFiles) {
39972
+ const cacheKey = cacheKeyByFile.get(missFile);
39973
+ if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
39974
+ }
39975
+ cache.persist();
39976
+ }
39977
+ return dedupeDiagnostics([
39978
+ ...replayedDiagnostics,
39979
+ ...cacheableResult.diagnostics,
39980
+ ...sidecarResult.diagnostics
39981
+ ]);
39982
+ }
39983
+ const baseArgs = makeBaseArgs(configPath);
39984
+ const fileBatches = batchIncludePaths(baseArgs, candidateFiles);
39256
39985
  const runBatches = () => spawnLintBatches({
39257
39986
  baseArgs,
39258
39987
  fileBatches,
@@ -39263,7 +39992,8 @@ const runOxlint = async (options) => {
39263
39992
  onFileProgress: options.onFileProgress,
39264
39993
  spawnTimeoutMs,
39265
39994
  outputMaxBytes,
39266
- concurrency: options.concurrency
39995
+ concurrency: options.concurrency,
39996
+ signal: options.signal
39267
39997
  });
39268
39998
  writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
39269
39999
  try {
@@ -39342,9 +40072,11 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
39342
40072
  const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
39343
40073
  const outputMaxBytes = yield* OxlintOutputMaxBytes;
39344
40074
  const concurrency = yield* OxlintConcurrency;
40075
+ const lintBatchOrdering = yield* LintBatchOrdering;
40076
+ const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
39345
40077
  const collectedFailures = [];
39346
40078
  const diagnostics = yield* tryPromise({
39347
- try: () => runOxlint({
40079
+ try: (signal) => runOxlint({
39348
40080
  rootDirectory: input.rootDirectory,
39349
40081
  project: input.project,
39350
40082
  includePaths: input.includePaths ? [...input.includePaths] : void 0,
@@ -39359,9 +40091,13 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
39359
40091
  collectedFailures.push(reason);
39360
40092
  },
39361
40093
  onFileProgress: input.onFileProgress,
40094
+ perFileLintCacheEnabled,
40095
+ onCacheStats: input.onCacheStats,
39362
40096
  spawnTimeoutMs,
39363
40097
  outputMaxBytes,
39364
- concurrency
40098
+ concurrency,
40099
+ signal,
40100
+ lintBatchOrdering
39365
40101
  }),
39366
40102
  catch: ensureReactDoctorError
39367
40103
  });
@@ -39753,14 +40489,49 @@ const parseArtifactFromBody = (body) => {
39753
40489
  }
39754
40490
  return null;
39755
40491
  };
39756
- const fetchSocketArtifact = (dependency) => tryPromise(async (signal) => {
40492
+ const isSupplyChainCacheDisabled = () => {
40493
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
40494
+ return noCache === "1" || noCache === "true";
40495
+ };
40496
+ const supplyChainCacheFile = (cacheDirectory, dependency) => {
40497
+ const purlHash = crypto.createHash("sha256").update(toPurl(dependency)).digest("hex").slice(0, 16);
40498
+ return Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR, `${purlHash}.json`);
40499
+ };
40500
+ const readCachedSocketBody = (cacheFile) => {
40501
+ try {
40502
+ const entry = JSON.parse(NFS.readFileSync(cacheFile, "utf-8"));
40503
+ 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;
40504
+ } catch {}
40505
+ return null;
40506
+ };
40507
+ const writeCachedSocketBody = (cacheFile, body) => {
40508
+ try {
40509
+ NFS.mkdirSync(Path.dirname(cacheFile), { recursive: true });
40510
+ NFS.writeFileSync(cacheFile, JSON.stringify({
40511
+ fetchedAtMs: Date.now(),
40512
+ body
40513
+ }));
40514
+ } catch {}
40515
+ };
40516
+ const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
40517
+ const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
40518
+ if (cacheFile !== null) {
40519
+ const cachedBody = readCachedSocketBody(cacheFile);
40520
+ if (cachedBody !== null) {
40521
+ const cachedArtifact = parseArtifactFromBody(cachedBody);
40522
+ if (cachedArtifact !== null) return cachedArtifact;
40523
+ }
40524
+ }
39757
40525
  const requestUrl = `${SOCKET_FREE_PURL_API_BASE}/${encodeURIComponent(toPurl(dependency))}`;
39758
40526
  const response = await fetch(requestUrl, {
39759
40527
  headers: { "User-Agent": SOCKET_FREE_USER_AGENT },
39760
40528
  signal
39761
40529
  });
39762
40530
  if (!response.ok) return null;
39763
- return parseArtifactFromBody(await response.text());
40531
+ const body = await response.text();
40532
+ const artifact = parseArtifactFromBody(body);
40533
+ if (artifact !== null && cacheFile !== null) writeCachedSocketBody(cacheFile, body);
40534
+ return artifact;
39764
40535
  }).pipe(timeout(FETCH_TIMEOUT_MS), orElseSucceed(() => null), tap$1((artifact) => {
39765
40536
  const scoreAttributes = {};
39766
40537
  if (artifact !== null) {
@@ -39865,7 +40636,8 @@ const checkSupplyChain = (input) => gen(function* () {
39865
40636
  const packageJsonPath = Path.join(input.rootDirectory, "package.json");
39866
40637
  const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
39867
40638
  if (dependencies.length === 0) return [];
39868
- const artifacts = yield* forEach$1(dependencies, fetchSocketArtifact, { concurrency: 8 });
40639
+ const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
40640
+ const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
39869
40641
  const diagnostics = [];
39870
40642
  for (let index = 0; index < dependencies.length; index += 1) {
39871
40643
  const artifact = artifacts[index];
@@ -39890,6 +40662,10 @@ const checkSupplyChain = (input) => gen(function* () {
39890
40662
  * The underlying `checkSupplyChain` Effect is total/fail-open — per-package
39891
40663
  * timeouts and network failures recover to "skip" — so the stream never
39892
40664
  * fails, mirroring `DeadCode`'s stream shape so the two compose the same way.
40665
+ * The orchestrator (`run-inspect.ts`) consumes this stream on a background
40666
+ * fiber whose network time overlaps the lint pass, joined under a generous
40667
+ * wall-clock budget; a budget expiry is the same fail-open outcome as a Socket
40668
+ * outage.
39893
40669
  */
39894
40670
  var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
39895
40671
  static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(checkSupplyChain(input).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
@@ -39948,18 +40724,45 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
39948
40724
  *
39949
40725
  * Phases:
39950
40726
  *
39951
- * 1. Config.resolve(directory) → Project.discover → Git metadata
40727
+ * 1. Config.resolve(directory) → Project.discover → Git metadata.
40728
+ * The GitHub viewer-permission lookup is forked onto a background
40729
+ * fiber here and joined late (it feeds score metadata, not
40730
+ * diagnostics).
39952
40731
  * 2. beforeLint hook (e.g. CLI renders the project-detection block)
39953
40732
  * 3. environment checks (reduced-motion + pnpm hardening +
39954
- * expo/react-native + security scan)
39955
- * 4. Linter.run + DeadCode.run forked as concurrent fibers so
39956
- * their wall-clock times overlap. Progress spinners stay
39957
- * sequential (lint first, then dead-code) for clean terminal
39958
- * output. GitHub viewer permission also runs as a background
39959
- * fiber during this phase.
39960
- * 5. afterLint hook
39961
- * 6. Reporter.finalize
39962
- * 7. Score.compute against the surface-filtered diagnostic set
40733
+ * expo/react-native), collected synchronously. The heavier
40734
+ * content-regex security scan is forked instead (like supply-chain
40735
+ * below) and joined before the concat, so its CPU overlaps lint
40736
+ * rather than blocking the event loop before it.
40737
+ * 4. The supply-chain check (Socket.dev) is forked onto a background
40738
+ * fiber so its ~100% network-bound time overlaps the ~100%
40739
+ * CPU/subprocess-bound lint pass below, collapsing two serial
40740
+ * phases into roughly `max(supplyChain, lint)`. It is capped by
40741
+ * `SupplyChainOverlapTimeoutMs` (measured from fork) so a hung
40742
+ * socket can't drag out its join; on timeout it fails open to no
40743
+ * diagnostics — the same outcome class as a Socket outage.
40744
+ * 5. Linter.run runs; DeadCode.run runs concurrently (forked child
40745
+ * fiber) ONLY when the memory gate has headroom to run the 8 GB
40746
+ * dead-code child alongside the oxlint workers — or when overlap is
40747
+ * forced via REACT_DOCTOR_DEAD_CODE_OVERLAP. Otherwise dead-code
40748
+ * runs sequentially after lint, exactly as it did pre-overlap. The
40749
+ * fiber is joined (or interrupted, SIGKILLing its worker, on lint
40750
+ * failure) before diagnostics are concatenated. The afterLint hook
40751
+ * fires between lint and dead-code. Progress spinner labels AND the
40752
+ * final diagnostic / score order stay independent of execution
40753
+ * order, so terminal output is identical either way; supply-chain
40754
+ * rides alongside without a spinner.
40755
+ * 6. Join the supply-chain fiber, then assemble the diagnostics in a
40756
+ * FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
40757
+ * byte-identical regardless of which fiber settled first. The
40758
+ * viewer-permission fiber is joined later, during score-metadata
40759
+ * assembly (it feeds score metadata, not diagnostics). The per-element
40760
+ * `Reporter.emit` side-channel now interleaves supply-chain with lint
40761
+ * emits, so capture-order assertions must target the deterministic
40762
+ * concat below, not emit order (production `Reporter.layerNoop` makes
40763
+ * emit a no-op).
40764
+ * 7. Reporter.finalize
40765
+ * 8. Score.compute against the surface-filtered diagnostic set
39963
40766
  *
39964
40767
  * The orchestrator owns spinner lifecycle via `Progress`; callers
39965
40768
  * choose `Progress.layerOra(...)` for CLI feedback or
@@ -40011,16 +40814,27 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40011
40814
  ...checkPnpmHardening(scanDirectory),
40012
40815
  ...checkReactServerComponentsAdvisory(scanDirectory, project),
40013
40816
  ...checkExpoProject(scanDirectory, project),
40014
- ...checkReactNativeProject(scanDirectory, project),
40015
- ...checkSecurityScan(scanDirectory, {
40016
- project,
40017
- ignoredTags: input.ignoredTags
40018
- })
40817
+ ...checkReactNativeProject(scanDirectory, project)
40019
40818
  ])));
40020
- const supplyChainCollected = !isDiffMode || (input.supplyChainManifestChanged ?? false) ? yield* runCollect(applyPerElementPipeline(supplyChainService.run({
40819
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(promise(() => checkSecurityScanCooperative(scanDirectory, {
40820
+ project,
40821
+ ignoredTags: input.ignoredTags
40822
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)))))).pipe(withSpan("SecurityScan.run")));
40823
+ const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
40824
+ const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
40825
+ const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
40021
40826
  rootDirectory: scanDirectory,
40022
40827
  userConfig: resolvedConfig.config
40023
- }))) : [];
40828
+ }))).pipe(map$3((diagnostics) => ({
40829
+ diagnostics,
40830
+ timedOut: false
40831
+ })), timeout(supplyChainOverlapTimeout), orElseSucceed(() => ({
40832
+ diagnostics: [],
40833
+ timedOut: true
40834
+ }))) : succeed$2({
40835
+ diagnostics: [],
40836
+ timedOut: false
40837
+ }));
40024
40838
  const lintFailure = yield* make$13({
40025
40839
  didFail: false,
40026
40840
  reason: null,
@@ -40031,12 +40845,49 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40031
40845
  didFail: false,
40032
40846
  reason: null
40033
40847
  });
40034
- const scanConcurrency = yield* OxlintConcurrency;
40848
+ const scanConcurrency = resolveScanConcurrency(yield* OxlintConcurrency);
40849
+ const lintPhaseTimeoutMs = yield* LintPhaseTimeoutMs;
40850
+ const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
40851
+ const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
40035
40852
  const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
40853
+ const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
40854
+ const deadCodeOverlapMode = yield* DeadCodeOverlap;
40855
+ const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
40856
+ const deadCodeParseConcurrency = shouldOverlapDeadCode ? Math.max(1, Math.floor(scanConcurrency * DEAD_CODE_OVERLAP_PARSE_SHARE)) : void 0;
40857
+ const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
40858
+ const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
40859
+ rootDirectory: scanDirectory,
40860
+ userConfig: resolvedConfig.config,
40861
+ parseConcurrency: deadCodeParseConcurrency,
40862
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
40863
+ }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
40864
+ yield* set(deadCodeFailure, {
40865
+ didFail: true,
40866
+ reason: error.message
40867
+ });
40868
+ return empty$4;
40869
+ })))))).pipe(timeoutOption(deadCodeTimeout.phaseTimeoutMs), flatMap$2(match$3({
40870
+ onNone: () => set(deadCodeFailure, {
40871
+ didFail: true,
40872
+ reason: `Dead-code analysis exceeded ${Math.round(deadCodeTimeout.phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
40873
+ }).pipe(as([])),
40874
+ onSome: succeed$2
40875
+ })));
40876
+ const overlapDeadCodeTimeout = resolveDeadCodeTimeout({
40877
+ sourceFileCount: project.sourceFileCount,
40878
+ deadCodeConcurrency: deadCodeParseConcurrency ?? scanConcurrency,
40879
+ fullConcurrency: scanConcurrency
40880
+ });
40881
+ const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
40882
+ workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
40883
+ phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs)
40884
+ })) : null;
40036
40885
  const scanProgress = yield* progressService.start("Scanning...");
40037
40886
  const scanStartTime = Date.now();
40038
40887
  let lastReportedTotalFileCount = 0;
40039
- const lintCollected = yield* runCollect(applyPerElementPipeline(linterService.run({
40888
+ let lintCacheHitFileCount = null;
40889
+ let lintCacheTotalFileCount = null;
40890
+ const baseLintStream = linterService.run({
40040
40891
  rootDirectory: scanDirectory,
40041
40892
  project,
40042
40893
  includePaths: lintIncludePaths ?? void 0,
@@ -40050,6 +40901,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40050
40901
  onFileProgress: (scannedFileCount, totalFileCount) => {
40051
40902
  lastReportedTotalFileCount = totalFileCount;
40052
40903
  runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})${workerCountSuffix}...`));
40904
+ },
40905
+ onCacheStats: (cacheHitFileCount, totalConsideredFileCount) => {
40906
+ lintCacheHitFileCount = cacheHitFileCount;
40907
+ lintCacheTotalFileCount = totalConsideredFileCount;
40053
40908
  }
40054
40909
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
40055
40910
  yield* set(lintFailure, {
@@ -40059,36 +40914,56 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40059
40914
  reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
40060
40915
  });
40061
40916
  return empty$4;
40062
- }))))));
40917
+ }))));
40918
+ const lintCollected = yield* runCollect(applyPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
40919
+ onNone: () => set(lintFailure, {
40920
+ didFail: true,
40921
+ reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
40922
+ reasonTag: "OxlintBatchExceeded",
40923
+ reasonKind: null
40924
+ }).pipe(as([])),
40925
+ onSome: succeed$2
40926
+ })));
40063
40927
  const lintFailureState = yield* get$2(lintFailure);
40064
40928
  yield* afterLint(lintFailureState.didFail);
40065
40929
  if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
40066
40930
  const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
40067
40931
  const scannedFilesLabel = `${totalFileCount} ${totalFileCount === 1 ? "file" : "files"}`;
40068
- const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
40069
- const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`).pipe(andThen(runCollect(applyPerElementPipeline(deadCodeService.run({
40070
- rootDirectory: scanDirectory,
40071
- userConfig: resolvedConfig.config
40072
- }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
40073
- yield* set(deadCodeFailure, {
40074
- didFail: true,
40075
- reason: error.message
40932
+ let deadCodeCollected = [];
40933
+ if (lintFailureState.didFail) {
40934
+ if (deadCodeFiber !== null) yield* interrupt(deadCodeFiber);
40935
+ } else if (shouldRunDeadCode) {
40936
+ yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
40937
+ const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
40938
+ sourceFileCount: totalFileCount,
40939
+ deadCodeConcurrency: scanConcurrency,
40940
+ fullConcurrency: scanConcurrency
40076
40941
  });
40077
- return empty$4;
40078
- }))))))));
40079
- const deadCodeFailureState = yield* get$2(deadCodeFailure);
40942
+ deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
40943
+ workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
40944
+ phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs)
40945
+ });
40946
+ }
40947
+ const deadCodeFailureState = lintFailureState.didFail ? {
40948
+ didFail: false,
40949
+ reason: null
40950
+ } : yield* get$2(deadCodeFailure);
40080
40951
  const scanElapsedMilliseconds = Date.now() - scanStartTime;
40081
40952
  const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
40082
40953
  if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
40083
40954
  else if (input.suppressScanSummary) yield* scanProgress.stop();
40084
40955
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
40956
+ const supplyChainResult = yield* join(supplyChainFiber);
40957
+ const supplyChainCollected = supplyChainResult.diagnostics;
40958
+ const securityScanCollected = yield* join(securityScanFiber);
40085
40959
  yield* reporterService.finalize;
40086
- const finalDiagnostics = assignFixGroups([
40960
+ const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
40087
40961
  ...envCollected,
40962
+ ...securityScanCollected,
40088
40963
  ...supplyChainCollected,
40089
40964
  ...lintCollected,
40090
40965
  ...deadCodeCollected
40091
- ]);
40966
+ ]));
40092
40967
  const githubViewerPermission = yield* join(githubViewerPermissionFiber);
40093
40968
  const scoreMetadata = {
40094
40969
  ...repo !== null ? { repo } : {},
@@ -40124,9 +40999,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40124
40999
  lintPartialFailures,
40125
41000
  didDeadCodeFail: deadCodeFailureState.didFail,
40126
41001
  deadCodeFailureReason: deadCodeFailureState.reason,
41002
+ deadCodeOverlapped: shouldOverlapDeadCode,
40127
41003
  scannedFileCount: totalFileCount,
40128
41004
  scannedFilePaths,
40129
- scanElapsedMilliseconds
41005
+ scanElapsedMilliseconds,
41006
+ scanConcurrency,
41007
+ supplyChainOverlapTimedOut: supplyChainResult.timedOut,
41008
+ lintCacheHitFileCount,
41009
+ lintCacheTotalFileCount
40130
41010
  };
40131
41011
  }).pipe(withSpan("runInspect", { attributes: {
40132
41012
  "inspect.directory": input.directory,
@@ -40134,7 +41014,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40134
41014
  "inspect.runDeadCode": input.runDeadCode,
40135
41015
  "inspect.isCi": input.isCi,
40136
41016
  "inspect.scoreSurface": input.scoreSurface ?? "score"
40137
- } }));
41017
+ } }), (scanProgram) => flatMap$2(ScanDeadlineMs, (scanDeadlineMs) => scanProgram.pipe(timeout(scanDeadlineMs), catchTag$1("TimeoutError", () => new ReactDoctorError({ reason: new ScanDeadlineExceeded({ detail: `${scanDeadlineMs / MILLISECONDS_PER_SECOND}s elapsed` }) })))));
40138
41018
  const parseNodeVersion = (versionString) => {
40139
41019
  const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
40140
41020
  return {
@@ -40678,6 +41558,7 @@ const clearCaches = () => {
40678
41558
  clearIgnorePatternsCache();
40679
41559
  clearPackageRoleCache();
40680
41560
  clearAutoSuppressionCaches();
41561
+ clearMinifiedFileCache();
40681
41562
  };
40682
41563
  const toJsonReport = (result, options) => buildJsonReport({
40683
41564
  version: options.version,
@@ -40701,4 +41582,4 @@ const toJsonReport = (result, options) => buildJsonReport({
40701
41582
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
40702
41583
 
40703
41584
  //# sourceMappingURL=index.js.map
40704
- //# debugId=0a49b285-88be-5b0e-803b-22151c5001c2
41585
+ //# debugId=b9d1a98e-f5ec-5357-a08b-f84864fdfa20