react-doctor 0.9.3-dev.b02bc69 → 0.9.3-dev.ba04952

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/cli.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]="e4075e6f-4128-5af6-bc97-656272450282")}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]="80fc688d-8e21-59d6-abcd-27ce2265a7d3")}catch(e){}}();
3
3
  import { createRequire } from "node:module";
4
4
  import * as NodeChildProcess from "node:child_process";
5
5
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
@@ -6666,7 +6666,7 @@ const flatMap$5 = /* @__PURE__ */ dual(2, (self, f) => isNone(self) ? none() : f
6666
6666
  * @category filtering
6667
6667
  * @since 2.0.0
6668
6668
  */
6669
- const filter = /* @__PURE__ */ dual(2, (self, predicate) => isNone(self) ? none() : predicate(self.value) ? some(self.value) : none());
6669
+ const filter$1 = /* @__PURE__ */ dual(2, (self, predicate) => isNone(self) ? none() : predicate(self.value) ? some(self.value) : none());
6670
6670
  //#endregion
6671
6671
  //#region ../../node_modules/.pnpm/effect@4.0.0-beta.70/node_modules/effect/dist/Result.js
6672
6672
  /**
@@ -18657,7 +18657,7 @@ function transformOptional(f) {
18657
18657
  */
18658
18658
  function withDefault$1(defaultValue) {
18659
18659
  return new Getter((o) => {
18660
- const filtered = filter(o, isNotUndefined);
18660
+ const filtered = filter$1(o, isNotUndefined);
18661
18661
  return isSome(filtered) ? succeed$2(filtered) : mapEager(defaultValue, some);
18662
18662
  });
18663
18663
  }
@@ -22505,6 +22505,10 @@ var JsonReportProjectEntryV3 = class extends Class("JsonReportProjectEntryV3")({
22505
22505
  scannedFileCount: optional(Number$1),
22506
22506
  elapsedMilliseconds: Number$1
22507
22507
  }) {};
22508
+ var JsonReportSkippedProject = class extends Class("JsonReportSkippedProject")({
22509
+ directory: String$1,
22510
+ reason: Literal("max-duration")
22511
+ }) {};
22508
22512
  /** Original full, diff, and staged report contract. */
22509
22513
  var JsonReportV1 = class extends Class("JsonReportV1")({
22510
22514
  schemaVersion: Literal(1),
@@ -22533,6 +22537,7 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
22533
22537
  reactDetected: optional(Boolean$1),
22534
22538
  diff: NullOr(JsonReportDiffInfo),
22535
22539
  projects: ArraySchema(JsonReportProjectEntry),
22540
+ skippedProjects: optional(ArraySchema(JsonReportSkippedProject)),
22536
22541
  diagnostics: ArraySchema(Diagnostic),
22537
22542
  summary: JsonReportSummary,
22538
22543
  elapsedMilliseconds: Number$1,
@@ -22561,6 +22566,7 @@ var JsonReportV2 = class extends Class("JsonReportV2")({
22561
22566
  diff: NullOr(JsonReportDiffInfo),
22562
22567
  baseline: JsonReportBaseline,
22563
22568
  projects: ArraySchema(JsonReportProjectEntry),
22569
+ skippedProjects: optional(ArraySchema(JsonReportSkippedProject)),
22564
22570
  diagnostics: ArraySchema(Diagnostic),
22565
22571
  summary: JsonReportSummary,
22566
22572
  elapsedMilliseconds: Number$1,
@@ -22577,6 +22583,7 @@ var JsonReportV3 = class extends Class("JsonReportV3")({
22577
22583
  diff: NullOr(JsonReportDiffInfo),
22578
22584
  baseline: optional(JsonReportBaseline),
22579
22585
  projects: ArraySchema(JsonReportProjectEntryV3),
22586
+ skippedProjects: optional(ArraySchema(JsonReportSkippedProject)),
22580
22587
  diagnostics: ArraySchema(JsonReportDiagnosticV3),
22581
22588
  summary: JsonReportSummary,
22582
22589
  elapsedMilliseconds: Number$1,
@@ -56559,6 +56566,47 @@ const flattenArray = (self) => transformPull$1(self, (pull) => {
56559
56566
  }));
56560
56567
  });
56561
56568
  /**
56569
+ * Filters arrays of elements emitted by a channel, applying the filter
56570
+ * to each element within the arrays and only emitting non-empty filtered arrays.
56571
+ *
56572
+ * **Example** (Filtering array output)
56573
+ *
56574
+ * ```ts
56575
+ * import { Array, Channel } from "effect"
56576
+ *
56577
+ * const nonEmptyArrayPredicate = Array.isReadonlyArrayNonEmpty
56578
+ *
56579
+ * // Create a channel that outputs arrays of mixed data
56580
+ * const arrayChannel = Channel.fromIterable([
56581
+ * Array.make(1, 2, 3, 4, 5),
56582
+ * Array.make(6, 7, 8, 9, 10),
56583
+ * Array.make(11, 12, 13, 14, 15)
56584
+ * ]).pipe(Channel.filter(nonEmptyArrayPredicate))
56585
+ *
56586
+ * // Filter arrays to keep only even numbers
56587
+ * const evenArraysChannel = Channel.filterArray(arrayChannel, (n) => n % 2 === 0)
56588
+ * // Outputs: [2, 4], [6, 8, 10], [12, 14]
56589
+ * // Note: Only non-empty filtered arrays are emitted
56590
+ *
56591
+ * // Arrays that would become empty after filtering are discarded entirely
56592
+ * const oddChannel = Channel.fromIterable([
56593
+ * Array.make(1, 3, 5),
56594
+ * Array.make(2, 4),
56595
+ * Array.make(7, 9)
56596
+ * ]).pipe(Channel.filter(nonEmptyArrayPredicate))
56597
+ * const filteredOddChannel = Channel.filterArray(oddChannel, (n) => n % 2 === 0)
56598
+ * // Outputs: [2, 4] (the arrays [1,3,5] and [7,9] are discarded)
56599
+ * ```
56600
+ *
56601
+ * @category filtering
56602
+ * @since 4.0.0
56603
+ */
56604
+ const filterArray = /* @__PURE__ */ dual(2, (self, predicate) => transformPull$1(self, (pull) => succeed$2(flatMap$2(pull, function loop(arr) {
56605
+ const passes = [];
56606
+ for (let i = 0; i < arr.length; i++) if (predicate(arr[i])) passes.push(arr[i]);
56607
+ return isReadonlyArrayNonEmpty(passes) ? succeed$2(passes) : flatMap$2(pull, loop);
56608
+ }))));
56609
+ /**
56562
56610
  * Filters and maps each element inside emitted non-empty arrays using a
56563
56611
  * `Filter`.
56564
56612
  *
@@ -58196,6 +58244,30 @@ const concat = /* @__PURE__ */ dual(2, (self, that) => flatten(fromArray([self,
58196
58244
  */
58197
58245
  const merge$1 = /* @__PURE__ */ dual((args) => isStream(args[0]) && isStream(args[1]), (self, that, options) => fromChannel(merge$2(toChannel(self), toChannel(that), options)));
58198
58246
  /**
58247
+ * Filters a stream to the elements that satisfy a predicate.
58248
+ *
58249
+ * **Example** (Filtering stream values)
58250
+ *
58251
+ * ```ts
58252
+ * import { Console, Effect, Stream } from "effect"
58253
+ *
58254
+ * const program = Effect.gen(function*() {
58255
+ * const stream = Stream.make(1, 2, 3, 4).pipe(
58256
+ * Stream.filter((n) => n % 2 === 0)
58257
+ * )
58258
+ * const values = yield* Stream.runCollect(stream)
58259
+ * yield* Console.log(values)
58260
+ * })
58261
+ *
58262
+ * Effect.runPromise(program)
58263
+ * // Output: [ 2, 4 ]
58264
+ * ```
58265
+ *
58266
+ * @category filtering
58267
+ * @since 2.0.0
58268
+ */
58269
+ const filter = /* @__PURE__ */ dual(2, (self, predicate) => fromChannel(filterArray(toChannel(self), predicate)));
58270
+ /**
58199
58271
  * Filters and maps stream elements in one pass using a `Filter`.
58200
58272
  *
58201
58273
  * @category filtering
@@ -65046,6 +65118,7 @@ const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.m?js$/i;
65046
65118
  const GENERATED_SOURCE_DIRECTORY_PATTERN = /(?:^|\/)__generated__\//;
65047
65119
  const MINIFIED_SNIFF_BYTES = 65536;
65048
65120
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
65121
+ const GIT_CHECK_ATTR_MAX_BUFFER_BYTES = GIT_LS_FILES_MAX_BUFFER_BYTES;
65049
65122
  const SCANNED_DOT_DIRECTORIES = new Set([".dumi", ".storybook"]);
65050
65123
  const IGNORED_DIRECTORIES = new Set([
65051
65124
  ".angular",
@@ -65365,13 +65438,9 @@ const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
65365
65438
  const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
65366
65439
  const ABORT_EXIT_CODES = new Set([134, 3221226505]);
65367
65440
  const OXLINT_OOM_RESCUE_BUDGET_MS = 6e4;
65368
- const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
65369
- const LINT_PHASE_TIMEOUT_MS = 3e5;
65370
- const SCAN_TOTAL_DEADLINE_MS = 9e5;
65371
65441
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
65372
65442
  const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
65373
65443
  const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
65374
- const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
65375
65444
  const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
65376
65445
  const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
65377
65446
  const REACT_SERVER_DOM_PACKAGES = [
@@ -65467,6 +65536,66 @@ const isPlainObject = (value) => {
65467
65536
  const prototype = Object.getPrototypeOf(value);
65468
65537
  return prototype === null || prototype === Object.prototype;
65469
65538
  };
65539
+ const LINGUIST_ATTRIBUTES = ["linguist-generated", "linguist-vendored"];
65540
+ const DISABLED_ATTRIBUTE_VALUES = new Set([
65541
+ "unspecified",
65542
+ "unset",
65543
+ "false",
65544
+ "0",
65545
+ "off",
65546
+ "no"
65547
+ ]);
65548
+ const buildInput = (relativePaths) => `${relativePaths.join("\0")}\0`;
65549
+ const parseIgnoredPaths = (output) => {
65550
+ const ignoredPaths = /* @__PURE__ */ new Set();
65551
+ const fields = output.split("\0");
65552
+ for (let fieldIndex = 0; fieldIndex + 2 < fields.length; fieldIndex += 3) {
65553
+ const filePath = fields[fieldIndex];
65554
+ const value = fields[fieldIndex + 2]?.toLowerCase();
65555
+ if (filePath && value && !DISABLED_ATTRIBUTE_VALUES.has(value)) ignoredPaths.add(filePath);
65556
+ }
65557
+ return ignoredPaths;
65558
+ };
65559
+ const collectGitLinguistIgnoredPaths = (rootDirectory, relativePaths) => {
65560
+ if (relativePaths.length === 0) return /* @__PURE__ */ new Set();
65561
+ const result = spawnSync("git", [
65562
+ "check-attr",
65563
+ "-z",
65564
+ "--stdin",
65565
+ ...LINGUIST_ATTRIBUTES
65566
+ ], {
65567
+ cwd: rootDirectory,
65568
+ encoding: "utf-8",
65569
+ input: buildInput(relativePaths),
65570
+ maxBuffer: GIT_CHECK_ATTR_MAX_BUFFER_BYTES
65571
+ });
65572
+ if (result.error || result.status !== 0) return /* @__PURE__ */ new Set();
65573
+ return parseIgnoredPaths(result.stdout);
65574
+ };
65575
+ const collectGitLinguistIgnoredPathsCooperative = (rootDirectory, relativePaths, signal) => {
65576
+ if (relativePaths.length === 0) return Promise.resolve(/* @__PURE__ */ new Set());
65577
+ return new Promise((resolve, reject) => {
65578
+ signal?.throwIfAborted();
65579
+ execFile("git", [
65580
+ "check-attr",
65581
+ "-z",
65582
+ "--stdin",
65583
+ ...LINGUIST_ATTRIBUTES
65584
+ ], {
65585
+ cwd: rootDirectory,
65586
+ encoding: "utf-8",
65587
+ killSignal: "SIGKILL",
65588
+ maxBuffer: GIT_CHECK_ATTR_MAX_BUFFER_BYTES,
65589
+ signal
65590
+ }, (error, stdout) => {
65591
+ if (signal?.aborted) {
65592
+ reject(signal.reason);
65593
+ return;
65594
+ }
65595
+ resolve(error ? /* @__PURE__ */ new Set() : parseIgnoredPaths(stdout));
65596
+ }).stdin?.end(buildInput(relativePaths));
65597
+ });
65598
+ };
65470
65599
  const lastSourceMappingUrl = (fileContent) => {
65471
65600
  const lastReference = [...fileContent.matchAll(/\/\/# sourceMappingURL=(\S+)/g)].at(-1);
65472
65601
  return lastReference ? lastReference[1] : null;
@@ -65568,6 +65697,9 @@ function* walkSourceTreeFiles(rootDirectory) {
65568
65697
  }
65569
65698
  }
65570
65699
  }
65700
+ const yieldToEventLoop = () => new Promise((resolve) => {
65701
+ setImmediate(resolve);
65702
+ });
65571
65703
  const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
65572
65704
  const entries = [];
65573
65705
  for (const relativePath of relativePaths) {
@@ -65581,23 +65713,55 @@ const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
65581
65713
  }
65582
65714
  return entries;
65583
65715
  };
65584
- const gitLsFiles = (rootDirectory, flags) => {
65716
+ const collectSizedSourceFilesCooperative = async (rootDirectory, relativePaths, signal) => {
65717
+ const entries = [];
65718
+ let sliceStartedAt = performance.now();
65719
+ for (const relativePath of relativePaths) {
65720
+ signal?.throwIfAborted();
65721
+ const absolutePath = Path.resolve(rootDirectory, relativePath);
65722
+ const sizeBytes = statSourceFileSize(absolutePath);
65723
+ if (!isLargeMinifiedFile(absolutePath, sizeBytes)) entries.push({
65724
+ path: relativePath,
65725
+ sizeBytes: sizeBytes ?? 0
65726
+ });
65727
+ if (performance.now() - sliceStartedAt >= 12) {
65728
+ await yieldToEventLoop();
65729
+ sliceStartedAt = performance.now();
65730
+ }
65731
+ }
65732
+ return entries;
65733
+ };
65734
+ const parseGitSourceFilePaths = (output) => {
65735
+ const trackedPaths = /* @__PURE__ */ new Set();
65736
+ const untrackedPaths = [];
65737
+ for (const entry of output.split("\0")) {
65738
+ if (entry.length === 0) continue;
65739
+ const trackedPath = /^[0-7]{6} [0-9a-f]+ [0-3]\t([\s\S]+)$/.exec(entry)?.[1];
65740
+ if (trackedPath === void 0) untrackedPaths.push(entry);
65741
+ else trackedPaths.add(trackedPath);
65742
+ }
65743
+ return {
65744
+ trackedPaths: [...trackedPaths],
65745
+ untrackedPaths
65746
+ };
65747
+ };
65748
+ const listGitSourceFilePaths = (rootDirectory) => {
65585
65749
  const result = spawnSync("git", [
65586
65750
  "ls-files",
65587
65751
  "-z",
65588
- ...flags
65752
+ "--stage",
65753
+ "--others",
65754
+ "--exclude-standard"
65589
65755
  ], {
65590
65756
  cwd: rootDirectory,
65591
65757
  encoding: "utf-8",
65592
65758
  maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
65593
65759
  });
65594
65760
  if (result.error || result.status !== 0) return null;
65595
- return result.stdout.split("\0").filter((filePath) => filePath.length > 0);
65761
+ return parseGitSourceFilePaths(result.stdout);
65596
65762
  };
65597
- const listSourceFilesViaGit = (rootDirectory) => {
65598
- const trackedPaths = gitLsFiles(rootDirectory, ["--cached"]);
65599
- const untrackedPaths = gitLsFiles(rootDirectory, ["--others", "--exclude-standard"]);
65600
- if (trackedPaths === null || untrackedPaths === null) return null;
65763
+ const collectGitCandidateSourceFilePaths = (rootDirectory, paths) => {
65764
+ const { trackedPaths, untrackedPaths } = paths;
65601
65765
  const emitDuplicateJsPaths = collectTypeScriptEmitDuplicateJsPaths({
65602
65766
  trackedPaths: new Set(trackedPaths),
65603
65767
  untrackedPaths,
@@ -65605,12 +65769,65 @@ const listSourceFilesViaGit = (rootDirectory) => {
65605
65769
  });
65606
65770
  return [...trackedPaths, ...untrackedPaths].filter((filePath) => isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath) && !emitDuplicateJsPaths.has(filePath));
65607
65771
  };
65772
+ const filterGitSourceFilePaths = (rootDirectory, paths) => {
65773
+ const candidatePaths = collectGitCandidateSourceFilePaths(rootDirectory, paths);
65774
+ const linguistIgnoredPaths = collectGitLinguistIgnoredPaths(rootDirectory, candidatePaths);
65775
+ return candidatePaths.filter((filePath) => !linguistIgnoredPaths.has(filePath));
65776
+ };
65777
+ const listSourceFilesViaGit = (rootDirectory) => {
65778
+ const paths = listGitSourceFilePaths(rootDirectory);
65779
+ return paths === null ? null : filterGitSourceFilePaths(rootDirectory, paths);
65780
+ };
65781
+ const listSourceFilesViaGitCooperative = async (rootDirectory, signal) => {
65782
+ const paths = await new Promise((resolve, reject) => {
65783
+ signal?.throwIfAborted();
65784
+ execFile("git", [
65785
+ "ls-files",
65786
+ "-z",
65787
+ "--stage",
65788
+ "--others",
65789
+ "--exclude-standard"
65790
+ ], {
65791
+ cwd: rootDirectory,
65792
+ encoding: "utf-8",
65793
+ killSignal: "SIGKILL",
65794
+ maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES,
65795
+ signal
65796
+ }, (error, stdout) => {
65797
+ if (signal?.aborted) {
65798
+ reject(signal.reason);
65799
+ return;
65800
+ }
65801
+ resolve(error ? null : parseGitSourceFilePaths(stdout));
65802
+ });
65803
+ });
65804
+ if (paths === null) return null;
65805
+ const candidatePaths = collectGitCandidateSourceFilePaths(rootDirectory, paths);
65806
+ const linguistIgnoredPaths = await collectGitLinguistIgnoredPathsCooperative(rootDirectory, candidatePaths, signal);
65807
+ return candidatePaths.filter((filePath) => !linguistIgnoredPaths.has(filePath));
65808
+ };
65608
65809
  const listSourceFilesViaFilesystem = (rootDirectory) => {
65609
65810
  const filePaths = [];
65610
65811
  for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) if (isLintableSourceFile(name)) filePaths.push(Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
65611
65812
  return filePaths;
65612
65813
  };
65613
- const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
65814
+ const listSourceFilesViaFilesystemCooperative = async (rootDirectory, signal) => {
65815
+ const filePaths = [];
65816
+ let sliceStartedAt = performance.now();
65817
+ for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
65818
+ signal?.throwIfAborted();
65819
+ if (isLintableSourceFile(name)) filePaths.push(Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
65820
+ if (performance.now() - sliceStartedAt >= 12) {
65821
+ await yieldToEventLoop();
65822
+ sliceStartedAt = performance.now();
65823
+ }
65824
+ }
65825
+ return filePaths;
65826
+ };
65827
+ const listSourceFilePaths = (rootDirectory) => (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort();
65828
+ const listSourceFilePathsCooperative = async (rootDirectory, signal) => (await listSourceFilesViaGitCooperative(rootDirectory, signal) ?? await listSourceFilesViaFilesystemCooperative(rootDirectory, signal)).sort();
65829
+ const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilePaths(rootDirectory));
65830
+ const listSourceFilesCooperative = async (rootDirectory, signal) => (await collectSizedSourceFilesCooperative(rootDirectory, await listSourceFilePathsCooperative(rootDirectory, signal), signal)).map((entry) => entry.path);
65614
65831
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
65615
65832
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
65616
65833
  const UTF8_BOM_CHARACTER = "";
@@ -67933,7 +68150,16 @@ const getBrowserslistProtectionRequirement = (directory) => {
67933
68150
  if (!queries) return void 0;
67934
68151
  let requirement;
67935
68152
  let hasUnsupportedTarget = false;
67936
- for (const browserTarget of (0, import_browserslist.default)(queries)) {
68153
+ const previousIgnoreOldData = process.env.BROWSERSLIST_IGNORE_OLD_DATA;
68154
+ let browserTargets = [];
68155
+ try {
68156
+ process.env.BROWSERSLIST_IGNORE_OLD_DATA = "true";
68157
+ browserTargets = (0, import_browserslist.default)(queries);
68158
+ } finally {
68159
+ if (previousIgnoreOldData === void 0) delete process.env.BROWSERSLIST_IGNORE_OLD_DATA;
68160
+ else process.env.BROWSERSLIST_IGNORE_OLD_DATA = previousIgnoreOldData;
68161
+ }
68162
+ for (const browserTarget of browserTargets) {
67937
68163
  const browserRequirement = getBrowserProtectionRequirement(browserTarget);
67938
68164
  if (browserRequirement === "unsupported") hasUnsupportedTarget = true;
67939
68165
  if (browserRequirement === "noreferrer") requirement = "noreferrer";
@@ -68039,8 +68265,8 @@ const cachedProjectInfos = /* @__PURE__ */ new Map();
68039
68265
  * framework-agnostic rules. Throws only when the directory has nothing
68040
68266
  * to scan (no enclosing project and no source files of its own).
68041
68267
  */
68042
- const discoverProjectWithoutPackageJson = (directory) => {
68043
- const sourceFileCount = countSourceFiles(directory);
68268
+ const discoverProjectWithoutPackageJson = (directory, options) => {
68269
+ const sourceFileCount = options.sourceFileCount ?? countSourceFiles(directory);
68044
68270
  const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
68045
68271
  const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
68046
68272
  const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
@@ -68107,12 +68333,15 @@ const discoverProjectWithoutPackageJson = (directory) => {
68107
68333
  sourceFileCount
68108
68334
  };
68109
68335
  };
68110
- const discoverProject = (directory) => {
68336
+ const discoverProject = (directory, options = {}) => {
68111
68337
  const cached = cachedProjectInfos.get(directory);
68112
- if (cached !== void 0) return cached;
68338
+ if (cached !== void 0) return options.sourceFileCount === void 0 ? cached : {
68339
+ ...cached,
68340
+ sourceFileCount: options.sourceFileCount
68341
+ };
68113
68342
  const packageJsonPath = Path.join(directory, "package.json");
68114
68343
  if (!isFile(packageJsonPath)) {
68115
- const synthesized = discoverProjectWithoutPackageJson(directory);
68344
+ const synthesized = discoverProjectWithoutPackageJson(directory, options);
68116
68345
  cachedProjectInfos.set(directory, synthesized);
68117
68346
  return synthesized;
68118
68347
  }
@@ -68175,7 +68404,7 @@ const discoverProject = (directory) => {
68175
68404
  const zodVersion = zod.version;
68176
68405
  const projectName = packageJson.name ?? Path.basename(directory);
68177
68406
  const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
68178
- const sourceFileCount = countSourceFiles(directory);
68407
+ const sourceFileCount = options.sourceFileCount ?? countSourceFiles(directory);
68179
68408
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || workspaceFacts.hasReactNativeAwarePackage;
68180
68409
  const expoVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
68181
68410
  rootDirectory: directory,
@@ -69564,7 +69793,8 @@ const readSystemFacts$1 = () => ({
69564
69793
  /**
69565
69794
  * Auto lint-worker count: the smaller of the (cgroup-CPU-aware) core count and
69566
69795
  * the number of `PER_WORKER_MEM_BUDGET_BYTES` workers that fit in available
69567
- * memory, then clamped to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
69796
+ * memory, and the measured automatic parallel-efficiency ceiling, then clamped
69797
+ * to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
69568
69798
  *
69569
69799
  * `os.availableParallelism()` already respects cgroup CPU quotas, so the core
69570
69800
  * term needs no help. Available memory is `os.totalmem()` floored by the cgroup
@@ -69580,7 +69810,7 @@ const readSystemFacts$1 = () => ({
69580
69810
  const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
69581
69811
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
69582
69812
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
69583
- return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
69813
+ return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers, 10));
69584
69814
  };
69585
69815
  const resolveLintBatchOrdering = () => {
69586
69816
  return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "arrival" ? "arrival" : "cost";
@@ -69593,24 +69823,22 @@ const resolveLintBatchOrdering = () => {
69593
69823
  */
69594
69824
  var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS", OXLINT_SPAWN_TIMEOUT_MS) }) {};
69595
69825
  /**
69596
- * Effect-side cap on the lint phase. The env var lets CI / eval runners
69597
- * raise the phase budget for slow large repos without recompiling.
69826
+ * Optional Effect-side cap on the lint phase. Unbounded unless explicitly
69827
+ * configured through the env var or a test layer.
69598
69828
  * Tests override via `Layer.succeed(LintPhaseTimeoutMs, ...)`.
69599
69829
  */
69600
- var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", LINT_PHASE_TIMEOUT_MS) }) {};
69830
+ var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", null) }) {};
69601
69831
  /**
69602
- * Effect-side cap on the dead-code phase, sitting above the in-worker
69603
- * timeout as a runtime-independent backstop. The env var raises it for
69604
- * type-heavy projects; tests override via
69832
+ * Optional Effect-side cap on the dead-code phase. Unbounded unless explicitly
69833
+ * configured through the env var or a test layer. Tests override via
69605
69834
  * `Layer.succeed(DeadCodePhaseTimeoutMs, ...)`.
69606
69835
  */
69607
- var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", DEAD_CODE_PHASE_TIMEOUT_MS) }) {};
69836
+ var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", null) }) {};
69608
69837
  /**
69609
- * Overall scan deadline backstop, bounding everything the per-phase
69610
- * timeouts don't (wedged git / IO). The env var raises it for very
69611
- * large repos; tests override via `Layer.succeed(ScanDeadlineMs, ...)`.
69838
+ * Optional overall scan deadline. Unbounded unless explicitly configured
69839
+ * through the env var or a test layer.
69612
69840
  */
69613
- var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", SCAN_TOTAL_DEADLINE_MS) }) {};
69841
+ var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", null) }) {};
69614
69842
  /**
69615
69843
  * Wall-clock budget for the supply-chain check when it runs on a background
69616
69844
  * fiber overlapping the lint pass. Reads from the env var on startup so the
@@ -69683,14 +69911,14 @@ var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", {
69683
69911
  /**
69684
69912
  * How the full-scan lint pass plans its file batches. `"cost"` (the default)
69685
69913
  * builds size-balanced LPT batches (`planLintBatches`): the same mandatory
69686
- * batch count as greedy chunking (`ceil(files / 100)`), but every batch gets
69687
- * an even share of files AND bytes, so no 100-file chunk is a straggler while
69914
+ * batch count as greedy chunking, but every batch gets an even share of files
69915
+ * AND bytes, so no full chunk is a straggler while
69688
69916
  * the remainder-batch worker idles — and the heavy files are SPREAD across
69689
69917
  * batches, the precondition the old sort-desc-then-chunk-100 `cost` mode
69690
69918
  * lacked (it packed the heaviest files into one wave-1 straggler batch,
69691
69919
  * measurably regressing size-skewed repos, which is why it never earned the
69692
69920
  * default). `"arrival"` (`REACT_DOCTOR_LINT_BATCH_ORDERING=arrival`) is the
69693
- * rollback hatch to plain greedy 100-file chunking in discovery order. Tests
69921
+ * rollback hatch to plain greedy fixed-size chunking in discovery order. Tests
69694
69922
  * override via `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans
69695
69923
  * never reach this — they pass user-scoped `includePaths` that skip discovery
69696
69924
  * and stay in arrival order; only the full-scan branch reads it.
@@ -70256,6 +70484,10 @@ const dedupeRelatedDiagnostics = (diagnostics) => {
70256
70484
  }
70257
70485
  return uniqueDiagnostics;
70258
70486
  };
70487
+ const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
70488
+ const relativePath = Path.relative(parentAbsolutePath, childAbsolutePath);
70489
+ return Boolean(relativePath) && !relativePath.startsWith("..") && !Path.isAbsolute(relativePath);
70490
+ };
70259
70491
  const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
70260
70492
  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));
70261
70493
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
@@ -71832,7 +72064,7 @@ const readScannedFile = (candidate) => {
71832
72064
  return null;
71833
72065
  }
71834
72066
  };
71835
- function* collectSecurityScanFiles(rootDirectory) {
72067
+ function* collectSecurityScanFiles(rootDirectory, excludedDirectories = /* @__PURE__ */ new Set()) {
71836
72068
  const priorityCandidates = [];
71837
72069
  const artifactCandidates = [];
71838
72070
  const otherCandidates = [];
@@ -71847,7 +72079,7 @@ function* collectSecurityScanFiles(rootDirectory) {
71847
72079
  for (const entry of readDirectoryEntries(current.absolutePath)) {
71848
72080
  const absolutePath = Path.join(current.absolutePath, entry.name);
71849
72081
  if (entry.isDirectory()) {
71850
- if (!SKIPPED_DIRECTORY_NAMES.has(entry.name)) stack.push({
72082
+ if (!SKIPPED_DIRECTORY_NAMES.has(entry.name) && !excludedDirectories.has(Path.resolve(absolutePath))) stack.push({
71851
72083
  absolutePath,
71852
72084
  depth: current.depth + 1
71853
72085
  });
@@ -72028,10 +72260,8 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhe
72028
72260
  }
72029
72261
  return true;
72030
72262
  };
72263
+ const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs - Date.now(), 0);
72031
72264
  const shouldEnableRuleByDefaultStatus = (options) => options.defaultEnabled !== false || options.hasExplicitOverride === true || options.includeTagDefaults && options.hasIncludedTags;
72032
- const yieldToEventLoop = () => new Promise((resolve) => {
72033
- setImmediate(resolve);
72034
- });
72035
72265
  const createSecurityScanSession = (rootDirectory, options) => {
72036
72266
  const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
72037
72267
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
@@ -72084,19 +72314,35 @@ const createSecurityScanSession = (rootDirectory, options) => {
72084
72314
  };
72085
72315
  };
72086
72316
  const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
72317
+ const throwIfScanAborted = () => {
72318
+ options.signal?.throwIfAborted();
72319
+ };
72320
+ const didReachDeadline = () => {
72321
+ const hasDeadlineElapsed = options.deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(options.deadlineEpochMs) === 0;
72322
+ if (hasDeadlineElapsed) options.onDeadlineExceeded?.();
72323
+ return hasDeadlineElapsed;
72324
+ };
72325
+ throwIfScanAborted();
72087
72326
  const session = createSecurityScanSession(rootDirectory, options);
72088
72327
  if (session === null) return [];
72328
+ if (didReachDeadline()) return [];
72089
72329
  let sliceStartedAt = performance.now();
72090
- for (const file of collectSecurityScanFiles(rootDirectory)) {
72330
+ for (const file of collectSecurityScanFiles(rootDirectory, options.excludedDirectories)) {
72331
+ throwIfScanAborted();
72332
+ if (didReachDeadline()) return session.diagnostics;
72091
72333
  if (file === null) {
72092
72334
  if (performance.now() - sliceStartedAt >= 12) {
72093
72335
  await yieldToEventLoop();
72336
+ throwIfScanAborted();
72337
+ if (didReachDeadline()) return session.diagnostics;
72094
72338
  sliceStartedAt = performance.now();
72095
72339
  }
72096
72340
  continue;
72097
72341
  }
72098
72342
  for (const _ruleStep of session.scanFileByRule(file)) if (performance.now() - sliceStartedAt >= 12) {
72099
72343
  await yieldToEventLoop();
72344
+ throwIfScanAborted();
72345
+ if (didReachDeadline()) return session.diagnostics;
72100
72346
  sliceStartedAt = performance.now();
72101
72347
  }
72102
72348
  }
@@ -73026,7 +73272,6 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
73026
73272
  return true;
73027
73273
  };
73028
73274
  const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
73029
- const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs - Date.now(), 0);
73030
73275
  /**
73031
73276
  * Budget for the dead-code phase, scaled to the work. deslop's graph build is
73032
73277
  * CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
@@ -73034,25 +73279,28 @@ const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs
73034
73279
  * tipped over by any concurrent load — silently dropping every dead-code
73035
73280
  * finding. Scaling the budget with file count (and inversely with the core
73036
73281
  * share when overlapped) lets the pass complete, while the ceiling still
73037
- * reclaims a genuinely wedged worker. Returns the in-worker SIGKILL deadline
73038
- * and the Effect-side phase backstop that sits a margin above it.
73282
+ * reclaims a genuinely wedged worker.
73039
73283
  */
73040
73284
  const resolveDeadCodeTimeout = (input) => {
73041
73285
  const coreShareFactor = Math.max(1, input.fullConcurrency / Math.max(1, input.deadCodeConcurrency));
73042
- const workerTimeoutMs = Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor)));
73043
- return {
73044
- workerTimeoutMs,
73045
- phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
73046
- };
73286
+ return { workerTimeoutMs: Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor))) };
73047
73287
  };
73048
- const resolveLintIncludePaths = (rootDirectory, userConfig) => {
73288
+ const resolveLintIncludePaths = (rootDirectory, userConfig, sourceFiles) => {
73049
73289
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
73050
73290
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
73051
- return listSourceFiles(rootDirectory).filter((filePath) => {
73291
+ return (sourceFiles ?? listSourceFiles(rootDirectory)).filter((filePath) => {
73052
73292
  if (!isLintableSourceFile(filePath)) return false;
73053
73293
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
73054
73294
  });
73055
73295
  };
73296
+ const filterPathsOutsideDirectories = (input) => {
73297
+ const excludedPrefixes = input.excludedDirectories.map((excludedDirectory) => Path.resolve(excludedDirectory)).filter((excludedDirectory) => isPathInsideDirectory(excludedDirectory, input.rootDirectory)).map((excludedDirectory) => Path.relative(input.rootDirectory, excludedDirectory).replaceAll("\\", "/")).sort((left, right) => left.length - right.length).filter((candidatePrefix, index, prefixes) => !prefixes.slice(0, index).some((prefix) => candidatePrefix.startsWith(`${prefix}/`)));
73298
+ if (excludedPrefixes.length === 0) return [...input.relativePaths];
73299
+ return input.relativePaths.filter((relativePath) => {
73300
+ const normalizedRelativePath = relativePath.replaceAll("\\", "/").replace(/^\.\//, "");
73301
+ return !excludedPrefixes.some((excludedPrefix) => normalizedRelativePath === excludedPrefix || normalizedRelativePath.startsWith(`${excludedPrefix}/`));
73302
+ });
73303
+ };
73056
73304
  var Config = class Config extends Service()("react-doctor/Config") {
73057
73305
  static layerNode = effect(Config, gen(function* () {
73058
73306
  const cache = yield* make$3({
@@ -73114,6 +73362,7 @@ var Files = class Files extends Service()("react-doctor/Files") {
73114
73362
  static layerNode = succeed$3(Files, Files.of({
73115
73363
  readLines: (input) => sync(() => createNodeReadFileLinesSync(input.rootDirectory)(input.filePath)),
73116
73364
  listSourceFiles: (rootDirectory) => sync(() => listSourceFiles(rootDirectory)),
73365
+ listSourceFilesCooperative: (input) => promise(() => listSourceFilesCooperative(input.rootDirectory, input.signal)),
73117
73366
  isFile: (filePath) => sync(() => isFile(filePath)),
73118
73367
  isDirectory: (filePath) => sync(() => isDirectory(filePath))
73119
73368
  }));
@@ -73140,6 +73389,16 @@ var Files = class Files extends Service()("react-doctor/Files") {
73140
73389
  }
73141
73390
  return files;
73142
73391
  }),
73392
+ listSourceFilesCooperative: (input) => sync(() => {
73393
+ const rootDirectory = input.rootDirectory;
73394
+ const prefix = rootDirectory.endsWith("/") ? rootDirectory : `${rootDirectory}/`;
73395
+ const files = [];
73396
+ for (const absolute of tree.keys()) {
73397
+ if (!absolute.startsWith(prefix)) continue;
73398
+ files.push(absolute.slice(prefix.length));
73399
+ }
73400
+ return files;
73401
+ }),
73143
73402
  isFile: (filePath) => sync(() => tree.has(filePath)),
73144
73403
  isDirectory: (filePath) => sync(() => {
73145
73404
  const prefix = filePath.endsWith("/") ? filePath : `${filePath}/`;
@@ -73715,7 +73974,7 @@ const batchIncludePaths = (baseArgs, includePaths) => {
73715
73974
  for (const filePath of includePaths) {
73716
73975
  const entryLength = filePath.length + 1;
73717
73976
  const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength > 24e3;
73718
- const exceedsFileCount = currentBatch.length >= 100;
73977
+ const exceedsFileCount = currentBatch.length >= 200;
73719
73978
  if (exceedsArgLength || exceedsFileCount) {
73720
73979
  batches.push(currentBatch);
73721
73980
  currentBatch = [];
@@ -74062,7 +74321,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
74062
74321
  }
74063
74322
  return enabled;
74064
74323
  };
74065
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), includedTags = /* @__PURE__ */ new Set(), includeTagDefaults = false, runtimeGlobals, unpluginAutoImportGlobalScopes, serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
74324
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), includedTags = /* @__PURE__ */ new Set(), includeTagDefaults = false, runtimeGlobals, unpluginAutoImportGlobalScopes, serverAuthFunctionNames, projectIndexModuleSources, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
74066
74325
  const hasIncludedTags = includedTags.size > 0;
74067
74326
  const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" || hasIncludedTags ? null : resolveReactHooksJsPlugin(project.hasReactCompiler || project.hasReactCompilerLintPlugin === true, customRulesOnly);
74068
74327
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
@@ -74126,7 +74385,8 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
74126
74385
  }))
74127
74386
  } : {},
74128
74387
  ...project.shopifyFlashListMajorVersion !== null ? { shopifyFlashListMajorVersion: project.shopifyFlashListMajorVersion } : {},
74129
- ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {}
74388
+ ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {},
74389
+ ...projectIndexModuleSources !== void 0 ? { projectIndexModuleSources: [...projectIndexModuleSources] } : {}
74130
74390
  } },
74131
74391
  rules: {
74132
74392
  ...reactCompilerRules,
@@ -75611,6 +75871,12 @@ const buildProfiledNodeArguments = (input) => [
75611
75871
  ...input.heapProfileDirectory ? ["--heap-prof", `--heap-prof-dir=${input.heapProfileDirectory}`] : [],
75612
75872
  ...input.argumentsList
75613
75873
  ];
75874
+ const lowerChildProcessPriority = (processId) => {
75875
+ if (processId === void 0) return;
75876
+ try {
75877
+ OS.setPriority(processId, OS.constants.priority.PRIORITY_BELOW_NORMAL);
75878
+ } catch {}
75879
+ };
75614
75880
  const SANITIZED_ENV = buildOxlintChildEnv(process.env);
75615
75881
  /**
75616
75882
  * Spawn one oxlint subprocess with hard ceilings on wall time and
@@ -75649,6 +75915,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
75649
75915
  "pipe"
75650
75916
  ]
75651
75917
  });
75918
+ lowerChildProcessPriority(child.pid);
75652
75919
  const onAbort = () => {
75653
75920
  child.kill("SIGKILL");
75654
75921
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
@@ -75771,7 +76038,7 @@ const isParallelismRelatedSpawnError = (error) => {
75771
76038
  * loop with a slimmer config in that case.
75772
76039
  */
75773
76040
  const spawnLintBatches = async (input) => {
75774
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, sourcePathByLintPath, sourceMapByLintPath, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal, deadlineEpochMs } = input;
76041
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, sourcePathByLintPath, sourceMapByLintPath, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 9, signal, deadlineEpochMs } = input;
75775
76042
  const resolveSourcePath = (filePath) => {
75776
76043
  const absoluteFilePath = Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory, filePath);
75777
76044
  return sourcePathByLintPath?.get(Path.normalize(absoluteFilePath)) ?? filePath;
@@ -75779,6 +76046,12 @@ const spawnLintBatches = async (input) => {
75779
76046
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
75780
76047
  let rescueDeadlineEpochMs;
75781
76048
  const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0 || rescueDeadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(rescueDeadlineEpochMs) === 0;
76049
+ const resolveSpawnTimeoutMs = () => {
76050
+ const deadlineBudgets = [deadlineEpochMs, rescueDeadlineEpochMs].filter((deadline) => deadline !== void 0).map(remainingDeadlineBudgetMs);
76051
+ if (deadlineBudgets.length === 0) return spawnTimeoutMs;
76052
+ const remainingDeadlineMs = Math.min(...deadlineBudgets);
76053
+ return spawnTimeoutMs === void 0 ? remainingDeadlineMs : Math.min(spawnTimeoutMs, remainingDeadlineMs);
76054
+ };
75782
76055
  const runBatchPass = async (concurrency, passBatches, passOnFileProgress) => {
75783
76056
  const totalFileCount = passBatches.reduce((sum, batch) => sum + batch.length, 0);
75784
76057
  const allDiagnostics = [];
@@ -75796,12 +76069,13 @@ const spawnLintBatches = async (input) => {
75796
76069
  const batchArgs = [...baseArgs, ...batch];
75797
76070
  try {
75798
76071
  const spawnBatch = () => {
75799
- if (isPastDeadline()) {
76072
+ const effectiveSpawnTimeoutMs = resolveSpawnTimeoutMs();
76073
+ if (effectiveSpawnTimeoutMs === 0) {
75800
76074
  deadlineSkippedFiles.push(...batch);
75801
76075
  batchState.deadlineSkippedFileCount += batch.length;
75802
76076
  return Promise.resolve(null);
75803
76077
  }
75804
- return spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal, () => {
76078
+ return spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, effectiveSpawnTimeoutMs, outputMaxBytes, signal, () => {
75805
76079
  if (batchState.didStart) return;
75806
76080
  batchState.didStart = true;
75807
76081
  startedFileCount += batchState.initialFileCount;
@@ -75956,11 +76230,36 @@ const validateRuleRegistration = () => {
75956
76230
  ].filter((entry) => entry !== null).join("; ");
75957
76231
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
75958
76232
  };
76233
+ const PROJECT_INDEX_MODULE_SOURCES = [
76234
+ "next/og",
76235
+ "@vercel/og",
76236
+ "satori",
76237
+ "remotion"
76238
+ ];
76239
+ const collectProjectIndexModuleSources = async (rootDirectory, candidateFiles) => {
76240
+ const foundModuleSources = /* @__PURE__ */ new Set();
76241
+ let sliceStartedAt = performance.now();
76242
+ for (const candidateFile of candidateFiles) {
76243
+ let sourceBuffer;
76244
+ try {
76245
+ sourceBuffer = NFS.readFileSync(Path.isAbsolute(candidateFile) ? candidateFile : Path.resolve(rootDirectory, candidateFile));
76246
+ } catch {
76247
+ continue;
76248
+ }
76249
+ for (const moduleSource of PROJECT_INDEX_MODULE_SOURCES) if (sourceBuffer.includes(`"${moduleSource}"`) || sourceBuffer.includes(`'${moduleSource}'`)) foundModuleSources.add(moduleSource);
76250
+ if (foundModuleSources.size === PROJECT_INDEX_MODULE_SOURCES.length) break;
76251
+ if (performance.now() - sliceStartedAt >= 12) {
76252
+ await yieldToEventLoop();
76253
+ sliceStartedAt = performance.now();
76254
+ }
76255
+ }
76256
+ return [...foundModuleSources].sort();
76257
+ };
75959
76258
  /**
75960
76259
  * Balanced LPT (longest-processing-time-first) lint batch planner. Where
75961
- * `batchIncludePaths` greedily fills 100-file batches and leaves a small
75962
- * remainder batch (617 files → 6×100 + 17, so the parallel pool's wall clock
75963
- * is set by whichever 100-file chunk happened to collect the most work while
76260
+ * `batchIncludePaths` greedily fills fixed-size batches and leaves a small
76261
+ * remainder batch, so the parallel pool's wall clock is set by whichever
76262
+ * full chunk happened to collect the most work while
75964
76263
  * the remainder's worker idles), this planner keeps the SAME mandatory batch
75965
76264
  * count — `ceil(files / OXLINT_MAX_FILES_PER_BATCH)`, the JS-plugin
75966
76265
  * perf-cliff / native-binding SIGABRT guard — and assigns files largest-first,
@@ -75989,7 +76288,7 @@ const validateRuleRegistration = () => {
75989
76288
  */
75990
76289
  const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
75991
76290
  if (files.length === 0) return [];
75992
- const batchCount = Math.ceil(files.length / 100);
76291
+ const batchCount = Math.ceil(files.length / 200);
75993
76292
  const maxFilesPerBatch = Math.ceil(files.length / batchCount);
75994
76293
  const baseArgsLengthChars = estimateArgsLength(baseArgs);
75995
76294
  const descendingSizeIndexes = [...files.keys()].sort((leftIndex, rightIndex) => (sizeByFile.get(files[rightIndex]) ?? 0) - (sizeByFile.get(files[leftIndex]) ?? 0));
@@ -76518,6 +76817,7 @@ const runOxlint = async (options) => {
76518
76817
  await yieldToEventLoop();
76519
76818
  const sizedScanFiles = includePaths === void 0 ? listSourceFilesWithSize(rootDirectory) : null;
76520
76819
  const candidateFiles = includePaths !== void 0 ? includePaths : (sizedScanFiles ?? []).map((entry) => entry.path);
76820
+ const projectIndexModuleSources = includePaths === void 0 && options.deadlineEpochMs === void 0 ? await collectProjectIndexModuleSources(rootDirectory, candidateFiles) : void 0;
76521
76821
  unpluginAutoImportGlobalScopes = collectUnpluginAutoImportGlobalScopes({
76522
76822
  rootDirectory,
76523
76823
  candidateFiles
@@ -76540,6 +76840,7 @@ const runOxlint = async (options) => {
76540
76840
  runtimeGlobals,
76541
76841
  unpluginAutoImportGlobalScopes,
76542
76842
  serverAuthFunctionNames,
76843
+ projectIndexModuleSources,
76543
76844
  severityControls,
76544
76845
  userPlugins,
76545
76846
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
@@ -77007,10 +77308,10 @@ const translateProjectInfoError = (cause, directory) => {
77007
77308
  }) });
77008
77309
  };
77009
77310
  var Project = class Project extends Service()("react-doctor/Project") {
77010
- static layerNode = succeed$3(Project, Project.of({ discover: fn("Project.discover")(function* (directory) {
77311
+ static layerNode = succeed$3(Project, Project.of({ discover: fn("Project.discover")(function* (input) {
77011
77312
  return yield* try_({
77012
- try: () => discoverProject(directory),
77013
- catch: (cause) => translateProjectInfoError(cause, directory)
77313
+ try: () => discoverProject(input.directory, { sourceFileCount: input.sourceFileCount }),
77314
+ catch: (cause) => translateProjectInfoError(cause, input.directory)
77014
77315
  });
77015
77316
  }) }));
77016
77317
  static layerOf = (projectInfo) => succeed$3(Project, Project.of({ discover: () => succeed$2(projectInfo) }));
@@ -77595,9 +77896,10 @@ const checkSupplyChain = (input) => gen(function* () {
77595
77896
  * outage.
77596
77897
  */
77597
77898
  var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
77598
- static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(flatMap$2(SupplyChainOverlapTimeoutMs, (totalTimeoutMs) => checkSupplyChain({
77599
- ...input,
77600
- totalTimeoutMs
77899
+ static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(flatMap$2(SupplyChainOverlapTimeoutMs, (configuredTimeoutMs) => checkSupplyChain({
77900
+ rootDirectory: input.rootDirectory,
77901
+ userConfig: input.userConfig,
77902
+ totalTimeoutMs: input.timeoutMs ?? configuredTimeoutMs
77601
77903
  })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
77602
77904
  static layerOf = (diagnostics) => succeed$3(SupplyChain, SupplyChain.of({ run: () => fromIterable$1(diagnostics) }));
77603
77905
  };
@@ -77713,8 +78015,28 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77713
78015
  const partialFailuresRef = yield* LintPartialFailures;
77714
78016
  const resolvedConfig = yield* configService.resolve(input.directory);
77715
78017
  const scanDirectory = resolvedConfig.resolvedDirectory;
77716
- const project = yield* projectService.discover(scanDirectory);
78018
+ const excludedProjectDirectories = (input.excludedProjectDirectories ?? []).map((excludedDirectory) => Path.resolve(excludedDirectory)).filter((excludedDirectory) => isPathInsideDirectory(excludedDirectory, scanDirectory));
78019
+ const excludedProjectRelativePaths = excludedProjectDirectories.map((excludedDirectory) => Path.relative(scanDirectory, excludedDirectory).replaceAll("\\", "/"));
78020
+ const isExcludedProjectDiagnostic = (diagnostic) => {
78021
+ const relativeFilePath = toNormalizedRelativePath(diagnostic.filePath, scanDirectory);
78022
+ return excludedProjectRelativePaths.some((excludedRelativePath) => relativeFilePath === excludedRelativePath || relativeFilePath.startsWith(`${excludedRelativePath}/`));
78023
+ };
78024
+ const precomputedSourceFiles = input.suppressScanSummary === true || excludedProjectDirectories.length > 0 ? yield* filesService.listSourceFilesCooperative({
78025
+ rootDirectory: scanDirectory,
78026
+ signal: input.signal
78027
+ }) : null;
78028
+ const includedPrecomputedSourceFiles = precomputedSourceFiles !== null && excludedProjectDirectories.length > 0 ? filterPathsOutsideDirectories({
78029
+ rootDirectory: scanDirectory,
78030
+ relativePaths: precomputedSourceFiles,
78031
+ excludedDirectories: excludedProjectDirectories
78032
+ }) : precomputedSourceFiles;
78033
+ const sourceFileCount = excludedProjectDirectories.length > 0 ? includedPrecomputedSourceFiles?.length : input.precomputedSourceFileCount ?? includedPrecomputedSourceFiles?.length;
78034
+ const project = yield* projectService.discover({
78035
+ directory: scanDirectory,
78036
+ sourceFileCount
78037
+ });
77717
78038
  if (!isAnalyzableProject(project)) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
78039
+ const deadCodeSourceFileCount = input.retainExcludedProjectDeadCodeDiagnostics === true && precomputedSourceFiles !== null ? precomputedSourceFiles.length : project.sourceFileCount;
77718
78040
  const [repo, sha, defaultBranch] = yield* all([
77719
78041
  gitService.githubRepo(scanDirectory).pipe(orElseSucceed(() => null)),
77720
78042
  gitService.headSha(scanDirectory).pipe(orElseSucceed(() => null)),
@@ -77725,8 +78047,13 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77725
78047
  directory: scanDirectory,
77726
78048
  repo
77727
78049
  }).pipe(orElseSucceed(() => null)) : succeed$2(null));
77728
- const lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
77729
- const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
78050
+ let lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config, includedPrecomputedSourceFiles ?? void 0);
78051
+ if (excludedProjectDirectories.length > 0) lintIncludePaths = filterPathsOutsideDirectories({
78052
+ rootDirectory: scanDirectory,
78053
+ relativePaths: lintIncludePaths ?? includedPrecomputedSourceFiles ?? (yield* filesService.listSourceFiles(scanDirectory)),
78054
+ excludedDirectories: excludedProjectDirectories
78055
+ });
78056
+ const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? includedPrecomputedSourceFiles ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
77730
78057
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
77731
78058
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
77732
78059
  yield* beforeLint(project, lintIncludePaths ?? void 0);
@@ -77749,17 +78076,31 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77749
78076
  ...checkReactNativeProject(scanDirectory, project)
77750
78077
  ])));
77751
78078
  const securityScanFailedRef = yield* make$13(false);
77752
- const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise(() => checkSecurityScanCooperative(scanDirectory, {
78079
+ let didSecurityScanReachDeadline = false;
78080
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise((signal) => checkSecurityScanCooperative(scanDirectory, {
77753
78081
  project,
77754
78082
  ignoredTags: input.ignoredTags,
77755
78083
  includedTags: input.includedTags,
77756
- includeTagDefaults: input.includeTagDefaults
78084
+ includeTagDefaults: input.includeTagDefaults,
78085
+ excludedDirectories: new Set(excludedProjectDirectories),
78086
+ deadlineEpochMs: input.deadlineEpochMs,
78087
+ signal,
78088
+ onDeadlineExceeded: () => {
78089
+ didSecurityScanReachDeadline = true;
78090
+ }
77757
78091
  })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), catch_$1(() => set(securityScanFailedRef, true).pipe(as(empty$4))))))).pipe(withSpan("SecurityScan.run")));
78092
+ const capToDeadline = (phaseTimeoutMs) => input.deadlineEpochMs === void 0 ? phaseTimeoutMs : Math.min(phaseTimeoutMs, remainingDeadlineBudgetMs(input.deadlineEpochMs));
78093
+ const capOptionalToDeadline = (phaseTimeoutMs) => {
78094
+ if (input.deadlineEpochMs === void 0) return phaseTimeoutMs;
78095
+ const remainingBudgetMs = remainingDeadlineBudgetMs(input.deadlineEpochMs);
78096
+ return phaseTimeoutMs === null ? remainingBudgetMs : Math.min(phaseTimeoutMs, remainingBudgetMs);
78097
+ };
77758
78098
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
77759
- const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
78099
+ const supplyChainOverlapTimeout = capToDeadline(yield* SupplyChainOverlapTimeoutMs);
77760
78100
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
77761
78101
  rootDirectory: scanDirectory,
77762
- userConfig: resolvedConfig.config
78102
+ userConfig: resolvedConfig.config,
78103
+ timeoutMs: supplyChainOverlapTimeout
77763
78104
  }))).pipe(map$3((diagnostics) => ({
77764
78105
  diagnostics,
77765
78106
  timedOut: false
@@ -77783,46 +78124,49 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77783
78124
  const scanConcurrency = resolveScanConcurrency(yield* OxlintConcurrency);
77784
78125
  const lintPhaseTimeoutMs = yield* LintPhaseTimeoutMs;
77785
78126
  const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
77786
- const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
77787
78127
  const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
77788
- const capToDeadline = (phaseTimeoutMs) => input.deadlineEpochMs === void 0 ? phaseTimeoutMs : Math.min(phaseTimeoutMs, remainingDeadlineBudgetMs(input.deadlineEpochMs));
77789
78128
  const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
77790
78129
  const deadCodeOverlapMode = yield* DeadCodeOverlap;
77791
78130
  const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
77792
78131
  const deadCodeParseConcurrency = shouldOverlapDeadCode ? Math.max(1, Math.floor(scanConcurrency * DEAD_CODE_OVERLAP_PARSE_SHARE)) : void 0;
77793
78132
  const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
77794
- const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
77795
- rootDirectory: scanDirectory,
77796
- parseConcurrency: deadCodeParseConcurrency,
77797
- workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
77798
- onCacheOutcome: (didHitCache) => {
77799
- deadCodeCacheHit = didHitCache;
77800
- },
77801
- onSummaryCacheStats: (stats) => {
77802
- deadCodeSummaryCacheHits = stats.hits;
77803
- deadCodeSummaryCacheMisses = stats.misses;
77804
- }
77805
- }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
77806
- yield* set(deadCodeFailure, {
77807
- didFail: true,
77808
- reason: error.message
77809
- });
77810
- return empty$4;
77811
- })))))).pipe(timeoutOption(deadCodeTimeout.phaseTimeoutMs), flatMap$2(match$3({
77812
- onNone: () => set(deadCodeFailure, {
77813
- didFail: true,
77814
- reason: `Dead-code analysis exceeded ${Math.round(deadCodeTimeout.phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
77815
- }).pipe(as([])),
77816
- onSome: succeed$2
77817
- })));
78133
+ const buildCollectDeadCode = (deadCodeTimeout) => {
78134
+ const collectDeadCode = runCollect(applyPerElementPipeline(deadCodeService.run({
78135
+ rootDirectory: scanDirectory,
78136
+ parseConcurrency: deadCodeParseConcurrency,
78137
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
78138
+ onCacheOutcome: (didHitCache) => {
78139
+ deadCodeCacheHit = didHitCache;
78140
+ },
78141
+ onSummaryCacheStats: (stats) => {
78142
+ deadCodeSummaryCacheHits = stats.hits;
78143
+ deadCodeSummaryCacheMisses = stats.misses;
78144
+ }
78145
+ }).pipe(filter((diagnostic) => input.retainExcludedProjectDeadCodeDiagnostics === true || !isExcludedProjectDiagnostic(diagnostic)), catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
78146
+ yield* set(deadCodeFailure, {
78147
+ didFail: true,
78148
+ reason: error.message
78149
+ });
78150
+ return empty$4;
78151
+ }))))));
78152
+ const phaseTimeoutMs = deadCodeTimeout.phaseTimeoutMs;
78153
+ if (phaseTimeoutMs === null) return collectDeadCode;
78154
+ return collectDeadCode.pipe(timeoutOption(phaseTimeoutMs), flatMap$2(match$3({
78155
+ onNone: () => set(deadCodeFailure, {
78156
+ didFail: true,
78157
+ reason: `Dead-code analysis exceeded ${Math.round(phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
78158
+ }).pipe(as([])),
78159
+ onSome: succeed$2
78160
+ })));
78161
+ };
77818
78162
  const overlapDeadCodeTimeout = resolveDeadCodeTimeout({
77819
- sourceFileCount: project.sourceFileCount,
78163
+ sourceFileCount: deadCodeSourceFileCount,
77820
78164
  deadCodeConcurrency: deadCodeParseConcurrency ?? scanConcurrency,
77821
78165
  fullConcurrency: scanConcurrency
77822
78166
  });
77823
78167
  const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
77824
78168
  workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
77825
- phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs))
78169
+ phaseTimeoutMs: capOptionalToDeadline(deadCodePhaseTimeoutMs)
77826
78170
  })) : null;
77827
78171
  const scanProgress = yield* progressService.start("Scanning...");
77828
78172
  const scanStartTime = Date.now();
@@ -77873,7 +78217,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77873
78217
  });
77874
78218
  return empty$4;
77875
78219
  }))));
77876
- const lintCollected = dedupeRelatedDiagnostics(yield* runCollect(filterPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
78220
+ const collectLintDiagnostics = runCollect(filterPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream));
78221
+ const lintCollected = dedupeRelatedDiagnostics(yield* lintPhaseTimeoutMs === null ? collectLintDiagnostics : collectLintDiagnostics.pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
77877
78222
  onNone: () => set(lintFailure, {
77878
78223
  didFail: true,
77879
78224
  reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
@@ -77903,13 +78248,13 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77903
78248
  } else {
77904
78249
  yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
77905
78250
  const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
77906
- sourceFileCount: totalFileCount,
78251
+ sourceFileCount: input.retainExcludedProjectDeadCodeDiagnostics === true ? deadCodeSourceFileCount : totalFileCount,
77907
78252
  deadCodeConcurrency: scanConcurrency,
77908
78253
  fullConcurrency: scanConcurrency
77909
78254
  });
77910
78255
  deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
77911
78256
  workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
77912
- phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs))
78257
+ phaseTimeoutMs: capOptionalToDeadline(deadCodePhaseTimeoutMs)
77913
78258
  });
77914
78259
  }
77915
78260
  const deadCodeFailureState = lintFailureState.didFail ? {
@@ -77953,7 +78298,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77953
78298
  metadata: scoreMetadata
77954
78299
  });
77955
78300
  const lintPartialFailures = yield* get$2(partialFailuresRef);
77956
- const securityScanFailed = yield* get$2(securityScanFailedRef);
78301
+ const didSecurityScanFail = yield* get$2(securityScanFailedRef);
78302
+ const securityScanFailed = didSecurityScanFail || didSecurityScanReachDeadline;
78303
+ let securityScanFailureReason = null;
78304
+ if (didSecurityScanReachDeadline) securityScanFailureReason = "Security scan reached the max scan duration; findings collected before the deadline were preserved.";
78305
+ else if (didSecurityScanFail) securityScanFailureReason = "Security scan failed and was skipped.";
77957
78306
  return {
77958
78307
  project,
77959
78308
  userConfig: resolvedConfig.config,
@@ -77976,6 +78325,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77976
78325
  scanConcurrency,
77977
78326
  supplyChainOverlapTimedOut: supplyChainResult.timedOut,
77978
78327
  securityScanFailed,
78328
+ securityScanFailureReason,
77979
78329
  lintCacheHitFileCount,
77980
78330
  lintCacheTotalFileCount,
77981
78331
  lintSidecarReplayedFileCount,
@@ -77991,7 +78341,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77991
78341
  "inspect.runDeadCode": input.runDeadCode,
77992
78342
  "inspect.isCi": input.isCi,
77993
78343
  "inspect.scoreSurface": input.scoreSurface ?? "score"
77994
- } }), (scanProgram) => flatMap$2(ScanDeadlineMs, (scanDeadlineMs) => scanProgram.pipe(timeout(scanDeadlineMs), catchTag$1("TimeoutError", () => new ReactDoctorError({ reason: new ScanDeadlineExceeded({ detail: `${scanDeadlineMs / MILLISECONDS_PER_SECOND}s elapsed` }) })))));
78344
+ } }), (scanProgram) => flatMap$2(ScanDeadlineMs, (scanDeadlineMs) => {
78345
+ if (scanDeadlineMs === null) return scanProgram;
78346
+ return scanProgram.pipe(timeout(scanDeadlineMs), catchTag$1("TimeoutError", () => new ReactDoctorError({ reason: new ScanDeadlineExceeded({ detail: `${scanDeadlineMs / MILLISECONDS_PER_SECOND}s elapsed` }) })));
78347
+ }));
77995
78348
  const parseNodeVersion = (versionString) => {
77996
78349
  const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
77997
78350
  return {
@@ -78103,16 +78456,6 @@ var NodeResolver = class NodeResolver extends Service()("react-doctor/NodeResolv
78103
78456
  });
78104
78457
  };
78105
78458
  /**
78106
- * Zip-Slip defense: relative paths come from git (`diff --name-only`), which
78107
- * normalizes during ordinary adds, but a crafted index/pack/symlinked tree can
78108
- * smuggle `..` segments that escape the temp root. Resolve against the temp dir
78109
- * and reject anything that lands outside before writing.
78110
- */
78111
- const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
78112
- const relative = path.relative(parentAbsolutePath, childAbsolutePath);
78113
- return Boolean(relative) && !relative.startsWith("..") && !path.isAbsolute(relative);
78114
- };
78115
- /**
78116
78459
  * Writes a set of source files (supplied by `readContent` — e.g.
78117
78460
  * `git show <ref>:<path>` for a baseline tree, or `git show :<path>` for the
78118
78461
  * index) into a temp tree that mirrors the project layout, copying the
@@ -78400,6 +78743,7 @@ const buildJsonReport = (input) => {
78400
78743
  directory: input.directory,
78401
78744
  diff: toJsonDiff(input.diff),
78402
78745
  projects,
78746
+ ...input.skippedProjects && input.skippedProjects.length > 0 ? { skippedProjects: [...input.skippedProjects] } : {},
78403
78747
  diagnostics: flattenedDiagnostics,
78404
78748
  summary,
78405
78749
  elapsedMilliseconds: input.totalElapsedMilliseconds,
@@ -78424,7 +78768,7 @@ const buildSkippedChecks = (input) => {
78424
78768
  else if (input.lintPartialFailures.length > 0) skippedCheckReasons["lint:partial"] = input.lintPartialFailures.join("; ");
78425
78769
  if (input.didDeadCodeFail && input.deadCodeFailureReason !== null) skippedCheckReasons["dead-code"] = input.deadCodeFailureReason;
78426
78770
  if (input.supplyChainOverlapTimedOut) skippedCheckReasons["supply-chain"] = "Supply-chain analysis timed out and was skipped.";
78427
- if (input.securityScanFailed) skippedCheckReasons["security-scan"] = "Security scan failed and was skipped.";
78771
+ if (input.securityScanFailed) skippedCheckReasons["security-scan"] = input.securityScanFailureReason ?? "Security scan failed and was skipped.";
78428
78772
  return {
78429
78773
  skippedChecks,
78430
78774
  skippedCheckReasons
@@ -78564,6 +78908,33 @@ const getChangedLineRanges = (input) => runPromise(gen(function* () {
78564
78908
  * instead of being improvised per diagnostic.
78565
78909
  */
78566
78910
  const buildRuleDocsUrl = (plugin, rule) => `${DOCS_RULES_BASE_URL}/${plugin}/${rule}`;
78911
+ const collectSourceFileCountsByDirectory = async (rootDirectory, projectDirectories, signal) => {
78912
+ const resolvedRootDirectory = Path.resolve(rootDirectory);
78913
+ const sourceFileCounts = /* @__PURE__ */ new Map();
78914
+ const projectDirectoryByRelativePath = /* @__PURE__ */ new Map();
78915
+ for (const projectDirectory of projectDirectories) {
78916
+ const resolvedProjectDirectory = Path.resolve(projectDirectory);
78917
+ const relativeDirectory = Path.relative(resolvedRootDirectory, resolvedProjectDirectory);
78918
+ if (Path.isAbsolute(relativeDirectory) || relativeDirectory.startsWith("..")) continue;
78919
+ sourceFileCounts.set(resolvedProjectDirectory, 0);
78920
+ projectDirectoryByRelativePath.set(relativeDirectory.replaceAll(Path.sep, "/"), resolvedProjectDirectory);
78921
+ }
78922
+ const sourceFilePaths = await listSourceFilesCooperative(resolvedRootDirectory, signal);
78923
+ for (const sourceFilePath of sourceFilePaths) {
78924
+ let relativeDirectory = Path.posix.dirname(sourceFilePath);
78925
+ while (true) {
78926
+ const normalizedRelativeDirectory = relativeDirectory === "." ? "" : relativeDirectory;
78927
+ const projectDirectory = projectDirectoryByRelativePath.get(normalizedRelativeDirectory);
78928
+ if (projectDirectory !== void 0) {
78929
+ sourceFileCounts.set(projectDirectory, (sourceFileCounts.get(projectDirectory) ?? 0) + 1);
78930
+ break;
78931
+ }
78932
+ if (relativeDirectory === ".") break;
78933
+ relativeDirectory = Path.posix.dirname(relativeDirectory);
78934
+ }
78935
+ }
78936
+ return sourceFileCounts;
78937
+ };
78567
78938
  const computeConfigFingerprint = (projectDirectory, version) => {
78568
78939
  const parts = [`v=${version}`];
78569
78940
  let directory = projectDirectory;
@@ -78934,8 +79305,11 @@ const METRIC = {
78934
79305
  scanPhaseDuration: "scan.phase_duration",
78935
79306
  scanFiles: "scan.files",
78936
79307
  scanScore: "scan.score",
79308
+ scanScoreRetry: "scan.score_retry",
78937
79309
  scanClean: "scan.clean",
78938
79310
  scanCheckSkipped: "scan.check_skipped",
79311
+ scanProjectSkipped: "scan.project_skipped",
79312
+ scanWorkspaceDeadCodeShared: "scan.workspace_deadcode_shared",
78939
79313
  scanNoReactDetected: "scan.no_react_detected",
78940
79314
  baselineDegraded: "baseline.degraded",
78941
79315
  ruleFired: "rule.fired",
@@ -78966,8 +79340,10 @@ const METRIC = {
78966
79340
  tuiFindingNavigated: "tui.finding_navigated",
78967
79341
  tuiIssueStreamShown: "tui.issue_stream_shown",
78968
79342
  tuiProjectPathContextShown: "tui.project_path_context_shown",
79343
+ tuiProjectSelectInlineShown: "tui.project_select_inline_shown",
78969
79344
  tuiReportActionSelected: "tui.report_action_selected",
78970
79345
  tuiCancelled: "tui.cancelled",
79346
+ tuiScanInlineShown: "tui.scan_inline_shown",
78971
79347
  tuiStackedReportCapped: "tui.stacked_report_capped",
78972
79348
  aiTrainingWarningShown: "ai.training.warning_shown",
78973
79349
  jsonOutUsed: "json.out_used"
@@ -79013,7 +79389,7 @@ const makeNoopConsole = () => ({
79013
79389
  });
79014
79390
  //#endregion
79015
79391
  //#region src/cli/utils/version.ts
79016
- const VERSION = "0.9.3-dev.b02bc69";
79392
+ const VERSION = "0.9.3-dev.ba04952";
79017
79393
  //#endregion
79018
79394
  //#region src/cli/utils/json-mode.ts
79019
79395
  let context = null;
@@ -79371,13 +79747,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
79371
79747
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
79372
79748
  * standard `SENTRY_RELEASE` override.
79373
79749
  */
79374
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.9.3-dev.b02bc69`;
79750
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.9.3-dev.ba04952`;
79375
79751
  /**
79376
79752
  * Deployment environment shown in Sentry's environment filter. Defaults to
79377
79753
  * `production` for tagged releases and `development` for dev/unbuilt versions,
79378
79754
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
79379
79755
  */
79380
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.9.3-dev.b02bc69") ? "development" : "production");
79756
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.9.3-dev.ba04952") ? "development" : "production");
79381
79757
  /**
79382
79758
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
79383
79759
  * (set to `0` to disable tracing) and falls back to
@@ -91053,6 +91429,19 @@ const ciInstallAction = (options) => runCiCommand("install", runCiInstall, optio
91053
91429
  const ciUpgradeAction = (options) => runCiCommand("upgrade", runCiUpgrade, options);
91054
91430
  const ciConfigAction = (options) => runCiCommand("config", runCiConfig, options);
91055
91431
  //#endregion
91432
+ //#region src/cli/utils/active-scan-abort-registry.ts
91433
+ const activeScanAbortControllers = /* @__PURE__ */ new Set();
91434
+ const activeScanAbortRegistry = {
91435
+ register: (controller) => {
91436
+ activeScanAbortControllers.add(controller);
91437
+ return () => activeScanAbortControllers.delete(controller);
91438
+ },
91439
+ abortAll: () => {
91440
+ for (const controller of activeScanAbortControllers) controller.abort();
91441
+ activeScanAbortControllers.clear();
91442
+ }
91443
+ };
91444
+ //#endregion
91056
91445
  //#region src/cli/utils/sentry-tracer.ts
91057
91446
  const toHrTime = (epochNanoseconds) => [Number(epochNanoseconds / NANOSECONDS_PER_SECOND), Number(epochNanoseconds % NANOSECONDS_PER_SECOND)];
91058
91447
  const SPAN_KIND_TO_OTEL = {
@@ -91469,7 +91858,14 @@ const isInspectResultComplete = (result) => isScanComplete({
91469
91858
  skippedCheckReasonCount: Object.keys(result.skippedCheckReasons ?? {}).length
91470
91859
  });
91471
91860
  //#endregion
91861
+ //#region src/cli/utils/resolve-score-unavailable-reason.ts
91862
+ const resolveScoreUnavailableReason = (input) => {
91863
+ if (input.isScoreDisabled) return "disabled";
91864
+ return input.isAnalysisIncomplete ? "analysis-incomplete" : "api-unavailable";
91865
+ };
91866
+ //#endregion
91472
91867
  //#region src/cli/utils/scan-result-cache.ts
91868
+ const createScanResultCacheInvocationState = () => ({ repositoryIdentityByRoot: /* @__PURE__ */ new Map() });
91473
91869
  const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
91474
91870
  const TOOLCHAIN_PACKAGE_SPECIFIERS = [
91475
91871
  "oxlint/package.json",
@@ -91554,7 +91950,7 @@ const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
91554
91950
  if (stats.size > 10485760) return null;
91555
91951
  return hashFileContents(absolutePath);
91556
91952
  };
91557
- const buildWorktreeFingerprint = (projectDirectory) => {
91953
+ const buildWorktreeFingerprint = (projectDirectory, repositoryRoot) => {
91558
91954
  const statusOutput = runGit(projectDirectory, [
91559
91955
  "status",
91560
91956
  "--porcelain=v1",
@@ -91566,8 +91962,6 @@ const buildWorktreeFingerprint = (projectDirectory) => {
91566
91962
  const statusEntries = parseWorktreeStatusRecords(statusOutput);
91567
91963
  if (statusEntries === null) return null;
91568
91964
  if (statusEntries.length > 300) return null;
91569
- const repositoryRoot = runGit(projectDirectory, ["rev-parse", "--show-toplevel"]);
91570
- if (repositoryRoot === null) return null;
91571
91965
  const dirtyEntries = [];
91572
91966
  for (const statusEntry of statusEntries) {
91573
91967
  const contentFingerprint = buildDirtyPathContentFingerprint(path.join(repositoryRoot, statusEntry.path), statusEntry.statusCode);
@@ -91583,6 +91977,20 @@ const buildWorktreeFingerprint = (projectDirectory) => {
91583
91977
  return firstSortKey < secondSortKey ? -1 : firstSortKey > secondSortKey ? 1 : 0;
91584
91978
  });
91585
91979
  };
91980
+ const resolveRepositoryCacheIdentity = (projectDirectory, invocationState) => {
91981
+ const repositoryRoot = runGit(projectDirectory, ["rev-parse", "--show-toplevel"]);
91982
+ if (repositoryRoot === null) return null;
91983
+ const cachedIdentity = invocationState.repositoryIdentityByRoot.get(repositoryRoot);
91984
+ if (cachedIdentity !== void 0) return cachedIdentity;
91985
+ const worktreeFingerprint = buildWorktreeFingerprint(projectDirectory, repositoryRoot);
91986
+ const headSha = readHeadSha(repositoryRoot);
91987
+ const identity = worktreeFingerprint === null || headSha === null ? null : {
91988
+ headSha,
91989
+ worktreeFingerprint
91990
+ };
91991
+ invocationState.repositoryIdentityByRoot.set(repositoryRoot, identity);
91992
+ return identity;
91993
+ };
91586
91994
  const DOTENV_FILE_NAME_PATTERN = /^\.env(\.|$)/;
91587
91995
  const resolveDotenvFingerprint = (projectDirectory) => {
91588
91996
  try {
@@ -91677,16 +92085,14 @@ const resolveToolchainFingerprint = (nodeBinaryPath) => {
91677
92085
  const buildScanResultCacheKey = (input) => {
91678
92086
  if (isCacheGloballyDisabled()) return null;
91679
92087
  if (!isGitIdentityTrustworthy(input.projectDirectory)) return null;
91680
- const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
91681
- if (worktreeFingerprint === null) return null;
91682
- const headSha = readHeadSha(input.projectDirectory);
91683
- if (headSha === null) return null;
92088
+ const repositoryIdentity = resolveRepositoryCacheIdentity(input.projectDirectory, input.invocationState);
92089
+ if (repositoryIdentity === null) return null;
91684
92090
  if (stringifyStableJson(input.userConfig) === null) return null;
91685
92091
  const cacheKeyJson = stringifyStableJson({
91686
92092
  schemaVersion: 6,
91687
92093
  projectIdentity: resolveProjectIdentity(input.projectDirectory),
91688
- headSha,
91689
- worktreeFingerprint,
92094
+ headSha: repositoryIdentity.headSha,
92095
+ worktreeFingerprint: repositoryIdentity.worktreeFingerprint,
91690
92096
  dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
91691
92097
  reactDoctorVersion: input.version,
91692
92098
  nodeVersion: process.version,
@@ -91700,6 +92106,8 @@ const buildScanResultCacheKey = (input) => {
91700
92106
  deadCode: input.options.deadCode,
91701
92107
  supplyChain: input.options.supplyChain,
91702
92108
  includePaths: [...input.options.includePaths].sort(),
92109
+ excludedProjectDirectories: [...input.options.excludedProjectDirectories ?? []].sort(),
92110
+ retainExcludedProjectDeadCodeDiagnostics: input.options.retainExcludedProjectDeadCodeDiagnostics ?? false,
91703
92111
  customRulesOnly: input.options.customRulesOnly,
91704
92112
  respectInlineDisables: input.options.respectInlineDisables,
91705
92113
  warnings: input.options.warnings,
@@ -91966,7 +92374,11 @@ const buildOutcomeAttributes = (input) => {
91966
92374
  ...withNamespace("score", {
91967
92375
  value: result.score ? result.score.score : null,
91968
92376
  label: result.score ? result.score.label : null,
91969
- available: result.score !== null
92377
+ available: result.score !== null,
92378
+ unavailableReason: result.score === null ? resolveScoreUnavailableReason({
92379
+ isScoreDisabled: input.noScore,
92380
+ isAnalysisIncomplete: input.didLintFail === true || input.didDeadCodeFail === true
92381
+ }) : null
91970
92382
  }),
91971
92383
  ...withNamespace("lint", {
91972
92384
  failed: input.didLintFail ?? null,
@@ -92534,10 +92946,29 @@ const createDiagnosticEvidenceReader = (rootDirectory, options = {}) => {
92534
92946
  //#endregion
92535
92947
  //#region src/cli/utils/build-no-score-message.ts
92536
92948
  const ENTERPRISE_CONTACT_HINT = `Want something custom to your company? Contact us at ${ENTERPRISE_CONTACT_URL}.`;
92537
- const buildNoScoreMessage = (isScoreDisabled, disabledMessage) => {
92538
- return `${isScoreDisabled ? disabledMessage ?? "Score disabled by --no-score." : "Score unavailable (could not reach the score API)."} ${ENTERPRISE_CONTACT_HINT}`;
92949
+ const buildNoScoreMessage = (input) => {
92950
+ const unavailableReason = resolveScoreUnavailableReason({
92951
+ isScoreDisabled: input.isScoreDisabled,
92952
+ isAnalysisIncomplete: input.isAnalysisIncomplete ?? false
92953
+ });
92954
+ let reason;
92955
+ switch (unavailableReason) {
92956
+ case "disabled":
92957
+ reason = input.disabledMessage ?? "Score disabled by --no-score.";
92958
+ break;
92959
+ case "analysis-incomplete":
92960
+ reason = "Score not shown because lint or dead-code analysis could not complete.";
92961
+ break;
92962
+ case "api-unavailable":
92963
+ reason = "Score unavailable (could not reach the score API).";
92964
+ break;
92965
+ }
92966
+ return `${reason} ${ENTERPRISE_CONTACT_HINT}`;
92539
92967
  };
92540
92968
  //#endregion
92969
+ //#region src/cli/utils/has-incomplete-score-analysis.ts
92970
+ const hasIncompleteScoreAnalysis = (skippedChecks) => skippedChecks.includes("lint") || skippedChecks.includes("dead-code");
92971
+ //#endregion
92541
92972
  //#region src/cli/utils/build-empty-report-message.ts
92542
92973
  const buildEmptyReportMessage = (input) => {
92543
92974
  const categoryFilters = [...input.categoryFilters];
@@ -93721,7 +94152,10 @@ const mergeInspectOptions = (inputOptions, userConfig) => {
93721
94152
  maxDurationMs: inputOptions.maxDurationMs ?? null,
93722
94153
  baseline: inputOptions.baseline ?? null,
93723
94154
  changedLineRanges: inputOptions.changedLineRanges ?? null,
93724
- supplyChainManifestChanged: inputOptions.supplyChainManifestChanged ?? false
94155
+ supplyChainManifestChanged: inputOptions.supplyChainManifestChanged ?? false,
94156
+ excludedProjectDirectories: inputOptions.excludedProjectDirectories ?? [],
94157
+ retainExcludedProjectDeadCodeDiagnostics: inputOptions.retainExcludedProjectDeadCodeDiagnostics ?? false,
94158
+ precomputedSourceFileCount: inputOptions.precomputedSourceFileCount
93725
94159
  };
93726
94160
  };
93727
94161
  const deriveScope = (options) => {
@@ -93793,11 +94227,22 @@ const inspectWithOxlintRuntime = async (directory, inputOptions, oxlintRuntime)
93793
94227
  };
93794
94228
  const createInvocationInspect = (requestedOxlintConcurrency) => {
93795
94229
  const concurrency = resolveScanConcurrency(requestedOxlintConcurrency ?? runSync(OxlintConcurrency));
93796
- const oxlintRuntime = {
93797
- concurrency,
93798
- spawnSlots: createOxlintSpawnSlots(concurrency)
94230
+ const spawnSlots = createOxlintSpawnSlots(concurrency);
94231
+ const scanResultCacheInvocationState = createScanResultCacheInvocationState();
94232
+ return async (directory, inputOptions = {}) => {
94233
+ const abortController = new AbortController();
94234
+ const unregisterAbortController = activeScanAbortRegistry.register(abortController);
94235
+ try {
94236
+ return await inspectWithOxlintRuntime(directory, inputOptions, {
94237
+ concurrency,
94238
+ spawnSlots,
94239
+ abortSignal: abortController.signal,
94240
+ scanResultCacheInvocationState
94241
+ });
94242
+ } finally {
94243
+ unregisterAbortController();
94244
+ }
93799
94245
  };
93800
- return (directory, inputOptions = {}) => inspectWithOxlintRuntime(directory, inputOptions, oxlintRuntime);
93801
94246
  };
93802
94247
  const inspect = async (directory, inputOptions = {}) => createInvocationInspect(inputOptions.concurrency)(directory, inputOptions);
93803
94248
  const countIncompleteLintFiles = (lintPartialFailures) => countDroppedLintFiles(lintPartialFailures) + countDeadlineSkippedFiles(lintPartialFailures);
@@ -93810,12 +94255,27 @@ const countIncompleteLintFiles = (lintPartialFailures) => countDroppedLintFiles(
93810
94255
  */
93811
94256
  const runBaselineComparison = async (params) => {
93812
94257
  const tempDirectory = mkdtempSync(path.join(tmpdir(), BASELINE_FILES_TEMP_DIR_PREFIX));
94258
+ const baselineIncludePaths = filterPathsOutsideDirectories({
94259
+ rootDirectory: params.directory,
94260
+ relativePaths: params.options.includePaths,
94261
+ excludedDirectories: params.options.excludedProjectDirectories
94262
+ });
94263
+ const baselineBaseFiles = params.baseFiles ? filterPathsOutsideDirectories({
94264
+ rootDirectory: params.directory,
94265
+ relativePaths: params.baseFiles,
94266
+ excludedDirectories: params.options.excludedProjectDirectories
94267
+ }) : void 0;
94268
+ const baselineHeadFiles = params.headFiles ? filterPathsOutsideDirectories({
94269
+ rootDirectory: params.directory,
94270
+ relativePaths: params.headFiles,
94271
+ excludedDirectories: params.options.excludedProjectDirectories
94272
+ }) : void 0;
93813
94273
  const snapshot = await materializeBaselineFiles({
93814
94274
  directory: params.directory,
93815
94275
  ref: params.baselineRef,
93816
- files: params.options.includePaths,
93817
- baseFiles: params.baseFiles,
93818
- headFiles: params.headFiles,
94276
+ files: baselineIncludePaths,
94277
+ baseFiles: baselineBaseFiles,
94278
+ headFiles: baselineHeadFiles,
93819
94279
  tempDirectory
93820
94280
  }).catch((error) => {
93821
94281
  rmSync(tempDirectory, {
@@ -93837,7 +94297,7 @@ const runBaselineComparison = async (params) => {
93837
94297
  const baseFiles = new Set(snapshot.baseFiles.map(toForwardSlashes));
93838
94298
  const trackedHeadFiles = new Set(snapshot.headFiles.map(toForwardSlashes));
93839
94299
  const expectedHeadFiles = new Set(trackedHeadFiles);
93840
- for (const filePath of params.options.includePaths) {
94300
+ for (const filePath of baselineIncludePaths) {
93841
94301
  const normalizedFilePath = toForwardSlashes(filePath);
93842
94302
  if (!baseFiles.has(normalizedFilePath)) expectedHeadFiles.add(normalizedFilePath);
93843
94303
  }
@@ -93874,8 +94334,9 @@ const runBaselineComparison = async (params) => {
93874
94334
  resolveLocalGithubViewerPermission: false,
93875
94335
  suppressScanSummary: true,
93876
94336
  supplyChainManifestChanged: params.options.supplyChainManifestChanged,
93877
- deadlineEpochMs: params.deadlineEpochMs ?? void 0
93878
- }, {}).pipe(provide(baseLayers), provideService$2(PerFileLintCacheEnabled, false), provideService$2(SidecarLintCacheEnabled, false), provideService$2(Console, silentConsole))));
94337
+ deadlineEpochMs: params.deadlineEpochMs ?? void 0,
94338
+ signal: params.oxlintRuntime.abortSignal
94339
+ }, {}).pipe(provide(baseLayers), provideService$2(PerFileLintCacheEnabled, false), provideService$2(SidecarLintCacheEnabled, false), provideService$2(Console, silentConsole))), { signal: params.oxlintRuntime.abortSignal });
93879
94340
  if (baseOutput.didLintFail || countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0) return null;
93880
94341
  const hasUnscannedUntrackedSourceFiles = filterSourceFiles(snapshot.untrackedFiles.map(toForwardSlashes)).some((filePath) => !analyzedHeadFiles.has(filePath));
93881
94342
  const delta = computeDiagnosticDelta({
@@ -93903,6 +94364,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
93903
94364
  const isDiffMode = options.includePaths.length > 0;
93904
94365
  const resolvedNodeBinaryPath = await resolveOxlintNode(options.lint, options.scoreOnly || options.silent);
93905
94366
  const lintBindingMissing = options.lint && !resolvedNodeBinaryPath;
94367
+ await yieldToEventLoop();
93906
94368
  const cacheKey = buildScanResultCacheKey({
93907
94369
  projectDirectory: directory,
93908
94370
  version: VERSION,
@@ -93910,7 +94372,8 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
93910
94372
  options,
93911
94373
  userConfig,
93912
94374
  hasConfigOverride,
93913
- configSourceDirectory
94375
+ configSourceDirectory,
94376
+ invocationState: oxlintRuntime.scanResultCacheInvocationState
93914
94377
  });
93915
94378
  const scanResultCache = cacheKey === null ? null : createScanResultCache(directory);
93916
94379
  const cachedPayload = cacheKey === null ? null : scanResultCache?.lookup(cacheKey) ?? null;
@@ -93956,6 +94419,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
93956
94419
  });
93957
94420
  const program = runInspect({
93958
94421
  directory,
94422
+ precomputedSourceFileCount: options.precomputedSourceFileCount,
93959
94423
  includePaths: options.includePaths,
93960
94424
  customRulesOnly: options.customRulesOnly,
93961
94425
  respectInlineDisables: options.respectInlineDisables,
@@ -93973,7 +94437,10 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
93973
94437
  suppressScanSummary: options.suppressRendering,
93974
94438
  supplyChainManifestChanged: options.supplyChainManifestChanged,
93975
94439
  concurrentScan: options.concurrentScan,
93976
- deadlineEpochMs: deadlineEpochMs ?? void 0
94440
+ deadlineEpochMs: deadlineEpochMs ?? void 0,
94441
+ signal: oxlintRuntime.abortSignal,
94442
+ excludedProjectDirectories: options.excludedProjectDirectories,
94443
+ retainExcludedProjectDeadCodeDiagnostics: options.retainExcludedProjectDeadCodeDiagnostics
93977
94444
  }, { beforeLint: (projectInfo, lintIncludePaths) => gen(function* () {
93978
94445
  recordSentryProjectContext(projectInfo, rootSentrySpan, { concurrentScan: options.concurrentScan });
93979
94446
  recordCount(METRIC.projectDetected, 1);
@@ -93987,7 +94454,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
93987
94454
  lintSourceFileCount
93988
94455
  });
93989
94456
  }) });
93990
- const output = await runPromise(restoreLegacyThrow(applyObservability(options.silent ? program.pipe(provide(layers), provideService$2(Console, silentConsole)) : program.pipe(provide(layers)), rootSentrySpan)));
94457
+ const output = await runPromise(restoreLegacyThrow(applyObservability(options.silent ? program.pipe(provide(layers), provideService$2(Console, silentConsole)) : program.pipe(provide(layers)), rootSentrySpan)), { signal: oxlintRuntime.abortSignal });
93991
94458
  const didLintFail = lintBindingMissing || output.didLintFail;
93992
94459
  const lintFailureReason = lintBindingMissing ? `oxlint native binding not found for Node ${process.version}; expected one matching ${OXLINT_NODE_REQUIREMENT}` : output.lintFailureReason;
93993
94460
  if (!options.scoreOnly && !options.uiLayers && !lintBindingMissing && output.didLintFail && lintFailureReason !== null) if (output.lintFailureReasonKind === "native-binding-missing") runConsole(log(highlighter.gray(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`)));
@@ -94041,6 +94508,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
94041
94508
  lintFailureReasonKind: lintBindingMissing ? "native-binding-missing" : output.lintFailureReasonKind,
94042
94509
  supplyChainOverlapTimedOut: output.supplyChainOverlapTimedOut,
94043
94510
  securityScanFailed: output.securityScanFailed,
94511
+ securityScanFailureReason: output.securityScanFailureReason,
94044
94512
  suppressedRuleCounts: output.suppressedRuleCounts
94045
94513
  };
94046
94514
  if (cacheKey !== null && scanResultCache !== null && shouldStoreScanPayload(payload) && !baselineDegraded) scanResultCache.store(cacheKey, payload);
@@ -94091,6 +94559,7 @@ const renderAndRecordScan = async (input) => {
94091
94559
  deadCodeFailureReason: input.payload.deadCodeFailureReason,
94092
94560
  supplyChainOverlapTimedOut: input.payload.supplyChainOverlapTimedOut,
94093
94561
  securityScanFailed: input.payload.securityScanFailed ?? false,
94562
+ securityScanFailureReason: input.payload.securityScanFailureReason ?? null,
94094
94563
  directory: input.payload.directory,
94095
94564
  scannedFileCount: input.payload.scannedFileCount,
94096
94565
  scannedFilePaths: input.payload.scannedFilePaths,
@@ -94142,7 +94611,7 @@ const renderAndRecordScan = async (input) => {
94142
94611
  return result;
94143
94612
  };
94144
94613
  const finalizeAndRender = (input) => gen(function* () {
94145
- const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, supplyChainOverlapTimedOut, securityScanFailed, directory, scannedFileCount, scannedFilePaths, analyzedFiles, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, lintSidecarReplayedFileCount, lintSidecarTotalFileCount, deadCodeCacheHit, deadCodeSummaryCacheHits, deadCodeSummaryCacheMisses, baselineDelta } = input;
94614
+ const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, supplyChainOverlapTimedOut, securityScanFailed, securityScanFailureReason, directory, scannedFileCount, scannedFilePaths, analyzedFiles, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, lintSidecarReplayedFileCount, lintSidecarTotalFileCount, deadCodeCacheHit, deadCodeSummaryCacheHits, deadCodeSummaryCacheMisses, baselineDelta } = input;
94146
94615
  const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
94147
94616
  didLintFail,
94148
94617
  lintFailureReason,
@@ -94150,10 +94619,15 @@ const finalizeAndRender = (input) => gen(function* () {
94150
94619
  didDeadCodeFail,
94151
94620
  deadCodeFailureReason,
94152
94621
  supplyChainOverlapTimedOut,
94153
- securityScanFailed
94622
+ securityScanFailed,
94623
+ securityScanFailureReason
94154
94624
  });
94155
94625
  const hasSkippedChecks = skippedChecks.length > 0;
94156
- const noScoreMessage = buildNoScoreMessage(options.noScore, options.scoreDisabledMessage);
94626
+ const noScoreMessage = buildNoScoreMessage({
94627
+ isScoreDisabled: options.noScore,
94628
+ isAnalysisIncomplete: hasIncompleteScoreAnalysis(skippedChecks),
94629
+ disabledMessage: options.scoreDisabledMessage
94630
+ });
94157
94631
  const buildResult = () => ({
94158
94632
  diagnostics: [...diagnostics],
94159
94633
  score,
@@ -94283,6 +94757,32 @@ const materializeStagedFiles = async (input) => {
94283
94757
  };
94284
94758
  };
94285
94759
  //#endregion
94760
+ //#region src/cli/utils/deduplicate-project-scans.ts
94761
+ const deduplicateProjectScans = (projectScans) => {
94762
+ const uniqueProjectScans = [];
94763
+ const seenDirectories = /* @__PURE__ */ new Set();
94764
+ for (const projectScan of projectScans) {
94765
+ if (seenDirectories.has(projectScan.directory)) continue;
94766
+ seenDirectories.add(projectScan.directory);
94767
+ uniqueProjectScans.push(projectScan);
94768
+ }
94769
+ return uniqueProjectScans;
94770
+ };
94771
+ //#endregion
94772
+ //#region src/cli/utils/collect-project-source-file-counts.ts
94773
+ const collectProjectSourceFileCounts = async (rootDirectory, projectDirectories) => {
94774
+ const abortController = new AbortController();
94775
+ const unregisterAbortController = activeScanAbortRegistry.register(abortController);
94776
+ try {
94777
+ return await collectSourceFileCountsByDirectory(rootDirectory, projectDirectories, abortController.signal);
94778
+ } finally {
94779
+ unregisterAbortController();
94780
+ }
94781
+ };
94782
+ //#endregion
94783
+ //#region src/cli/utils/format-skipped-projects-message.ts
94784
+ const formatSkippedProjectsMessage = (projectCount) => `${projectCount} ${projectCount === 1 ? "project was" : "projects were"} skipped because the max scan duration was reached.`;
94785
+ //#endregion
94286
94786
  //#region src/cli/utils/is-share-opted-out.ts
94287
94787
  const isShareOptedOut = (scans, flagNoScore) => Boolean(flagNoScore) || scans.some((scan) => Boolean(scan.config?.noScore) || scan.config?.share === false);
94288
94788
  //#endregion
@@ -96301,6 +96801,27 @@ const resolveCliInspectOptions = (flags, userConfig) => {
96301
96801
  };
96302
96802
  };
96303
96803
  //#endregion
96804
+ //#region src/cli/utils/resolve-workspace-dead-code-owner.ts
96805
+ const resolveWorkspaceDeadCodeOwner = (input) => {
96806
+ if (!input.isRootDeadCodeEnabled) return null;
96807
+ const resolvedRootDirectory = Path.resolve(input.rootDirectory);
96808
+ return input.projectDirectories.find((projectDirectory) => Path.resolve(projectDirectory) === resolvedRootDirectory) ?? null;
96809
+ };
96810
+ //#endregion
96811
+ //#region src/cli/utils/retry-missing-project-scores.ts
96812
+ const retryMissingProjectScores = async (projectScans) => mapWithConcurrency(projectScans, 64, async (projectScan) => {
96813
+ if (!(projectScan.result.score === null && !projectScan.isScoreDisabled && !hasIncompleteScoreAnalysis(projectScan.result.skippedChecks))) return projectScan;
96814
+ const score = await calculateScore(filterScansForSurface([projectScan], "score"));
96815
+ recordCount(METRIC.scanScoreRetry, 1, { succeeded: score !== null });
96816
+ return score === null ? projectScan : {
96817
+ ...projectScan,
96818
+ result: {
96819
+ ...projectScan.result,
96820
+ score
96821
+ }
96822
+ };
96823
+ });
96824
+ //#endregion
96304
96825
  //#region src/cli/utils/resolve-project-relative-directory.ts
96305
96826
  /**
96306
96827
  * The scanned project's directory relative to the diff root, forward-slashed —
@@ -96361,6 +96882,16 @@ const resolveProjectChangedLineRanges = (rootDirectory, projectDirectory, ranges
96361
96882
  });
96362
96883
  };
96363
96884
  //#endregion
96885
+ //#region src/cli/utils/resolve-project-scan.ts
96886
+ const resolveProjectScan = async (rootScanTarget, projectDirectory) => {
96887
+ const projectScanTarget = projectDirectory === rootScanTarget.resolvedDirectory ? rootScanTarget : await resolveScanTarget(projectDirectory, { allowAmbiguous: true });
96888
+ return {
96889
+ directory: projectScanTarget.resolvedDirectory,
96890
+ config: projectDirectory === rootScanTarget.resolvedDirectory ? rootScanTarget.userConfig : mergeReactDoctorConfigs(rootScanTarget.userConfig, projectScanTarget.userConfig ?? void 0),
96891
+ configSourceDirectory: projectScanTarget.userConfig?.plugins === void 0 ? rootScanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory
96892
+ };
96893
+ };
96894
+ //#endregion
96364
96895
  //#region src/cli/utils/build-diagnostic-issue-url.ts
96365
96896
  const formatRuleIdentifier = (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`;
96366
96897
  const buildDiagnosticIssueBody = (input) => {
@@ -96722,6 +97253,15 @@ const filterCompletedScansByCategories = (completedScans, categoryFilters) => {
96722
97253
  }
96723
97254
  }));
96724
97255
  };
97256
+ const reportSkippedProjects = (input) => {
97257
+ input.skippedProjects.sort((left, right) => left.directory.localeCompare(right.directory));
97258
+ if (input.skippedProjects.length === 0) return;
97259
+ recordCount(METRIC.scanProjectSkipped, input.skippedProjects.length, { reason: "max-duration" });
97260
+ if (!input.isQuiet) {
97261
+ cliLogger.warn(formatSkippedProjectsMessage(input.skippedProjects.length));
97262
+ cliLogger.break();
97263
+ }
97264
+ };
96725
97265
  /**
96726
97266
  * Post-scan finalization shared by the staged-arm and project-loop
96727
97267
  * paths of `inspectAction`: emit the JSON report (when in JSON mode)
@@ -96736,7 +97276,7 @@ const filterCompletedScansByCategories = (completedScans, categoryFilters) => {
96736
97276
  */
96737
97277
  const finalizeScans = (input) => {
96738
97278
  const baselineDeltas = input.completedScans.flatMap((scan) => scan.result.baselineDelta ? [scan.result.baselineDelta] : []);
96739
- const baselineComputed = input.completedScans.length > 0 && input.completedScans.every((scan) => scan.result.baselineDelta !== void 0);
97279
+ const baselineComputed = input.skippedProjects.length === 0 && input.completedScans.length > 0 && input.completedScans.every((scan) => scan.result.baselineDelta !== void 0);
96740
97280
  const baselineDegraded = input.baselineIntended && !baselineComputed;
96741
97281
  const mode = baselineDegraded ? "diff" : input.mode;
96742
97282
  const isReactDetected = input.completedScans.some((scan) => hasReactRuntime(scan.result.project));
@@ -96757,6 +97297,7 @@ const finalizeScans = (input) => {
96757
97297
  mode,
96758
97298
  diff: input.diff,
96759
97299
  scans: jsonCompletedScans,
97300
+ skippedProjects: input.skippedProjects,
96760
97301
  totalElapsedMilliseconds: performance$1.now() - input.startTime,
96761
97302
  baseline,
96762
97303
  baselineDegraded
@@ -96877,16 +97418,16 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
96877
97418
  return;
96878
97419
  }
96879
97420
  const buildProjectScanContext = async (projectDirectory) => {
96880
- const projectScanTarget = projectDirectory === resolvedDirectory ? scanTarget : await resolveScanTarget(projectDirectory, { allowAmbiguous: true });
96881
- const scanDirectory = projectScanTarget.resolvedDirectory;
97421
+ const projectScan = await resolveProjectScan(scanTarget, projectDirectory);
97422
+ const scanDirectory = projectScan.directory;
96882
97423
  const treeRelativeDirectory = resolveProjectRelativeDirectory(resolvedDirectory, scanDirectory);
96883
97424
  if (treeRelativeDirectory === null) return null;
96884
97425
  return {
96885
97426
  projectDirectory,
96886
97427
  scanDirectory,
96887
97428
  treeRelativeDirectory,
96888
- projectConfig: projectDirectory === resolvedDirectory ? userConfig : mergeReactDoctorConfigs(userConfig, projectScanTarget.userConfig ?? void 0),
96889
- projectConfigSourceDirectory: projectScanTarget.userConfig?.plugins === void 0 ? scanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory
97429
+ projectConfig: projectScan.config,
97430
+ projectConfigSourceDirectory: projectScan.configSourceDirectory
96890
97431
  };
96891
97432
  };
96892
97433
  const projectScanContexts = [];
@@ -96973,6 +97514,7 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
96973
97514
  includePaths: resolveProjectSourceFilePaths(resolvedDirectory, projectScan.scanDirectory, projectScan.stagedFiles.filter((stagedFile) => materializedStagedFiles.has(stagedFile)))
96974
97515
  })).filter((projectRun) => projectRun.includePaths.length > 0);
96975
97516
  const isMultiProject = stagedProjectRuns.length > 1;
97517
+ const skippedProjects = [];
96976
97518
  if (stagedProjectRuns.length === 0) {
96977
97519
  snapshot.cleanup();
96978
97520
  reportNothingToScan({
@@ -96988,6 +97530,13 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
96988
97530
  }
96989
97531
  const scanStagedProject = async (projectRun) => {
96990
97532
  const { projectScan, includePaths } = projectRun;
97533
+ if (scanDeadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(scanDeadlineEpochMs) === 0) {
97534
+ skippedProjects.push({
97535
+ directory: projectScan.scanDirectory,
97536
+ reason: "max-duration"
97537
+ });
97538
+ return null;
97539
+ }
96991
97540
  const projectTempDirectory = Path.join(snapshot.tempDirectory, projectScan.treeRelativeDirectory);
96992
97541
  const scanResult = await inspectProject(projectTempDirectory, {
96993
97542
  ...scanOptions,
@@ -97028,7 +97577,11 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97028
97577
  });
97029
97578
  const completedScans = stagedBatch.completedScans;
97030
97579
  try {
97031
- if (!isQuiet && isMultiProject) {
97580
+ reportSkippedProjects({
97581
+ skippedProjects,
97582
+ isQuiet
97583
+ });
97584
+ if (!isQuiet && isMultiProject && completedScans.length > 0) {
97032
97585
  const showShareLink = !isShareOptedOut(completedScans, scanOptions.noScore) && !scanOptions.isCi;
97033
97586
  await runPromise(printMultiProjectSummary({
97034
97587
  completedScans,
@@ -97046,6 +97599,7 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97046
97599
  }
97047
97600
  finalizeScans({
97048
97601
  completedScans,
97602
+ skippedProjects,
97049
97603
  mode: "staged",
97050
97604
  diff: null,
97051
97605
  baselineIntended: false,
@@ -97121,12 +97675,28 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97121
97675
  }
97122
97676
  cliLogger.break();
97123
97677
  }
97124
- const isMultiProject = projectDirectories.length > 1;
97125
- const scanProject = async (projectDirectory) => {
97126
- const projectScanTarget = projectDirectory === resolvedDirectory ? scanTarget : await resolveScanTarget(projectDirectory, { allowAmbiguous: true });
97127
- const scanDirectory = projectScanTarget.resolvedDirectory;
97128
- const projectConfig = projectDirectory === resolvedDirectory ? userConfig : mergeReactDoctorConfigs(userConfig, projectScanTarget.userConfig ?? void 0);
97129
- const projectConfigSourceDirectory = projectScanTarget.userConfig?.plugins === void 0 ? scanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory;
97678
+ const projectScans = deduplicateProjectScans(await Promise.all(projectDirectories.map((projectDirectory) => resolveProjectScan(scanTarget, projectDirectory))));
97679
+ const isMultiProject = projectScans.length > 1;
97680
+ const rootProjectScan = projectScans.find((projectScan) => Path.resolve(projectScan.directory) === Path.resolve(resolvedDirectory));
97681
+ const workspaceDeadCodeOwner = resolveWorkspaceDeadCodeOwner({
97682
+ rootDirectory: resolvedDirectory,
97683
+ projectDirectories: projectScans.map((projectScan) => projectScan.directory),
97684
+ isRootDeadCodeEnabled: scanOptions.deadCode ?? rootProjectScan?.config?.deadCode ?? true
97685
+ });
97686
+ if (workspaceDeadCodeOwner !== null) recordCount(METRIC.scanWorkspaceDeadCodeShared, 1, { projectCount: projectScans.length });
97687
+ const precomputedSourceFileCounts = isMultiProject && !isDiffMode ? await collectProjectSourceFileCounts(resolvedDirectory, projectScans.map((projectScan) => projectScan.directory)) : null;
97688
+ const skippedProjects = [];
97689
+ const scanProject = async (projectScan) => {
97690
+ if (scanDeadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(scanDeadlineEpochMs) === 0) {
97691
+ skippedProjects.push({
97692
+ directory: projectScan.directory,
97693
+ reason: "max-duration"
97694
+ });
97695
+ return null;
97696
+ }
97697
+ const scanDirectory = projectScan.directory;
97698
+ const projectConfig = projectScan.config;
97699
+ const ownsWorkspaceDeadCode = scanDirectory === workspaceDeadCodeOwner;
97130
97700
  const supplyChainEnabled = flags.supplyChain ?? projectConfig?.supplyChain?.enabled !== false;
97131
97701
  let includePaths;
97132
97702
  let supplyChainManifestChanged = false;
@@ -97150,12 +97720,16 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97150
97720
  if (!isQuiet && !isMultiProject) cliLogger.dim(" ");
97151
97721
  const scanResult = await inspectProject(scanDirectory, {
97152
97722
  ...scanOptions,
97723
+ deadCode: workspaceDeadCodeOwner === null ? scanOptions.deadCode : ownsWorkspaceDeadCode,
97724
+ precomputedSourceFileCount: precomputedSourceFileCounts?.get(scanDirectory),
97153
97725
  deadlineEpochMs: scanDeadlineEpochMs,
97154
97726
  includePaths,
97155
97727
  configOverride: projectConfig,
97156
- configSourceDirectory: projectConfigSourceDirectory ?? void 0,
97728
+ configSourceDirectory: projectScan.configSourceDirectory ?? void 0,
97157
97729
  suppressRendering: isMultiProject,
97158
97730
  concurrentScan: isMultiProject,
97731
+ excludedProjectDirectories: projectScans.filter((candidateProjectScan) => isPathInsideDirectory(candidateProjectScan.directory, scanDirectory)).map((candidateProjectScan) => candidateProjectScan.directory),
97732
+ retainExcludedProjectDeadCodeDiagnostics: ownsWorkspaceDeadCode,
97159
97733
  baseline: baselineRef !== null && projectBaselineBaseFiles !== null && projectBaselineHeadFiles !== null ? {
97160
97734
  ref: baselineRef,
97161
97735
  baseFiles: projectBaselineBaseFiles,
@@ -97172,12 +97746,19 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97172
97746
  };
97173
97747
  };
97174
97748
  const projectBatch = await runProjectScanBatch({
97175
- projects: projectDirectories,
97749
+ projects: projectScans,
97176
97750
  isQuiet,
97177
97751
  isSilent: scanOptions.silent === true,
97178
97752
  scanProject
97179
97753
  });
97180
- const completedScans = projectBatch.completedScans;
97754
+ const completedScans = await retryMissingProjectScores(projectBatch.completedScans.map((completedScan) => ({
97755
+ ...completedScan,
97756
+ isScoreDisabled: scanOptions.noScore ?? completedScan.config?.noScore ?? false
97757
+ })));
97758
+ reportSkippedProjects({
97759
+ skippedProjects,
97760
+ isQuiet
97761
+ });
97181
97762
  if (!isQuiet && isMultiProject && completedScans.length > 0) {
97182
97763
  const showShareLink = !isShareOptedOut(completedScans, scanOptions.noScore) && !scanOptions.isCi;
97183
97764
  await runPromise(printMultiProjectSummary({
@@ -97195,6 +97776,7 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97195
97776
  if (flags.outputDir && !didScansWriteDump) await runPromise(printDiagnosticsDump(selectedSurfaceDiagnostics, flags.outputDir, false, isQuiet ? "stderr" : "stdout"));
97196
97777
  finalizeScans({
97197
97778
  completedScans,
97779
+ skippedProjects,
97198
97780
  mode: baselineRef ? "baseline" : isDiffMode ? "diff" : "full",
97199
97781
  diff: isDiffMode ? diffInfo : null,
97200
97782
  baselineIntended: scope === "changed" && diffInfo !== null && !diffInfo.isCurrentChanges,
@@ -97292,6 +97874,7 @@ const shouldUseTui = (input) => {
97292
97874
  //#endregion
97293
97875
  //#region src/cli/commands/scan.ts
97294
97876
  const runScanCommand = async (input) => {
97877
+ if (input.flags.cache === false) process.env.REACT_DOCTOR_NO_CACHE = "1";
97295
97878
  const nodeMajorVersion = Number(process.versions.node.split(".")[0]);
97296
97879
  const tuiEnvironment = {
97297
97880
  flags: input.flags,
@@ -97316,7 +97899,7 @@ const runScanCommand = async (input) => {
97316
97899
  return;
97317
97900
  }
97318
97901
  recordCount(METRIC.cliInvoked, 1, { command: input.invocationCommand });
97319
- const { runScanApp } = await import("./run-scan-app-DtYeufgr.js");
97902
+ const { runScanApp } = await import("./run-scan-app-QbtG43NL.js");
97320
97903
  const { shouldFail } = await runScanApp({
97321
97904
  directory: input.directory,
97322
97905
  scanTarget,
@@ -97968,11 +98551,27 @@ const ensureWindowsUtf8Console = () => {
97968
98551
  } catch {}
97969
98552
  };
97970
98553
  //#endregion
98554
+ //#region src/cli/utils/active-tui-renderer.ts
98555
+ let activeTuiRenderer = null;
98556
+ const registerActiveTuiRenderer = (renderer) => {
98557
+ activeTuiRenderer = renderer;
98558
+ return () => {
98559
+ if (activeTuiRenderer === renderer) activeTuiRenderer = null;
98560
+ };
98561
+ };
98562
+ const clearActiveTuiRenderer = () => {
98563
+ const renderer = activeTuiRenderer;
98564
+ activeTuiRenderer = null;
98565
+ renderer?.clear();
98566
+ };
98567
+ //#endregion
97971
98568
  //#region src/cli/utils/exit-gracefully.ts
97972
98569
  let didStartExiting = false;
97973
98570
  const exitGracefully = () => {
97974
98571
  if (didStartExiting) return process.exit(130);
97975
98572
  didStartExiting = true;
98573
+ activeScanAbortRegistry.abortAll();
98574
+ clearActiveTuiRenderer();
97976
98575
  try {
97977
98576
  if (isJsonModeActive()) writeJsonErrorReport(/* @__PURE__ */ new Error("Scan cancelled by user (SIGINT/SIGTERM)"));
97978
98577
  else console.log([
@@ -98102,6 +98701,7 @@ const ROOT_FLAG_SPEC = {
98102
98701
  "--json-compact",
98103
98702
  "--lint",
98104
98703
  "--no-color",
98704
+ "--no-cache",
98105
98705
  "--no-dead-code",
98106
98706
  "--no-lint",
98107
98707
  "--no-parallel",
@@ -98225,13 +98825,18 @@ const COMMAND_FLAG_SPECS = new Map([
98225
98825
  longOptionsWithoutValues: new Set([
98226
98826
  "--color",
98227
98827
  "--help",
98828
+ "--no-cache",
98228
98829
  "--no-color",
98229
98830
  "--no-dead-code",
98230
98831
  "--no-score",
98231
98832
  "--no-supply-chain",
98232
98833
  "--yes"
98233
98834
  ]),
98234
- longOptionsWithRequiredValues: new Set(["--blocking", "--project"]),
98835
+ longOptionsWithRequiredValues: new Set([
98836
+ "--blocking",
98837
+ "--max-duration",
98838
+ "--project"
98839
+ ]),
98235
98840
  longOptionsWithOptionalValues: /* @__PURE__ */ new Set(),
98236
98841
  shortOptionsWithoutValues: new Set(["-h", "-y"]),
98237
98842
  shortOptionsWithRequiredValues: new Set(["-p"])
@@ -98373,6 +98978,7 @@ ${highlighter.dim("Scope:")}
98373
98978
  Dead-code, supply-chain, external lint-config, custom-plugin, and health-score passes are skipped.
98374
98979
  Standard scan flags such as ${highlighter.info("--scope")}, ${highlighter.info("--project")}, ${highlighter.info("--verbose")}, and ${highlighter.info("--json")} still work.
98375
98980
  `;
98981
+ const MAX_DURATION_OPTION_DESCRIPTION = "scan time budget for the whole run, shared across workspace projects: past it, queued projects, remaining lint batches, and dead-code are skipped and partial results are reported (skipped files and projects are listed in the JSON report)";
98376
98982
  const renderCiHelpEpilog = () => `
98377
98983
  ${highlighter.dim("Examples:")}
98378
98984
  ${formatExampleLines([
@@ -98392,7 +98998,7 @@ ${highlighter.dim("Learn more:")}
98392
98998
  ${highlighter.info(CI_URL)}
98393
98999
  `;
98394
99000
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
98395
- const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much supported JS/TS and inline HTML script source to scan/report: full (default), files, changed (only new issues vs base), or lines (issues whose source spans touch changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").option("--include-untracked", "with --scope files/changed/lines, also scan ordinary untracked files (respects .gitignore)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks (honors --project and config `projects`; exits 0 when nothing is staged)").option("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
99001
+ const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("--no-cache", "disable all scan caches for this run").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much supported JS/TS and inline HTML script source to scan/report: full (default), files, changed (only new issues vs base), or lines (issues whose source spans touch changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").option("--include-untracked", "with --scope files/changed/lines, also scan ordinary untracked files (respects .gitignore)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks (honors --project and config `projects`; exits 0 when nothing is staged)").option("--max-duration <seconds>", MAX_DURATION_OPTION_DESCRIPTION).option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
98396
99002
  program.action((directory = ".", flags) => runScanCommand({
98397
99003
  directory,
98398
99004
  flags,
@@ -98418,7 +99024,7 @@ rules.command("category <category> <severity>").description("Set the severity fo
98418
99024
  rules.command("ignore-tag <tag>").description("Skip a whole rule family by tag before linting (e.g. design)").option("-c, --cwd <cwd>", "working directory", process.cwd()).action((tag, _options, command) => rulesIgnoreTagAction(tag, command.optsWithGlobals()));
98419
99025
  rules.command("unignore-tag <tag>").description("Stop ignoring a tag previously skipped via ignore-tag").option("-c, --cwd <cwd>", "working directory", process.cwd()).action((tag, _options, command) => rulesUnignoreTagAction(tag, command.optsWithGlobals()));
98420
99026
  program.command("experimental-lsp", { hidden: false }).description("[experimental] run the React Doctor language server over stdio (for editors)").allowUnknownOption().action(() => {});
98421
- program.command("experimental-tui [directory]", { hidden: true }).description("[experimental] interactive, scrollable scan report").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").option("--no-dead-code", "skip dead-code analysis").option("--no-supply-chain", "skip the dependency supply-chain scan").option("--score", "only print the numeric score (for scripts and CI)").option("--no-score", "skip the score API, the share URL, and crash reporting").option("-p, --project <names>", "scan specific workspace projects (comma-separated, or *)").option("-y, --yes", "skip the project prompt and scan every discovered project").action((directory = ".", _localOptions, command) => runScanCommand({
99027
+ program.command("experimental-tui [directory]", { hidden: true }).description("[experimental] interactive, scrollable scan report").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").option("--no-dead-code", "skip dead-code analysis").option("--no-supply-chain", "skip the dependency supply-chain scan").option("--score", "only print the numeric score (for scripts and CI)").option("--no-score", "skip the score API, the share URL, and crash reporting").option("--no-cache", "disable all scan caches for this run").option("--max-duration <seconds>", MAX_DURATION_OPTION_DESCRIPTION).option("-p, --project <names>", "scan specific workspace projects (comma-separated, or *)").option("-y, --yes", "skip the project prompt and scan every discovered project").action((directory = ".", _localOptions, command) => runScanCommand({
98422
99028
  directory,
98423
99029
  flags: command.optsWithGlobals(),
98424
99030
  invocationCommand: "experimental-tui"
@@ -98449,7 +99055,7 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
98449
99055
  handleError(error, { sentryEventId });
98450
99056
  });
98451
99057
  //#endregion
98452
- export { highlighter as $, hasLintHardFailure as A, isUnicodeSupported as B, shouldRecordOnboarding as C, buildCodeFrame as D, formatElapsedTime as E, resolveBlockingLevel as F, isCiEnvironment as G, METRIC as H, isReactDoctorWorkflowInstalled as I, Progress as J, DIAGNOSTIC_CATEGORY_BUCKETS as K, recordCount as L, buildSortedRuleGroups as M, formatFixRecipeLine as N, buildEmptyReportMessage as O, formatLearnMoreLine as P, hasPublishedFixRecipe as Q, toForwardSlashes as R, isOnboardingForced as S, computeProjectedScore as T, TUI_REPORT_COMPACT_MAX_ROWS as U, CI_TRUST_COMPANIES as V, TUI_REPORT_DETAIL_WIDTH_FRACTION as W, buildRuleDocsUrl as X, Reporter as Y, getCategoryImpact as Z, printFooter as _, resolveCliInspectOptions as a, void_ as at, markOnboardingComplete as b, setUpGitHubActions as c, CLI_AGENT_BINARIES as d, mapWithConcurrency as et, copyToClipboard as f, createInvocationInspect as g, isShareOptedOut as h, selectProjects as i, sync as it, buildRulePriorityMap as j, buildNoScoreMessage as k, openUrl as l, buildHandoffPayload as m, shouldFailScanGate as n, resolveScanTarget as nt, filterScansForSurface as o, succeed$3 as ot, launchCliAgent as p, GITHUB_ACTIONS_SETUP_URL as q, discoverWorkspacePackages as r, runPromise as rt, countUniqueScannedFiles as s, exitGracefully as t, mergeReactDoctorConfigs as tt, detectLaunchableAgents as u, easeOutCubic as v, filterDiagnosticsByCategories as w, canAnimateOnboarding as x, doctorFace as y, findNearestPackageDirectory as z };
99058
+ export { TUI_REPORT_DETAIL_WIDTH_FRACTION as $, shouldRecordOnboarding as A, buildSortedRuleGroups as B, createInvocationInspect as C, markOnboardingComplete as D, doctorFace as E, buildEmptyReportMessage as F, recordCount as G, formatLearnMoreLine as H, hasIncompleteScoreAnalysis as I, findNearestPackageDirectory as J, recordDistribution as K, buildNoScoreMessage as L, computeProjectedScore as M, formatElapsedTime as N, canAnimateOnboarding as O, buildCodeFrame as P, TUI_REPORT_COMPACT_MAX_ROWS as Q, hasLintHardFailure as R, deduplicateProjectScans as S, easeOutCubic as T, resolveBlockingLevel as U, formatFixRecipeLine as V, isReactDoctorWorkflowInstalled as W, CI_TRUST_COMPANIES as X, isUnicodeSupported as Y, METRIC as Z, launchCliAgent as _, succeed$3 as _t, selectProjects as a, buildRuleDocsUrl as at, formatSkippedProjectsMessage as b, resolveWorkspaceDeadCodeOwner as c, highlighter as ct, countUniqueScannedFiles as d, remainingDeadlineBudgetMs as dt, isCiEnvironment as et, setUpGitHubActions as f, resolveScanTarget as ft, copyToClipboard as g, void_ as gt, CLI_AGENT_BINARIES as h, sync as ht, discoverWorkspacePackages as i, Reporter as it, filterDiagnosticsByCategories as j, isOnboardingForced as k, resolveCliInspectOptions as l, isPathInsideDirectory as lt, detectLaunchableAgents as m, runPromise as mt, registerActiveTuiRenderer as n, GITHUB_ACTIONS_SETUP_URL as nt, resolveProjectScan as o, getCategoryImpact as ot, openUrl as p, yieldToEventLoop as pt, toForwardSlashes as q, shouldFailScanGate as r, Progress as rt, retryMissingProjectScores as s, hasPublishedFixRecipe as st, exitGracefully as t, DIAGNOSTIC_CATEGORY_BUCKETS as tt, filterScansForSurface as u, mapWithConcurrency as ut, buildHandoffPayload as v, printFooter as w, collectProjectSourceFileCounts as x, isShareOptedOut as y, buildRulePriorityMap as z };
98453
99059
 
98454
99060
  //# sourceMappingURL=cli.js.map
98455
- //# debugId=e4075e6f-4128-5af6-bc97-656272450282
99061
+ //# debugId=80fc688d-8e21-59d6-abcd-27ce2265a7d3