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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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]="a4394ddc-4e6c-5a18-aeeb-d60322b1c0dd")}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]="33508ee6-c977-5b5f-8585-9939928ce74d")}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,6 +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 crypto, { createHash } from "node:crypto";
20
21
  import { gzipSync } from "node:zlib";
21
22
  //#region ../../node_modules/.pnpm/effect@4.0.0-beta.70/node_modules/effect/dist/Pipeable.js
22
23
  /**
@@ -7198,7 +7199,7 @@ const provideContext$1 = /* @__PURE__ */ dual(2, (self, context) => {
7198
7199
  return updateContext$1(self, merge$3(context));
7199
7200
  });
7200
7201
  /** @internal */
7201
- const provideService$1 = function() {
7202
+ const provideService$3 = function() {
7202
7203
  if (arguments.length === 1) return dual(2, (self, impl) => provideServiceImpl(self, arguments[0], impl));
7203
7204
  return dual(3, (self, service, impl) => provideServiceImpl(self, service, impl)).apply(this, arguments);
7204
7205
  };
@@ -7429,7 +7430,7 @@ const constScopeEmpty = { _tag: "Empty" };
7429
7430
  /** @internal */
7430
7431
  const scope = scopeTag;
7431
7432
  /** @internal */
7432
- const provideScope = /* @__PURE__ */ provideService$1(scopeTag);
7433
+ const provideScope = /* @__PURE__ */ provideService$3(scopeTag);
7433
7434
  /** @internal */
7434
7435
  const scoped$1 = (self) => withFiber$1((fiber) => {
7435
7436
  const prev = fiber.context;
@@ -7870,7 +7871,7 @@ const makeLatchUnsafe = (open) => new Latch(open ?? false);
7870
7871
  /** @internal */
7871
7872
  const makeLatch = (open) => sync$2(() => makeLatchUnsafe(open));
7872
7873
  /** @internal */
7873
- const withTracerEnabled$1 = /* @__PURE__ */ provideService$1(TracerEnabled);
7874
+ const withTracerEnabled$1 = /* @__PURE__ */ provideService$3(TracerEnabled);
7874
7875
  const bigint0 = /* @__PURE__ */ BigInt(0);
7875
7876
  const NoopSpanProto = {
7876
7877
  _tag: "Span",
@@ -7951,7 +7952,7 @@ const useSpan$1 = (name, ...args) => {
7951
7952
  }));
7952
7953
  });
7953
7954
  };
7954
- const provideParentSpan = /* @__PURE__ */ provideService$1(ParentSpan);
7955
+ const provideParentSpan = /* @__PURE__ */ provideService$3(ParentSpan);
7955
7956
  /** @internal */
7956
7957
  const withParentSpan$1 = function() {
7957
7958
  const dataFirst = isEffect$1(arguments[0]);
@@ -9518,7 +9519,7 @@ var CurrentMemoMap = class extends Service()("effect/Layer/CurrentMemoMap") {
9518
9519
  * @category memo map
9519
9520
  * @since 2.0.0
9520
9521
  */
9521
- 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));
9522
9523
  /**
9523
9524
  * Builds a layer into an `Effect` value. Any resources associated with this
9524
9525
  * layer will be released when the specified scope is closed unless their scope
@@ -10871,7 +10872,7 @@ const provide$1 = /* @__PURE__ */ dual((args) => isEffect$1(args[0]), (self, sou
10871
10872
  /** @internal */
10872
10873
  const repeatOrElse = /* @__PURE__ */ dual(3, (self, schedule, orElse) => flatMap$4(toStepWithMetadata(schedule), (step) => {
10873
10874
  let meta = CurrentMetadata.defaultValue();
10874
- 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(() => {
10875
10876
  meta = meta_;
10876
10877
  })), { disableYield: true }), (error) => isDone$2(error) ? succeed$5(error.value) : orElse(error, meta.attempt === 0 ? none() : some(meta)));
10877
10878
  }));
@@ -10879,7 +10880,7 @@ const repeatOrElse = /* @__PURE__ */ dual(3, (self, schedule, orElse) => flatMap
10879
10880
  const retryOrElse = /* @__PURE__ */ dual(3, (self, policy, orElse) => flatMap$4(toStepWithMetadata(policy), (step) => {
10880
10881
  let meta = CurrentMetadata.defaultValue();
10881
10882
  let lastError;
10882
- 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) => {
10883
10884
  lastError = error;
10884
10885
  return flatMap$4(step(error), (meta_) => {
10885
10886
  meta = meta_;
@@ -12995,7 +12996,7 @@ const updateContext = updateContext$1;
12995
12996
  * @category Context
12996
12997
  * @since 2.0.0
12997
12998
  */
12998
- const provideService = provideService$1;
12999
+ const provideService$2 = provideService$3;
12999
13000
  /**
13000
13001
  * Scopes all resources used in this workflow to the lifetime of the workflow,
13001
13002
  * ensuring that their finalizers are run as soon as this workflow completes
@@ -17989,6 +17990,20 @@ function decodeUnknownOption$1(schema, options) {
17989
17990
  return asOption(decodeUnknownEffect(schema, options));
17990
17991
  }
17991
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
+ /**
17992
18007
  * Creates an effectful encoder for `unknown` input.
17993
18008
  *
17994
18009
  * **Details**
@@ -18290,6 +18305,40 @@ function isSchemaError(u) {
18290
18305
  */
18291
18306
  const decodeUnknownOption = decodeUnknownOption$1;
18292
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
+ /**
18293
18342
  * Encodes an `unknown` input against a schema synchronously, throwing a
18294
18343
  * {@link SchemaError} on failure. Use this when you want to serialize data at a
18295
18344
  * boundary and treat a schema mismatch as an unrecoverable error. For
@@ -19256,7 +19305,8 @@ var Diagnostic = class extends Class("Diagnostic")({
19256
19305
  category: String$1,
19257
19306
  fileContext: optional(Literals(["test", "story"])),
19258
19307
  suppressionHint: optional(String$1),
19259
- relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation))
19308
+ relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation)),
19309
+ fixGroupId: optional(String$1)
19260
19310
  }) {};
19261
19311
  const JsonReportMode = Literals([
19262
19312
  "full",
@@ -19298,6 +19348,7 @@ var JsonReportProjectEntry = class extends Class("JsonReportProjectEntry")({
19298
19348
  score: Unknown,
19299
19349
  skippedChecks: ArraySchema(String$1),
19300
19350
  skippedCheckReasons: optional(Record$1(String$1, String$1)),
19351
+ scannedFileCount: optional(Number$1),
19301
19352
  elapsedMilliseconds: Number$1
19302
19353
  }) {};
19303
19354
  /**
@@ -25167,6 +25218,14 @@ const runWith = (self, f, onHalt) => suspend$2(() => {
25167
25218
  return catchDone(flatMap$2(toTransform(self)(done$1(), scope), f), onHalt ? onHalt : succeed$2).pipe(onExit$1((exit) => close(scope, exit)));
25168
25219
  });
25169
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
+ /**
25170
25229
  * Runs a channel and applies an effect to each output element.
25171
25230
  *
25172
25231
  * **Example** (Running effects for each output)
@@ -26555,6 +26614,44 @@ const splitLines = (self) => self.channel.pipe(pipeTo(splitLines$1()), fromChann
26555
26614
  */
26556
26615
  const ensuring = /* @__PURE__ */ dual(2, (self, finalizer) => fromChannel(ensuring$1(self.channel, finalizer)));
26557
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
+ /**
26558
26655
  * Runs a stream with a sink and returns the sink result.
26559
26656
  *
26560
26657
  * **Example** (Running a stream with a sink)
@@ -29984,7 +30081,7 @@ const make$8 = /* @__PURE__ */ fnUntraced(function* (options) {
29984
30081
  const runFork = runForkWith(services);
29985
30082
  const exportInterval = max(fromInputUnsafe(options.exportInterval), zero);
29986
30083
  let disabledUntil = void 0;
29987
- 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({
29988
30085
  schedule: policy,
29989
30086
  times: 3
29990
30087
  }));
@@ -32714,16 +32811,26 @@ const isMinifiedSource = (absolutePath) => {
32714
32811
  if (fileDescriptor !== void 0) NFS.closeSync(fileDescriptor);
32715
32812
  }
32716
32813
  };
32717
- const isLargeMinifiedFile = (absolutePath) => {
32718
- let sizeBytes;
32814
+ const cachedIsLargeMinifiedByPath = /* @__PURE__ */ new Map();
32815
+ const clearMinifiedFileCache = () => {
32816
+ cachedIsLargeMinifiedByPath.clear();
32817
+ };
32818
+ const statSourceFileSize = (absolutePath) => {
32719
32819
  try {
32720
- sizeBytes = NFS.statSync(absolutePath).size;
32820
+ return NFS.statSync(absolutePath).size;
32721
32821
  } catch {
32722
- return false;
32822
+ return null;
32723
32823
  }
32724
- if (sizeBytes < 2e4) return false;
32725
- return isMinifiedSource(absolutePath);
32726
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;
32832
+ };
32833
+ const isErrnoException = (error) => error instanceof Error && "code" in error;
32727
32834
  const IGNORABLE_READDIR_ERROR_CODES = new Set([
32728
32835
  "EACCES",
32729
32836
  "EPERM",
@@ -32733,11 +32840,7 @@ const IGNORABLE_READDIR_ERROR_CODES = new Set([
32733
32840
  "ELOOP",
32734
32841
  "ENAMETOOLONG"
32735
32842
  ]);
32736
- const isIgnorableReaddirError = (error) => {
32737
- if (typeof error !== "object" || error === null) return false;
32738
- const errorCode = error.code;
32739
- return typeof errorCode === "string" && IGNORABLE_READDIR_ERROR_CODES.has(errorCode);
32740
- };
32843
+ const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
32741
32844
  const readDirectoryEntries = (directoryPath) => {
32742
32845
  try {
32743
32846
  return NFS.readdirSync(directoryPath, { withFileTypes: true });
@@ -32787,7 +32890,7 @@ const readPackageJsonUncached = (packageJsonPath) => {
32787
32890
  return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
32788
32891
  } catch (error) {
32789
32892
  if (error instanceof SyntaxError) return {};
32790
- if (error instanceof Error && "code" in error) {
32893
+ if (isErrnoException(error)) {
32791
32894
  const { code } = error;
32792
32895
  if (code === "EISDIR" || code === "EACCES" || code === "EPERM" || code === "ENOENT") return {};
32793
32896
  }
@@ -33512,17 +33615,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
33512
33615
  return false;
33513
33616
  };
33514
33617
  const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
33515
- const getExpoDependencySpec = (packageJson) => {
33516
- const spec = packageJson.dependencies?.expo ?? packageJson.devDependencies?.expo ?? packageJson.peerDependencies?.expo ?? packageJson.optionalDependencies?.expo;
33618
+ const getDependencySpec = (packageJson, packageName) => {
33619
+ const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
33517
33620
  return typeof spec === "string" ? spec : null;
33518
33621
  };
33519
- const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getExpoDependencySpec);
33622
+ const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
33520
33623
  const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
33521
- const getShopifyFlashListDependencySpec = (packageJson) => {
33522
- const spec = packageJson.dependencies?.["@shopify/flash-list"] ?? packageJson.devDependencies?.["@shopify/flash-list"] ?? packageJson.peerDependencies?.["@shopify/flash-list"] ?? packageJson.optionalDependencies?.["@shopify/flash-list"];
33523
- return typeof spec === "string" ? spec : null;
33524
- };
33525
- const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getShopifyFlashListDependencySpec);
33624
+ const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
33526
33625
  const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
33527
33626
  if (version === null || !isCatalogReference(version)) return version;
33528
33627
  const catalogName = extractCatalogName(version);
@@ -33534,11 +33633,7 @@ const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson,
33534
33633
  if (!isFile(monorepoPackageJsonPath)) return version;
33535
33634
  return resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
33536
33635
  };
33537
- const getNextjsDependencySpec = (packageJson) => {
33538
- const spec = packageJson.dependencies?.next ?? packageJson.devDependencies?.next ?? packageJson.peerDependencies?.next ?? packageJson.optionalDependencies?.next;
33539
- return typeof spec === "string" ? spec : null;
33540
- };
33541
- const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getNextjsDependencySpec);
33636
+ const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
33542
33637
  const getPreactVersion = (packageJson) => {
33543
33638
  return {
33544
33639
  ...packageJson.peerDependencies,
@@ -33599,6 +33694,7 @@ const MILLISECONDS_PER_SECOND = 1e3;
33599
33694
  const SCORE_API_URL = "https://www.react.doctor/api/score";
33600
33695
  const FETCH_TIMEOUT_MS = 1e4;
33601
33696
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
33697
+ const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
33602
33698
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
33603
33699
  const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
33604
33700
  const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
@@ -33620,6 +33716,11 @@ const ES_TARGET_YEAR_BY_NAME = {
33620
33716
  esnext: 9999
33621
33717
  };
33622
33718
  /**
33719
+ * tsconfig filenames probed when resolving a project's TypeScript
33720
+ * compiler options — the root config first, then a monorepo base config.
33721
+ */
33722
+ const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
33723
+ /**
33623
33724
  * Project-config files that `StagedFiles.materialize` copies into
33624
33725
  * the temp directory alongside staged sources so oxlint resolves
33625
33726
  * `tsconfig` / `package.json` / lint configs the same way it would
@@ -33643,7 +33744,16 @@ const STAGED_FILES_PROJECT_CONFIG_FILENAMES = [
33643
33744
  ];
33644
33745
  const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
33645
33746
  const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
33747
+ const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
33748
+ const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
33749
+ const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
33750
+ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
33751
+ const LINT_PHASE_TIMEOUT_MS = 3e5;
33752
+ const SCAN_TOTAL_DEADLINE_MS = 9e5;
33646
33753
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
33754
+ const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
33755
+ const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
33756
+ const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
33647
33757
  const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
33648
33758
  const REACT_SERVER_DOM_PACKAGES = [
33649
33759
  "react-server-dom-webpack",
@@ -33666,14 +33776,25 @@ const APP_ONLY_RULE_KEYS = new Set([
33666
33776
  ]);
33667
33777
  const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
33668
33778
  const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
33779
+ const ROOT_CAUSE_GROUPABLE_RULE_KEYS = new Set([
33780
+ "react-doctor/no-derived-state",
33781
+ "react-doctor/no-derived-state-effect",
33782
+ "react-doctor/no-derived-useState",
33783
+ "react-doctor/no-adjust-state-on-prop-change",
33784
+ "react-doctor/no-reset-all-state-on-prop-change"
33785
+ ]);
33669
33786
  const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
33670
33787
  const CONFIG_CACHE_TTL_MS = 300 * 1e3;
33671
33788
  const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
33672
33789
  const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
33673
33790
  const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
33791
+ const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
33792
+ const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
33674
33793
  const SUPPLY_CHAIN_PLUGIN = "socket";
33675
33794
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
33676
33795
  const SUPPLY_CHAIN_CATEGORY = "Security";
33796
+ const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
33797
+ const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
33677
33798
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
33678
33799
  const TSCONFIG_FILENAME = "tsconfig.json";
33679
33800
  const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
@@ -34118,6 +34239,7 @@ const isTailwindAtLeast = (detected, required) => {
34118
34239
  if (detected.major !== required.major) return detected.major > required.major;
34119
34240
  return detected.minor >= required.minor;
34120
34241
  };
34242
+ const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
34121
34243
  var InvalidGlobPatternError = class extends Error {
34122
34244
  pattern;
34123
34245
  reason;
@@ -34146,7 +34268,7 @@ const compileGlobPattern = (rawPattern) => {
34146
34268
  try {
34147
34269
  return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
34148
34270
  } catch (caughtError) {
34149
- throw new InvalidGlobPatternError(rawPattern, caughtError instanceof Error ? caughtError.message : String(caughtError));
34271
+ throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
34150
34272
  }
34151
34273
  };
34152
34274
  const compileGlobPatternsLenient = (patterns, onInvalid) => {
@@ -34242,115 +34364,6 @@ const buildRuleSeverityControls = (config) => {
34242
34364
  ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
34243
34365
  };
34244
34366
  };
34245
- const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
34246
- const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
34247
- const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
34248
- let stringDelimiter = null;
34249
- for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
34250
- const character = line[charIndex];
34251
- if (stringDelimiter !== null) {
34252
- if (character === "\\") {
34253
- charIndex++;
34254
- continue;
34255
- }
34256
- if (character === stringDelimiter) stringDelimiter = null;
34257
- continue;
34258
- }
34259
- if (character === "\"" || character === "'" || character === "`") {
34260
- stringDelimiter = character;
34261
- continue;
34262
- }
34263
- if (character === "/" && line[charIndex + 1] === "/") return true;
34264
- }
34265
- return false;
34266
- };
34267
- const findOpenerTagOnLine = (line) => {
34268
- for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
34269
- if (match.index === void 0) continue;
34270
- if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
34271
- }
34272
- return null;
34273
- };
34274
- const findJsxOpenerSpan = (lines, openerLineIndex) => {
34275
- const openerLine = lines[openerLineIndex];
34276
- if (openerLine === void 0) return null;
34277
- const opener = findOpenerTagOnLine(openerLine);
34278
- if (!opener) return null;
34279
- const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
34280
- let braceDepth = 0;
34281
- let innerAngleDepth = 0;
34282
- let stringDelimiter = null;
34283
- for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
34284
- const currentLine = lines[lineIndex];
34285
- const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
34286
- for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
34287
- const character = currentLine[charIndex];
34288
- if (stringDelimiter !== null) {
34289
- if (character === "\\") {
34290
- charIndex++;
34291
- continue;
34292
- }
34293
- if (character === stringDelimiter) stringDelimiter = null;
34294
- continue;
34295
- }
34296
- if (character === "\"" || character === "'" || character === "`") {
34297
- stringDelimiter = character;
34298
- continue;
34299
- }
34300
- if (character === "{") {
34301
- braceDepth++;
34302
- continue;
34303
- }
34304
- if (character === "}") {
34305
- braceDepth--;
34306
- continue;
34307
- }
34308
- if (braceDepth !== 0) continue;
34309
- if (character === "<") {
34310
- const followCharacter = currentLine[charIndex + 1];
34311
- if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
34312
- continue;
34313
- }
34314
- if (character !== ">") continue;
34315
- const previousCharacter = currentLine[charIndex - 1];
34316
- const nextCharacter = currentLine[charIndex + 1];
34317
- if (previousCharacter === "=" || nextCharacter === "=") continue;
34318
- if (innerAngleDepth > 0) {
34319
- innerAngleDepth--;
34320
- continue;
34321
- }
34322
- return lineIndex;
34323
- }
34324
- }
34325
- return null;
34326
- };
34327
- const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
34328
- for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
34329
- const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
34330
- if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
34331
- }
34332
- return null;
34333
- };
34334
- const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
34335
- const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
34336
- const collected = [];
34337
- let isStillInChain = true;
34338
- for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
34339
- const candidateLine = lines[candidateIndex];
34340
- if (candidateLine === void 0) break;
34341
- const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
34342
- if (match) {
34343
- collected.push({
34344
- commentLineIndex: candidateIndex,
34345
- ruleList: match[1],
34346
- isInChain: isStillInChain
34347
- });
34348
- continue;
34349
- }
34350
- isStillInChain = false;
34351
- }
34352
- return collected;
34353
- };
34354
34367
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
34355
34368
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
34356
34369
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -34475,7 +34488,13 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
34475
34488
  }
34476
34489
  const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
34477
34490
  const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
34478
- const isSameRuleKey = (candidateRuleKey, targetRuleKey) => canonicalizeRuleKey(candidateRuleKey) === canonicalizeRuleKey(targetRuleKey);
34491
+ const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
34492
+ const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
34493
+ const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
34494
+ const canonicalTarget = canonicalizeRuleKey(targetRuleKey);
34495
+ if (canonicalCandidate === canonicalTarget) return true;
34496
+ return isReactDoctorShortIdOf(canonicalCandidate, canonicalTarget) || isReactDoctorShortIdOf(canonicalTarget, canonicalCandidate);
34497
+ };
34479
34498
  const getEquivalentRuleKeys = (ruleKey) => {
34480
34499
  const nativeRuleKey = canonicalizeRuleKey(ruleKey);
34481
34500
  return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
@@ -34485,12 +34504,182 @@ const stripDescriptionTail = (ruleList) => {
34485
34504
  if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
34486
34505
  return ruleList.slice(0, descriptionMatch.index);
34487
34506
  };
34488
- const isRuleListedInComment = (ruleList, ruleId) => {
34507
+ const tokenizeRuleList = (ruleList) => {
34489
34508
  const trimmed = ruleList?.trim();
34490
- if (!trimmed) return true;
34509
+ if (!trimmed) return [];
34491
34510
  const ruleSection = stripDescriptionTail(trimmed).trim();
34492
- if (!ruleSection) return true;
34493
- return ruleSection.split(/[,\s]+/).some((token) => isSameRuleKey(token.trim(), ruleId));
34511
+ if (!ruleSection) return [];
34512
+ return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
34513
+ };
34514
+ const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34515
+ const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34516
+ const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34517
+ const buildHint = (tool, token, ruleId) => `oxlint matches plugin rules only by their full name, so \`${token}\` in your ${tool}-disable comment does not silence \`${ruleId}\` — change it to \`${ruleId}\`.`;
34518
+ const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
34519
+ const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34520
+ const candidates = [{
34521
+ line: lines[diagnosticLineIndex],
34522
+ requiredScope: "line"
34523
+ }, {
34524
+ line: lines[diagnosticLineIndex - 1],
34525
+ requiredScope: "next-line"
34526
+ }];
34527
+ for (const { line, requiredScope } of candidates) {
34528
+ const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
34529
+ if (!match) continue;
34530
+ const [, tool, scope, ruleList] = match;
34531
+ if (scope !== requiredScope) continue;
34532
+ const tokens = tokenizeRuleList(ruleList);
34533
+ if (tokens.includes(ruleId)) continue;
34534
+ for (const token of tokens) if (tokenMisnamesRule(token, ruleId)) return buildHint(tool, token, ruleId);
34535
+ }
34536
+ return null;
34537
+ };
34538
+ const detectBlockNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34539
+ let openMisname = null;
34540
+ const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
34541
+ for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
34542
+ const line = lines[lineIndex];
34543
+ if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
34544
+ const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
34545
+ if (disableMatch) {
34546
+ const [, tool, ruleList] = disableMatch;
34547
+ const tokens = tokenizeRuleList(ruleList);
34548
+ if (tokens.includes(ruleId)) openMisname = null;
34549
+ else {
34550
+ const misnamed = tokens.find((token) => tokenMisnamesRule(token, ruleId));
34551
+ if (misnamed) openMisname = {
34552
+ tool,
34553
+ token: misnamed
34554
+ };
34555
+ }
34556
+ continue;
34557
+ }
34558
+ const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
34559
+ if (enableMatch) {
34560
+ const enabledRules = tokenizeRuleList(enableMatch[1]);
34561
+ if (enabledRules.length === 0 || enabledRules.some((rule) => isSameRuleKey(rule, ruleId))) openMisname = null;
34562
+ }
34563
+ }
34564
+ return openMisname ? buildHint(openMisname.tool, openMisname.token, ruleId) : null;
34565
+ };
34566
+ const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34567
+ if (!ruleId.startsWith("react-doctor/")) return null;
34568
+ return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
34569
+ };
34570
+ const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
34571
+ const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
34572
+ const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
34573
+ let stringDelimiter = null;
34574
+ for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
34575
+ const character = line[charIndex];
34576
+ if (stringDelimiter !== null) {
34577
+ if (character === "\\") {
34578
+ charIndex++;
34579
+ continue;
34580
+ }
34581
+ if (character === stringDelimiter) stringDelimiter = null;
34582
+ continue;
34583
+ }
34584
+ if (character === "\"" || character === "'" || character === "`") {
34585
+ stringDelimiter = character;
34586
+ continue;
34587
+ }
34588
+ if (character === "/" && line[charIndex + 1] === "/") return true;
34589
+ }
34590
+ return false;
34591
+ };
34592
+ const findOpenerTagOnLine = (line) => {
34593
+ for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
34594
+ if (match.index === void 0) continue;
34595
+ if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
34596
+ }
34597
+ return null;
34598
+ };
34599
+ const findJsxOpenerSpan = (lines, openerLineIndex) => {
34600
+ const openerLine = lines[openerLineIndex];
34601
+ if (openerLine === void 0) return null;
34602
+ const opener = findOpenerTagOnLine(openerLine);
34603
+ if (!opener) return null;
34604
+ const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
34605
+ let braceDepth = 0;
34606
+ let innerAngleDepth = 0;
34607
+ let stringDelimiter = null;
34608
+ for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
34609
+ const currentLine = lines[lineIndex];
34610
+ const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
34611
+ for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
34612
+ const character = currentLine[charIndex];
34613
+ if (stringDelimiter !== null) {
34614
+ if (character === "\\") {
34615
+ charIndex++;
34616
+ continue;
34617
+ }
34618
+ if (character === stringDelimiter) stringDelimiter = null;
34619
+ continue;
34620
+ }
34621
+ if (character === "\"" || character === "'" || character === "`") {
34622
+ stringDelimiter = character;
34623
+ continue;
34624
+ }
34625
+ if (character === "{") {
34626
+ braceDepth++;
34627
+ continue;
34628
+ }
34629
+ if (character === "}") {
34630
+ braceDepth--;
34631
+ continue;
34632
+ }
34633
+ if (braceDepth !== 0) continue;
34634
+ if (character === "<") {
34635
+ const followCharacter = currentLine[charIndex + 1];
34636
+ if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
34637
+ continue;
34638
+ }
34639
+ if (character !== ">") continue;
34640
+ const previousCharacter = currentLine[charIndex - 1];
34641
+ const nextCharacter = currentLine[charIndex + 1];
34642
+ if (previousCharacter === "=" || nextCharacter === "=") continue;
34643
+ if (innerAngleDepth > 0) {
34644
+ innerAngleDepth--;
34645
+ continue;
34646
+ }
34647
+ return lineIndex;
34648
+ }
34649
+ }
34650
+ return null;
34651
+ };
34652
+ const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
34653
+ for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
34654
+ const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
34655
+ if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
34656
+ }
34657
+ return null;
34658
+ };
34659
+ const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
34660
+ const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
34661
+ const collected = [];
34662
+ let isStillInChain = true;
34663
+ for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
34664
+ const candidateLine = lines[candidateIndex];
34665
+ if (candidateLine === void 0) break;
34666
+ const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
34667
+ if (match) {
34668
+ collected.push({
34669
+ commentLineIndex: candidateIndex,
34670
+ ruleList: match[1],
34671
+ isInChain: isStillInChain
34672
+ });
34673
+ continue;
34674
+ }
34675
+ isStillInChain = false;
34676
+ }
34677
+ return collected;
34678
+ };
34679
+ const isRuleListedInComment = (ruleList, ruleId) => {
34680
+ const tokens = tokenizeRuleList(ruleList);
34681
+ if (tokens.length === 0) return true;
34682
+ return tokens.some((token) => isSameRuleKey(token, ruleId));
34494
34683
  };
34495
34684
  const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
34496
34685
  const formatLineGap = (gapLineCount) => `${gapLineCount} line${gapLineCount === 1 ? "" : "s"}`;
@@ -34534,7 +34723,7 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
34534
34723
  };
34535
34724
  return {
34536
34725
  isSuppressed: false,
34537
- nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId)
34726
+ nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId) ?? detectForeignDisableNearMiss(lines, diagnosticLineIndex, ruleId)
34538
34727
  };
34539
34728
  };
34540
34729
  /**
@@ -34964,6 +35153,11 @@ var OxlintBatchExceeded = class extends TaggedErrorClass()("OxlintBatchExceeded"
34964
35153
  }
34965
35154
  }
34966
35155
  };
35156
+ var ScanDeadlineExceeded = class extends TaggedErrorClass()("ScanDeadlineExceeded", { detail: String$1 }) {
35157
+ get message() {
35158
+ return `Scan exceeded its overall time budget: ${this.detail}`;
35159
+ }
35160
+ };
34967
35161
  var OxlintSpawnFailed = class extends TaggedErrorClass()("OxlintSpawnFailed", { cause: Unknown }) {
34968
35162
  get message() {
34969
35163
  return `Failed to run oxlint: ${pretty(fail$6(this.cause))}`;
@@ -35027,6 +35221,7 @@ var GitBaseBranchInvalid = class extends TaggedErrorClass()("GitBaseBranchInvali
35027
35221
  const ReactDoctorErrorReason = Union([
35028
35222
  OxlintUnavailable,
35029
35223
  OxlintBatchExceeded,
35224
+ ScanDeadlineExceeded,
35030
35225
  OxlintSpawnFailed,
35031
35226
  OxlintOutputUnparseable,
35032
35227
  ConfigParseFailed,
@@ -35099,15 +35294,105 @@ const layerOtlp = unwrap$3(gen(function* () {
35099
35294
  }).pipe(provide$2(layer$8));
35100
35295
  }).pipe(orDie));
35101
35296
  /**
35102
- * Resolves a requested lint worker count to a clamped integer within
35103
- * `[MIN_SCAN_CONCURRENCY, MAX_SCAN_CONCURRENCY]`. `"auto"` uses the
35104
- * machine's CPU cores; out-of-range or non-finite requests degrade to
35297
+ * Read a positive-millisecond timeout from an env var, falling back to
35298
+ * `defaultMs` when the var is unset, non-finite, or not strictly positive.
35299
+ */
35300
+ const readPositiveEnvMs = (envVarName, defaultMs) => {
35301
+ const rawValue = process.env[envVarName];
35302
+ if (rawValue === void 0) return defaultMs;
35303
+ const parsedValue = Number(rawValue);
35304
+ if (!Number.isFinite(parsedValue) || parsedValue <= 0) return defaultMs;
35305
+ return parsedValue;
35306
+ };
35307
+ const CGROUP_V2_MEMORY_MAX_PATH = "/sys/fs/cgroup/memory.max";
35308
+ const CGROUP_V1_MEMORY_LIMIT_PATH = "/sys/fs/cgroup/memory/memory.limit_in_bytes";
35309
+ const CGROUP_UNLIMITED_SENTINEL_BYTES = Number.MAX_SAFE_INTEGER;
35310
+ /**
35311
+ * Parses one raw cgroup memory-limit file value into a positive byte count, or
35312
+ * `undefined` when it represents "no limit" (the v2 `"max"` literal, an empty
35313
+ * read, a non-positive / non-finite value, or v1's near-2^63 unlimited
35314
+ * sentinel). Pure and exported so the classification is unit-testable without
35315
+ * touching the filesystem.
35316
+ */
35317
+ const parseCgroupMemoryLimitBytes = (raw) => {
35318
+ if (raw === void 0) return void 0;
35319
+ const trimmed = raw.trim();
35320
+ if (trimmed === "" || trimmed === "max") return void 0;
35321
+ const parsed = Number(trimmed);
35322
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed >= CGROUP_UNLIMITED_SENTINEL_BYTES) return;
35323
+ return parsed;
35324
+ };
35325
+ const CGROUP_MEMORY_LIMIT_PATHS = [CGROUP_V2_MEMORY_MAX_PATH, CGROUP_V1_MEMORY_LIMIT_PATH];
35326
+ /**
35327
+ * Reads this process's cgroup memory limit in bytes from the first candidate
35328
+ * path that yields a real limit, or `undefined` when none does — no cgroup, no
35329
+ * limit, or the files are unreadable (e.g. macOS / Windows dev machines).
35330
+ * `os.totalmem()` reports the HOST total and ignores cgroup memory limits, so a
35331
+ * memory-constrained container over-reports total memory; `resolveAutoScan-
35332
+ * Concurrency` takes `min(totalmem, this)` to honor the limit.
35333
+ *
35334
+ * The cgroup v2 read is the mount-root `memory.max`, which IS the container's
35335
+ * limit under the standard cgroup-namespace setup CI runners use (the
35336
+ * container's own cgroup is the root of its namespaced view). A process in a
35337
+ * non-namespaced nested/delegated cgroup whose root reads `"max"` is not
35338
+ * detected here and falls back to the host total; the EAGAIN/ENOMEM serial
35339
+ * replay in `spawnLintBatches` remains the runtime backstop for that case.
35340
+ *
35341
+ * `candidatePaths` is injectable so tests exercise the v2-wins-over-v1
35342
+ * precedence, the skip-unreadable fallback, and the all-missing case without a
35343
+ * real `/sys/fs/cgroup`.
35344
+ */
35345
+ const readCgroupMemoryLimitBytes = (candidatePaths = CGROUP_MEMORY_LIMIT_PATHS) => {
35346
+ for (const limitPath of candidatePaths) {
35347
+ let raw;
35348
+ try {
35349
+ raw = fs.readFileSync(limitPath, "utf8");
35350
+ } catch {
35351
+ continue;
35352
+ }
35353
+ const limitBytes = parseCgroupMemoryLimitBytes(raw);
35354
+ if (limitBytes !== void 0) return limitBytes;
35355
+ }
35356
+ };
35357
+ /**
35358
+ * Clamps a requested lint worker count to `[MIN_SCAN_CONCURRENCY,
35359
+ * HARD_MAX_SCAN_CONCURRENCY]` as a finite integer. This is the explicit-pin and
35360
+ * spawn-boundary clamp — the memory-and-core-budgeted auto count comes from
35361
+ * `resolveAutoScanConcurrency`. Out-of-range or non-finite requests degrade to
35105
35362
  * `MIN_SCAN_CONCURRENCY` rather than oversubscribing or running zero workers.
35106
35363
  */
35107
35364
  const resolveScanConcurrency = (requested) => {
35108
- const desired = requested === "auto" ? os.availableParallelism() : requested;
35109
- if (!Number.isFinite(desired) || desired < 1) return 1;
35110
- return Math.max(1, Math.min(Math.floor(desired), 16));
35365
+ if (!Number.isFinite(requested) || requested < 1) return 1;
35366
+ return Math.min(Math.floor(requested), 32);
35367
+ };
35368
+ const readSystemFacts = () => ({
35369
+ availableCores: os.availableParallelism(),
35370
+ totalMemoryBytes: os.totalmem(),
35371
+ cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
35372
+ });
35373
+ /**
35374
+ * Auto lint-worker count: the smaller of the (cgroup-CPU-aware) core count and
35375
+ * the number of `PER_WORKER_MEM_BUDGET_BYTES` workers that fit in available
35376
+ * memory, then clamped to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
35377
+ *
35378
+ * `os.availableParallelism()` already respects cgroup CPU quotas, so the core
35379
+ * term needs no help. Available memory is `os.totalmem()` floored by the cgroup
35380
+ * memory limit — `os.freemem()` is deliberately NOT used: it excludes
35381
+ * reclaimable page cache and reads near-zero on macOS / cache-heavy Linux, which
35382
+ * would collapse the auto path to a single worker. `os.totalmem()` reports the
35383
+ * host total even inside a container, so the cgroup limit (read directly,
35384
+ * because Node doesn't fold it into `totalmem()`) is the real ceiling there.
35385
+ *
35386
+ * `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
35387
+ * limited, and ceiling cases without mocking `os` or the filesystem.
35388
+ */
35389
+ const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
35390
+ const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
35391
+ const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
35392
+ return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
35393
+ };
35394
+ const resolveLintBatchOrdering = () => {
35395
+ return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "cost" ? "cost" : "arrival";
35111
35396
  };
35112
35397
  /**
35113
35398
  * Per-batch oxlint wall-clock budget. Reads from the env var on
@@ -35115,11 +35400,38 @@ const resolveScanConcurrency = (requested) => {
35115
35400
  * microVMs without recompiling react-doctor. Tests override via
35116
35401
  * `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
35117
35402
  */
35118
- var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
35119
- const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
35120
- if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
35403
+ var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS", OXLINT_SPAWN_TIMEOUT_MS) }) {};
35404
+ /**
35405
+ * Effect-side cap on the lint phase. The env var lets CI / eval runners
35406
+ * raise the phase budget for slow large repos without recompiling.
35407
+ * Tests override via `Layer.succeed(LintPhaseTimeoutMs, ...)`.
35408
+ */
35409
+ var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", LINT_PHASE_TIMEOUT_MS) }) {};
35410
+ /**
35411
+ * Effect-side cap on the dead-code phase, sitting above the in-worker
35412
+ * timeout as a runtime-independent backstop. The env var raises it for
35413
+ * type-heavy projects; tests override via
35414
+ * `Layer.succeed(DeadCodePhaseTimeoutMs, ...)`.
35415
+ */
35416
+ var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", DEAD_CODE_PHASE_TIMEOUT_MS) }) {};
35417
+ /**
35418
+ * Overall scan deadline backstop, bounding everything the per-phase
35419
+ * timeouts don't (wedged git / IO). The env var raises it for very
35420
+ * large repos; tests override via `Layer.succeed(ScanDeadlineMs, ...)`.
35421
+ */
35422
+ var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", SCAN_TOTAL_DEADLINE_MS) }) {};
35423
+ /**
35424
+ * Wall-clock budget for the supply-chain check when it runs on a background
35425
+ * fiber overlapping the lint pass. Reads from the env var on startup so the
35426
+ * eval harness can raise the budget under sandbox microVMs (slower network)
35427
+ * without recompiling react-doctor. Tests override via
35428
+ * `Layer.succeed(SupplyChainOverlapTimeoutMs, ...)`.
35429
+ */
35430
+ var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => {
35431
+ const raw = process.env["REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS"];
35432
+ if (raw === void 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35121
35433
  const parsed = Number(raw);
35122
- if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
35434
+ if (!Number.isFinite(parsed) || parsed <= 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35123
35435
  return parsed;
35124
35436
  } }) {};
35125
35437
  /**
@@ -35130,31 +35442,93 @@ var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTime
35130
35442
  */
35131
35443
  var OxlintOutputMaxBytes = class extends Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
35132
35444
  /**
35133
- * Number of oxlint subprocesses the lint pass runs in parallel. Defaults
35134
- * to auto-detected CPU cores (parallel) so large repos scan fast out of
35135
- * the box; `spawnLintBatches` transparently falls back to a single worker
35136
- * if a parallel run exhausts system resources. The CLI's `--no-parallel`
35137
- * flag forces serial via `Layer.succeed`; the `REACT_DOCTOR_PARALLEL` env
35138
- * var seeds the default for programmatic / CI callers that never touch the
35139
- * flag parallelism is opt-OUT, so only the explicit serial values pin
35140
- * one worker:
35141
- *
35142
- * - unset / `auto` / `true` / `on` → available CPU cores (clamped)
35445
+ * Number of oxlint subprocesses the lint pass runs in parallel. Defaults to a
35446
+ * memory-and-core-budgeted auto count (`resolveAutoScanConcurrency`) so large
35447
+ * repos scan fast out of the box without OOMing the native binding on a
35448
+ * high-core / low-memory box; `spawnLintBatches` transparently falls back to a
35449
+ * single worker if a parallel run still exhausts system resources. The CLI's
35450
+ * `--no-parallel` flag forces serial via `Layer.succeed`; the
35451
+ * `REACT_DOCTOR_PARALLEL` env var seeds the default for programmatic / CI
35452
+ * callers that never touch the flag — parallelism is opt-OUT, so only the
35453
+ * explicit serial values pin one worker:
35454
+ *
35455
+ * - unset / `auto` / `true` / `on` → memory-and-core-budgeted auto count
35143
35456
  * - `0` / `false` / `off` → `1` (serial)
35144
35457
  * - a positive integer → that many workers (clamped)
35145
- * - any other value → available CPU cores (clamped)
35458
+ * - any other value → memory-and-core-budgeted auto count
35146
35459
  *
35147
35460
  * The resolved value is always within
35148
- * `[MIN_SCAN_CONCURRENCY, MAX_SCAN_CONCURRENCY]`.
35461
+ * `[MIN_SCAN_CONCURRENCY, HARD_MAX_SCAN_CONCURRENCY]`.
35149
35462
  */
35150
35463
  var OxlintConcurrency = class extends Reference("react-doctor/OxlintConcurrency", { defaultValue: () => {
35151
35464
  const raw = process.env["REACT_DOCTOR_PARALLEL"];
35152
- if (raw === void 0) return resolveScanConcurrency("auto");
35465
+ if (raw === void 0) return resolveAutoScanConcurrency();
35153
35466
  const normalized = raw.trim().toLowerCase();
35154
35467
  if (normalized === "0" || normalized === "false" || normalized === "off") return 1;
35155
35468
  const parsed = Number.parseInt(normalized, 10);
35156
35469
  if (Number.isInteger(parsed) && parsed > 0) return resolveScanConcurrency(parsed);
35157
- return resolveScanConcurrency("auto");
35470
+ return resolveAutoScanConcurrency();
35471
+ } }) {};
35472
+ /**
35473
+ * Three-state control for overlapping the dead-code pass with the lint pass —
35474
+ * forking dead-code as a child fiber that runs DURING lint instead of strictly
35475
+ * after it.
35476
+ *
35477
+ * - `"auto"` (default) / `"off"` → strictly SEQUENTIAL: dead-code runs after
35478
+ * lint with the full core budget. Both deslop's parse pool and the oxlint
35479
+ * pool are CPU-bound and each size themselves to all cores, so overlapping
35480
+ * them only oversubscribes (~2x the cores) and starves the parse pass past
35481
+ * its timeout — for no wall-clock win, since there are no spare cores to
35482
+ * absorb the second pass. Sequential is both faster per-phase and safe.
35483
+ * - `"on"` → force the overlap anyway. The orchestrator then SPLITS the core
35484
+ * budget (`DEAD_CODE_OVERLAP_PARSE_SHARE`): deslop's parse pool is capped
35485
+ * and lint shrinks to the remainder, so the two sum to the cores instead of
35486
+ * doubling them, and the dead-code timeout scales up for the reduced share.
35487
+ *
35488
+ * Seeded from `REACT_DOCTOR_DEAD_CODE_OVERLAP` so operators get a redeploy-free
35489
+ * switch; tests pin it via `Layer.succeed(DeadCodeOverlap, ...)`.
35490
+ */
35491
+ var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", { defaultValue: () => {
35492
+ const raw = process.env["REACT_DOCTOR_DEAD_CODE_OVERLAP"]?.trim().toLowerCase();
35493
+ if (raw === "on" || raw === "true" || raw === "1") return "on";
35494
+ if (raw === "off" || raw === "false" || raw === "0") return "off";
35495
+ return "auto";
35496
+ } }) {};
35497
+ /**
35498
+ * How the full-scan lint pass orders its file batches. `"arrival"` (the
35499
+ * default) keeps `git ls-files` discovery order. `"cost"` opts into LPT (feed
35500
+ * the largest files first); set `REACT_DOCTOR_LINT_BATCH_ORDERING=cost`. NOTE:
35501
+ * `cost` is OFF by default because the current sort-desc-then-chunk-100 packs
35502
+ * the heaviest files into one wave-1 batch — on size-skewed repos that mega-
35503
+ * batch is a straggler (and can trip the per-batch timeout + split), measurably
35504
+ * regressing the common full-scan case. LPT needs the heavy files SPREAD across
35505
+ * batches before `cost` earns the default. Tests override via
35506
+ * `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans never reach this
35507
+ * — they pass user-scoped `includePaths` that skip discovery and stay in
35508
+ * arrival order; only the full-scan branch reads it.
35509
+ */
35510
+ var LintBatchOrdering = class extends Reference("react-doctor/LintBatchOrdering", { defaultValue: resolveLintBatchOrdering }) {};
35511
+ const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
35512
+ /**
35513
+ * Whether the per-file lint cache (`runners/oxlint/file-lint-cache.ts`) is
35514
+ * active. Defaults ON — repeat scans re-lint only the files whose content
35515
+ * changed, and correctness is guaranteed byte-identical to a cold scan by the
35516
+ * always-fresh cross-file sidecar. Opt-OUT, two knobs (matching the whole-repo
35517
+ * scan cache's `REACT_DOCTOR_NO_CACHE`):
35518
+ *
35519
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch; disables BOTH the
35520
+ * whole-repo scan cache and this per-file cache.
35521
+ * - `REACT_DOCTOR_NO_FILE_CACHE` — granular: bust only the per-file cache
35522
+ * while keeping the whole-repo short-circuit.
35523
+ *
35524
+ * Tests override via `Layer.succeed(PerFileLintCacheEnabled, false)`.
35525
+ */
35526
+ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintCacheEnabled", { defaultValue: () => {
35527
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
35528
+ const noFileCache = process.env["REACT_DOCTOR_NO_FILE_CACHE"]?.toLowerCase() ?? "";
35529
+ if (CACHE_DISABLED_VALUES.has(noCache)) return false;
35530
+ if (CACHE_DISABLED_VALUES.has(noFileCache)) return false;
35531
+ return true;
35158
35532
  } }) {};
35159
35533
  const DIAGNOSTIC_SURFACES = [
35160
35534
  "cli",
@@ -35328,7 +35702,6 @@ const PACKAGE_JSON_FILENAME = "package.json";
35328
35702
  const PACKAGE_JSON_CONFIG_KEY = "reactDoctor";
35329
35703
  const LEGACY_CONFIG_FILENAME = "react-doctor.config.json";
35330
35704
  const jiti = createJiti(import.meta.url);
35331
- const formatError = (error) => error instanceof Error ? error.message : String(error);
35332
35705
  const importDefaultExport = async (jitiInstance, filePath) => {
35333
35706
  const imported = await jitiInstance.import(filePath);
35334
35707
  return imported?.default ?? imported;
@@ -35360,7 +35733,7 @@ const loadModuleConfig = async (filePath) => {
35360
35733
  try {
35361
35734
  return await importDefaultExport(aliasJiti, filePath);
35362
35735
  } catch (retryError) {
35363
- throw new Error(`${formatError(error)} (retry with ${SELF_PACKAGE_IMPORT_SPECIFIER} aliased to the running react-doctor package also failed: ${formatError(retryError)})`, { cause: retryError });
35736
+ throw new Error(`${messageFromUnknown(error)} (retry with ${SELF_PACKAGE_IMPORT_SPECIFIER} aliased to the running react-doctor package also failed: ${messageFromUnknown(retryError)})`, { cause: retryError });
35364
35737
  }
35365
35738
  }
35366
35739
  };
@@ -35409,7 +35782,7 @@ const loadLegacyConfig = (directory) => {
35409
35782
  }
35410
35783
  warn(`${LEGACY_CONFIG_FILENAME} must contain an object, ignoring.`);
35411
35784
  } catch (error) {
35412
- warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${formatError(error)}`);
35785
+ warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${messageFromUnknown(error)}`);
35413
35786
  }
35414
35787
  return {
35415
35788
  status: "invalid",
@@ -35436,7 +35809,7 @@ const loadConfigFromDirectory = async (directory) => {
35436
35809
  warn(`${CONFIG_BASENAME}.${extension} must export an object, ignoring.`);
35437
35810
  sawBrokenConfigFile = true;
35438
35811
  } catch (error) {
35439
- warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${formatError(error)}`);
35812
+ warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${messageFromUnknown(error)}`);
35440
35813
  sawBrokenConfigFile = true;
35441
35814
  }
35442
35815
  }
@@ -35538,6 +35911,31 @@ const resolveScanTarget = async (requestedDirectory, options = {}) => {
35538
35911
  didRedirectViaRootDir: redirectedDirectory !== null
35539
35912
  };
35540
35913
  };
35914
+ const buildFixGroupId = (diagnostic) => createHash("sha1").update(JSON.stringify([
35915
+ diagnostic.filePath,
35916
+ `${diagnostic.plugin}/${diagnostic.rule}`,
35917
+ diagnostic.message
35918
+ ])).digest("hex").slice(0, 16);
35919
+ const isGroupableRule = (diagnostic) => ROOT_CAUSE_GROUPABLE_RULE_KEYS.has(`${diagnostic.plugin}/${diagnostic.rule}`);
35920
+ const assignFixGroups = (diagnostics) => {
35921
+ const siteCountByGroupId = /* @__PURE__ */ new Map();
35922
+ for (const diagnostic of diagnostics) {
35923
+ if (!isGroupableRule(diagnostic)) continue;
35924
+ const groupId = buildFixGroupId(diagnostic);
35925
+ siteCountByGroupId.set(groupId, (siteCountByGroupId.get(groupId) ?? 0) + 1);
35926
+ }
35927
+ return diagnostics.map((diagnostic) => {
35928
+ if (!isGroupableRule(diagnostic)) return diagnostic;
35929
+ const groupId = buildFixGroupId(diagnostic);
35930
+ if ((siteCountByGroupId.get(groupId) ?? 0) < 2) return diagnostic;
35931
+ return {
35932
+ ...diagnostic,
35933
+ fixGroupId: groupId
35934
+ };
35935
+ });
35936
+ };
35937
+ const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
35938
+ 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));
35541
35939
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
35542
35940
  const buildExpoCheckContext = (rootDirectory, expoVersion) => {
35543
35941
  const packageJson = readPackageJson(Path.join(rootDirectory, "package.json"));
@@ -36044,10 +36442,15 @@ const buildHardeningDiagnostic = (input) => ({
36044
36442
  column: input.column ?? 0,
36045
36443
  category: "Security"
36046
36444
  });
36047
- const checkPnpmHardening = (rootDirectory) => {
36048
- if (!isPnpmManagedProject(rootDirectory)) return [];
36049
- const workspacePath = Path.join(rootDirectory, PNPM_WORKSPACE_FILE);
36050
- const settings = parseHardeningSettings(isFile(workspacePath) ? NFS.readFileSync(workspacePath, "utf-8") : "");
36445
+ const checkPnpmHardening = (scanDirectory) => {
36446
+ if (!isPnpmManagedProject(scanDirectory)) return [];
36447
+ const workspacePath = Path.join(scanDirectory, PNPM_WORKSPACE_FILE);
36448
+ const hasWorkspaceFile = isFile(workspacePath);
36449
+ if (!hasWorkspaceFile) {
36450
+ const monorepoRoot = findMonorepoRoot(scanDirectory);
36451
+ if (monorepoRoot !== null && isFile(Path.join(monorepoRoot, PNPM_WORKSPACE_FILE))) return [];
36452
+ }
36453
+ const settings = parseHardeningSettings(hasWorkspaceFile ? NFS.readFileSync(workspacePath, "utf-8") : "");
36051
36454
  const diagnostics = [];
36052
36455
  if (settings.minimumReleaseAge === null) diagnostics.push(buildHardeningDiagnostic({
36053
36456
  message: "pnpm-workspace.yaml is missing `minimumReleaseAge` — newly published versions can ship malware that gets caught and unpublished within hours",
@@ -36664,7 +37067,7 @@ const readIgnoreFile = (filePath) => {
36664
37067
  try {
36665
37068
  content = NFS.readFileSync(filePath, "utf-8");
36666
37069
  } catch (error) {
36667
- const errnoCode = error?.code;
37070
+ const errnoCode = isErrnoException(error) ? error.code : void 0;
36668
37071
  if (errnoCode && errnoCode !== "ENOENT") runSync(warn$1(`Could not read ignore file ${filePath}: ${errnoCode}`));
36669
37072
  return [];
36670
37073
  }
@@ -36705,8 +37108,8 @@ const collectIgnorePatterns = (rootDirectory) => {
36705
37108
  cachedPatternsByRoot.set(rootDirectory, patterns);
36706
37109
  return patterns;
36707
37110
  };
37111
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
36708
37112
  const KNIP_JSON_FILENAME = "knip.json";
36709
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
36710
37113
  const readJsonFileSafe = (filePath) => {
36711
37114
  let rawContents;
36712
37115
  try {
@@ -36722,10 +37125,10 @@ const readJsonFileSafe = (filePath) => {
36722
37125
  };
36723
37126
  const readKnipConfig = (rootDirectory) => {
36724
37127
  const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
36725
- if (isRecord$1(knipJson)) return knipJson;
37128
+ if (isRecord(knipJson)) return knipJson;
36726
37129
  const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
36727
- const packageKnipConfig = isRecord$1(packageJson) ? packageJson.knip : null;
36728
- return isRecord$1(packageKnipConfig) ? packageKnipConfig : null;
37130
+ const packageKnipConfig = isRecord(packageJson) ? packageJson.knip : null;
37131
+ return isRecord(packageKnipConfig) ? packageKnipConfig : null;
36729
37132
  };
36730
37133
  const normalizePatternList = (value) => {
36731
37134
  if (typeof value === "string" && value.length > 0) return [value];
@@ -36737,10 +37140,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
36737
37140
  return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
36738
37141
  };
36739
37142
  const collectKnipWorkspacePatterns = (workspaces, settingName) => {
36740
- if (!isRecord$1(workspaces)) return [];
37143
+ if (!isRecord(workspaces)) return [];
36741
37144
  const patterns = [];
36742
37145
  for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
36743
- if (!isRecord$1(workspaceConfig)) continue;
37146
+ if (!isRecord(workspaceConfig)) continue;
36744
37147
  patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
36745
37148
  }
36746
37149
  return patterns;
@@ -36785,8 +37188,6 @@ const toCanonicalPath = (filePath) => {
36785
37188
  };
36786
37189
  const DEAD_CODE_PLUGIN = "deslop";
36787
37190
  const DEAD_CODE_CATEGORY = "Maintainability";
36788
- const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
36789
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
36790
37191
  const DEAD_CODE_WORKER_SCRIPT = `
36791
37192
  const inputChunks = [];
36792
37193
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -36834,6 +37235,22 @@ process.stdin.on("end", () => {
36834
37235
  ...(workerInput.ignorePatterns.length > 0
36835
37236
  ? { ignorePatterns: workerInput.ignorePatterns }
36836
37237
  : {}),
37238
+ // We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
37239
+ // unusedDependencies, circularDependencies). Everything else deslop can compute
37240
+ // is pure wasted work for us, and it's the bulk of the runtime:
37241
+ // - semantic: a full TS Program for unusedTypes/enum/class-members/
37242
+ // misclassifiedDependencies (~37-45% of the phase).
37243
+ // - reportCodeQuality: the duplicate-block, complexity, feature-flag,
37244
+ // TypeScript-smell, private-type-leak and re-export-cycle detectors. These
37245
+ // are the single most expensive pass — duplicate-block detection alone was
37246
+ // ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
37247
+ // speedup on a large repo.
37248
+ // Both are provably safe: the consumed graph findings are computed by their own
37249
+ // detectors, independent of these passes (confirmed byte-identical on
37250
+ // excalidraw + mui-material + sentry). tsConfigPath stays — the module resolver
37251
+ // needs it for path-alias resolution in the import graph.
37252
+ semantic: { enabled: false },
37253
+ reportCodeQuality: false,
36837
37254
  };
36838
37255
  const result = await analyze(defineConfig(config));
36839
37256
  emit({ ok: true, result: normalizeResult(result) });
@@ -36844,7 +37261,7 @@ process.stdin.on("end", () => {
36844
37261
  });
36845
37262
  `;
36846
37263
  const resolveTsConfigPath = (rootDirectory) => {
36847
- for (const filename of TSCONFIG_FILENAMES$1) {
37264
+ for (const filename of TSCONFIG_FILENAMES) {
36848
37265
  const candidate = Path.join(rootDirectory, filename);
36849
37266
  if (NFS.existsSync(candidate)) return candidate;
36850
37267
  }
@@ -36963,7 +37380,11 @@ const createDeadCodeWorker = (input) => {
36963
37380
  "pipe",
36964
37381
  "pipe"
36965
37382
  ],
36966
- windowsHide: true
37383
+ windowsHide: true,
37384
+ env: input.parseConcurrency === void 0 ? process.env : {
37385
+ ...process.env,
37386
+ DESLOP_PARSE_CONCURRENCY: String(input.parseConcurrency)
37387
+ }
36967
37388
  });
36968
37389
  const stdoutChunks = [];
36969
37390
  const stderrChunks = [];
@@ -37008,28 +37429,25 @@ const createDeadCodeWorker = (input) => {
37008
37429
  }
37009
37430
  };
37010
37431
  };
37011
- const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
37432
+ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs, abortSignal) => new Promise((resolve, reject) => {
37012
37433
  let didSettle = false;
37013
- const timeoutHandle = setTimeout(() => {
37014
- if (didSettle) return;
37015
- didSettle = true;
37016
- handle.terminate?.();
37017
- reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
37018
- }, timeoutMs);
37019
- timeoutHandle.unref?.();
37020
- handle.result.then((value) => {
37021
- if (didSettle) return;
37022
- didSettle = true;
37023
- clearTimeout(timeoutHandle);
37024
- handle.terminate?.();
37025
- resolve(value);
37026
- }, (error) => {
37434
+ const settle = (finish) => {
37027
37435
  if (didSettle) return;
37028
37436
  didSettle = true;
37029
37437
  clearTimeout(timeoutHandle);
37438
+ abortSignal?.removeEventListener("abort", onAbort);
37030
37439
  handle.terminate?.();
37031
- reject(error);
37032
- });
37440
+ finish();
37441
+ };
37442
+ const onAbort = () => settle(() => reject(/* @__PURE__ */ new Error("Dead-code worker aborted.")));
37443
+ const timeoutHandle = setTimeout(() => settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`))), timeoutMs);
37444
+ timeoutHandle.unref?.();
37445
+ if (abortSignal?.aborted) {
37446
+ onAbort();
37447
+ return;
37448
+ }
37449
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
37450
+ handle.result.then((value) => settle(() => resolve(value)), (error) => settle(() => reject(error)));
37033
37451
  });
37034
37452
  const checkDeadCode = async (options) => {
37035
37453
  const rootDirectory = toCanonicalPath(options.rootDirectory);
@@ -37041,8 +37459,9 @@ const checkDeadCode = async (options) => {
37041
37459
  entryPatterns,
37042
37460
  tsConfigPath: resolveTsConfigPath(rootDirectory),
37043
37461
  ignorePatterns,
37044
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
37045
- }), options.workerTimeoutMs ?? 12e4));
37462
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37463
+ parseConcurrency: options.parseConcurrency
37464
+ }), options.workerTimeoutMs ?? 12e4, options.abortSignal));
37046
37465
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
37047
37466
  const diagnostics = [];
37048
37467
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -37140,7 +37559,37 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
37140
37559
  return true;
37141
37560
  };
37142
37561
  const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
37143
- const excludeMinifiedFiles = (rootDirectory, relativePaths) => relativePaths.filter((relativePath) => !isLargeMinifiedFile(Path.resolve(rootDirectory, relativePath)));
37562
+ /**
37563
+ * Budget for the dead-code phase, scaled to the work. deslop's graph build is
37564
+ * CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
37565
+ * for a large repo (where the pass legitimately runs that long) and is then
37566
+ * tipped over by any concurrent load — silently dropping every dead-code
37567
+ * finding. Scaling the budget with file count (and inversely with the core
37568
+ * share when overlapped) lets the pass complete, while the ceiling still
37569
+ * reclaims a genuinely wedged worker. Returns the in-worker SIGKILL deadline
37570
+ * and the Effect-side phase backstop that sits a margin above it.
37571
+ */
37572
+ const resolveDeadCodeTimeout = (input) => {
37573
+ const coreShareFactor = Math.max(1, input.fullConcurrency / Math.max(1, input.deadCodeConcurrency));
37574
+ const workerTimeoutMs = Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor)));
37575
+ return {
37576
+ workerTimeoutMs,
37577
+ phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
37578
+ };
37579
+ };
37580
+ const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
37581
+ const entries = [];
37582
+ for (const relativePath of relativePaths) {
37583
+ const absolutePath = Path.resolve(rootDirectory, relativePath);
37584
+ const sizeBytes = statSourceFileSize(absolutePath);
37585
+ if (isLargeMinifiedFile(absolutePath, sizeBytes)) continue;
37586
+ entries.push({
37587
+ path: relativePath,
37588
+ sizeBytes: sizeBytes ?? 0
37589
+ });
37590
+ }
37591
+ return entries;
37592
+ };
37144
37593
  const listSourceFilesViaGit = (rootDirectory) => {
37145
37594
  const result = spawnSync("git", [
37146
37595
  "ls-files",
@@ -37173,7 +37622,8 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
37173
37622
  }
37174
37623
  return filePaths;
37175
37624
  };
37176
- const listSourceFiles = (rootDirectory) => excludeMinifiedFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
37625
+ const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
37626
+ const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
37177
37627
  const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
37178
37628
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
37179
37629
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
@@ -37216,24 +37666,25 @@ var Config = class Config extends Service()("react-doctor/Config") {
37216
37666
  var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37217
37667
  static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
37218
37668
  return yield* tryPromise({
37219
- try: () => checkDeadCode({
37669
+ try: (signal) => checkDeadCode({
37220
37670
  rootDirectory: input.rootDirectory,
37221
- userConfig: input.userConfig
37671
+ userConfig: input.userConfig,
37672
+ parseConcurrency: input.parseConcurrency,
37673
+ workerTimeoutMs: input.workerTimeoutMs,
37674
+ abortSignal: signal
37222
37675
  }),
37223
37676
  catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
37224
37677
  }).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
37225
37678
  })()) }));
37226
37679
  static layerOf = (diagnostics) => succeed$3(DeadCode, DeadCode.of({ run: () => fromIterable$1(diagnostics) }));
37227
37680
  };
37228
- const createNodeReadFileLinesSync = (rootDirectory) => {
37229
- return (filePath) => {
37230
- const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
37231
- try {
37232
- return NFS.readFileSync(absolutePath, "utf-8").split("\n");
37233
- } catch {
37234
- return null;
37235
- }
37236
- };
37681
+ const createNodeReadFileLinesSync = (rootDirectory) => (filePath) => {
37682
+ const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
37683
+ try {
37684
+ return NFS.readFileSync(absolutePath, "utf-8").split("\n");
37685
+ } catch {
37686
+ return null;
37687
+ }
37237
37688
  };
37238
37689
  var Files = class Files extends Service()("react-doctor/Files") {
37239
37690
  static layerNode = succeed$3(Files, Files.of({
@@ -37444,7 +37895,10 @@ var Git = class Git extends Service()("react-doctor/Git") {
37444
37895
  directory: input.directory,
37445
37896
  cause
37446
37897
  }) });
37447
- }));
37898
+ }), withSpan("git.exec", { attributes: {
37899
+ "git.command": input.command,
37900
+ "git.subcommand": input.args[0] ?? ""
37901
+ } }));
37448
37902
  const runGit = (directory, args) => runCommand({
37449
37903
  command: "git",
37450
37904
  args,
@@ -37472,7 +37926,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37472
37926
  ]);
37473
37927
  if (candidates.status !== 0) return null;
37474
37928
  return trimOrNull(candidates.stdout.split("\n")[0] ?? "");
37475
- });
37929
+ }).pipe(withSpan("Git.defaultBranch"));
37476
37930
  const branchExists = (directory, branch) => runGit(directory, [
37477
37931
  "rev-parse",
37478
37932
  "--verify",
@@ -37519,7 +37973,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37519
37973
  const result = resultOption.value;
37520
37974
  if (result.status !== 0) return null;
37521
37975
  return parseGithubViewerPermission(result.stdout);
37522
- }).pipe(catch_$1(() => succeed$2(null)));
37976
+ }).pipe(catch_$1(() => succeed$2(null)), withSpan("Git.githubViewerPermission"));
37523
37977
  /**
37524
37978
  * Resolves a `--diff A..B` / `A...B` commit range into a changed-file
37525
37979
  * selection. Each endpoint is validated with `isSafeGitRevision`
@@ -37633,7 +38087,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37633
38087
  changedFiles: splitNullSeparated(diff.stdout),
37634
38088
  isCurrentChanges: false
37635
38089
  };
37636
- }),
38090
+ }).pipe(withSpan("Git.diffSelection")),
37637
38091
  stagedFilePaths: (directory) => runGit(directory, [
37638
38092
  "diff",
37639
38093
  "--cached",
@@ -37675,7 +38129,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37675
38129
  status: result.status,
37676
38130
  stdout: result.stdout
37677
38131
  };
37678
- }),
38132
+ }).pipe(withSpan("Git.grep")),
37679
38133
  changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
37680
38134
  if (files.length === 0) return [];
37681
38135
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
@@ -37691,7 +38145,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37691
38145
  ]);
37692
38146
  if (result.status !== 0) return null;
37693
38147
  return parseChangedLineRanges(result.stdout);
37694
- })
38148
+ }).pipe(withSpan("Git.changedLineRanges"))
37695
38149
  });
37696
38150
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
37697
38151
  /**
@@ -37906,7 +38360,7 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
37906
38360
  for (const [absolutePath, originalContent] of originalContents) try {
37907
38361
  NFS.writeFileSync(absolutePath, originalContent);
37908
38362
  } catch (error) {
37909
- process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${error instanceof Error ? error.message : String(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
38363
+ process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${messageFromUnknown(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
37910
38364
  }
37911
38365
  };
37912
38366
  const onExit = () => restore();
@@ -37930,6 +38384,14 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
37930
38384
  process.removeListener("exit", onExit);
37931
38385
  };
37932
38386
  };
38387
+ const ROOT_DIRECTORY_PLACEHOLDER = "<root>";
38388
+ const normalizeConfigForHash = (config) => {
38389
+ const clone = JSON.parse(JSON.stringify(config));
38390
+ if (clone?.settings?.["react-doctor"]) clone.settings["react-doctor"].rootDirectory = ROOT_DIRECTORY_PLACEHOLDER;
38391
+ if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
38392
+ return clone;
38393
+ };
38394
+ 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");
37933
38395
  /**
37934
38396
  * Loads a plugin module via the local require resolver and extracts
37935
38397
  * `(name, ruleNames)` from either `module.exports.meta + rules` or
@@ -37956,16 +38418,16 @@ const readPluginShape = (pluginSpecifier, loadModule) => {
37956
38418
  ruleNames: new Set(Object.keys(rules))
37957
38419
  };
37958
38420
  };
37959
- const bundledRequire = createRequire(import.meta.url);
38421
+ const bundledRequire$1 = createRequire(import.meta.url);
37960
38422
  const resolveReactHooksJsPlugin = (hasReactCompiler, customRulesOnly) => {
37961
38423
  if (!hasReactCompiler || customRulesOnly) return null;
37962
38424
  let pluginSpecifier;
37963
38425
  try {
37964
- pluginSpecifier = bundledRequire.resolve("eslint-plugin-react-hooks");
38426
+ pluginSpecifier = bundledRequire$1.resolve("eslint-plugin-react-hooks");
37965
38427
  } catch {
37966
38428
  return null;
37967
38429
  }
37968
- const { ruleNames } = readPluginShape(pluginSpecifier, (spec) => bundledRequire(spec));
38430
+ const { ruleNames } = readPluginShape(pluginSpecifier, (spec) => bundledRequire$1(spec));
37969
38431
  return {
37970
38432
  entry: {
37971
38433
  name: "react-hooks-js",
@@ -38012,7 +38474,7 @@ const resolveUserPlugin = (spec, configSourceDirectory) => {
38012
38474
  try {
38013
38475
  resolvedSpecifier = isRelative ? Path.resolve(configSourceDirectory, spec) : candidateRequire.resolve(spec);
38014
38476
  } catch (error) {
38015
- warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${error instanceof Error ? error.message : String(error)}`);
38477
+ warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${messageFromUnknown(error)}`);
38016
38478
  return null;
38017
38479
  }
38018
38480
  const { name, ruleNames } = readPluginShape(resolvedSpecifier, (target) => candidateRequire(target));
@@ -38084,8 +38546,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
38084
38546
  }
38085
38547
  return enabled;
38086
38548
  };
38087
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
38088
- const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38549
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
38550
+ const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
38089
38551
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
38090
38552
  const jsPlugins = [];
38091
38553
  if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
@@ -38094,6 +38556,8 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38094
38556
  for (const registryEntry of REACT_DOCTOR_RULES) {
38095
38557
  const rule = reactDoctorPlugin.rules[registryEntry.id];
38096
38558
  if (!rule) continue;
38559
+ if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
38560
+ if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
38097
38561
  if (rule.scan !== void 0) continue;
38098
38562
  if (customRulesOnly && registryEntry.originallyExternal) continue;
38099
38563
  if (rule.framework !== "global" && !rule.requires) continue;
@@ -38108,7 +38572,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38108
38572
  enabledReactDoctorRules[registryEntry.key] = severity;
38109
38573
  }
38110
38574
  const userPluginRules = {};
38111
- for (const userPlugin of userPlugins) {
38575
+ if (ruleSelection !== "sidecar") for (const userPlugin of userPlugins) {
38112
38576
  Object.assign(userPluginRules, buildUserPluginRules(userPlugin, severityControls));
38113
38577
  jsPlugins.push(userPlugin.entry);
38114
38578
  }
@@ -38138,6 +38602,100 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38138
38602
  }
38139
38603
  };
38140
38604
  };
38605
+ const atomicWriteJson = (filePath, value) => {
38606
+ try {
38607
+ NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
38608
+ const temporaryPath = `${filePath}.${process.pid}.tmp`;
38609
+ NFS.writeFileSync(temporaryPath, JSON.stringify(value));
38610
+ NFS.renameSync(temporaryPath, filePath);
38611
+ } catch {
38612
+ return;
38613
+ }
38614
+ };
38615
+ const failOpenReadJson = (filePath, fallback) => {
38616
+ try {
38617
+ return JSON.parse(NFS.readFileSync(filePath, "utf8"));
38618
+ } catch {
38619
+ return fallback;
38620
+ }
38621
+ };
38622
+ const validateDiagnostic = decodeUnknownSync(Diagnostic);
38623
+ const decodeFileDiagnostics = (raw) => {
38624
+ if (!Array.isArray(raw)) return null;
38625
+ try {
38626
+ for (const entry of raw) validateDiagnostic(entry);
38627
+ return raw;
38628
+ } catch {
38629
+ return null;
38630
+ }
38631
+ };
38632
+ const emptyCache = () => ({
38633
+ version: 1,
38634
+ rulesets: {}
38635
+ });
38636
+ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
38637
+ const entries = /* @__PURE__ */ new Map();
38638
+ const persisted = failOpenReadJson(cacheFilePath, emptyCache());
38639
+ if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
38640
+ const bucket = persisted.rulesets[rulesetHash];
38641
+ if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
38642
+ for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
38643
+ const decoded = decodeFileDiagnostics(rawDiagnostics);
38644
+ if (decoded !== null) entries.set(fileKey, decoded);
38645
+ }
38646
+ return entries;
38647
+ };
38648
+ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38649
+ const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
38650
+ const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
38651
+ return {
38652
+ lookup: (fileKey) => entries.get(fileKey) ?? null,
38653
+ store: (fileKey, diagnostics) => {
38654
+ entries.delete(fileKey);
38655
+ entries.set(fileKey, diagnostics);
38656
+ },
38657
+ persist: () => {
38658
+ const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
38659
+ const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
38660
+ const existingBucket = rulesets[rulesetHash];
38661
+ const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
38662
+ const ourFiles = {};
38663
+ for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
38664
+ const cappedEntries = Object.entries({
38665
+ ...existingFiles,
38666
+ ...ourFiles
38667
+ }).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
38668
+ rulesets[rulesetHash] = {
38669
+ updatedAtMs: Date.now(),
38670
+ files: Object.fromEntries(cappedEntries)
38671
+ };
38672
+ const keptHashes = Object.entries(rulesets).sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8).map(([hash]) => hash);
38673
+ const prunedRulesets = {};
38674
+ for (const hash of keptHashes) prunedRulesets[hash] = rulesets[hash];
38675
+ atomicWriteJson(cacheFilePath, {
38676
+ version: 1,
38677
+ rulesets: prunedRulesets
38678
+ });
38679
+ }
38680
+ };
38681
+ };
38682
+ const bundledRequire = createRequire(import.meta.url);
38683
+ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
38684
+ "oxlint/package.json",
38685
+ "oxlint-plugin-react-doctor/package.json",
38686
+ "eslint-plugin-react-hooks/package.json"
38687
+ ];
38688
+ const resolveOxlintToolchainVersions = () => {
38689
+ const versions = [`node=${process.version}`];
38690
+ for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
38691
+ const packageJson = bundledRequire(specifier);
38692
+ const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
38693
+ versions.push(`${specifier}=${version}`);
38694
+ } catch {
38695
+ versions.push(`${specifier}=missing`);
38696
+ }
38697
+ return versions;
38698
+ };
38141
38699
  const esmRequire = createRequire(import.meta.url);
38142
38700
  const resolveOxlintBinary = () => {
38143
38701
  const oxlintMainPath = esmRequire.resolve("oxlint");
@@ -38145,7 +38703,6 @@ const resolveOxlintBinary = () => {
38145
38703
  return Path.join(oxlintPackageDirectory, "bin", "oxlint");
38146
38704
  };
38147
38705
  const resolvePluginPath = () => esmRequire.resolve("oxlint-plugin-react-doctor");
38148
- const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
38149
38706
  const resolveTsConfigRelativePath = (rootDirectory) => {
38150
38707
  for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
38151
38708
  return null;
@@ -38517,7 +39074,7 @@ const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
38517
39074
  const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
38518
39075
  let currentNode = identifier.parent;
38519
39076
  while (currentNode) {
38520
- if (isScopeNode(currentNode)) {
39077
+ if (isScopeBoundary(currentNode)) {
38521
39078
  if (scopeContainsNonImportBinding(currentNode, currentNode, identifier.text)) return true;
38522
39079
  }
38523
39080
  if (currentNode === sourceFile) return false;
@@ -38608,11 +39165,10 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
38608
39165
  });
38609
39166
  return resolution;
38610
39167
  };
38611
- const isScopeNode = isScopeBoundary;
38612
39168
  const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, visitedDeclarations = /* @__PURE__ */ new Set()) => {
38613
39169
  let currentNode = identifier.parent;
38614
39170
  while (currentNode) {
38615
- if (isScopeNode(currentNode)) {
39171
+ if (isScopeBoundary(currentNode)) {
38616
39172
  const resolution = findResolutionInScope(currentNode, identifier.text, reactImportBindings, sourceFile, visitedDeclarations);
38617
39173
  if (resolution) return resolution;
38618
39174
  }
@@ -38782,9 +39338,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
38782
39338
  try {
38783
39339
  parsed = JSON.parse(sanitizedStdout);
38784
39340
  } catch {
38785
- throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
39341
+ throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
38786
39342
  }
38787
- if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 200) }) });
39343
+ if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
38788
39344
  const minifiedFileCache = /* @__PURE__ */ new Map();
38789
39345
  const isMinifiedDiagnosticFile = (filename) => {
38790
39346
  const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
@@ -38821,15 +39377,19 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
38821
39377
  };
38822
39378
  });
38823
39379
  };
38824
- const SANITIZED_ENV = (() => {
38825
- const sanitized = {};
38826
- for (const [name, value] of Object.entries(process.env)) {
39380
+ const buildOxlintChildEnv = (sourceEnv) => {
39381
+ const childEnv = {};
39382
+ for (const [name, value] of Object.entries(sourceEnv)) {
38827
39383
  if (name === "NODE_OPTIONS" || name === "NODE_DEBUG") continue;
38828
39384
  if (name.startsWith("npm_config_")) continue;
38829
- sanitized[name] = value;
39385
+ childEnv[name] = value;
38830
39386
  }
38831
- return sanitized;
38832
- })();
39387
+ const isCompileCacheDisabled = Boolean(sourceEnv.NODE_DISABLE_COMPILE_CACHE);
39388
+ const isCompileCacheAlreadySet = childEnv.NODE_COMPILE_CACHE !== void 0;
39389
+ if (!isCompileCacheDisabled && !isCompileCacheAlreadySet) childEnv.NODE_COMPILE_CACHE = Path.join(os.tmpdir(), NODE_COMPILE_CACHE_DIR_NAME);
39390
+ return childEnv;
39391
+ };
39392
+ const SANITIZED_ENV = buildOxlintChildEnv(process.env);
38833
39393
  /**
38834
39394
  * Spawn one oxlint subprocess with hard ceilings on wall time and
38835
39395
  * output size. Returns stdout on success; raises a tagged
@@ -38846,7 +39406,11 @@ const SANITIZED_ENV = (() => {
38846
39406
  * The first three are splittable (the caller's binary-split retry
38847
39407
  * shrinks the batch and re-spawns); the fourth isn't.
38848
39408
  */
38849
- const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
39409
+ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES, abortSignal) => new Promise((resolve, reject) => {
39410
+ if (abortSignal?.aborted) {
39411
+ reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
39412
+ return;
39413
+ }
38850
39414
  const child = spawn(nodeBinaryPath, args, {
38851
39415
  cwd: rootDirectory,
38852
39416
  env: SANITIZED_ENV,
@@ -38856,11 +39420,18 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
38856
39420
  "pipe"
38857
39421
  ]
38858
39422
  });
39423
+ const onAbort = () => {
39424
+ child.kill("SIGKILL");
39425
+ reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
39426
+ };
39427
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
39428
+ const clearAbortListener = () => abortSignal?.removeEventListener("abort", onAbort);
38859
39429
  const timeoutHandle = setTimeout(() => {
39430
+ clearAbortListener();
38860
39431
  child.kill("SIGKILL");
38861
39432
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
38862
39433
  kind: "timeout",
38863
- detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
39434
+ detail: `${spawnTimeoutMs / MILLISECONDS_PER_SECOND}s budget exceeded`
38864
39435
  }) }));
38865
39436
  }, spawnTimeoutMs);
38866
39437
  timeoutHandle.unref?.();
@@ -38891,10 +39462,12 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
38891
39462
  });
38892
39463
  child.on("error", (error) => {
38893
39464
  clearTimeout(timeoutHandle);
39465
+ clearAbortListener();
38894
39466
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: error }) }));
38895
39467
  });
38896
39468
  child.on("close", (_code, signal) => {
38897
39469
  clearTimeout(timeoutHandle);
39470
+ clearAbortListener();
38898
39471
  if (didKillForSize) {
38899
39472
  reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
38900
39473
  kind: "output-too-large",
@@ -38961,26 +39534,28 @@ const isParallelismRelatedSpawnError = (error) => {
38961
39534
  * loop with a slimmer config in that case.
38962
39535
  */
38963
39536
  const spawnLintBatches = async (input) => {
38964
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
39537
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal } = input;
38965
39538
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
38966
39539
  const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
38967
39540
  const runBatchPass = async (concurrency) => {
38968
39541
  const allDiagnostics = [];
38969
39542
  const droppedFiles = [];
38970
39543
  let firstDropReason = null;
38971
- const spawnLintBatch = async (batch) => {
39544
+ const splitDeadlineMs = Date.now() + splitTotalBudgetMs;
39545
+ const spawnLintBatch = async (batch, depth) => {
38972
39546
  const batchArgs = [...baseArgs, ...batch];
38973
39547
  try {
38974
- return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
39548
+ return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
38975
39549
  } catch (error) {
38976
39550
  if (!isSplittableReactDoctorError(error)) throw error;
38977
- if (batch.length <= 1) {
39551
+ const splitBudgetExhausted = Date.now() >= splitDeadlineMs || depth >= splitMaxDepth;
39552
+ if (batch.length <= 1 || splitBudgetExhausted) {
38978
39553
  droppedFiles.push(...batch);
38979
- if (firstDropReason === null) firstDropReason = error.message;
39554
+ if (firstDropReason === null) firstDropReason = splitBudgetExhausted && batch.length > 1 ? `${error.message} (split budget exhausted after ${splitMaxDepth} levels / ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s)` : error.message;
38980
39555
  return [];
38981
39556
  }
38982
39557
  const splitIndex = Math.ceil(batch.length / 2);
38983
- return [...await spawnLintBatch(batch.slice(0, splitIndex)), ...await spawnLintBatch(batch.slice(splitIndex))];
39558
+ return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1)];
38984
39559
  }
38985
39560
  };
38986
39561
  let startedFileCount = 0;
@@ -38997,7 +39572,7 @@ const spawnLintBatches = async (input) => {
38997
39572
  try {
38998
39573
  const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
38999
39574
  startedFileCount += batch.length;
39000
- const batchDiagnostics = await spawnLintBatch(batch);
39575
+ const batchDiagnostics = await spawnLintBatch(batch, 0);
39001
39576
  scannedFileCount += batch.length;
39002
39577
  if (onFileProgress) {
39003
39578
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
@@ -39058,6 +39633,22 @@ const validateRuleRegistration = () => {
39058
39633
  ].filter((entry) => entry !== null).join("; ");
39059
39634
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
39060
39635
  };
39636
+ const hashFileContents = (filePath) => {
39637
+ try {
39638
+ return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
39639
+ } catch {
39640
+ return null;
39641
+ }
39642
+ };
39643
+ const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
39644
+ const resolveReactDoctorCacheDir = (projectDirectory) => {
39645
+ const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
39646
+ if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
39647
+ const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
39648
+ if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
39649
+ return Path.join(os.tmpdir(), "react-doctor-cache", projectCacheSubdir(projectDirectory));
39650
+ };
39651
+ const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
39061
39652
  /**
39062
39653
  * Atomically (re)writes the generated oxlintrc.json. Used twice in
39063
39654
  * the runner: once for the primary scan, once for the
@@ -39075,6 +39666,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
39075
39666
  NFS.closeSync(fileHandle);
39076
39667
  }
39077
39668
  };
39669
+ const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
39670
+ /**
39671
+ * Detects an oxlint config-load crash caused by the optional
39672
+ * `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
39673
+ * builds the partial-failure note for it; returns `null` when the failure
39674
+ * was anything else.
39675
+ *
39676
+ * oxlint prints a framed error to stdout (not stderr) and exits non-zero
39677
+ * when a `jsPlugins` entry can't be imported; that non-JSON stdout
39678
+ * surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
39679
+ * config load on it, leaving the plugin in would drop every curated
39680
+ * react-doctor diagnostic too — so the caller retries with the plugin
39681
+ * stripped (issue #833). Both markers sit at the start of oxlint's
39682
+ * message, so they survive the `preview` slice even for deep pnpm paths.
39683
+ */
39684
+ const reactHooksJsPluginDropNote = (error) => {
39685
+ if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
39686
+ const { preview } = error.reason;
39687
+ if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
39688
+ const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
39689
+ return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
39690
+ };
39078
39691
  /**
39079
39692
  * The oxlint runner. Composed of three pieces in `runners/oxlint/`:
39080
39693
  *
@@ -39094,7 +39707,7 @@ const writeOxlintConfig = (configPath, configToWrite) => {
39094
39707
  * 6. always restore disable directives + clean up the temp dir
39095
39708
  */
39096
39709
  const runOxlint = async (options) => {
39097
- const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, spawnTimeoutMs, outputMaxBytes } = options;
39710
+ 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;
39098
39711
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
39099
39712
  const severityControls = buildRuleSeverityControls(userConfig);
39100
39713
  validateRuleRegistration();
@@ -39102,38 +39715,165 @@ const runOxlint = async (options) => {
39102
39715
  const pluginPath = resolvePluginPath();
39103
39716
  const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
39104
39717
  const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
39105
- const buildConfig = (extendsForThisAttempt) => createOxlintConfig({
39718
+ const buildConfig = (overrides) => createOxlintConfig({
39106
39719
  pluginPath,
39107
39720
  project,
39108
39721
  customRulesOnly,
39109
- extendsPaths: extendsForThisAttempt,
39722
+ extendsPaths: overrides.extendsPaths,
39110
39723
  ignoredTags,
39111
39724
  serverAuthFunctionNames,
39112
39725
  severityControls,
39113
- userPlugins
39726
+ userPlugins,
39727
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
39728
+ ruleSelection: overrides.ruleSelection
39114
39729
  });
39115
39730
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
39116
39731
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
39117
39732
  const configPath = Path.join(configDirectory, "oxlintrc.json");
39118
39733
  try {
39119
- const baseArgs = [
39120
- resolveOxlintBinary(),
39121
- "-c",
39122
- configPath,
39123
- "--format",
39124
- "json"
39125
- ];
39734
+ const oxlintBinary = resolveOxlintBinary();
39735
+ const sharedArgs = [];
39736
+ let tsconfigContent = null;
39126
39737
  if (project.hasTypeScript) {
39127
39738
  const tsconfigRelativePath = resolveTsConfigRelativePath(rootDirectory);
39128
- if (tsconfigRelativePath) baseArgs.push("--tsconfig", tsconfigRelativePath);
39739
+ if (tsconfigRelativePath) {
39740
+ sharedArgs.push("--tsconfig", tsconfigRelativePath);
39741
+ try {
39742
+ tsconfigContent = NFS.readFileSync(Path.resolve(rootDirectory, tsconfigRelativePath), "utf8");
39743
+ } catch {
39744
+ tsconfigContent = null;
39745
+ }
39746
+ }
39129
39747
  }
39130
39748
  const combinedPatterns = collectIgnorePatterns(rootDirectory);
39131
39749
  if (combinedPatterns.length > 0) {
39132
39750
  const combinedIgnorePath = Path.join(configDirectory, "combined.ignore");
39133
39751
  NFS.writeFileSync(combinedIgnorePath, `${combinedPatterns.join("\n")}\n`);
39134
- baseArgs.push("--ignore-path", combinedIgnorePath);
39752
+ sharedArgs.push("--ignore-path", combinedIgnorePath);
39753
+ }
39754
+ const makeBaseArgs = (oxlintConfigPath) => [
39755
+ oxlintBinary,
39756
+ "-c",
39757
+ oxlintConfigPath,
39758
+ "--format",
39759
+ "json",
39760
+ ...sharedArgs
39761
+ ];
39762
+ const discoverScanFiles = () => lintBatchOrdering === "cost" ? sortSourceFilesByCost(listSourceFilesWithSize(rootDirectory)) : listSourceFiles(rootDirectory);
39763
+ const candidateFiles = includePaths !== void 0 ? includePaths : discoverScanFiles();
39764
+ const runConfigOverFiles = async (buildConfigForPass, configFileName, files, fileProgress) => {
39765
+ if (files.length === 0) return {
39766
+ diagnostics: [],
39767
+ didDropReactHooksJsPlugin: false,
39768
+ hadPartialFailure: false
39769
+ };
39770
+ let hadPartialFailure = false;
39771
+ const reportPartialFailure = (reason) => {
39772
+ hadPartialFailure = true;
39773
+ onPartialFailure?.(reason);
39774
+ };
39775
+ const passConfigPath = Path.join(configDirectory, configFileName);
39776
+ const passBaseArgs = makeBaseArgs(passConfigPath);
39777
+ const passFileBatches = batchIncludePaths(passBaseArgs, files);
39778
+ const spawnPass = () => spawnLintBatches({
39779
+ baseArgs: passBaseArgs,
39780
+ fileBatches: passFileBatches,
39781
+ rootDirectory,
39782
+ nodeBinaryPath,
39783
+ project,
39784
+ onPartialFailure: reportPartialFailure,
39785
+ onFileProgress: fileProgress,
39786
+ spawnTimeoutMs,
39787
+ outputMaxBytes,
39788
+ concurrency: options.concurrency,
39789
+ signal: options.signal
39790
+ });
39791
+ writeOxlintConfig(passConfigPath, buildConfigForPass({}));
39792
+ try {
39793
+ return {
39794
+ diagnostics: await spawnPass(),
39795
+ didDropReactHooksJsPlugin: false,
39796
+ hadPartialFailure
39797
+ };
39798
+ } catch (error) {
39799
+ const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
39800
+ if (reactHooksJsDropNote === null) throw error;
39801
+ writeOxlintConfig(passConfigPath, buildConfigForPass({ disableReactHooksJsPlugin: true }));
39802
+ const diagnostics = await spawnPass();
39803
+ reportPartialFailure(reactHooksJsDropNote);
39804
+ return {
39805
+ diagnostics,
39806
+ didDropReactHooksJsPlugin: true,
39807
+ hadPartialFailure
39808
+ };
39809
+ }
39810
+ };
39811
+ if (perFileLintCacheEnabled && respectInlineDisables && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
39812
+ const rulesetHash = computeRulesetHash({
39813
+ config: buildConfig({
39814
+ extendsPaths: [],
39815
+ ruleSelection: "cacheable"
39816
+ }),
39817
+ toolchainVersions: resolveOxlintToolchainVersions(),
39818
+ ignorePatterns: combinedPatterns,
39819
+ tsconfigContent
39820
+ });
39821
+ const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
39822
+ const cacheKeyByFile = /* @__PURE__ */ new Map();
39823
+ const missFiles = [];
39824
+ const replayedDiagnostics = [];
39825
+ for (const candidateFile of candidateFiles) {
39826
+ const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
39827
+ if (contentHash === null) {
39828
+ missFiles.push(candidateFile);
39829
+ continue;
39830
+ }
39831
+ const cacheKey = `${candidateFile.replaceAll("\\", "/")}${contentHash}`;
39832
+ cacheKeyByFile.set(candidateFile, cacheKey);
39833
+ const cachedDiagnostics = cache.lookup(cacheKey);
39834
+ if (cachedDiagnostics === null) missFiles.push(candidateFile);
39835
+ else replayedDiagnostics.push(...cachedDiagnostics);
39836
+ }
39837
+ const cacheHitFileCount = candidateFiles.length - missFiles.length;
39838
+ const cacheableResult = await runConfigOverFiles((overrides) => buildConfig({
39839
+ extendsPaths: [],
39840
+ ruleSelection: "cacheable",
39841
+ disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
39842
+ }), "oxlintrc.cacheable.json", missFiles, void 0);
39843
+ const sidecarResult = await runConfigOverFiles(() => buildConfig({
39844
+ extendsPaths: [],
39845
+ ruleSelection: "sidecar"
39846
+ }), "oxlintrc.sidecar.json", candidateFiles, options.onFileProgress);
39847
+ onCacheStats?.(cacheHitFileCount, candidateFiles.length);
39848
+ const missFileByNormalizedPath = /* @__PURE__ */ new Map();
39849
+ for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
39850
+ const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
39851
+ let isAttributionSound = true;
39852
+ for (const diagnostic of cacheableResult.diagnostics) {
39853
+ const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
39854
+ if (missFile === void 0) {
39855
+ isAttributionSound = false;
39856
+ break;
39857
+ }
39858
+ const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
39859
+ fileDiagnostics.push(diagnostic);
39860
+ freshDiagnosticsByFile.set(missFile, fileDiagnostics);
39861
+ }
39862
+ if (!cacheableResult.didDropReactHooksJsPlugin && !cacheableResult.hadPartialFailure && isAttributionSound) {
39863
+ for (const missFile of missFiles) {
39864
+ const cacheKey = cacheKeyByFile.get(missFile);
39865
+ if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
39866
+ }
39867
+ cache.persist();
39868
+ }
39869
+ return dedupeDiagnostics([
39870
+ ...replayedDiagnostics,
39871
+ ...cacheableResult.diagnostics,
39872
+ ...sidecarResult.diagnostics
39873
+ ]);
39135
39874
  }
39136
- const fileBatches = batchIncludePaths(baseArgs, includePaths !== void 0 ? includePaths : listSourceFiles(rootDirectory));
39875
+ const baseArgs = makeBaseArgs(configPath);
39876
+ const fileBatches = batchIncludePaths(baseArgs, candidateFiles);
39137
39877
  const runBatches = () => spawnLintBatches({
39138
39878
  baseArgs,
39139
39879
  fileBatches,
@@ -39144,14 +39884,25 @@ const runOxlint = async (options) => {
39144
39884
  onFileProgress: options.onFileProgress,
39145
39885
  spawnTimeoutMs,
39146
39886
  outputMaxBytes,
39147
- concurrency: options.concurrency
39887
+ concurrency: options.concurrency,
39888
+ signal: options.signal
39148
39889
  });
39149
- writeOxlintConfig(configPath, buildConfig(extendsPaths));
39890
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
39150
39891
  try {
39151
39892
  return await runBatches();
39152
39893
  } catch (error) {
39894
+ const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
39895
+ if (reactHooksJsDropNote !== null) {
39896
+ writeOxlintConfig(configPath, buildConfig({
39897
+ extendsPaths,
39898
+ disableReactHooksJsPlugin: true
39899
+ }));
39900
+ const diagnostics = await runBatches();
39901
+ onPartialFailure?.(reactHooksJsDropNote);
39902
+ return diagnostics;
39903
+ }
39153
39904
  if (extendsPaths.length === 0) throw error;
39154
- writeOxlintConfig(configPath, buildConfig([]));
39905
+ writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
39155
39906
  return await runBatches();
39156
39907
  }
39157
39908
  } finally {
@@ -39213,9 +39964,11 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
39213
39964
  const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
39214
39965
  const outputMaxBytes = yield* OxlintOutputMaxBytes;
39215
39966
  const concurrency = yield* OxlintConcurrency;
39967
+ const lintBatchOrdering = yield* LintBatchOrdering;
39968
+ const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
39216
39969
  const collectedFailures = [];
39217
39970
  const diagnostics = yield* tryPromise({
39218
- try: () => runOxlint({
39971
+ try: (signal) => runOxlint({
39219
39972
  rootDirectory: input.rootDirectory,
39220
39973
  project: input.project,
39221
39974
  includePaths: input.includePaths ? [...input.includePaths] : void 0,
@@ -39230,9 +39983,13 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
39230
39983
  collectedFailures.push(reason);
39231
39984
  },
39232
39985
  onFileProgress: input.onFileProgress,
39986
+ perFileLintCacheEnabled,
39987
+ onCacheStats: input.onCacheStats,
39233
39988
  spawnTimeoutMs,
39234
39989
  outputMaxBytes,
39235
- concurrency
39990
+ concurrency,
39991
+ signal,
39992
+ lintBatchOrdering
39236
39993
  }),
39237
39994
  catch: ensureReactDoctorError
39238
39995
  });
@@ -39624,14 +40381,49 @@ const parseArtifactFromBody = (body) => {
39624
40381
  }
39625
40382
  return null;
39626
40383
  };
39627
- const fetchSocketArtifact = (dependency) => tryPromise(async (signal) => {
40384
+ const isSupplyChainCacheDisabled = () => {
40385
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
40386
+ return noCache === "1" || noCache === "true";
40387
+ };
40388
+ const supplyChainCacheFile = (cacheDirectory, dependency) => {
40389
+ const purlHash = crypto.createHash("sha256").update(toPurl(dependency)).digest("hex").slice(0, 16);
40390
+ return Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR, `${purlHash}.json`);
40391
+ };
40392
+ const readCachedSocketBody = (cacheFile) => {
40393
+ try {
40394
+ const entry = JSON.parse(NFS.readFileSync(cacheFile, "utf-8"));
40395
+ 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;
40396
+ } catch {}
40397
+ return null;
40398
+ };
40399
+ const writeCachedSocketBody = (cacheFile, body) => {
40400
+ try {
40401
+ NFS.mkdirSync(Path.dirname(cacheFile), { recursive: true });
40402
+ NFS.writeFileSync(cacheFile, JSON.stringify({
40403
+ fetchedAtMs: Date.now(),
40404
+ body
40405
+ }));
40406
+ } catch {}
40407
+ };
40408
+ const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
40409
+ const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
40410
+ if (cacheFile !== null) {
40411
+ const cachedBody = readCachedSocketBody(cacheFile);
40412
+ if (cachedBody !== null) {
40413
+ const cachedArtifact = parseArtifactFromBody(cachedBody);
40414
+ if (cachedArtifact !== null) return cachedArtifact;
40415
+ }
40416
+ }
39628
40417
  const requestUrl = `${SOCKET_FREE_PURL_API_BASE}/${encodeURIComponent(toPurl(dependency))}`;
39629
40418
  const response = await fetch(requestUrl, {
39630
40419
  headers: { "User-Agent": SOCKET_FREE_USER_AGENT },
39631
40420
  signal
39632
40421
  });
39633
40422
  if (!response.ok) return null;
39634
- return parseArtifactFromBody(await response.text());
40423
+ const body = await response.text();
40424
+ const artifact = parseArtifactFromBody(body);
40425
+ if (artifact !== null && cacheFile !== null) writeCachedSocketBody(cacheFile, body);
40426
+ return artifact;
39635
40427
  }).pipe(timeout(FETCH_TIMEOUT_MS), orElseSucceed(() => null), tap$1((artifact) => {
39636
40428
  const scoreAttributes = {};
39637
40429
  if (artifact !== null) {
@@ -39736,7 +40528,8 @@ const checkSupplyChain = (input) => gen(function* () {
39736
40528
  const packageJsonPath = Path.join(input.rootDirectory, "package.json");
39737
40529
  const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
39738
40530
  if (dependencies.length === 0) return [];
39739
- const artifacts = yield* forEach$1(dependencies, fetchSocketArtifact, { concurrency: 8 });
40531
+ const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
40532
+ const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
39740
40533
  const diagnostics = [];
39741
40534
  for (let index = 0; index < dependencies.length; index += 1) {
39742
40535
  const artifact = artifacts[index];
@@ -39761,6 +40554,10 @@ const checkSupplyChain = (input) => gen(function* () {
39761
40554
  * The underlying `checkSupplyChain` Effect is total/fail-open — per-package
39762
40555
  * timeouts and network failures recover to "skip" — so the stream never
39763
40556
  * fails, mirroring `DeadCode`'s stream shape so the two compose the same way.
40557
+ * The orchestrator (`run-inspect.ts`) consumes this stream on a background
40558
+ * fiber whose network time overlaps the lint pass, joined under a generous
40559
+ * wall-clock budget; a budget expiry is the same fail-open outcome as a Socket
40560
+ * outage.
39764
40561
  */
39765
40562
  var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
39766
40563
  static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(checkSupplyChain(input).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
@@ -39819,18 +40616,42 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
39819
40616
  *
39820
40617
  * Phases:
39821
40618
  *
39822
- * 1. Config.resolve(directory) → Project.discover → Git metadata
40619
+ * 1. Config.resolve(directory) → Project.discover → Git metadata.
40620
+ * The GitHub viewer-permission lookup is forked onto a background
40621
+ * fiber here and joined late (it feeds score metadata, not
40622
+ * diagnostics).
39823
40623
  * 2. beforeLint hook (e.g. CLI renders the project-detection block)
39824
40624
  * 3. environment checks (reduced-motion + pnpm hardening +
39825
- * expo/react-native + security scan)
39826
- * 4. Linter.run + DeadCode.run forked as concurrent fibers so
39827
- * their wall-clock times overlap. Progress spinners stay
39828
- * sequential (lint first, then dead-code) for clean terminal
39829
- * output. GitHub viewer permission also runs as a background
39830
- * fiber during this phase.
39831
- * 5. afterLint hook
39832
- * 6. Reporter.finalize
39833
- * 7. Score.compute against the surface-filtered diagnostic set
40625
+ * expo/react-native + security scan), collected synchronously
40626
+ * 4. The supply-chain check (Socket.dev) is forked onto a background
40627
+ * fiber so its ~100% network-bound time overlaps the ~100%
40628
+ * CPU/subprocess-bound lint pass below, collapsing two serial
40629
+ * phases into roughly `max(supplyChain, lint)`. It is capped by
40630
+ * `SupplyChainOverlapTimeoutMs` (measured from fork) so a hung
40631
+ * socket can't drag out its join; on timeout it fails open to no
40632
+ * diagnostics — the same outcome class as a Socket outage.
40633
+ * 5. Linter.run runs; DeadCode.run runs concurrently (forked child
40634
+ * fiber) ONLY when the memory gate has headroom to run the 8 GB
40635
+ * dead-code child alongside the oxlint workers — or when overlap is
40636
+ * forced via REACT_DOCTOR_DEAD_CODE_OVERLAP. Otherwise dead-code
40637
+ * runs sequentially after lint, exactly as it did pre-overlap. The
40638
+ * fiber is joined (or interrupted, SIGKILLing its worker, on lint
40639
+ * failure) before diagnostics are concatenated. The afterLint hook
40640
+ * fires between lint and dead-code. Progress spinner labels AND the
40641
+ * final diagnostic / score order stay independent of execution
40642
+ * order, so terminal output is identical either way; supply-chain
40643
+ * rides alongside without a spinner.
40644
+ * 6. Join the supply-chain fiber, then assemble the diagnostics in a
40645
+ * FIXED order (env, supply-chain, lint, dead-code) so the output is
40646
+ * byte-identical regardless of which fiber settled first. The
40647
+ * viewer-permission fiber is joined later, during score-metadata
40648
+ * assembly (it feeds score metadata, not diagnostics). The per-element
40649
+ * `Reporter.emit` side-channel now interleaves supply-chain with lint
40650
+ * emits, so capture-order assertions must target the deterministic
40651
+ * concat below, not emit order (production `Reporter.layerNoop` makes
40652
+ * emit a no-op).
40653
+ * 7. Reporter.finalize
40654
+ * 8. Score.compute against the surface-filtered diagnostic set
39834
40655
  *
39835
40656
  * The orchestrator owns spinner lifecycle via `Progress`; callers
39836
40657
  * choose `Progress.layerOra(...)` for CLI feedback or
@@ -39888,10 +40709,21 @@ const runInspect = (input, hooks = {}) => gen(function* () {
39888
40709
  ignoredTags: input.ignoredTags
39889
40710
  })
39890
40711
  ])));
39891
- const supplyChainCollected = !isDiffMode || (input.supplyChainManifestChanged ?? false) ? yield* runCollect(applyPerElementPipeline(supplyChainService.run({
40712
+ const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
40713
+ const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
40714
+ const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
39892
40715
  rootDirectory: scanDirectory,
39893
40716
  userConfig: resolvedConfig.config
39894
- }))) : [];
40717
+ }))).pipe(map$3((diagnostics) => ({
40718
+ diagnostics,
40719
+ timedOut: false
40720
+ })), timeout(supplyChainOverlapTimeout), orElseSucceed(() => ({
40721
+ diagnostics: [],
40722
+ timedOut: true
40723
+ }))) : succeed$2({
40724
+ diagnostics: [],
40725
+ timedOut: false
40726
+ }));
39895
40727
  const lintFailure = yield* make$13({
39896
40728
  didFail: false,
39897
40729
  reason: null,
@@ -39902,12 +40734,49 @@ const runInspect = (input, hooks = {}) => gen(function* () {
39902
40734
  didFail: false,
39903
40735
  reason: null
39904
40736
  });
39905
- const scanConcurrency = yield* OxlintConcurrency;
40737
+ const scanConcurrency = resolveScanConcurrency(yield* OxlintConcurrency);
40738
+ const lintPhaseTimeoutMs = yield* LintPhaseTimeoutMs;
40739
+ const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
40740
+ const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
39906
40741
  const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
40742
+ const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
40743
+ const deadCodeOverlapMode = yield* DeadCodeOverlap;
40744
+ const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
40745
+ const deadCodeParseConcurrency = shouldOverlapDeadCode ? Math.max(1, Math.floor(scanConcurrency * DEAD_CODE_OVERLAP_PARSE_SHARE)) : void 0;
40746
+ const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
40747
+ const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
40748
+ rootDirectory: scanDirectory,
40749
+ userConfig: resolvedConfig.config,
40750
+ parseConcurrency: deadCodeParseConcurrency,
40751
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
40752
+ }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
40753
+ yield* set(deadCodeFailure, {
40754
+ didFail: true,
40755
+ reason: error.message
40756
+ });
40757
+ return empty$4;
40758
+ })))))).pipe(timeoutOption(deadCodeTimeout.phaseTimeoutMs), flatMap$2(match$3({
40759
+ onNone: () => set(deadCodeFailure, {
40760
+ didFail: true,
40761
+ reason: `Dead-code analysis exceeded ${Math.round(deadCodeTimeout.phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
40762
+ }).pipe(as([])),
40763
+ onSome: succeed$2
40764
+ })));
40765
+ const overlapDeadCodeTimeout = resolveDeadCodeTimeout({
40766
+ sourceFileCount: project.sourceFileCount,
40767
+ deadCodeConcurrency: deadCodeParseConcurrency ?? scanConcurrency,
40768
+ fullConcurrency: scanConcurrency
40769
+ });
40770
+ const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
40771
+ workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
40772
+ phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs)
40773
+ })) : null;
39907
40774
  const scanProgress = yield* progressService.start("Scanning...");
39908
40775
  const scanStartTime = Date.now();
39909
40776
  let lastReportedTotalFileCount = 0;
39910
- const lintCollected = yield* runCollect(applyPerElementPipeline(linterService.run({
40777
+ let lintCacheHitFileCount = null;
40778
+ let lintCacheTotalFileCount = null;
40779
+ const baseLintStream = linterService.run({
39911
40780
  rootDirectory: scanDirectory,
39912
40781
  project,
39913
40782
  includePaths: lintIncludePaths ?? void 0,
@@ -39921,6 +40790,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
39921
40790
  onFileProgress: (scannedFileCount, totalFileCount) => {
39922
40791
  lastReportedTotalFileCount = totalFileCount;
39923
40792
  runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})${workerCountSuffix}...`));
40793
+ },
40794
+ onCacheStats: (cacheHitFileCount, totalConsideredFileCount) => {
40795
+ lintCacheHitFileCount = cacheHitFileCount;
40796
+ lintCacheTotalFileCount = totalConsideredFileCount;
39924
40797
  }
39925
40798
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
39926
40799
  yield* set(lintFailure, {
@@ -39930,36 +40803,54 @@ const runInspect = (input, hooks = {}) => gen(function* () {
39930
40803
  reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
39931
40804
  });
39932
40805
  return empty$4;
39933
- }))))));
40806
+ }))));
40807
+ const lintCollected = yield* runCollect(applyPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
40808
+ onNone: () => set(lintFailure, {
40809
+ didFail: true,
40810
+ reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
40811
+ reasonTag: "OxlintBatchExceeded",
40812
+ reasonKind: null
40813
+ }).pipe(as([])),
40814
+ onSome: succeed$2
40815
+ })));
39934
40816
  const lintFailureState = yield* get$2(lintFailure);
39935
40817
  yield* afterLint(lintFailureState.didFail);
39936
40818
  if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
39937
40819
  const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
39938
40820
  const scannedFilesLabel = `${totalFileCount} ${totalFileCount === 1 ? "file" : "files"}`;
39939
- const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
39940
- const deadCodeCollected = lintFailureState.didFail || !shouldRunDeadCode ? [] : yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`).pipe(andThen(runCollect(applyPerElementPipeline(deadCodeService.run({
39941
- rootDirectory: scanDirectory,
39942
- userConfig: resolvedConfig.config
39943
- }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
39944
- yield* set(deadCodeFailure, {
39945
- didFail: true,
39946
- reason: error.message
40821
+ let deadCodeCollected = [];
40822
+ if (lintFailureState.didFail) {
40823
+ if (deadCodeFiber !== null) yield* interrupt(deadCodeFiber);
40824
+ } else if (shouldRunDeadCode) {
40825
+ yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
40826
+ const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
40827
+ sourceFileCount: totalFileCount,
40828
+ deadCodeConcurrency: scanConcurrency,
40829
+ fullConcurrency: scanConcurrency
39947
40830
  });
39948
- return empty$4;
39949
- }))))))));
39950
- const deadCodeFailureState = yield* get$2(deadCodeFailure);
40831
+ deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
40832
+ workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
40833
+ phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs)
40834
+ });
40835
+ }
40836
+ const deadCodeFailureState = lintFailureState.didFail ? {
40837
+ didFail: false,
40838
+ reason: null
40839
+ } : yield* get$2(deadCodeFailure);
39951
40840
  const scanElapsedMilliseconds = Date.now() - scanStartTime;
39952
- const scanElapsedSeconds = (scanElapsedMilliseconds / 1e3).toFixed(1);
40841
+ const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
39953
40842
  if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
39954
40843
  else if (input.suppressScanSummary) yield* scanProgress.stop();
39955
40844
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
40845
+ const supplyChainResult = yield* join(supplyChainFiber);
40846
+ const supplyChainCollected = supplyChainResult.diagnostics;
39956
40847
  yield* reporterService.finalize;
39957
- const finalDiagnostics = [
40848
+ const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
39958
40849
  ...envCollected,
39959
40850
  ...supplyChainCollected,
39960
40851
  ...lintCollected,
39961
40852
  ...deadCodeCollected
39962
- ];
40853
+ ]));
39963
40854
  const githubViewerPermission = yield* join(githubViewerPermissionFiber);
39964
40855
  const scoreMetadata = {
39965
40856
  ...repo !== null ? { repo } : {},
@@ -39995,9 +40886,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
39995
40886
  lintPartialFailures,
39996
40887
  didDeadCodeFail: deadCodeFailureState.didFail,
39997
40888
  deadCodeFailureReason: deadCodeFailureState.reason,
40889
+ deadCodeOverlapped: shouldOverlapDeadCode,
39998
40890
  scannedFileCount: totalFileCount,
39999
40891
  scannedFilePaths,
40000
- scanElapsedMilliseconds
40892
+ scanElapsedMilliseconds,
40893
+ scanConcurrency,
40894
+ supplyChainOverlapTimedOut: supplyChainResult.timedOut,
40895
+ lintCacheHitFileCount,
40896
+ lintCacheTotalFileCount
40001
40897
  };
40002
40898
  }).pipe(withSpan("runInspect", { attributes: {
40003
40899
  "inspect.directory": input.directory,
@@ -40005,7 +40901,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40005
40901
  "inspect.runDeadCode": input.runDeadCode,
40006
40902
  "inspect.isCi": input.isCi,
40007
40903
  "inspect.scoreSurface": input.scoreSurface ?? "score"
40008
- } }));
40904
+ } }), (scanProgram) => flatMap$2(ScanDeadlineMs, (scanDeadlineMs) => scanProgram.pipe(timeout(scanDeadlineMs), catchTag$1("TimeoutError", () => new ReactDoctorError({ reason: new ScanDeadlineExceeded({ detail: `${scanDeadlineMs / MILLISECONDS_PER_SECOND}s elapsed` }) })))));
40009
40905
  const parseNodeVersion = (versionString) => {
40010
40906
  const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
40011
40907
  return {
@@ -40163,7 +41059,7 @@ const materializeSourceTree = (input) => gen(function* () {
40163
41059
  static layerNode = effect(StagedFiles, gen(function* () {
40164
41060
  const git = yield* Git;
40165
41061
  return StagedFiles.of({
40166
- discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile))),
41062
+ discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile)), withSpan("StagedFiles.discoverSourceFiles")),
40167
41063
  materialize: ({ directory, stagedFiles, tempDirectory }) => materializeSourceTree({
40168
41064
  directory,
40169
41065
  files: stagedFiles,
@@ -40173,7 +41069,7 @@ const materializeSourceTree = (input) => gen(function* () {
40173
41069
  tempDirectory: tree.tempDirectory,
40174
41070
  stagedFiles: tree.materializedFiles,
40175
41071
  cleanup: tree.cleanup
40176
- })))
41072
+ })), withSpan("StagedFiles.materialize"))
40177
41073
  });
40178
41074
  }));
40179
41075
  /**
@@ -40305,6 +41201,7 @@ const buildJsonReport = (input) => {
40305
41201
  score: result.score,
40306
41202
  skippedChecks: result.skippedChecks,
40307
41203
  ...result.skippedCheckReasons ? { skippedCheckReasons: result.skippedCheckReasons } : {},
41204
+ ...typeof result.scannedFileCount === "number" ? { scannedFileCount: result.scannedFileCount } : {},
40308
41205
  elapsedMilliseconds: result.elapsedMilliseconds
40309
41206
  }));
40310
41207
  const flattenedDiagnostics = projects.flatMap((entry) => entry.diagnostics);
@@ -40548,6 +41445,7 @@ const clearCaches = () => {
40548
41445
  clearIgnorePatternsCache();
40549
41446
  clearPackageRoleCache();
40550
41447
  clearAutoSuppressionCaches();
41448
+ clearMinifiedFileCache();
40551
41449
  };
40552
41450
  const toJsonReport = (result, options) => buildJsonReport({
40553
41451
  version: options.version,
@@ -40571,4 +41469,4 @@ const toJsonReport = (result, options) => buildJsonReport({
40571
41469
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
40572
41470
 
40573
41471
  //# sourceMappingURL=index.js.map
40574
- //# debugId=a4394ddc-4e6c-5a18-aeeb-d60322b1c0dd
41472
+ //# debugId=33508ee6-c977-5b5f-8585-9939928ce74d