react-doctor 0.9.3 → 0.9.4

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]="707bd0ef-8490-5120-ba82-f65ad0b490e1")}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]="d76ac03b-d6e7-5598-a5a0-ce6e9210ddaf")}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 = [
@@ -65419,7 +65488,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
65419
65488
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
65420
65489
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
65421
65490
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
65422
- const CORE_PACKAGE_VERSION = "0.9.3";
65491
+ const CORE_PACKAGE_VERSION = "0.9.4";
65423
65492
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
65424
65493
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
65425
65494
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -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 = "";
@@ -67185,13 +67402,7 @@ const hasReactDependency = (packageJson) => {
67185
67402
  };
67186
67403
  return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
67187
67404
  };
67188
- const getPreactVersion = (packageJson) => {
67189
- return {
67190
- ...packageJson.peerDependencies,
67191
- ...packageJson.dependencies,
67192
- ...packageJson.devDependencies
67193
- }.preact ?? null;
67194
- };
67405
+ const getPreactVersion = (packageJson) => getDependencySpec(packageJson, "preact");
67195
67406
  const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, sourceDirectory = rootDirectory, sourcePackageJson = rootPackageJson, version }) => {
67196
67407
  if (version === null || !isCatalogReference(version)) return version;
67197
67408
  const catalogName = extractCatalogName(version);
@@ -67933,7 +68144,16 @@ const getBrowserslistProtectionRequirement = (directory) => {
67933
68144
  if (!queries) return void 0;
67934
68145
  let requirement;
67935
68146
  let hasUnsupportedTarget = false;
67936
- for (const browserTarget of (0, import_browserslist.default)(queries)) {
68147
+ const previousIgnoreOldData = process.env.BROWSERSLIST_IGNORE_OLD_DATA;
68148
+ let browserTargets = [];
68149
+ try {
68150
+ process.env.BROWSERSLIST_IGNORE_OLD_DATA = "true";
68151
+ browserTargets = (0, import_browserslist.default)(queries);
68152
+ } finally {
68153
+ if (previousIgnoreOldData === void 0) delete process.env.BROWSERSLIST_IGNORE_OLD_DATA;
68154
+ else process.env.BROWSERSLIST_IGNORE_OLD_DATA = previousIgnoreOldData;
68155
+ }
68156
+ for (const browserTarget of browserTargets) {
67937
68157
  const browserRequirement = getBrowserProtectionRequirement(browserTarget);
67938
68158
  if (browserRequirement === "unsupported") hasUnsupportedTarget = true;
67939
68159
  if (browserRequirement === "noreferrer") requirement = "noreferrer";
@@ -68039,9 +68259,9 @@ const cachedProjectInfos = /* @__PURE__ */ new Map();
68039
68259
  * framework-agnostic rules. Throws only when the directory has nothing
68040
68260
  * to scan (no enclosing project and no source files of its own).
68041
68261
  */
68042
- const discoverProjectWithoutPackageJson = (directory) => {
68043
- const sourceFileCount = countSourceFiles(directory);
68044
- const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
68262
+ const discoverProjectWithoutPackageJson = (directory, options) => {
68263
+ const sourceFileCount = options.sourceFileCount ?? countSourceFiles(directory);
68264
+ const hasOwnTsConfig = isFile(Path.join(directory, "tsconfig.json"));
68045
68265
  const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
68046
68266
  const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
68047
68267
  if (enclosingProject !== null) return {
@@ -68107,12 +68327,15 @@ const discoverProjectWithoutPackageJson = (directory) => {
68107
68327
  sourceFileCount
68108
68328
  };
68109
68329
  };
68110
- const discoverProject = (directory) => {
68330
+ const discoverProject = (directory, options = {}) => {
68111
68331
  const cached = cachedProjectInfos.get(directory);
68112
- if (cached !== void 0) return cached;
68332
+ if (cached !== void 0) return options.sourceFileCount === void 0 ? cached : {
68333
+ ...cached,
68334
+ sourceFileCount: options.sourceFileCount
68335
+ };
68113
68336
  const packageJsonPath = Path.join(directory, "package.json");
68114
68337
  if (!isFile(packageJsonPath)) {
68115
- const synthesized = discoverProjectWithoutPackageJson(directory);
68338
+ const synthesized = discoverProjectWithoutPackageJson(directory, options);
68116
68339
  cachedProjectInfos.set(directory, synthesized);
68117
68340
  return synthesized;
68118
68341
  }
@@ -68174,8 +68397,8 @@ const discoverProject = (directory) => {
68174
68397
  const tailwindVersion = tailwindcss.version;
68175
68398
  const zodVersion = zod.version;
68176
68399
  const projectName = packageJson.name ?? Path.basename(directory);
68177
- const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
68178
- const sourceFileCount = countSourceFiles(directory);
68400
+ const hasTypeScript = isFile(Path.join(directory, "tsconfig.json"));
68401
+ const sourceFileCount = options.sourceFileCount ?? countSourceFiles(directory);
68179
68402
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || workspaceFacts.hasReactNativeAwarePackage;
68180
68403
  const expoVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
68181
68404
  rootDirectory: directory,
@@ -68304,6 +68527,155 @@ const discoverProject = (directory) => {
68304
68527
  * single source file has nothing to analyze.
68305
68528
  */
68306
68529
  const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null || project.sourceFileCount > 0;
68530
+ const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
68531
+ "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
68532
+ "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
68533
+ "effect/no-derived-state": "react-doctor/no-derived-state",
68534
+ "effect/no-event-handler": "react-doctor/no-event-handler",
68535
+ "effect/no-initialize-state": "react-doctor/no-initialize-state",
68536
+ "effect/no-pass-data-to-parent": "react-doctor/no-pass-data-to-parent",
68537
+ "effect/no-pass-live-state-to-parent": "react-doctor/no-pass-live-state-to-parent",
68538
+ "effect/no-reset-all-state-on-prop-change": "react-doctor/no-reset-all-state-on-prop-change",
68539
+ "jsx-a11y/alt-text": "react-doctor/alt-text",
68540
+ "jsx-a11y/anchor-ambiguous-text": "react-doctor/anchor-ambiguous-text",
68541
+ "jsx-a11y/anchor-has-content": "react-doctor/anchor-has-content",
68542
+ "jsx-a11y/anchor-is-valid": "react-doctor/anchor-is-valid",
68543
+ "jsx-a11y/aria-activedescendant-has-tabindex": "react-doctor/aria-activedescendant-has-tabindex",
68544
+ "jsx-a11y/aria-props": "react-doctor/aria-props",
68545
+ "jsx-a11y/aria-proptypes": "react-doctor/aria-proptypes",
68546
+ "jsx-a11y/aria-role": "react-doctor/aria-role",
68547
+ "jsx-a11y/aria-unsupported-elements": "react-doctor/aria-unsupported-elements",
68548
+ "jsx-a11y/autocomplete-valid": "react-doctor/autocomplete-valid",
68549
+ "jsx-a11y/click-events-have-key-events": "react-doctor/click-events-have-key-events",
68550
+ "jsx-a11y/control-has-associated-label": "react-doctor/control-has-associated-label",
68551
+ "jsx-a11y/heading-has-content": "react-doctor/heading-has-content",
68552
+ "jsx-a11y/html-has-lang": "react-doctor/html-has-lang",
68553
+ "jsx-a11y/iframe-has-title": "react-doctor/iframe-has-title",
68554
+ "jsx-a11y/img-redundant-alt": "react-doctor/img-redundant-alt",
68555
+ "jsx-a11y/interactive-supports-focus": "react-doctor/interactive-supports-focus",
68556
+ "jsx-a11y/label-has-associated-control": "react-doctor/label-has-associated-control",
68557
+ "jsx-a11y/lang": "react-doctor/lang",
68558
+ "jsx-a11y/media-has-caption": "react-doctor/media-has-caption",
68559
+ "jsx-a11y/mouse-events-have-key-events": "react-doctor/mouse-events-have-key-events",
68560
+ "jsx-a11y/no-access-key": "react-doctor/no-access-key",
68561
+ "jsx-a11y/no-aria-hidden-on-focusable": "react-doctor/no-aria-hidden-on-focusable",
68562
+ "jsx-a11y/no-autofocus": "react-doctor/no-autofocus",
68563
+ "jsx-a11y/no-distracting-elements": "react-doctor/no-distracting-elements",
68564
+ "jsx-a11y/no-interactive-element-to-noninteractive-role": "react-doctor/no-interactive-element-to-noninteractive-role",
68565
+ "jsx-a11y/no-noninteractive-element-interactions": "react-doctor/no-noninteractive-element-interactions",
68566
+ "jsx-a11y/no-noninteractive-element-to-interactive-role": "react-doctor/no-noninteractive-element-to-interactive-role",
68567
+ "jsx-a11y/no-noninteractive-tabindex": "react-doctor/no-noninteractive-tabindex",
68568
+ "jsx-a11y/no-redundant-roles": "react-doctor/no-redundant-roles",
68569
+ "jsx-a11y/no-static-element-interactions": "react-doctor/no-static-element-interactions",
68570
+ "jsx-a11y/prefer-tag-over-role": "react-doctor/prefer-tag-over-role",
68571
+ "jsx-a11y/role-has-required-aria-props": "react-doctor/role-has-required-aria-props",
68572
+ "jsx-a11y/role-supports-aria-props": "react-doctor/role-supports-aria-props",
68573
+ "jsx-a11y/scope": "react-doctor/scope",
68574
+ "jsx-a11y/tabindex-no-positive": "react-doctor/tabindex-no-positive",
68575
+ "react-hooks/exhaustive-deps": "react-doctor/exhaustive-deps",
68576
+ "react-hooks/rules-of-hooks": "react-doctor/rules-of-hooks",
68577
+ "react-perf/jsx-no-jsx-as-prop": "react-doctor/jsx-no-jsx-as-prop",
68578
+ "react-perf/jsx-no-new-array-as-prop": "react-doctor/jsx-no-new-array-as-prop",
68579
+ "react-perf/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
68580
+ "react-perf/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
68581
+ "react-refresh/only-export-components": "react-doctor/only-export-components",
68582
+ "react/button-has-type": "react-doctor/button-has-type",
68583
+ "react/checked-requires-onchange-or-readonly": "react-doctor/checked-requires-onchange-or-readonly",
68584
+ "react/display-name": "react-doctor/display-name",
68585
+ "react/exhaustive-deps": "react-doctor/exhaustive-deps",
68586
+ "react/forbid-component-props": "react-doctor/forbid-component-props",
68587
+ "react/forbid-dom-props": "react-doctor/forbid-dom-props",
68588
+ "react/forbid-elements": "react-doctor/forbid-elements",
68589
+ "react/forward-ref-uses-ref": "react-doctor/forward-ref-uses-ref",
68590
+ "react/hook-use-state": "react-doctor/hook-use-state",
68591
+ "react/iframe-missing-sandbox": "react-doctor/iframe-missing-sandbox",
68592
+ "react/jsx-boolean-value": "react-doctor/jsx-boolean-value",
68593
+ "react/jsx-curly-brace-presence": "react-doctor/jsx-curly-brace-presence",
68594
+ "react/jsx-filename-extension": "react-doctor/jsx-filename-extension",
68595
+ "react/jsx-fragments": "react-doctor/jsx-fragments",
68596
+ "react/jsx-handler-names": "react-doctor/jsx-handler-names",
68597
+ "react/jsx-key": "react-doctor/jsx-key",
68598
+ "react/jsx-max-depth": "react-doctor/jsx-max-depth",
68599
+ "react/jsx-no-comment-textnodes": "react-doctor/jsx-no-comment-textnodes",
68600
+ "react/jsx-no-constructed-context-values": "react-doctor/jsx-no-constructed-context-values",
68601
+ "react/jsx-no-duplicate-props": "react-doctor/jsx-no-duplicate-props",
68602
+ "react/jsx-no-jsx-as-prop": "react-doctor/jsx-no-jsx-as-prop",
68603
+ "react/jsx-no-new-array-as-prop": "react-doctor/jsx-no-new-array-as-prop",
68604
+ "react/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
68605
+ "react/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
68606
+ "react/jsx-no-script-url": "react-doctor/jsx-no-script-url",
68607
+ "react/jsx-no-undef": "react-doctor/jsx-no-undef",
68608
+ "react/jsx-no-useless-fragment": "react-doctor/jsx-no-useless-fragment",
68609
+ "react/jsx-pascal-case": "react-doctor/jsx-pascal-case",
68610
+ "react/jsx-props-no-spread-multi": "react-doctor/jsx-props-no-spread-multi",
68611
+ "react/jsx-props-no-spreading": "react-doctor/jsx-props-no-spreading",
68612
+ "react/no-array-index-key": "react-doctor/no-array-index-key",
68613
+ "react/no-children-prop": "react-doctor/no-children-prop",
68614
+ "react/no-clone-element": "react-doctor/no-clone-element",
68615
+ "react/no-danger": "react-doctor/no-danger",
68616
+ "react/no-danger-with-children": "react-doctor/no-danger-with-children",
68617
+ "react/no-did-mount-set-state": "react-doctor/no-did-mount-set-state",
68618
+ "react/no-did-update-set-state": "react-doctor/no-did-update-set-state",
68619
+ "react/no-direct-mutation-state": "react-doctor/no-direct-mutation-state",
68620
+ "react/no-find-dom-node": "react-doctor/no-find-dom-node",
68621
+ "react/no-is-mounted": "react-doctor/no-is-mounted",
68622
+ "react/no-multi-comp": "react-doctor/no-multi-comp",
68623
+ "react/no-namespace": "react-doctor/no-namespace",
68624
+ "react/no-react-children": "react-doctor/no-react-children",
68625
+ "react/no-redundant-should-component-update": "react-doctor/no-redundant-should-component-update",
68626
+ "react/no-render-return-value": "react-doctor/no-render-return-value",
68627
+ "react/no-set-state": "react-doctor/no-set-state",
68628
+ "react/no-string-refs": "react-doctor/no-string-refs",
68629
+ "react/no-this-in-sfc": "react-doctor/no-this-in-sfc",
68630
+ "react/no-unescaped-entities": "react-doctor/no-unescaped-entities",
68631
+ "react/no-unknown-property": "react-doctor/no-unknown-property",
68632
+ "react/no-unsafe": "react-doctor/no-unsafe",
68633
+ "react/no-unstable-nested-components": "react-doctor/no-unstable-nested-components",
68634
+ "react/no-will-update-set-state": "react-doctor/no-will-update-set-state",
68635
+ "react/only-export-components": "react-doctor/only-export-components",
68636
+ "react/prefer-es6-class": "react-doctor/prefer-es6-class",
68637
+ "react/prefer-function-component": "react-doctor/prefer-function-component",
68638
+ "react/react-in-jsx-scope": "react-doctor/react-in-jsx-scope",
68639
+ "react/require-render-return": "react-doctor/require-render-return",
68640
+ "react/rules-of-hooks": "react-doctor/rules-of-hooks",
68641
+ "react/self-closing-comp": "react-doctor/self-closing-comp",
68642
+ "react/state-in-constructor": "react-doctor/state-in-constructor",
68643
+ "react/style-prop-object": "react-doctor/style-prop-object",
68644
+ "react/void-dom-elements-no-children": "react-doctor/void-dom-elements-no-children"
68645
+ };
68646
+ const NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS = /* @__PURE__ */ new Map();
68647
+ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY)) {
68648
+ const aliases = NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(nativeRuleKey) ?? [];
68649
+ aliases.push(legacyRuleKey);
68650
+ NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
68651
+ }
68652
+ const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
68653
+ const REACT_DOCTOR_RULE_KEY_PREFIX = "react-doctor/";
68654
+ const canonicalizeRuleKey = (ruleKey) => {
68655
+ const nativeRuleKey = LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey];
68656
+ return typeof nativeRuleKey === "string" ? nativeRuleKey : ruleKey;
68657
+ };
68658
+ const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
68659
+ /**
68660
+ * Canonicalizes a rule key as users write it in config: a legacy alias
68661
+ * (`react/jsx-key`) maps to its native key, and a bare short id (`no-eval`)
68662
+ * qualifies as `react-doctor/<id>` — mirroring `isSameRuleKey`'s matching —
68663
+ * so telemetry groups every spelling of one rule under one key.
68664
+ */
68665
+ const canonicalizeUserRuleKey = (ruleKey) => {
68666
+ const nativeRuleKey = canonicalizeRuleKey(ruleKey);
68667
+ return nativeRuleKey.includes("/") ? nativeRuleKey : `${REACT_DOCTOR_RULE_KEY_PREFIX}${nativeRuleKey}`;
68668
+ };
68669
+ const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
68670
+ const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
68671
+ const canonicalTarget = canonicalizeRuleKey(targetRuleKey);
68672
+ if (canonicalCandidate === canonicalTarget) return true;
68673
+ return isReactDoctorShortIdOf(canonicalCandidate, canonicalTarget) || isReactDoctorShortIdOf(canonicalTarget, canonicalCandidate);
68674
+ };
68675
+ const getEquivalentRuleKeys = (ruleKey) => {
68676
+ const nativeRuleKey = canonicalizeRuleKey(ruleKey);
68677
+ return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
68678
+ };
68307
68679
  const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
68308
68680
  var InvalidGlobPatternError = class extends Error {
68309
68681
  pattern;
@@ -68357,6 +68729,11 @@ const warnConfigIssue = (message) => {
68357
68729
  };
68358
68730
  const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
68359
68731
  const collectStringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
68732
+ const hasMatchingRuleOverride = (ruleIds, ruleIdentifier) => {
68733
+ if (ruleIds.size === 0) return true;
68734
+ for (const ruleId of ruleIds) if (isSameRuleKey(ruleId, ruleIdentifier)) return true;
68735
+ return false;
68736
+ };
68360
68737
  const validateOverrideEntry = (entry, index) => {
68361
68738
  if (!isPlainObject(entry)) {
68362
68739
  warnConfigIssue(`ignore.overrides[${index}] must be an object with { files, rules }; ignoring this entry.`);
@@ -68397,7 +68774,7 @@ const isDiagnosticIgnoredByOverrides = (diagnostic, rootDirectory, overrides) =>
68397
68774
  if (overrides.length === 0) return false;
68398
68775
  const relativeFilePath = toRelativePath(diagnostic.filePath, rootDirectory);
68399
68776
  const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
68400
- return overrides.some((override) => override.filePatterns.some((pattern) => pattern.test(relativeFilePath)) && (override.ruleIds.size === 0 || override.ruleIds.has(ruleIdentifier)));
68777
+ return overrides.some((override) => override.filePatterns.some((pattern) => pattern.test(relativeFilePath)) && hasMatchingRuleOverride(override.ruleIds, ruleIdentifier));
68401
68778
  };
68402
68779
  const SEVERITY_FOR_OVERRIDE = {
68403
68780
  error: "error",
@@ -68432,155 +68809,6 @@ const buildRuleSeverityControls = (config) => {
68432
68809
  const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
68433
68810
  const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
68434
68811
  const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
68435
- const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
68436
- "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
68437
- "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
68438
- "effect/no-derived-state": "react-doctor/no-derived-state",
68439
- "effect/no-event-handler": "react-doctor/no-event-handler",
68440
- "effect/no-initialize-state": "react-doctor/no-initialize-state",
68441
- "effect/no-pass-data-to-parent": "react-doctor/no-pass-data-to-parent",
68442
- "effect/no-pass-live-state-to-parent": "react-doctor/no-pass-live-state-to-parent",
68443
- "effect/no-reset-all-state-on-prop-change": "react-doctor/no-reset-all-state-on-prop-change",
68444
- "jsx-a11y/alt-text": "react-doctor/alt-text",
68445
- "jsx-a11y/anchor-ambiguous-text": "react-doctor/anchor-ambiguous-text",
68446
- "jsx-a11y/anchor-has-content": "react-doctor/anchor-has-content",
68447
- "jsx-a11y/anchor-is-valid": "react-doctor/anchor-is-valid",
68448
- "jsx-a11y/aria-activedescendant-has-tabindex": "react-doctor/aria-activedescendant-has-tabindex",
68449
- "jsx-a11y/aria-props": "react-doctor/aria-props",
68450
- "jsx-a11y/aria-proptypes": "react-doctor/aria-proptypes",
68451
- "jsx-a11y/aria-role": "react-doctor/aria-role",
68452
- "jsx-a11y/aria-unsupported-elements": "react-doctor/aria-unsupported-elements",
68453
- "jsx-a11y/autocomplete-valid": "react-doctor/autocomplete-valid",
68454
- "jsx-a11y/click-events-have-key-events": "react-doctor/click-events-have-key-events",
68455
- "jsx-a11y/control-has-associated-label": "react-doctor/control-has-associated-label",
68456
- "jsx-a11y/heading-has-content": "react-doctor/heading-has-content",
68457
- "jsx-a11y/html-has-lang": "react-doctor/html-has-lang",
68458
- "jsx-a11y/iframe-has-title": "react-doctor/iframe-has-title",
68459
- "jsx-a11y/img-redundant-alt": "react-doctor/img-redundant-alt",
68460
- "jsx-a11y/interactive-supports-focus": "react-doctor/interactive-supports-focus",
68461
- "jsx-a11y/label-has-associated-control": "react-doctor/label-has-associated-control",
68462
- "jsx-a11y/lang": "react-doctor/lang",
68463
- "jsx-a11y/media-has-caption": "react-doctor/media-has-caption",
68464
- "jsx-a11y/mouse-events-have-key-events": "react-doctor/mouse-events-have-key-events",
68465
- "jsx-a11y/no-access-key": "react-doctor/no-access-key",
68466
- "jsx-a11y/no-aria-hidden-on-focusable": "react-doctor/no-aria-hidden-on-focusable",
68467
- "jsx-a11y/no-autofocus": "react-doctor/no-autofocus",
68468
- "jsx-a11y/no-distracting-elements": "react-doctor/no-distracting-elements",
68469
- "jsx-a11y/no-interactive-element-to-noninteractive-role": "react-doctor/no-interactive-element-to-noninteractive-role",
68470
- "jsx-a11y/no-noninteractive-element-interactions": "react-doctor/no-noninteractive-element-interactions",
68471
- "jsx-a11y/no-noninteractive-element-to-interactive-role": "react-doctor/no-noninteractive-element-to-interactive-role",
68472
- "jsx-a11y/no-noninteractive-tabindex": "react-doctor/no-noninteractive-tabindex",
68473
- "jsx-a11y/no-redundant-roles": "react-doctor/no-redundant-roles",
68474
- "jsx-a11y/no-static-element-interactions": "react-doctor/no-static-element-interactions",
68475
- "jsx-a11y/prefer-tag-over-role": "react-doctor/prefer-tag-over-role",
68476
- "jsx-a11y/role-has-required-aria-props": "react-doctor/role-has-required-aria-props",
68477
- "jsx-a11y/role-supports-aria-props": "react-doctor/role-supports-aria-props",
68478
- "jsx-a11y/scope": "react-doctor/scope",
68479
- "jsx-a11y/tabindex-no-positive": "react-doctor/tabindex-no-positive",
68480
- "react-hooks/exhaustive-deps": "react-doctor/exhaustive-deps",
68481
- "react-hooks/rules-of-hooks": "react-doctor/rules-of-hooks",
68482
- "react-perf/jsx-no-jsx-as-prop": "react-doctor/jsx-no-jsx-as-prop",
68483
- "react-perf/jsx-no-new-array-as-prop": "react-doctor/jsx-no-new-array-as-prop",
68484
- "react-perf/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
68485
- "react-perf/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
68486
- "react-refresh/only-export-components": "react-doctor/only-export-components",
68487
- "react/button-has-type": "react-doctor/button-has-type",
68488
- "react/checked-requires-onchange-or-readonly": "react-doctor/checked-requires-onchange-or-readonly",
68489
- "react/display-name": "react-doctor/display-name",
68490
- "react/exhaustive-deps": "react-doctor/exhaustive-deps",
68491
- "react/forbid-component-props": "react-doctor/forbid-component-props",
68492
- "react/forbid-dom-props": "react-doctor/forbid-dom-props",
68493
- "react/forbid-elements": "react-doctor/forbid-elements",
68494
- "react/forward-ref-uses-ref": "react-doctor/forward-ref-uses-ref",
68495
- "react/hook-use-state": "react-doctor/hook-use-state",
68496
- "react/iframe-missing-sandbox": "react-doctor/iframe-missing-sandbox",
68497
- "react/jsx-boolean-value": "react-doctor/jsx-boolean-value",
68498
- "react/jsx-curly-brace-presence": "react-doctor/jsx-curly-brace-presence",
68499
- "react/jsx-filename-extension": "react-doctor/jsx-filename-extension",
68500
- "react/jsx-fragments": "react-doctor/jsx-fragments",
68501
- "react/jsx-handler-names": "react-doctor/jsx-handler-names",
68502
- "react/jsx-key": "react-doctor/jsx-key",
68503
- "react/jsx-max-depth": "react-doctor/jsx-max-depth",
68504
- "react/jsx-no-comment-textnodes": "react-doctor/jsx-no-comment-textnodes",
68505
- "react/jsx-no-constructed-context-values": "react-doctor/jsx-no-constructed-context-values",
68506
- "react/jsx-no-duplicate-props": "react-doctor/jsx-no-duplicate-props",
68507
- "react/jsx-no-jsx-as-prop": "react-doctor/jsx-no-jsx-as-prop",
68508
- "react/jsx-no-new-array-as-prop": "react-doctor/jsx-no-new-array-as-prop",
68509
- "react/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
68510
- "react/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
68511
- "react/jsx-no-script-url": "react-doctor/jsx-no-script-url",
68512
- "react/jsx-no-undef": "react-doctor/jsx-no-undef",
68513
- "react/jsx-no-useless-fragment": "react-doctor/jsx-no-useless-fragment",
68514
- "react/jsx-pascal-case": "react-doctor/jsx-pascal-case",
68515
- "react/jsx-props-no-spread-multi": "react-doctor/jsx-props-no-spread-multi",
68516
- "react/jsx-props-no-spreading": "react-doctor/jsx-props-no-spreading",
68517
- "react/no-array-index-key": "react-doctor/no-array-index-key",
68518
- "react/no-children-prop": "react-doctor/no-children-prop",
68519
- "react/no-clone-element": "react-doctor/no-clone-element",
68520
- "react/no-danger": "react-doctor/no-danger",
68521
- "react/no-danger-with-children": "react-doctor/no-danger-with-children",
68522
- "react/no-did-mount-set-state": "react-doctor/no-did-mount-set-state",
68523
- "react/no-did-update-set-state": "react-doctor/no-did-update-set-state",
68524
- "react/no-direct-mutation-state": "react-doctor/no-direct-mutation-state",
68525
- "react/no-find-dom-node": "react-doctor/no-find-dom-node",
68526
- "react/no-is-mounted": "react-doctor/no-is-mounted",
68527
- "react/no-multi-comp": "react-doctor/no-multi-comp",
68528
- "react/no-namespace": "react-doctor/no-namespace",
68529
- "react/no-react-children": "react-doctor/no-react-children",
68530
- "react/no-redundant-should-component-update": "react-doctor/no-redundant-should-component-update",
68531
- "react/no-render-return-value": "react-doctor/no-render-return-value",
68532
- "react/no-set-state": "react-doctor/no-set-state",
68533
- "react/no-string-refs": "react-doctor/no-string-refs",
68534
- "react/no-this-in-sfc": "react-doctor/no-this-in-sfc",
68535
- "react/no-unescaped-entities": "react-doctor/no-unescaped-entities",
68536
- "react/no-unknown-property": "react-doctor/no-unknown-property",
68537
- "react/no-unsafe": "react-doctor/no-unsafe",
68538
- "react/no-unstable-nested-components": "react-doctor/no-unstable-nested-components",
68539
- "react/no-will-update-set-state": "react-doctor/no-will-update-set-state",
68540
- "react/only-export-components": "react-doctor/only-export-components",
68541
- "react/prefer-es6-class": "react-doctor/prefer-es6-class",
68542
- "react/prefer-function-component": "react-doctor/prefer-function-component",
68543
- "react/react-in-jsx-scope": "react-doctor/react-in-jsx-scope",
68544
- "react/require-render-return": "react-doctor/require-render-return",
68545
- "react/rules-of-hooks": "react-doctor/rules-of-hooks",
68546
- "react/self-closing-comp": "react-doctor/self-closing-comp",
68547
- "react/state-in-constructor": "react-doctor/state-in-constructor",
68548
- "react/style-prop-object": "react-doctor/style-prop-object",
68549
- "react/void-dom-elements-no-children": "react-doctor/void-dom-elements-no-children"
68550
- };
68551
- const NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS = /* @__PURE__ */ new Map();
68552
- for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY)) {
68553
- const aliases = NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(nativeRuleKey) ?? [];
68554
- aliases.push(legacyRuleKey);
68555
- NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
68556
- }
68557
- const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
68558
- const REACT_DOCTOR_RULE_KEY_PREFIX = "react-doctor/";
68559
- const canonicalizeRuleKey = (ruleKey) => {
68560
- const nativeRuleKey = LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey];
68561
- return typeof nativeRuleKey === "string" ? nativeRuleKey : ruleKey;
68562
- };
68563
- const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
68564
- /**
68565
- * Canonicalizes a rule key as users write it in config: a legacy alias
68566
- * (`react/jsx-key`) maps to its native key, and a bare short id (`no-eval`)
68567
- * qualifies as `react-doctor/<id>` — mirroring `isSameRuleKey`'s matching —
68568
- * so telemetry groups every spelling of one rule under one key.
68569
- */
68570
- const canonicalizeUserRuleKey = (ruleKey) => {
68571
- const nativeRuleKey = canonicalizeRuleKey(ruleKey);
68572
- return nativeRuleKey.includes("/") ? nativeRuleKey : `${REACT_DOCTOR_RULE_KEY_PREFIX}${nativeRuleKey}`;
68573
- };
68574
- const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
68575
- const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
68576
- const canonicalTarget = canonicalizeRuleKey(targetRuleKey);
68577
- if (canonicalCandidate === canonicalTarget) return true;
68578
- return isReactDoctorShortIdOf(canonicalCandidate, canonicalTarget) || isReactDoctorShortIdOf(canonicalTarget, canonicalCandidate);
68579
- };
68580
- const getEquivalentRuleKeys = (ruleKey) => {
68581
- const nativeRuleKey = canonicalizeRuleKey(ruleKey);
68582
- return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
68583
- };
68584
68812
  const stripDescriptionTail = (ruleList) => {
68585
68813
  const descriptionMatch = ruleList.match(/(?:^|\s)--\s/);
68586
68814
  if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
@@ -69227,12 +69455,8 @@ const buildDiagnosticPipeline = (input) => {
69227
69455
  if (rule.tags.includes("migration-hint")) return false;
69228
69456
  return getFileContext(diagnostic.filePath) !== "production";
69229
69457
  };
69230
- const isRuleIgnored = (ruleIdentifier) => {
69231
- for (const ignored of ignoredRules) if (isSameRuleKey(ignored, ruleIdentifier)) return true;
69232
- return false;
69233
- };
69234
- const isAppOnlyRule = (ruleIdentifier) => {
69235
- for (const appOnlyRuleKey of APP_ONLY_RULE_KEYS) if (isSameRuleKey(appOnlyRuleKey, ruleIdentifier)) return true;
69458
+ const matchesRuleKey = (ruleIdentifier, candidates) => {
69459
+ for (const candidate of candidates) if (isSameRuleKey(candidate, ruleIdentifier)) return true;
69236
69460
  return false;
69237
69461
  };
69238
69462
  const isRnRawTextSuppressedByConfig = (diagnostic) => {
@@ -69274,12 +69498,13 @@ const buildDiagnosticPipeline = (input) => {
69274
69498
  if (explicitSeverityOverride === "off") return suppress(current, "config");
69275
69499
  if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
69276
69500
  }
69501
+ const ruleIdentifier = `${current.plugin}/${current.rule}`;
69277
69502
  if (explicitRuleOverride === void 0) {
69278
- if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
69503
+ if (matchesRuleKey(ruleIdentifier, APP_ONLY_RULE_KEYS) && isLibraryFile(current.filePath)) return null;
69279
69504
  }
69280
69505
  if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
69281
69506
  if (userConfig) {
69282
- if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return suppress(current, "config");
69507
+ if (matchesRuleKey(ruleIdentifier, ignoredRules)) return suppress(current, "config");
69283
69508
  if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
69284
69509
  if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return suppress(current, "override");
69285
69510
  if (isRnRawTextSuppressedByConfig(current)) return null;
@@ -69288,7 +69513,6 @@ const buildDiagnosticPipeline = (input) => {
69288
69513
  if (respectInlineDisables && current.line > 0) {
69289
69514
  const lines = getFileLines(current.filePath);
69290
69515
  if (lines) {
69291
- const ruleIdentifier = `${current.plugin}/${current.rule}`;
69292
69516
  const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
69293
69517
  if (evaluation.isSuppressed) return suppress(current, evaluation.isForeignDirective ? "foreign-inline" : "inline");
69294
69518
  if (evaluation.nearMissHint) current = {
@@ -69564,7 +69788,8 @@ const readSystemFacts$1 = () => ({
69564
69788
  /**
69565
69789
  * Auto lint-worker count: the smaller of the (cgroup-CPU-aware) core count and
69566
69790
  * the number of `PER_WORKER_MEM_BUDGET_BYTES` workers that fit in available
69567
- * memory, then clamped to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
69791
+ * memory, and the measured automatic parallel-efficiency ceiling, then clamped
69792
+ * to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
69568
69793
  *
69569
69794
  * `os.availableParallelism()` already respects cgroup CPU quotas, so the core
69570
69795
  * term needs no help. Available memory is `os.totalmem()` floored by the cgroup
@@ -69580,7 +69805,7 @@ const readSystemFacts$1 = () => ({
69580
69805
  const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
69581
69806
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
69582
69807
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
69583
- return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
69808
+ return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers, 10));
69584
69809
  };
69585
69810
  const resolveLintBatchOrdering = () => {
69586
69811
  return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "arrival" ? "arrival" : "cost";
@@ -69593,24 +69818,22 @@ const resolveLintBatchOrdering = () => {
69593
69818
  */
69594
69819
  var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS", OXLINT_SPAWN_TIMEOUT_MS) }) {};
69595
69820
  /**
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.
69821
+ * Optional Effect-side cap on the lint phase. Unbounded unless explicitly
69822
+ * configured through the env var or a test layer.
69598
69823
  * Tests override via `Layer.succeed(LintPhaseTimeoutMs, ...)`.
69599
69824
  */
69600
- var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", LINT_PHASE_TIMEOUT_MS) }) {};
69825
+ var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", null) }) {};
69601
69826
  /**
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
69827
+ * Optional Effect-side cap on the dead-code phase. Unbounded unless explicitly
69828
+ * configured through the env var or a test layer. Tests override via
69605
69829
  * `Layer.succeed(DeadCodePhaseTimeoutMs, ...)`.
69606
69830
  */
69607
- var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", DEAD_CODE_PHASE_TIMEOUT_MS) }) {};
69831
+ var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", null) }) {};
69608
69832
  /**
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, ...)`.
69833
+ * Optional overall scan deadline. Unbounded unless explicitly configured
69834
+ * through the env var or a test layer.
69612
69835
  */
69613
- var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", SCAN_TOTAL_DEADLINE_MS) }) {};
69836
+ var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", null) }) {};
69614
69837
  /**
69615
69838
  * Wall-clock budget for the supply-chain check when it runs on a background
69616
69839
  * fiber overlapping the lint pass. Reads from the env var on startup so the
@@ -69683,14 +69906,14 @@ var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", {
69683
69906
  /**
69684
69907
  * How the full-scan lint pass plans its file batches. `"cost"` (the default)
69685
69908
  * 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
69909
+ * batch count as greedy chunking, but every batch gets an even share of files
69910
+ * AND bytes, so no full chunk is a straggler while
69688
69911
  * the remainder-batch worker idles — and the heavy files are SPREAD across
69689
69912
  * batches, the precondition the old sort-desc-then-chunk-100 `cost` mode
69690
69913
  * lacked (it packed the heaviest files into one wave-1 straggler batch,
69691
69914
  * measurably regressing size-skewed repos, which is why it never earned the
69692
69915
  * default). `"arrival"` (`REACT_DOCTOR_LINT_BATCH_ORDERING=arrival`) is the
69693
- * rollback hatch to plain greedy 100-file chunking in discovery order. Tests
69916
+ * rollback hatch to plain greedy fixed-size chunking in discovery order. Tests
69694
69917
  * override via `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans
69695
69918
  * never reach this — they pass user-scoped `includePaths` that skip discovery
69696
69919
  * and stay in arrival order; only the full-scan branch reads it.
@@ -69785,6 +70008,7 @@ const DEFAULT_SURFACE_EXCLUDED_TAGS = {
69785
70008
  score: ["design"],
69786
70009
  ciFailure: ["design"]
69787
70010
  };
70011
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
69788
70012
  const VALID_RULE_SEVERITIES = [
69789
70013
  "error",
69790
70014
  "warn",
@@ -69827,7 +70051,6 @@ const SURFACE_CONTROL_FIELD_NAMES = [
69827
70051
  ];
69828
70052
  const isIncludedFileContext = (value) => VALID_INCLUDED_FILE_CONTEXTS.some((fileContext) => fileContext === value);
69829
70053
  const SEVERITY_FIELD_NAMES = ["rules", "categories"];
69830
- const isPlainObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
69831
70054
  const formatType = (value) => typeof value === "string" ? `"${value}"` : typeof value;
69832
70055
  const isRuleSeverity = (value) => typeof value === "string" && VALID_RULE_SEVERITIES.includes(value);
69833
70056
  const coerceMaybeBooleanString = (fieldName, value) => {
@@ -69855,7 +70078,7 @@ const validateStringArrayField = (fieldName, value) => {
69855
70078
  });
69856
70079
  };
69857
70080
  const validateSurfaceControls = (surface, rawControls) => {
69858
- if (!isPlainObject$1(rawControls)) {
70081
+ if (!isRecord$1(rawControls)) {
69859
70082
  warnConfigIssue(`config field "surfaces.${surface}" must be an object (got ${typeof rawControls}); ignoring this surface.`);
69860
70083
  return;
69861
70084
  }
@@ -69879,7 +70102,7 @@ const validateSurfaceControls = (surface, rawControls) => {
69879
70102
  return validatedSurfaceControls;
69880
70103
  };
69881
70104
  const validateSurfacesField = (rawSurfaces) => {
69882
- if (!isPlainObject$1(rawSurfaces)) {
70105
+ if (!isRecord$1(rawSurfaces)) {
69883
70106
  warnConfigIssue(`config field "surfaces" must be an object (got ${typeof rawSurfaces}); ignoring this field.`);
69884
70107
  return;
69885
70108
  }
@@ -69895,7 +70118,7 @@ const validateSurfacesField = (rawSurfaces) => {
69895
70118
  return validated;
69896
70119
  };
69897
70120
  const validateSeverityMap = (fieldName, rawMap, keysAreCategories = false) => {
69898
- if (!isPlainObject$1(rawMap)) {
70121
+ if (!isRecord$1(rawMap)) {
69899
70122
  warnConfigIssue(`config field "${fieldName}" must be an object (got ${typeof rawMap}); ignoring this field.`);
69900
70123
  return;
69901
70124
  }
@@ -70256,6 +70479,10 @@ const dedupeRelatedDiagnostics = (diagnostics) => {
70256
70479
  }
70257
70480
  return uniqueDiagnostics;
70258
70481
  };
70482
+ const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
70483
+ const relativePath = Path.relative(parentAbsolutePath, childAbsolutePath);
70484
+ return Boolean(relativePath) && !relativePath.startsWith("..") && !Path.isAbsolute(relativePath);
70485
+ };
70259
70486
  const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
70260
70487
  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
70488
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
@@ -71832,7 +72059,7 @@ const readScannedFile = (candidate) => {
71832
72059
  return null;
71833
72060
  }
71834
72061
  };
71835
- function* collectSecurityScanFiles(rootDirectory) {
72062
+ function* collectSecurityScanFiles(rootDirectory, excludedDirectories = /* @__PURE__ */ new Set()) {
71836
72063
  const priorityCandidates = [];
71837
72064
  const artifactCandidates = [];
71838
72065
  const otherCandidates = [];
@@ -71847,7 +72074,7 @@ function* collectSecurityScanFiles(rootDirectory) {
71847
72074
  for (const entry of readDirectoryEntries(current.absolutePath)) {
71848
72075
  const absolutePath = Path.join(current.absolutePath, entry.name);
71849
72076
  if (entry.isDirectory()) {
71850
- if (!SKIPPED_DIRECTORY_NAMES.has(entry.name)) stack.push({
72077
+ if (!SKIPPED_DIRECTORY_NAMES.has(entry.name) && !excludedDirectories.has(Path.resolve(absolutePath))) stack.push({
71851
72078
  absolutePath,
71852
72079
  depth: current.depth + 1
71853
72080
  });
@@ -72028,10 +72255,8 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhe
72028
72255
  }
72029
72256
  return true;
72030
72257
  };
72258
+ const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs - Date.now(), 0);
72031
72259
  const shouldEnableRuleByDefaultStatus = (options) => options.defaultEnabled !== false || options.hasExplicitOverride === true || options.includeTagDefaults && options.hasIncludedTags;
72032
- const yieldToEventLoop = () => new Promise((resolve) => {
72033
- setImmediate(resolve);
72034
- });
72035
72260
  const createSecurityScanSession = (rootDirectory, options) => {
72036
72261
  const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
72037
72262
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
@@ -72084,19 +72309,35 @@ const createSecurityScanSession = (rootDirectory, options) => {
72084
72309
  };
72085
72310
  };
72086
72311
  const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
72312
+ const throwIfScanAborted = () => {
72313
+ options.signal?.throwIfAborted();
72314
+ };
72315
+ const didReachDeadline = () => {
72316
+ const hasDeadlineElapsed = options.deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(options.deadlineEpochMs) === 0;
72317
+ if (hasDeadlineElapsed) options.onDeadlineExceeded?.();
72318
+ return hasDeadlineElapsed;
72319
+ };
72320
+ throwIfScanAborted();
72087
72321
  const session = createSecurityScanSession(rootDirectory, options);
72088
72322
  if (session === null) return [];
72323
+ if (didReachDeadline()) return [];
72089
72324
  let sliceStartedAt = performance.now();
72090
- for (const file of collectSecurityScanFiles(rootDirectory)) {
72325
+ for (const file of collectSecurityScanFiles(rootDirectory, options.excludedDirectories)) {
72326
+ throwIfScanAborted();
72327
+ if (didReachDeadline()) return session.diagnostics;
72091
72328
  if (file === null) {
72092
72329
  if (performance.now() - sliceStartedAt >= 12) {
72093
72330
  await yieldToEventLoop();
72331
+ throwIfScanAborted();
72332
+ if (didReachDeadline()) return session.diagnostics;
72094
72333
  sliceStartedAt = performance.now();
72095
72334
  }
72096
72335
  continue;
72097
72336
  }
72098
72337
  for (const _ruleStep of session.scanFileByRule(file)) if (performance.now() - sliceStartedAt >= 12) {
72099
72338
  await yieldToEventLoop();
72339
+ throwIfScanAborted();
72340
+ if (didReachDeadline()) return session.diagnostics;
72100
72341
  sliceStartedAt = performance.now();
72101
72342
  }
72102
72343
  }
@@ -72275,25 +72516,18 @@ const collectIgnorePatterns = (rootDirectory) => {
72275
72516
  cachedPatternsByRoot.set(rootDirectory, patterns);
72276
72517
  return patterns;
72277
72518
  };
72278
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
72279
- const KNIP_JSON_FILENAME = "knip.json";
72280
- const readJsonFileSafe = (filePath) => {
72281
- let rawContents;
72282
- try {
72283
- rawContents = fs.readFileSync(filePath, "utf-8");
72284
- } catch {
72285
- return null;
72286
- }
72519
+ const failOpenReadJson = (filePath, fallback) => {
72287
72520
  try {
72288
- return JSON.parse(rawContents);
72521
+ return JSON.parse(NFS.readFileSync(filePath, "utf8"));
72289
72522
  } catch {
72290
- return null;
72523
+ return fallback;
72291
72524
  }
72292
72525
  };
72526
+ const KNIP_JSON_FILENAME = "knip.json";
72293
72527
  const readKnipConfig = (rootDirectory) => {
72294
- const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
72528
+ const knipJson = failOpenReadJson(path.join(rootDirectory, KNIP_JSON_FILENAME), null);
72295
72529
  if (isRecord$1(knipJson)) return knipJson;
72296
- const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
72530
+ const packageJson = failOpenReadJson(path.join(rootDirectory, "package.json"), null);
72297
72531
  const packageKnipConfig = isRecord$1(packageJson) ? packageJson.knip : null;
72298
72532
  return isRecord$1(packageKnipConfig) ? packageKnipConfig : null;
72299
72533
  };
@@ -72341,13 +72575,6 @@ const atomicWriteJson = (filePath, value) => {
72341
72575
  return;
72342
72576
  }
72343
72577
  };
72344
- const failOpenReadJson = (filePath, fallback) => {
72345
- try {
72346
- return JSON.parse(NFS.readFileSync(filePath, "utf8"));
72347
- } catch {
72348
- return fallback;
72349
- }
72350
- };
72351
72578
  const hashFileContents = (filePath) => {
72352
72579
  try {
72353
72580
  return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
@@ -73026,7 +73253,6 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
73026
73253
  return true;
73027
73254
  };
73028
73255
  const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
73029
- const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs - Date.now(), 0);
73030
73256
  /**
73031
73257
  * Budget for the dead-code phase, scaled to the work. deslop's graph build is
73032
73258
  * CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
@@ -73034,25 +73260,28 @@ const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs
73034
73260
  * tipped over by any concurrent load — silently dropping every dead-code
73035
73261
  * finding. Scaling the budget with file count (and inversely with the core
73036
73262
  * 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.
73263
+ * reclaims a genuinely wedged worker.
73039
73264
  */
73040
73265
  const resolveDeadCodeTimeout = (input) => {
73041
73266
  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
- };
73267
+ return { workerTimeoutMs: Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor))) };
73047
73268
  };
73048
- const resolveLintIncludePaths = (rootDirectory, userConfig) => {
73269
+ const resolveLintIncludePaths = (rootDirectory, userConfig, sourceFiles) => {
73049
73270
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
73050
73271
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
73051
- return listSourceFiles(rootDirectory).filter((filePath) => {
73272
+ return (sourceFiles ?? listSourceFiles(rootDirectory)).filter((filePath) => {
73052
73273
  if (!isLintableSourceFile(filePath)) return false;
73053
73274
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
73054
73275
  });
73055
73276
  };
73277
+ const filterPathsOutsideDirectories = (input) => {
73278
+ 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}/`)));
73279
+ if (excludedPrefixes.length === 0) return [...input.relativePaths];
73280
+ return input.relativePaths.filter((relativePath) => {
73281
+ const normalizedRelativePath = relativePath.replaceAll("\\", "/").replace(/^\.\//, "");
73282
+ return !excludedPrefixes.some((excludedPrefix) => normalizedRelativePath === excludedPrefix || normalizedRelativePath.startsWith(`${excludedPrefix}/`));
73283
+ });
73284
+ };
73056
73285
  var Config = class Config extends Service()("react-doctor/Config") {
73057
73286
  static layerNode = effect(Config, gen(function* () {
73058
73287
  const cache = yield* make$3({
@@ -73114,6 +73343,7 @@ var Files = class Files extends Service()("react-doctor/Files") {
73114
73343
  static layerNode = succeed$3(Files, Files.of({
73115
73344
  readLines: (input) => sync(() => createNodeReadFileLinesSync(input.rootDirectory)(input.filePath)),
73116
73345
  listSourceFiles: (rootDirectory) => sync(() => listSourceFiles(rootDirectory)),
73346
+ listSourceFilesCooperative: (input) => promise(() => listSourceFilesCooperative(input.rootDirectory, input.signal)),
73117
73347
  isFile: (filePath) => sync(() => isFile(filePath)),
73118
73348
  isDirectory: (filePath) => sync(() => isDirectory(filePath))
73119
73349
  }));
@@ -73140,6 +73370,16 @@ var Files = class Files extends Service()("react-doctor/Files") {
73140
73370
  }
73141
73371
  return files;
73142
73372
  }),
73373
+ listSourceFilesCooperative: (input) => sync(() => {
73374
+ const rootDirectory = input.rootDirectory;
73375
+ const prefix = rootDirectory.endsWith("/") ? rootDirectory : `${rootDirectory}/`;
73376
+ const files = [];
73377
+ for (const absolute of tree.keys()) {
73378
+ if (!absolute.startsWith(prefix)) continue;
73379
+ files.push(absolute.slice(prefix.length));
73380
+ }
73381
+ return files;
73382
+ }),
73143
73383
  isFile: (filePath) => sync(() => tree.has(filePath)),
73144
73384
  isDirectory: (filePath) => sync(() => {
73145
73385
  const prefix = filePath.endsWith("/") ? filePath : `${filePath}/`;
@@ -73715,7 +73955,7 @@ const batchIncludePaths = (baseArgs, includePaths) => {
73715
73955
  for (const filePath of includePaths) {
73716
73956
  const entryLength = filePath.length + 1;
73717
73957
  const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength > 24e3;
73718
- const exceedsFileCount = currentBatch.length >= 100;
73958
+ const exceedsFileCount = currentBatch.length >= 200;
73719
73959
  if (exceedsArgLength || exceedsFileCount) {
73720
73960
  batches.push(currentBatch);
73721
73961
  currentBatch = [];
@@ -74062,7 +74302,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
74062
74302
  }
74063
74303
  return enabled;
74064
74304
  };
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 }) => {
74305
+ 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
74306
  const hasIncludedTags = includedTags.size > 0;
74067
74307
  const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" || hasIncludedTags ? null : resolveReactHooksJsPlugin(project.hasReactCompiler || project.hasReactCompilerLintPlugin === true, customRulesOnly);
74068
74308
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
@@ -74126,7 +74366,8 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
74126
74366
  }))
74127
74367
  } : {},
74128
74368
  ...project.shopifyFlashListMajorVersion !== null ? { shopifyFlashListMajorVersion: project.shopifyFlashListMajorVersion } : {},
74129
- ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {}
74369
+ ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {},
74370
+ ...projectIndexModuleSources !== void 0 ? { projectIndexModuleSources: [...projectIndexModuleSources] } : {}
74130
74371
  } },
74131
74372
  rules: {
74132
74373
  ...reactCompilerRules,
@@ -74375,11 +74616,17 @@ const collectUnpluginAutoImportGlobalScopes = ({ rootDirectory, candidateFiles }
74375
74616
  return scopes.some((scope) => scope.names.length > 0) ? scopes : [];
74376
74617
  };
74377
74618
  const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
74619
+ const decodeDiagnostic = (raw) => {
74620
+ const diagnostic = validateDiagnostic$1(raw);
74621
+ return {
74622
+ ...diagnostic,
74623
+ relatedLocations: diagnostic.relatedLocations?.map((location) => ({ ...location }))
74624
+ };
74625
+ };
74378
74626
  const decodeFileDiagnostics = (raw) => {
74379
74627
  if (!Array.isArray(raw)) return null;
74380
74628
  try {
74381
- for (const entry of raw) validateDiagnostic$1(entry);
74382
- return raw;
74629
+ return raw.map((entry) => decodeDiagnostic(entry));
74383
74630
  } catch {
74384
74631
  return null;
74385
74632
  }
@@ -75611,6 +75858,12 @@ const buildProfiledNodeArguments = (input) => [
75611
75858
  ...input.heapProfileDirectory ? ["--heap-prof", `--heap-prof-dir=${input.heapProfileDirectory}`] : [],
75612
75859
  ...input.argumentsList
75613
75860
  ];
75861
+ const lowerChildProcessPriority = (processId) => {
75862
+ if (processId === void 0) return;
75863
+ try {
75864
+ OS.setPriority(processId, OS.constants.priority.PRIORITY_BELOW_NORMAL);
75865
+ } catch {}
75866
+ };
75614
75867
  const SANITIZED_ENV = buildOxlintChildEnv(process.env);
75615
75868
  /**
75616
75869
  * Spawn one oxlint subprocess with hard ceilings on wall time and
@@ -75649,6 +75902,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
75649
75902
  "pipe"
75650
75903
  ]
75651
75904
  });
75905
+ lowerChildProcessPriority(child.pid);
75652
75906
  const onAbort = () => {
75653
75907
  child.kill("SIGKILL");
75654
75908
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
@@ -75771,7 +76025,7 @@ const isParallelismRelatedSpawnError = (error) => {
75771
76025
  * loop with a slimmer config in that case.
75772
76026
  */
75773
76027
  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;
76028
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, sourcePathByLintPath, sourceMapByLintPath, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 9, signal, deadlineEpochMs } = input;
75775
76029
  const resolveSourcePath = (filePath) => {
75776
76030
  const absoluteFilePath = Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory, filePath);
75777
76031
  return sourcePathByLintPath?.get(Path.normalize(absoluteFilePath)) ?? filePath;
@@ -75779,6 +76033,12 @@ const spawnLintBatches = async (input) => {
75779
76033
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
75780
76034
  let rescueDeadlineEpochMs;
75781
76035
  const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0 || rescueDeadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(rescueDeadlineEpochMs) === 0;
76036
+ const resolveSpawnTimeoutMs = () => {
76037
+ const deadlineBudgets = [deadlineEpochMs, rescueDeadlineEpochMs].filter((deadline) => deadline !== void 0).map(remainingDeadlineBudgetMs);
76038
+ if (deadlineBudgets.length === 0) return spawnTimeoutMs;
76039
+ const remainingDeadlineMs = Math.min(...deadlineBudgets);
76040
+ return spawnTimeoutMs === void 0 ? remainingDeadlineMs : Math.min(spawnTimeoutMs, remainingDeadlineMs);
76041
+ };
75782
76042
  const runBatchPass = async (concurrency, passBatches, passOnFileProgress) => {
75783
76043
  const totalFileCount = passBatches.reduce((sum, batch) => sum + batch.length, 0);
75784
76044
  const allDiagnostics = [];
@@ -75796,12 +76056,13 @@ const spawnLintBatches = async (input) => {
75796
76056
  const batchArgs = [...baseArgs, ...batch];
75797
76057
  try {
75798
76058
  const spawnBatch = () => {
75799
- if (isPastDeadline()) {
76059
+ const effectiveSpawnTimeoutMs = resolveSpawnTimeoutMs();
76060
+ if (effectiveSpawnTimeoutMs === 0) {
75800
76061
  deadlineSkippedFiles.push(...batch);
75801
76062
  batchState.deadlineSkippedFileCount += batch.length;
75802
76063
  return Promise.resolve(null);
75803
76064
  }
75804
- return spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal, () => {
76065
+ return spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, effectiveSpawnTimeoutMs, outputMaxBytes, signal, () => {
75805
76066
  if (batchState.didStart) return;
75806
76067
  batchState.didStart = true;
75807
76068
  startedFileCount += batchState.initialFileCount;
@@ -75956,11 +76217,36 @@ const validateRuleRegistration = () => {
75956
76217
  ].filter((entry) => entry !== null).join("; ");
75957
76218
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
75958
76219
  };
76220
+ const PROJECT_INDEX_MODULE_SOURCES = [
76221
+ "next/og",
76222
+ "@vercel/og",
76223
+ "satori",
76224
+ "remotion"
76225
+ ];
76226
+ const collectProjectIndexModuleSources = async (rootDirectory, candidateFiles) => {
76227
+ const foundModuleSources = /* @__PURE__ */ new Set();
76228
+ let sliceStartedAt = performance.now();
76229
+ for (const candidateFile of candidateFiles) {
76230
+ let sourceBuffer;
76231
+ try {
76232
+ sourceBuffer = NFS.readFileSync(Path.isAbsolute(candidateFile) ? candidateFile : Path.resolve(rootDirectory, candidateFile));
76233
+ } catch {
76234
+ continue;
76235
+ }
76236
+ for (const moduleSource of PROJECT_INDEX_MODULE_SOURCES) if (sourceBuffer.includes(`"${moduleSource}"`) || sourceBuffer.includes(`'${moduleSource}'`)) foundModuleSources.add(moduleSource);
76237
+ if (foundModuleSources.size === PROJECT_INDEX_MODULE_SOURCES.length) break;
76238
+ if (performance.now() - sliceStartedAt >= 12) {
76239
+ await yieldToEventLoop();
76240
+ sliceStartedAt = performance.now();
76241
+ }
76242
+ }
76243
+ return [...foundModuleSources].sort();
76244
+ };
75959
76245
  /**
75960
76246
  * 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
76247
+ * `batchIncludePaths` greedily fills fixed-size batches and leaves a small
76248
+ * remainder batch, so the parallel pool's wall clock is set by whichever
76249
+ * full chunk happened to collect the most work while
75964
76250
  * the remainder's worker idles), this planner keeps the SAME mandatory batch
75965
76251
  * count — `ceil(files / OXLINT_MAX_FILES_PER_BATCH)`, the JS-plugin
75966
76252
  * perf-cliff / native-binding SIGABRT guard — and assigns files largest-first,
@@ -75989,7 +76275,7 @@ const validateRuleRegistration = () => {
75989
76275
  */
75990
76276
  const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
75991
76277
  if (files.length === 0) return [];
75992
- const batchCount = Math.ceil(files.length / 100);
76278
+ const batchCount = Math.ceil(files.length / 200);
75993
76279
  const maxFilesPerBatch = Math.ceil(files.length / batchCount);
75994
76280
  const baseArgsLengthChars = estimateArgsLength(baseArgs);
75995
76281
  const descendingSizeIndexes = [...files.keys()].sort((leftIndex, rightIndex) => (sizeByFile.get(files[rightIndex]) ?? 0) - (sizeByFile.get(files[leftIndex]) ?? 0));
@@ -76518,6 +76804,7 @@ const runOxlint = async (options) => {
76518
76804
  await yieldToEventLoop();
76519
76805
  const sizedScanFiles = includePaths === void 0 ? listSourceFilesWithSize(rootDirectory) : null;
76520
76806
  const candidateFiles = includePaths !== void 0 ? includePaths : (sizedScanFiles ?? []).map((entry) => entry.path);
76807
+ const projectIndexModuleSources = includePaths === void 0 && options.deadlineEpochMs === void 0 ? await collectProjectIndexModuleSources(rootDirectory, candidateFiles) : void 0;
76521
76808
  unpluginAutoImportGlobalScopes = collectUnpluginAutoImportGlobalScopes({
76522
76809
  rootDirectory,
76523
76810
  candidateFiles
@@ -76540,6 +76827,7 @@ const runOxlint = async (options) => {
76540
76827
  runtimeGlobals,
76541
76828
  unpluginAutoImportGlobalScopes,
76542
76829
  serverAuthFunctionNames,
76830
+ projectIndexModuleSources,
76543
76831
  severityControls,
76544
76832
  userPlugins,
76545
76833
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
@@ -77007,10 +77295,10 @@ const translateProjectInfoError = (cause, directory) => {
77007
77295
  }) });
77008
77296
  };
77009
77297
  var Project = class Project extends Service()("react-doctor/Project") {
77010
- static layerNode = succeed$3(Project, Project.of({ discover: fn("Project.discover")(function* (directory) {
77298
+ static layerNode = succeed$3(Project, Project.of({ discover: fn("Project.discover")(function* (input) {
77011
77299
  return yield* try_({
77012
- try: () => discoverProject(directory),
77013
- catch: (cause) => translateProjectInfoError(cause, directory)
77300
+ try: () => discoverProject(input.directory, { sourceFileCount: input.sourceFileCount }),
77301
+ catch: (cause) => translateProjectInfoError(cause, input.directory)
77014
77302
  });
77015
77303
  }) }));
77016
77304
  static layerOf = (projectInfo) => succeed$3(Project, Project.of({ discover: () => succeed$2(projectInfo) }));
@@ -77595,9 +77883,10 @@ const checkSupplyChain = (input) => gen(function* () {
77595
77883
  * outage.
77596
77884
  */
77597
77885
  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
77886
+ static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(flatMap$2(SupplyChainOverlapTimeoutMs, (configuredTimeoutMs) => checkSupplyChain({
77887
+ rootDirectory: input.rootDirectory,
77888
+ userConfig: input.userConfig,
77889
+ totalTimeoutMs: input.timeoutMs ?? configuredTimeoutMs
77601
77890
  })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
77602
77891
  static layerOf = (diagnostics) => succeed$3(SupplyChain, SupplyChain.of({ run: () => fromIterable$1(diagnostics) }));
77603
77892
  };
@@ -77632,10 +77921,6 @@ const NO_HOOKS = {
77632
77921
  beforeLint: () => void_,
77633
77922
  afterLint: () => void_
77634
77923
  };
77635
- const filterMapNullable = (transform) => fromPredicateOption((value) => {
77636
- const result = transform(value);
77637
- return result === null ? none() : some(result);
77638
- });
77639
77924
  const fileReader = (filesService, rootDirectory) => (filePath) => {
77640
77925
  const lines = runSync(filesService.readLines({
77641
77926
  filePath,
@@ -77659,7 +77944,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
77659
77944
  * The GitHub viewer-permission lookup is forked onto a background
77660
77945
  * fiber here and joined late (it feeds score metadata, not
77661
77946
  * diagnostics).
77662
- * 2. beforeLint hook (e.g. CLI renders the project-detection block)
77947
+ * 2. beforeLint hook (e.g. CLI records project telemetry)
77663
77948
  * 3. environment checks (reduced-motion + pnpm hardening +
77664
77949
  * expo/react-native), collected synchronously. The heavier
77665
77950
  * content-regex security scan is forked instead (like supply-chain
@@ -77672,13 +77957,11 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
77672
77957
  * `SupplyChainOverlapTimeoutMs` (measured from fork) so a hung
77673
77958
  * socket can't drag out its join; on timeout it fails open to no
77674
77959
  * diagnostics — the same outcome class as a Socket outage.
77675
- * 5. Linter.run runs; DeadCode.run runs concurrently (forked child
77676
- * fiber) ONLY when the memory gate has headroom to run the 8 GB
77677
- * dead-code child alongside the oxlint workers — or when overlap is
77678
- * forced via REACT_DOCTOR_DEAD_CODE_OVERLAP. Otherwise dead-code
77679
- * runs sequentially after lint, exactly as it did pre-overlap. The
77680
- * fiber is joined (or interrupted, SIGKILLing its worker, on lint
77681
- * failure) before diagnostics are concatenated. The afterLint hook
77960
+ * 5. Linter.run runs; DeadCode.run forks only when
77961
+ * REACT_DOCTOR_DEAD_CODE_OVERLAP=on. The default "auto" and explicit
77962
+ * "off" paths stay sequential. The fiber is joined (or interrupted,
77963
+ * SIGKILLing its worker, on lint failure) before diagnostics are
77964
+ * concatenated. The afterLint hook
77682
77965
  * fires between lint and dead-code. Progress spinner labels AND the
77683
77966
  * final diagnostic / score order stay independent of execution
77684
77967
  * order, so terminal output is identical either way; supply-chain
@@ -77713,8 +77996,28 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77713
77996
  const partialFailuresRef = yield* LintPartialFailures;
77714
77997
  const resolvedConfig = yield* configService.resolve(input.directory);
77715
77998
  const scanDirectory = resolvedConfig.resolvedDirectory;
77716
- const project = yield* projectService.discover(scanDirectory);
77999
+ const excludedProjectDirectories = (input.excludedProjectDirectories ?? []).map((excludedDirectory) => Path.resolve(excludedDirectory)).filter((excludedDirectory) => isPathInsideDirectory(excludedDirectory, scanDirectory));
78000
+ const excludedProjectRelativePaths = excludedProjectDirectories.map((excludedDirectory) => Path.relative(scanDirectory, excludedDirectory).replaceAll("\\", "/"));
78001
+ const isExcludedProjectDiagnostic = (diagnostic) => {
78002
+ const relativeFilePath = toNormalizedRelativePath(diagnostic.filePath, scanDirectory);
78003
+ return excludedProjectRelativePaths.some((excludedRelativePath) => relativeFilePath === excludedRelativePath || relativeFilePath.startsWith(`${excludedRelativePath}/`));
78004
+ };
78005
+ const precomputedSourceFiles = input.suppressScanSummary === true || excludedProjectDirectories.length > 0 ? yield* filesService.listSourceFilesCooperative({
78006
+ rootDirectory: scanDirectory,
78007
+ signal: input.signal
78008
+ }) : null;
78009
+ const includedPrecomputedSourceFiles = precomputedSourceFiles !== null && excludedProjectDirectories.length > 0 ? filterPathsOutsideDirectories({
78010
+ rootDirectory: scanDirectory,
78011
+ relativePaths: precomputedSourceFiles,
78012
+ excludedDirectories: excludedProjectDirectories
78013
+ }) : precomputedSourceFiles;
78014
+ const sourceFileCount = excludedProjectDirectories.length > 0 ? includedPrecomputedSourceFiles?.length : input.precomputedSourceFileCount ?? includedPrecomputedSourceFiles?.length;
78015
+ const project = yield* projectService.discover({
78016
+ directory: scanDirectory,
78017
+ sourceFileCount
78018
+ });
77717
78019
  if (!isAnalyzableProject(project)) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
78020
+ const deadCodeSourceFileCount = input.retainExcludedProjectDeadCodeDiagnostics === true && precomputedSourceFiles !== null ? precomputedSourceFiles.length : project.sourceFileCount;
77718
78021
  const [repo, sha, defaultBranch] = yield* all([
77719
78022
  gitService.githubRepo(scanDirectory).pipe(orElseSucceed(() => null)),
77720
78023
  gitService.headSha(scanDirectory).pipe(orElseSucceed(() => null)),
@@ -77725,11 +78028,16 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77725
78028
  directory: scanDirectory,
77726
78029
  repo
77727
78030
  }).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)) : [];
78031
+ let lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config, includedPrecomputedSourceFiles ?? void 0);
78032
+ if (excludedProjectDirectories.length > 0) lintIncludePaths = filterPathsOutsideDirectories({
78033
+ rootDirectory: scanDirectory,
78034
+ relativePaths: lintIncludePaths ?? includedPrecomputedSourceFiles ?? (yield* filesService.listSourceFiles(scanDirectory)),
78035
+ excludedDirectories: excludedProjectDirectories
78036
+ });
78037
+ const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? includedPrecomputedSourceFiles ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
77730
78038
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
77731
78039
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
77732
- yield* beforeLint(project, lintIncludePaths ?? void 0);
78040
+ yield* beforeLint(project);
77733
78041
  const isDiffMode = input.includePaths.length > 0;
77734
78042
  const showWarnings = input.warnings ?? resolvedConfig.config?.warnings ?? true;
77735
78043
  const transform = buildDiagnosticPipeline({
@@ -77739,7 +78047,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77739
78047
  respectInlineDisables: input.respectInlineDisables,
77740
78048
  showWarnings
77741
78049
  });
77742
- const filterPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(filterMapNullable(transform.apply)));
78050
+ const filterPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(fromPredicateOption((diagnostic) => {
78051
+ const filteredDiagnostic = transform.apply(diagnostic);
78052
+ return filteredDiagnostic === null ? none() : some(filteredDiagnostic);
78053
+ })));
77743
78054
  const applyPerElementPipeline = (rawStream) => filterPerElementPipeline(rawStream).pipe(tap((diagnostic) => reporterService.emit(diagnostic)));
77744
78055
  const envCollected = yield* runCollect(applyPerElementPipeline(fromIterable$1(isDiffMode ? [] : [
77745
78056
  ...checkReducedMotion(scanDirectory),
@@ -77749,17 +78060,31 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77749
78060
  ...checkReactNativeProject(scanDirectory, project)
77750
78061
  ])));
77751
78062
  const securityScanFailedRef = yield* make$13(false);
77752
- const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise(() => checkSecurityScanCooperative(scanDirectory, {
78063
+ let didSecurityScanReachDeadline = false;
78064
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise((signal) => checkSecurityScanCooperative(scanDirectory, {
77753
78065
  project,
77754
78066
  ignoredTags: input.ignoredTags,
77755
78067
  includedTags: input.includedTags,
77756
- includeTagDefaults: input.includeTagDefaults
78068
+ includeTagDefaults: input.includeTagDefaults,
78069
+ excludedDirectories: new Set(excludedProjectDirectories),
78070
+ deadlineEpochMs: input.deadlineEpochMs,
78071
+ signal,
78072
+ onDeadlineExceeded: () => {
78073
+ didSecurityScanReachDeadline = true;
78074
+ }
77757
78075
  })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), catch_$1(() => set(securityScanFailedRef, true).pipe(as(empty$4))))))).pipe(withSpan("SecurityScan.run")));
78076
+ const capToDeadline = (phaseTimeoutMs) => input.deadlineEpochMs === void 0 ? phaseTimeoutMs : Math.min(phaseTimeoutMs, remainingDeadlineBudgetMs(input.deadlineEpochMs));
78077
+ const capOptionalToDeadline = (phaseTimeoutMs) => {
78078
+ if (input.deadlineEpochMs === void 0) return phaseTimeoutMs;
78079
+ const remainingBudgetMs = remainingDeadlineBudgetMs(input.deadlineEpochMs);
78080
+ return phaseTimeoutMs === null ? remainingBudgetMs : Math.min(phaseTimeoutMs, remainingBudgetMs);
78081
+ };
77758
78082
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
77759
- const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
78083
+ const supplyChainOverlapTimeout = capToDeadline(yield* SupplyChainOverlapTimeoutMs);
77760
78084
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
77761
78085
  rootDirectory: scanDirectory,
77762
- userConfig: resolvedConfig.config
78086
+ userConfig: resolvedConfig.config,
78087
+ timeoutMs: supplyChainOverlapTimeout
77763
78088
  }))).pipe(map$3((diagnostics) => ({
77764
78089
  diagnostics,
77765
78090
  timedOut: false
@@ -77783,46 +78108,49 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77783
78108
  const scanConcurrency = resolveScanConcurrency(yield* OxlintConcurrency);
77784
78109
  const lintPhaseTimeoutMs = yield* LintPhaseTimeoutMs;
77785
78110
  const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
77786
- const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
77787
78111
  const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
77788
- const capToDeadline = (phaseTimeoutMs) => input.deadlineEpochMs === void 0 ? phaseTimeoutMs : Math.min(phaseTimeoutMs, remainingDeadlineBudgetMs(input.deadlineEpochMs));
77789
78112
  const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
77790
78113
  const deadCodeOverlapMode = yield* DeadCodeOverlap;
77791
78114
  const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
77792
78115
  const deadCodeParseConcurrency = shouldOverlapDeadCode ? Math.max(1, Math.floor(scanConcurrency * DEAD_CODE_OVERLAP_PARSE_SHARE)) : void 0;
77793
78116
  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
- })));
78117
+ const buildCollectDeadCode = (deadCodeTimeout) => {
78118
+ const collectDeadCode = runCollect(applyPerElementPipeline(deadCodeService.run({
78119
+ rootDirectory: scanDirectory,
78120
+ parseConcurrency: deadCodeParseConcurrency,
78121
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
78122
+ onCacheOutcome: (didHitCache) => {
78123
+ deadCodeCacheHit = didHitCache;
78124
+ },
78125
+ onSummaryCacheStats: (stats) => {
78126
+ deadCodeSummaryCacheHits = stats.hits;
78127
+ deadCodeSummaryCacheMisses = stats.misses;
78128
+ }
78129
+ }).pipe(filter((diagnostic) => input.retainExcludedProjectDeadCodeDiagnostics === true || !isExcludedProjectDiagnostic(diagnostic)), catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
78130
+ yield* set(deadCodeFailure, {
78131
+ didFail: true,
78132
+ reason: error.message
78133
+ });
78134
+ return empty$4;
78135
+ }))))));
78136
+ const phaseTimeoutMs = deadCodeTimeout.phaseTimeoutMs;
78137
+ if (phaseTimeoutMs === null) return collectDeadCode;
78138
+ return collectDeadCode.pipe(timeoutOption(phaseTimeoutMs), flatMap$2(match$3({
78139
+ onNone: () => set(deadCodeFailure, {
78140
+ didFail: true,
78141
+ reason: `Dead-code analysis exceeded ${Math.round(phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
78142
+ }).pipe(as([])),
78143
+ onSome: succeed$2
78144
+ })));
78145
+ };
77818
78146
  const overlapDeadCodeTimeout = resolveDeadCodeTimeout({
77819
- sourceFileCount: project.sourceFileCount,
78147
+ sourceFileCount: deadCodeSourceFileCount,
77820
78148
  deadCodeConcurrency: deadCodeParseConcurrency ?? scanConcurrency,
77821
78149
  fullConcurrency: scanConcurrency
77822
78150
  });
77823
78151
  const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
77824
78152
  workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
77825
- phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs))
78153
+ phaseTimeoutMs: capOptionalToDeadline(deadCodePhaseTimeoutMs)
77826
78154
  })) : null;
77827
78155
  const scanProgress = yield* progressService.start("Scanning...");
77828
78156
  const scanStartTime = Date.now();
@@ -77873,7 +78201,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77873
78201
  });
77874
78202
  return empty$4;
77875
78203
  }))));
77876
- const lintCollected = dedupeRelatedDiagnostics(yield* runCollect(filterPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
78204
+ const collectLintDiagnostics = runCollect(filterPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream));
78205
+ const lintCollected = dedupeRelatedDiagnostics(yield* lintPhaseTimeoutMs === null ? collectLintDiagnostics : collectLintDiagnostics.pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
77877
78206
  onNone: () => set(lintFailure, {
77878
78207
  didFail: true,
77879
78208
  reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
@@ -77903,13 +78232,13 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77903
78232
  } else {
77904
78233
  yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
77905
78234
  const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
77906
- sourceFileCount: totalFileCount,
78235
+ sourceFileCount: input.retainExcludedProjectDeadCodeDiagnostics === true ? deadCodeSourceFileCount : totalFileCount,
77907
78236
  deadCodeConcurrency: scanConcurrency,
77908
78237
  fullConcurrency: scanConcurrency
77909
78238
  });
77910
78239
  deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
77911
78240
  workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
77912
- phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs))
78241
+ phaseTimeoutMs: capOptionalToDeadline(deadCodePhaseTimeoutMs)
77913
78242
  });
77914
78243
  }
77915
78244
  const deadCodeFailureState = lintFailureState.didFail ? {
@@ -77953,7 +78282,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77953
78282
  metadata: scoreMetadata
77954
78283
  });
77955
78284
  const lintPartialFailures = yield* get$2(partialFailuresRef);
77956
- const securityScanFailed = yield* get$2(securityScanFailedRef);
78285
+ const didSecurityScanFail = yield* get$2(securityScanFailedRef);
78286
+ const securityScanFailed = didSecurityScanFail || didSecurityScanReachDeadline;
78287
+ let securityScanFailureReason = null;
78288
+ if (didSecurityScanReachDeadline) securityScanFailureReason = "Security scan reached the max scan duration; findings collected before the deadline were preserved.";
78289
+ else if (didSecurityScanFail) securityScanFailureReason = "Security scan failed and was skipped.";
77957
78290
  return {
77958
78291
  project,
77959
78292
  userConfig: resolvedConfig.config,
@@ -77976,6 +78309,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77976
78309
  scanConcurrency,
77977
78310
  supplyChainOverlapTimedOut: supplyChainResult.timedOut,
77978
78311
  securityScanFailed,
78312
+ securityScanFailureReason,
77979
78313
  lintCacheHitFileCount,
77980
78314
  lintCacheTotalFileCount,
77981
78315
  lintSidecarReplayedFileCount,
@@ -77991,7 +78325,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77991
78325
  "inspect.runDeadCode": input.runDeadCode,
77992
78326
  "inspect.isCi": input.isCi,
77993
78327
  "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` }) })))));
78328
+ } }), (scanProgram) => flatMap$2(ScanDeadlineMs, (scanDeadlineMs) => {
78329
+ if (scanDeadlineMs === null) return scanProgram;
78330
+ return scanProgram.pipe(timeout(scanDeadlineMs), catchTag$1("TimeoutError", () => new ReactDoctorError({ reason: new ScanDeadlineExceeded({ detail: `${scanDeadlineMs / MILLISECONDS_PER_SECOND}s elapsed` }) })));
78331
+ }));
77995
78332
  const parseNodeVersion = (versionString) => {
77996
78333
  const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
77997
78334
  return {
@@ -78103,16 +78440,6 @@ var NodeResolver = class NodeResolver extends Service()("react-doctor/NodeResolv
78103
78440
  });
78104
78441
  };
78105
78442
  /**
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
78443
  * Writes a set of source files (supplied by `readContent` — e.g.
78117
78444
  * `git show <ref>:<path>` for a baseline tree, or `git show :<path>` for the
78118
78445
  * index) into a temp tree that mirrors the project layout, copying the
@@ -78400,6 +78727,7 @@ const buildJsonReport = (input) => {
78400
78727
  directory: input.directory,
78401
78728
  diff: toJsonDiff(input.diff),
78402
78729
  projects,
78730
+ ...input.skippedProjects && input.skippedProjects.length > 0 ? { skippedProjects: [...input.skippedProjects] } : {},
78403
78731
  diagnostics: flattenedDiagnostics,
78404
78732
  summary,
78405
78733
  elapsedMilliseconds: input.totalElapsedMilliseconds,
@@ -78424,7 +78752,7 @@ const buildSkippedChecks = (input) => {
78424
78752
  else if (input.lintPartialFailures.length > 0) skippedCheckReasons["lint:partial"] = input.lintPartialFailures.join("; ");
78425
78753
  if (input.didDeadCodeFail && input.deadCodeFailureReason !== null) skippedCheckReasons["dead-code"] = input.deadCodeFailureReason;
78426
78754
  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.";
78755
+ if (input.securityScanFailed) skippedCheckReasons["security-scan"] = input.securityScanFailureReason ?? "Security scan failed and was skipped.";
78428
78756
  return {
78429
78757
  skippedChecks,
78430
78758
  skippedCheckReasons
@@ -78564,6 +78892,33 @@ const getChangedLineRanges = (input) => runPromise(gen(function* () {
78564
78892
  * instead of being improvised per diagnostic.
78565
78893
  */
78566
78894
  const buildRuleDocsUrl = (plugin, rule) => `${DOCS_RULES_BASE_URL}/${plugin}/${rule}`;
78895
+ const collectSourceFileCountsByDirectory = async (rootDirectory, projectDirectories, signal) => {
78896
+ const resolvedRootDirectory = Path.resolve(rootDirectory);
78897
+ const sourceFileCounts = /* @__PURE__ */ new Map();
78898
+ const projectDirectoryByRelativePath = /* @__PURE__ */ new Map();
78899
+ for (const projectDirectory of projectDirectories) {
78900
+ const resolvedProjectDirectory = Path.resolve(projectDirectory);
78901
+ const relativeDirectory = Path.relative(resolvedRootDirectory, resolvedProjectDirectory);
78902
+ if (Path.isAbsolute(relativeDirectory) || relativeDirectory.startsWith("..")) continue;
78903
+ sourceFileCounts.set(resolvedProjectDirectory, 0);
78904
+ projectDirectoryByRelativePath.set(relativeDirectory.replaceAll(Path.sep, "/"), resolvedProjectDirectory);
78905
+ }
78906
+ const sourceFilePaths = await listSourceFilesCooperative(resolvedRootDirectory, signal);
78907
+ for (const sourceFilePath of sourceFilePaths) {
78908
+ let relativeDirectory = Path.posix.dirname(sourceFilePath);
78909
+ while (true) {
78910
+ const normalizedRelativeDirectory = relativeDirectory === "." ? "" : relativeDirectory;
78911
+ const projectDirectory = projectDirectoryByRelativePath.get(normalizedRelativeDirectory);
78912
+ if (projectDirectory !== void 0) {
78913
+ sourceFileCounts.set(projectDirectory, (sourceFileCounts.get(projectDirectory) ?? 0) + 1);
78914
+ break;
78915
+ }
78916
+ if (relativeDirectory === ".") break;
78917
+ relativeDirectory = Path.posix.dirname(relativeDirectory);
78918
+ }
78919
+ }
78920
+ return sourceFileCounts;
78921
+ };
78567
78922
  const computeConfigFingerprint = (projectDirectory, version) => {
78568
78923
  const parts = [`v=${version}`];
78569
78924
  let directory = projectDirectory;
@@ -78583,59 +78938,34 @@ const createOxlintSpawnSlots = (concurrency) => createWorkerSlots({
78583
78938
  slotCount: resolveScanConcurrency(concurrency),
78584
78939
  createAbortError: createLintPhaseAbortError
78585
78940
  });
78586
- const AI_TRAINING_ENV_VARS = [
78587
- ["HF_DATASETS_CACHE", "huggingface"],
78588
- ["HF_HOME", "huggingface"],
78589
- ["HUGGINGFACE_HUB_CACHE", "huggingface"],
78590
- ["SPACE_ID", "huggingface-spaces"],
78591
- ["CUDA_VISIBLE_DEVICES", "cuda"],
78592
- ["NVIDIA_VISIBLE_DEVICES", "nvidia"],
78593
- ["ROCR_VISIBLE_DEVICES", "rocm"],
78594
- ["TPU_NAME", "google-tpu"],
78941
+ const AI_TRAINING_BY_ENVIRONMENT_VARIABLE = [
78595
78942
  ["WANDB_RUN_ID", "wandb"],
78596
78943
  ["WANDB_SWEEP_ID", "wandb"],
78597
78944
  ["MLFLOW_RUN_ID", "mlflow"],
78598
- ["MLFLOW_TRACKING_URI", "mlflow"],
78599
78945
  ["COMET_EXPERIMENT_KEY", "comet"],
78600
78946
  ["NEPTUNE_RUN_ID", "neptune"],
78601
78947
  ["CLEARML_TASK_ID", "clearml"],
78602
78948
  ["DVC_STAGE", "dvc"],
78603
78949
  ["RAY_WORKER_PROCESS", "ray"],
78604
- ["RAY_ADDRESS", "ray"],
78605
- ["MUJOCO_GL", "mujoco"],
78606
- ["MUJOCO_PATH", "mujoco"],
78607
- ["GYM_DISABLE_ENV_CHECKER", "gymnasium"],
78608
78950
  ["SM_TRAINING_ENV", "sagemaker"],
78609
78951
  ["TRAINING_JOB_ARN", "sagemaker"],
78610
- ["SAGEMAKER_BASE_DIR", "sagemaker"],
78611
78952
  ["AZUREML_RUN_ID", "azure-ml"],
78612
- ["AZURE_ML_MODEL_DIR", "azure-ml"],
78613
- ["CLOUD_ML_PROJECT_ID", "vertex-ai"],
78614
- ["VERTEX_AI_LOG_LEVEL", "vertex-ai"],
78615
- ["DET_MASTER", "determined-ai"],
78616
- ["LIGHTNING_USER_ID", "lightning-ai"],
78617
78953
  ["FLYTE_INTERNAL_EXECUTION_ID", "flyte"],
78618
- ["ARGO_WORKFLOW_NAME", "argo-workflows"],
78619
78954
  ["KFP_POD_NAME", "kubeflow-pipelines"],
78620
78955
  ["KAGGLE_KERNEL_RUN_TYPE", "kaggle"],
78621
78956
  ["COLAB_BACKEND_VERSION", "google-colab"],
78622
- ["DATABRICKS_RUNTIME_VERSION", "databricks"],
78623
78957
  ["DAYTONA_WS_ID", "daytona"],
78624
- ["DAYTONA_WS_NAME", "daytona"],
78625
78958
  ["E2B_SANDBOX_ID", "e2b"],
78626
78959
  ["MODAL_FUNCTION_ID", "modal"],
78627
78960
  ["MODAL_TASK_ID", "modal"],
78628
78961
  ["RUNPOD_POD_ID", "runpod"],
78629
- ["REPLICATE_USERNAME", "replicate"],
78630
78962
  ["VAST_CONTAINERLABEL", "vast-ai"],
78631
- ["HARBOR_URL", "harbor"],
78632
- ["HARBOR_HOSTNAME", "harbor"],
78633
78963
  ["SWE_BENCH_TASK", "swe-bench"],
78634
78964
  ["SWEBENCH_TASK", "swe-bench"],
78635
78965
  ["SWE_AGENT_MODEL", "swe-agent"]
78636
78966
  ];
78637
- const detectAiTrainingEnvironment = () => {
78638
- for (const [envVar, label] of AI_TRAINING_ENV_VARS) if (process.env[envVar]) return label;
78967
+ const detectAiTrainingEnvironment = (environment = process.env) => {
78968
+ for (const [environmentVariable, label] of AI_TRAINING_BY_ENVIRONMENT_VARIABLE) if (environment[environmentVariable]?.trim()) return label;
78639
78969
  return null;
78640
78970
  };
78641
78971
  const groupBy = (items, keyFn) => {
@@ -78911,7 +79241,7 @@ const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
78911
79241
  const SCAN_RESULT_CACHE_FILENAME = "scan-cache.json";
78912
79242
  const RUN_GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
78913
79243
  const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
78914
- const TUI_REPORT_COMPACT_MAX_ROWS = 29;
79244
+ const TUI_REPORT_COMPACT_MAX_ROWS = 23;
78915
79245
  const TUI_REPORT_DETAIL_WIDTH_FRACTION = .6;
78916
79246
  const CI_TRUST_COMPANIES = "PayPal, Rippling, and Alibaba";
78917
79247
  const WELCOME_EXPLANATION_HOLD_MS = 1e3;
@@ -78934,8 +79264,11 @@ const METRIC = {
78934
79264
  scanPhaseDuration: "scan.phase_duration",
78935
79265
  scanFiles: "scan.files",
78936
79266
  scanScore: "scan.score",
79267
+ scanScoreRetry: "scan.score_retry",
78937
79268
  scanClean: "scan.clean",
78938
79269
  scanCheckSkipped: "scan.check_skipped",
79270
+ scanProjectSkipped: "scan.project_skipped",
79271
+ scanWorkspaceDeadCodeShared: "scan.workspace_deadcode_shared",
78939
79272
  scanNoReactDetected: "scan.no_react_detected",
78940
79273
  baselineDegraded: "baseline.degraded",
78941
79274
  ruleFired: "rule.fired",
@@ -78962,9 +79295,14 @@ const METRIC = {
78962
79295
  lspScanCompleted: "lsp.scan.completed",
78963
79296
  lspScanDuration: "lsp.scan.duration",
78964
79297
  lspScanDiagnostics: "lsp.scan.diagnostics",
78965
- tuiActionMenuOpened: "tui.action_menu_opened",
78966
79298
  tuiCompactReportShown: "tui.compact_report_shown",
78967
79299
  tuiFindingNavigated: "tui.finding_navigated",
79300
+ tuiIssueStreamShown: "tui.issue_stream_shown",
79301
+ tuiProjectPathContextShown: "tui.project_path_context_shown",
79302
+ tuiProjectSelectInlineShown: "tui.project_select_inline_shown",
79303
+ tuiReportActionSelected: "tui.report_action_selected",
79304
+ tuiCancelled: "tui.cancelled",
79305
+ tuiScanInlineShown: "tui.scan_inline_shown",
78968
79306
  tuiStackedReportCapped: "tui.stacked_report_capped",
78969
79307
  aiTrainingWarningShown: "ai.training.warning_shown",
78970
79308
  jsonOutUsed: "json.out_used"
@@ -79010,7 +79348,7 @@ const makeNoopConsole = () => ({
79010
79348
  });
79011
79349
  //#endregion
79012
79350
  //#region src/cli/utils/version.ts
79013
- const VERSION = "0.9.3";
79351
+ const VERSION = "0.9.4";
79014
79352
  //#endregion
79015
79353
  //#region src/cli/utils/json-mode.ts
79016
79354
  let context = null;
@@ -79247,6 +79585,9 @@ const buildSentryScope = (runContext = buildRunContext()) => {
79247
79585
  };
79248
79586
  };
79249
79587
  //#endregion
79588
+ //#region src/cli/utils/is-env-flag-enabled.ts
79589
+ const isEnvFlagEnabled = (value) => value === "1" || value?.toLowerCase() === "true";
79590
+ //#endregion
79250
79591
  //#region src/cli/utils/anonymize-text.ts
79251
79592
  /**
79252
79593
  * Free-text fields can carry both a home-directory path (the OS username) and a
@@ -79368,13 +79709,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
79368
79709
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
79369
79710
  * standard `SENTRY_RELEASE` override.
79370
79711
  */
79371
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.9.3`;
79712
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.9.4`;
79372
79713
  /**
79373
79714
  * Deployment environment shown in Sentry's environment filter. Defaults to
79374
79715
  * `production` for tagged releases and `development` for dev/unbuilt versions,
79375
79716
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
79376
79717
  */
79377
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.9.3") ? "development" : "production");
79718
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.9.4") ? "development" : "production");
79378
79719
  /**
79379
79720
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
79380
79721
  * (set to `0` to disable tracing) and falls back to
@@ -79397,7 +79738,6 @@ const shouldEnableSentry = () => {
79397
79738
  if (process.env.VITEST || process.env.NODE_ENV === "test") return false;
79398
79739
  return true;
79399
79740
  };
79400
- const isEnvFlagEnabled = (value) => value === "1" || value?.toLowerCase() === "true";
79401
79741
  /**
79402
79742
  * Whether performance traces will actually be recorded — Sentry is live and the
79403
79743
  * resolved sample rate is above zero. Used to gate the per-run root span and
@@ -83013,27 +83353,21 @@ const prompts = (questions, options = {}) => {
83013
83353
  //#endregion
83014
83354
  //#region src/cli/utils/to-span-attributes.ts
83015
83355
  /**
83016
- * Converts a Sentry tag map (which permits `null` to denote an absent signal)
83017
- * into Sentry/OTel span attributes, which only accept primitives. `null` values
83018
- * are dropped rather than coerced, so an absent signal doesn't become a
83019
- * misleading `"null"` attribute.
83356
+ * Converts a Sentry tag map (which permits `null`/`undefined` to denote an
83357
+ * absent signal) into Sentry/OTel span attributes, which only accept primitives.
83358
+ * Missing values are dropped rather than coerced, so an absent signal doesn't
83359
+ * become a misleading `"null"` or `"undefined"` attribute.
83020
83360
  */
83021
83361
  const toSpanAttributes = (tags) => {
83022
83362
  const attributes = {};
83023
- for (const [key, value] of Object.entries(tags)) if (value !== null) attributes[key] = value;
83363
+ for (const [key, value] of Object.entries(tags)) if (value != null) attributes[key] = value;
83024
83364
  return attributes;
83025
83365
  };
83026
83366
  //#endregion
83027
83367
  //#region src/cli/utils/record-metric.ts
83028
- const cleanAttributes = (attributes) => {
83029
- const cleaned = {};
83030
- if (!attributes) return cleaned;
83031
- for (const [key, value] of Object.entries(attributes)) if (value !== null && value !== void 0) cleaned[key] = value;
83032
- return cleaned;
83033
- };
83034
83368
  const withRunAttributes = (attributes) => ({
83035
83369
  ...toSpanAttributes(buildSentryScope().tags),
83036
- ...cleanAttributes(attributes)
83370
+ ...toSpanAttributes(attributes ?? {})
83037
83371
  });
83038
83372
  /**
83039
83373
  * Emits a Sentry counter. A guarded, swallow-on-throw no-op unless the CLI's
@@ -91050,6 +91384,19 @@ const ciInstallAction = (options) => runCiCommand("install", runCiInstall, optio
91050
91384
  const ciUpgradeAction = (options) => runCiCommand("upgrade", runCiUpgrade, options);
91051
91385
  const ciConfigAction = (options) => runCiCommand("config", runCiConfig, options);
91052
91386
  //#endregion
91387
+ //#region src/cli/utils/active-scan-abort-registry.ts
91388
+ const activeScanAbortControllers = /* @__PURE__ */ new Set();
91389
+ const activeScanAbortRegistry = {
91390
+ register: (controller) => {
91391
+ activeScanAbortControllers.add(controller);
91392
+ return () => activeScanAbortControllers.delete(controller);
91393
+ },
91394
+ abortAll: () => {
91395
+ for (const controller of activeScanAbortControllers) controller.abort();
91396
+ activeScanAbortControllers.clear();
91397
+ }
91398
+ };
91399
+ //#endregion
91053
91400
  //#region src/cli/utils/sentry-tracer.ts
91054
91401
  const toHrTime = (epochNanoseconds) => [Number(epochNanoseconds / NANOSECONDS_PER_SECOND), Number(epochNanoseconds % NANOSECONDS_PER_SECOND)];
91055
91402
  const SPAN_KIND_TO_OTEL = {
@@ -91466,7 +91813,14 @@ const isInspectResultComplete = (result) => isScanComplete({
91466
91813
  skippedCheckReasonCount: Object.keys(result.skippedCheckReasons ?? {}).length
91467
91814
  });
91468
91815
  //#endregion
91816
+ //#region src/cli/utils/resolve-score-unavailable-reason.ts
91817
+ const resolveScoreUnavailableReason = (input) => {
91818
+ if (input.isScoreDisabled) return "disabled";
91819
+ return input.isAnalysisIncomplete ? "analysis-incomplete" : "api-unavailable";
91820
+ };
91821
+ //#endregion
91469
91822
  //#region src/cli/utils/scan-result-cache.ts
91823
+ const createScanResultCacheInvocationState = () => ({ repositoryIdentityByRoot: /* @__PURE__ */ new Map() });
91470
91824
  const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
91471
91825
  const TOOLCHAIN_PACKAGE_SPECIFIERS = [
91472
91826
  "oxlint/package.json",
@@ -91551,7 +91905,7 @@ const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
91551
91905
  if (stats.size > 10485760) return null;
91552
91906
  return hashFileContents(absolutePath);
91553
91907
  };
91554
- const buildWorktreeFingerprint = (projectDirectory) => {
91908
+ const buildWorktreeFingerprint = (projectDirectory, repositoryRoot) => {
91555
91909
  const statusOutput = runGit(projectDirectory, [
91556
91910
  "status",
91557
91911
  "--porcelain=v1",
@@ -91563,8 +91917,6 @@ const buildWorktreeFingerprint = (projectDirectory) => {
91563
91917
  const statusEntries = parseWorktreeStatusRecords(statusOutput);
91564
91918
  if (statusEntries === null) return null;
91565
91919
  if (statusEntries.length > 300) return null;
91566
- const repositoryRoot = runGit(projectDirectory, ["rev-parse", "--show-toplevel"]);
91567
- if (repositoryRoot === null) return null;
91568
91920
  const dirtyEntries = [];
91569
91921
  for (const statusEntry of statusEntries) {
91570
91922
  const contentFingerprint = buildDirtyPathContentFingerprint(path.join(repositoryRoot, statusEntry.path), statusEntry.statusCode);
@@ -91580,6 +91932,20 @@ const buildWorktreeFingerprint = (projectDirectory) => {
91580
91932
  return firstSortKey < secondSortKey ? -1 : firstSortKey > secondSortKey ? 1 : 0;
91581
91933
  });
91582
91934
  };
91935
+ const resolveRepositoryCacheIdentity = (projectDirectory, invocationState) => {
91936
+ const repositoryRoot = runGit(projectDirectory, ["rev-parse", "--show-toplevel"]);
91937
+ if (repositoryRoot === null) return null;
91938
+ const cachedIdentity = invocationState.repositoryIdentityByRoot.get(repositoryRoot);
91939
+ if (cachedIdentity !== void 0) return cachedIdentity;
91940
+ const worktreeFingerprint = buildWorktreeFingerprint(projectDirectory, repositoryRoot);
91941
+ const headSha = readHeadSha(repositoryRoot);
91942
+ const identity = worktreeFingerprint === null || headSha === null ? null : {
91943
+ headSha,
91944
+ worktreeFingerprint
91945
+ };
91946
+ invocationState.repositoryIdentityByRoot.set(repositoryRoot, identity);
91947
+ return identity;
91948
+ };
91583
91949
  const DOTENV_FILE_NAME_PATTERN = /^\.env(\.|$)/;
91584
91950
  const resolveDotenvFingerprint = (projectDirectory) => {
91585
91951
  try {
@@ -91674,16 +92040,14 @@ const resolveToolchainFingerprint = (nodeBinaryPath) => {
91674
92040
  const buildScanResultCacheKey = (input) => {
91675
92041
  if (isCacheGloballyDisabled()) return null;
91676
92042
  if (!isGitIdentityTrustworthy(input.projectDirectory)) return null;
91677
- const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
91678
- if (worktreeFingerprint === null) return null;
91679
- const headSha = readHeadSha(input.projectDirectory);
91680
- if (headSha === null) return null;
92043
+ const repositoryIdentity = resolveRepositoryCacheIdentity(input.projectDirectory, input.invocationState);
92044
+ if (repositoryIdentity === null) return null;
91681
92045
  if (stringifyStableJson(input.userConfig) === null) return null;
91682
92046
  const cacheKeyJson = stringifyStableJson({
91683
92047
  schemaVersion: 6,
91684
92048
  projectIdentity: resolveProjectIdentity(input.projectDirectory),
91685
- headSha,
91686
- worktreeFingerprint,
92049
+ headSha: repositoryIdentity.headSha,
92050
+ worktreeFingerprint: repositoryIdentity.worktreeFingerprint,
91687
92051
  dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
91688
92052
  reactDoctorVersion: input.version,
91689
92053
  nodeVersion: process.version,
@@ -91697,6 +92061,8 @@ const buildScanResultCacheKey = (input) => {
91697
92061
  deadCode: input.options.deadCode,
91698
92062
  supplyChain: input.options.supplyChain,
91699
92063
  includePaths: [...input.options.includePaths].sort(),
92064
+ excludedProjectDirectories: [...input.options.excludedProjectDirectories ?? []].sort(),
92065
+ retainExcludedProjectDeadCodeDiagnostics: input.options.retainExcludedProjectDeadCodeDiagnostics ?? false,
91700
92066
  customRulesOnly: input.options.customRulesOnly,
91701
92067
  respectInlineDisables: input.options.respectInlineDisables,
91702
92068
  warnings: input.options.warnings,
@@ -91774,8 +92140,8 @@ const toCategoryKey = (category) => category.toLowerCase().replace(/[^a-z0-9]+/g
91774
92140
  * browser and stay group-/filter-/aggregate-able in the Spans dataset.
91775
92141
  *
91776
92142
  * Value types are preserved verbatim (numbers stay numbers so Sentry infers a
91777
- * numeric attribute and `p75(...)`/`avg(...)` work; `null` is kept so
91778
- * `toSpanAttributes` can drop absent signals rather than coerce them).
92143
+ * numeric attribute and `p75(...)`/`avg(...)` work; `null`/`undefined` are kept
92144
+ * so `toSpanAttributes` can drop absent signals rather than coerce them).
91779
92145
  */
91780
92146
  const withNamespace = (namespace, attributes) => {
91781
92147
  const namespaced = {};
@@ -91841,7 +92207,7 @@ const buildCacheAttributes = (input) => {
91841
92207
  const warmth = knownSubsystemCount > 0 ? reuseRatioSum / knownSubsystemCount : null;
91842
92208
  const temperature = warmth !== null && warmth > 0 ? "warm" : isCacheGloballyDisabled() ? "disabled" : "cold";
91843
92209
  return withNamespace("cache", {
91844
- wholeRepoHit: input.wholeRepoCacheHit ?? null,
92210
+ wholeRepoHit: input.wholeRepoCacheHit,
91845
92211
  temperature,
91846
92212
  warmth
91847
92213
  });
@@ -91963,33 +92329,37 @@ const buildOutcomeAttributes = (input) => {
91963
92329
  ...withNamespace("score", {
91964
92330
  value: result.score ? result.score.score : null,
91965
92331
  label: result.score ? result.score.label : null,
91966
- available: result.score !== null
92332
+ available: result.score !== null,
92333
+ unavailableReason: result.score === null ? resolveScoreUnavailableReason({
92334
+ isScoreDisabled: input.noScore,
92335
+ isAnalysisIncomplete: input.didLintFail === true || input.didDeadCodeFail === true
92336
+ }) : null
91967
92337
  }),
91968
92338
  ...withNamespace("lint", {
91969
- failed: input.didLintFail ?? null,
91970
- failureReasonKind: input.lintFailureReasonKind ?? null,
91971
- partialFailureCount: input.lintPartialFailureCount ?? null,
91972
- droppedFileCount: input.lintDroppedFileCount ?? null,
91973
- deadlineSkippedFileCount: input.lintDeadlineSkippedFileCount ?? null,
91974
- cacheHitFiles: result.lintCacheHitFileCount ?? null,
91975
- cacheTotalFiles: result.lintCacheTotalFileCount ?? null,
92339
+ failed: input.didLintFail,
92340
+ failureReasonKind: input.lintFailureReasonKind,
92341
+ partialFailureCount: input.lintPartialFailureCount,
92342
+ droppedFileCount: input.lintDroppedFileCount,
92343
+ deadlineSkippedFileCount: input.lintDeadlineSkippedFileCount,
92344
+ cacheHitFiles: result.lintCacheHitFileCount,
92345
+ cacheTotalFiles: result.lintCacheTotalFileCount,
91976
92346
  cacheHitRatio: ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
91977
- sidecarReplayedFiles: result.lintSidecarReplayedFileCount ?? null,
91978
- sidecarTotalFiles: result.lintSidecarTotalFileCount ?? null,
92347
+ sidecarReplayedFiles: result.lintSidecarReplayedFileCount,
92348
+ sidecarTotalFiles: result.lintSidecarTotalFileCount,
91979
92349
  sidecarReplayRatio: ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)
91980
92350
  }),
91981
92351
  ...withNamespace("deadCode", {
91982
- failed: input.didDeadCodeFail ?? null,
91983
- overlapped: input.deadCodeOverlapped ?? null,
91984
- cacheHit: result.deadCodeCacheHit ?? null,
91985
- summaryCacheHits: result.deadCodeSummaryCacheHits ?? null,
91986
- summaryCacheMisses: result.deadCodeSummaryCacheMisses ?? null
92352
+ failed: input.didDeadCodeFail,
92353
+ overlapped: input.deadCodeOverlapped,
92354
+ cacheHit: result.deadCodeCacheHit,
92355
+ summaryCacheHits: result.deadCodeSummaryCacheHits,
92356
+ summaryCacheMisses: result.deadCodeSummaryCacheMisses
91987
92357
  }),
91988
- ...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut ?? null }),
91989
- ...withNamespace("securityScan", { failed: input.securityScanFailed ?? null }),
92358
+ ...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut }),
92359
+ ...withNamespace("securityScan", { failed: input.securityScanFailed }),
91990
92360
  ...withNamespace("timing", {
91991
92361
  elapsedMs: result.elapsedMilliseconds,
91992
- scanMs: result.scanElapsedMilliseconds ?? null
92362
+ scanMs: result.scanElapsedMilliseconds
91993
92363
  }),
91994
92364
  ...withNamespace("migration", {
91995
92365
  largestRuleBucketFiles: largestRuleBucket ? largestRuleBucket.fileCount : null,
@@ -92001,7 +92371,7 @@ const buildOutcomeAttributes = (input) => {
92001
92371
  new: summary.totalDiagnosticCount,
92002
92372
  fixed: result.baselineDelta.fixedCount,
92003
92373
  baseTotal: result.baselineDelta.baseTotalCount,
92004
- crossFileMatches: result.baselineDelta.crossFileMatchCount ?? null,
92374
+ crossFileMatches: result.baselineDelta.crossFileMatchCount,
92005
92375
  degraded: false
92006
92376
  }));
92007
92377
  else if (input.gateExempt) attributes["baseline.degraded"] = true;
@@ -92010,7 +92380,7 @@ const buildOutcomeAttributes = (input) => {
92010
92380
  const buildActionAttributes = () => {
92011
92381
  const { githubActorAssociation } = resolveGithubActionsScoreMetadata();
92012
92382
  return withNamespace("action", {
92013
- actorAssociation: githubActorAssociation ?? null,
92383
+ actorAssociation: githubActorAssociation,
92014
92384
  runnerOs: detectRunnerOs(),
92015
92385
  comment: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.comment),
92016
92386
  reviewComments: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.reviewComments),
@@ -92024,7 +92394,7 @@ const buildScanAttributes = (input) => {
92024
92394
  mode: input.mode,
92025
92395
  scope: input.scope,
92026
92396
  parallel: input.parallel,
92027
- workerCount: input.workerCount ?? null,
92397
+ workerCount: input.workerCount,
92028
92398
  maxDurationMs: input.maxDurationMs,
92029
92399
  lint: input.lint,
92030
92400
  deadCode: input.deadCode,
@@ -92038,10 +92408,10 @@ const buildScanAttributes = (input) => {
92038
92408
  hasCustomConfig: input.hasCustomConfig,
92039
92409
  rulesConfigured: ruleKeys.length,
92040
92410
  rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
92041
- fileCount: input.result?.scannedFileCount ?? null,
92042
- nonJsxFileCount: input.result?.analyzedFiles?.filter((filePath) => !JSX_FILE_PATTERN.test(filePath)).length ?? null,
92043
- htmlFileCount: input.result?.analyzedFiles?.filter((filePath) => HTML_FILE_PATTERN.test(filePath)).length ?? null,
92044
- multilineDiagnosticCount: input.result?.diagnostics.filter((diagnostic) => (diagnostic.endLine ?? diagnostic.line) > diagnostic.line).length ?? null
92411
+ fileCount: input.result?.scannedFileCount,
92412
+ nonJsxFileCount: input.result?.analyzedFiles?.filter((filePath) => !JSX_FILE_PATTERN.test(filePath)).length,
92413
+ htmlFileCount: input.result?.analyzedFiles?.filter((filePath) => HTML_FILE_PATTERN.test(filePath)).length,
92414
+ multilineDiagnosticCount: input.result?.diagnostics.filter((diagnostic) => (diagnostic.endLine ?? diagnostic.line) > diagnostic.line).length
92045
92415
  });
92046
92416
  };
92047
92417
  /**
@@ -92053,11 +92423,11 @@ const buildScanAttributes = (input) => {
92053
92423
  * tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
92054
92424
  * in the Spans dataset. Pure and exported so the projection (outcome
92055
92425
  * precedence, rule/category rollups, CI knobs, config shape) is unit-testable
92056
- * without a live Sentry client. `null` values are dropped so absent signals
92057
- * never become misleading `"null"` attributes. The run + project base context
92058
- * (version, command, ci/provider, framework, …) is already on the span from
92059
- * `withSentryRunSpan` / `recordSentryProjectContext`, so this adds only what
92060
- * those don't carry.
92426
+ * without a live Sentry client. `null`/`undefined` values are dropped so absent
92427
+ * signals never become misleading `"null"`/`"undefined"` attributes. The run +
92428
+ * project base context (version, command, ci/provider, framework, …) is already
92429
+ * on the span from `withSentryRunSpan` / `recordSentryProjectContext`, so this
92430
+ * adds only what those don't carry.
92061
92431
  */
92062
92432
  const buildRunEventAttributes = (input) => toSpanAttributes({
92063
92433
  ...buildScanAttributes(input),
@@ -92531,8 +92901,35 @@ const createDiagnosticEvidenceReader = (rootDirectory, options = {}) => {
92531
92901
  //#endregion
92532
92902
  //#region src/cli/utils/build-no-score-message.ts
92533
92903
  const ENTERPRISE_CONTACT_HINT = `Want something custom to your company? Contact us at ${ENTERPRISE_CONTACT_URL}.`;
92534
- const buildNoScoreMessage = (isScoreDisabled, disabledMessage) => {
92535
- return `${isScoreDisabled ? disabledMessage ?? "Score disabled by --no-score." : "Score unavailable (could not reach the score API)."} ${ENTERPRISE_CONTACT_HINT}`;
92904
+ const buildNoScoreMessage = (input) => {
92905
+ const unavailableReason = resolveScoreUnavailableReason({
92906
+ isScoreDisabled: input.isScoreDisabled,
92907
+ isAnalysisIncomplete: input.isAnalysisIncomplete ?? false
92908
+ });
92909
+ let reason;
92910
+ switch (unavailableReason) {
92911
+ case "disabled":
92912
+ reason = input.disabledMessage ?? "Score disabled by --no-score.";
92913
+ break;
92914
+ case "analysis-incomplete":
92915
+ reason = "Score not shown because lint or dead-code analysis could not complete.";
92916
+ break;
92917
+ case "api-unavailable":
92918
+ reason = "Score unavailable (could not reach the score API).";
92919
+ break;
92920
+ }
92921
+ return `${reason} ${ENTERPRISE_CONTACT_HINT}`;
92922
+ };
92923
+ //#endregion
92924
+ //#region src/cli/utils/has-incomplete-score-analysis.ts
92925
+ const hasIncompleteScoreAnalysis = (skippedChecks) => skippedChecks.includes("lint") || skippedChecks.includes("dead-code");
92926
+ //#endregion
92927
+ //#region src/cli/utils/build-empty-report-message.ts
92928
+ const buildEmptyReportMessage = (input) => {
92929
+ const categoryFilters = [...input.categoryFilters];
92930
+ if (categoryFilters.length > 0) return `No issues found in category ${categoryFilters.join(", ")}!`;
92931
+ if (input.demotedDiagnosticCount > 0) return `No issues found! (${input.demotedDiagnosticCount} demoted from the ${input.outputSurface} surface — see config.surfaces.)`;
92932
+ return "No issues found!";
92536
92933
  };
92537
92934
  //#endregion
92538
92935
  //#region src/cli/utils/render-agent-guidance.ts
@@ -93279,9 +93676,6 @@ const markOnboardingComplete = (options = {}) => {
93279
93676
  if (isGatePending(ONBOARDING_GATE, {}, options)) recordGate(ONBOARDING_GATE, {}, options);
93280
93677
  };
93281
93678
  //#endregion
93282
- //#region src/cli/utils/render-project-detection.ts
93283
- const printProjectDetection = (_input) => void_;
93284
- //#endregion
93285
93679
  //#region src/cli/utils/colorize-by-score.ts
93286
93680
  const colorizeByScore = (text, score) => {
93287
93681
  if (score >= 75) return highlighter.success(text);
@@ -93495,6 +93889,26 @@ const printNoScoreHeader = (noScoreMessage) => gen(function* () {
93495
93889
  yield* log("");
93496
93890
  });
93497
93891
  //#endregion
93892
+ //#region src/cli/utils/build-footer-description-lines.ts
93893
+ const FOOTER_DESCRIPTION_INDENT = " ";
93894
+ const buildFooterDescriptionLines = (description) => {
93895
+ return wrapTextToWidth(description, resolveMeasureWidth(2)).map((line) => highlighter.dim(`${FOOTER_DESCRIPTION_INDENT}${line}`));
93896
+ };
93897
+ //#endregion
93898
+ //#region src/cli/utils/build-footer-link-lines.ts
93899
+ const buildFooterLinkLines = ({ shareUrl }) => [
93900
+ ...shareUrl === null ? [] : [
93901
+ ` ${highlighter.bold("Share:")} ${highlighter.info(shareUrl)}`,
93902
+ ...buildFooterDescriptionLines("Tell others how you did on socials"),
93903
+ ""
93904
+ ],
93905
+ ` ${highlighter.bold("Docs:")} ${highlighter.info(DOCS_URL)}`,
93906
+ ...buildFooterDescriptionLines("Learn more about fixing issues, setting up CI/CD, and configuring rules with a config file"),
93907
+ "",
93908
+ ` ${highlighter.bold("GitHub:")} ${highlighter.info(CANONICAL_GITHUB_URL)}`,
93909
+ ...buildFooterDescriptionLines("Report issues and star the repository!")
93910
+ ];
93911
+ //#endregion
93498
93912
  //#region src/cli/utils/write-diagnostics-directory.ts
93499
93913
  const ruleDumpFileName = (ruleKey) => ruleKey.replace(/\//g, "--") + ".txt";
93500
93914
  const readPreviousRuleDumpFileNames = (directory) => {
@@ -93515,11 +93929,6 @@ const writeDiagnosticsDirectory = (diagnostics, outputDirectory) => {
93515
93929
  };
93516
93930
  //#endregion
93517
93931
  //#region src/cli/utils/render-summary.ts
93518
- const FOOTER_DESCRIPTION_INDENT = " ";
93519
- const printFooterDescription = (description) => gen(function* () {
93520
- const wrapWidth = resolveMeasureWidth(2);
93521
- for (const line of wrapTextToWidth(description, wrapWidth)) yield* log(highlighter.dim(`${FOOTER_DESCRIPTION_INDENT}${line}`));
93522
- });
93523
93932
  const buildShareUrl = (diagnostics, scoreResult, projectName) => {
93524
93933
  const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
93525
93934
  const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
@@ -93536,17 +93945,8 @@ const printFooter = (input) => gen(function* () {
93536
93945
  yield* log("");
93537
93946
  yield* log(buildSectionDivider());
93538
93947
  yield* log("");
93539
- if (!input.isOffline) {
93540
- const shareUrl = buildShareUrl(input.diagnostics, input.scoreResult, input.projectName);
93541
- yield* log(` ${highlighter.bold("Share:")} ${highlighter.info(shareUrl)}`);
93542
- yield* printFooterDescription("Tell others how you did on socials");
93543
- yield* log("");
93544
- }
93545
- yield* log(` ${highlighter.bold("Docs:")} ${highlighter.info(DOCS_URL)}`);
93546
- yield* printFooterDescription("Learn more about fixing issues, setting up CI/CD, and configuring rules with a config file");
93547
- yield* log("");
93548
- yield* log(` ${highlighter.bold("GitHub:")} ${highlighter.info(CANONICAL_GITHUB_URL)}`);
93549
- yield* printFooterDescription("Report issues and star the repository!");
93948
+ const shareUrl = input.isOffline ? null : buildShareUrl(input.diagnostics, input.scoreResult, input.projectName);
93949
+ for (const line of buildFooterLinkLines({ shareUrl })) yield* log(line);
93550
93950
  });
93551
93951
  const printDiagnosticsDump = (diagnostics, outputDirectory, verbose, stream = "stdout") => gen(function* () {
93552
93952
  const writtenDirectory = yield* try_({
@@ -93643,9 +94043,6 @@ const resolveCliCategories = (categoryFlag) => {
93643
94043
  //#endregion
93644
94044
  //#region src/inspect.ts
93645
94045
  const silentConsole = makeNoopConsole();
93646
- const runConsole = (effect) => {
93647
- runSync(effect);
93648
- };
93649
94046
  const recordOnboardingCompletion = (options) => {
93650
94047
  const forceOnboarding = isOnboardingForced();
93651
94048
  if (shouldRecordOnboarding({
@@ -93655,7 +94052,6 @@ const recordOnboardingCompletion = (options) => {
93655
94052
  isNonInteractiveEnvironment: options.isNonInteractiveEnvironment
93656
94053
  })) markOnboardingComplete();
93657
94054
  };
93658
- const formatCategorySelection = (categoryFilters) => [...categoryFilters].join(", ");
93659
94055
  const buildChangedLineMatcher = (directory, changedLineRanges) => {
93660
94056
  const rangesByFile = /* @__PURE__ */ new Map();
93661
94057
  for (const entry of changedLineRanges) rangesByFile.set(toForwardSlashes(entry.file), entry.ranges);
@@ -93705,7 +94101,10 @@ const mergeInspectOptions = (inputOptions, userConfig) => {
93705
94101
  maxDurationMs: inputOptions.maxDurationMs ?? null,
93706
94102
  baseline: inputOptions.baseline ?? null,
93707
94103
  changedLineRanges: inputOptions.changedLineRanges ?? null,
93708
- supplyChainManifestChanged: inputOptions.supplyChainManifestChanged ?? false
94104
+ supplyChainManifestChanged: inputOptions.supplyChainManifestChanged ?? false,
94105
+ excludedProjectDirectories: inputOptions.excludedProjectDirectories ?? [],
94106
+ retainExcludedProjectDeadCodeDiagnostics: inputOptions.retainExcludedProjectDeadCodeDiagnostics ?? false,
94107
+ precomputedSourceFileCount: inputOptions.precomputedSourceFileCount
93709
94108
  };
93710
94109
  };
93711
94110
  const deriveScope = (options) => {
@@ -93738,13 +94137,14 @@ const inspectWithOxlintRuntime = async (directory, inputOptions, oxlintRuntime)
93738
94137
  const deadlineEpochMs = inputOptions.deadlineEpochMs ?? (inputOptions.maxDurationMs != null ? Date.now() + inputOptions.maxDurationMs : null);
93739
94138
  const isConcurrentScan = inputOptions.concurrentScan === true;
93740
94139
  if (!isConcurrentScan) resetSentryRunState();
93741
- const hasConfigOverride = inputOptions.configOverride !== void 0;
94140
+ const configOverride = inputOptions.configOverride;
94141
+ const hasConfigOverride = configOverride !== void 0;
93742
94142
  let scanDirectory;
93743
94143
  let userConfig;
93744
94144
  let configSourceDirectory;
93745
94145
  if (hasConfigOverride) {
93746
94146
  scanDirectory = directory;
93747
- userConfig = inputOptions.configOverride ?? null;
94147
+ userConfig = configOverride;
93748
94148
  configSourceDirectory = inputOptions.configSourceDirectory ?? null;
93749
94149
  } else {
93750
94150
  const scanTarget = await resolveScanTarget(directory);
@@ -93777,11 +94177,22 @@ const inspectWithOxlintRuntime = async (directory, inputOptions, oxlintRuntime)
93777
94177
  };
93778
94178
  const createInvocationInspect = (requestedOxlintConcurrency) => {
93779
94179
  const concurrency = resolveScanConcurrency(requestedOxlintConcurrency ?? runSync(OxlintConcurrency));
93780
- const oxlintRuntime = {
93781
- concurrency,
93782
- spawnSlots: createOxlintSpawnSlots(concurrency)
94180
+ const spawnSlots = createOxlintSpawnSlots(concurrency);
94181
+ const scanResultCacheInvocationState = createScanResultCacheInvocationState();
94182
+ return async (directory, inputOptions = {}) => {
94183
+ const abortController = new AbortController();
94184
+ const unregisterAbortController = activeScanAbortRegistry.register(abortController);
94185
+ try {
94186
+ return await inspectWithOxlintRuntime(directory, inputOptions, {
94187
+ concurrency,
94188
+ spawnSlots,
94189
+ abortSignal: abortController.signal,
94190
+ scanResultCacheInvocationState
94191
+ });
94192
+ } finally {
94193
+ unregisterAbortController();
94194
+ }
93783
94195
  };
93784
- return (directory, inputOptions = {}) => inspectWithOxlintRuntime(directory, inputOptions, oxlintRuntime);
93785
94196
  };
93786
94197
  const inspect = async (directory, inputOptions = {}) => createInvocationInspect(inputOptions.concurrency)(directory, inputOptions);
93787
94198
  const countIncompleteLintFiles = (lintPartialFailures) => countDroppedLintFiles(lintPartialFailures) + countDeadlineSkippedFiles(lintPartialFailures);
@@ -93794,12 +94205,27 @@ const countIncompleteLintFiles = (lintPartialFailures) => countDroppedLintFiles(
93794
94205
  */
93795
94206
  const runBaselineComparison = async (params) => {
93796
94207
  const tempDirectory = mkdtempSync(path.join(tmpdir(), BASELINE_FILES_TEMP_DIR_PREFIX));
94208
+ const baselineIncludePaths = filterPathsOutsideDirectories({
94209
+ rootDirectory: params.directory,
94210
+ relativePaths: params.options.includePaths,
94211
+ excludedDirectories: params.options.excludedProjectDirectories
94212
+ });
94213
+ const baselineBaseFiles = params.baseFiles ? filterPathsOutsideDirectories({
94214
+ rootDirectory: params.directory,
94215
+ relativePaths: params.baseFiles,
94216
+ excludedDirectories: params.options.excludedProjectDirectories
94217
+ }) : void 0;
94218
+ const baselineHeadFiles = params.headFiles ? filterPathsOutsideDirectories({
94219
+ rootDirectory: params.directory,
94220
+ relativePaths: params.headFiles,
94221
+ excludedDirectories: params.options.excludedProjectDirectories
94222
+ }) : void 0;
93797
94223
  const snapshot = await materializeBaselineFiles({
93798
94224
  directory: params.directory,
93799
94225
  ref: params.baselineRef,
93800
- files: params.options.includePaths,
93801
- baseFiles: params.baseFiles,
93802
- headFiles: params.headFiles,
94226
+ files: baselineIncludePaths,
94227
+ baseFiles: baselineBaseFiles,
94228
+ headFiles: baselineHeadFiles,
93803
94229
  tempDirectory
93804
94230
  }).catch((error) => {
93805
94231
  rmSync(tempDirectory, {
@@ -93821,7 +94247,7 @@ const runBaselineComparison = async (params) => {
93821
94247
  const baseFiles = new Set(snapshot.baseFiles.map(toForwardSlashes));
93822
94248
  const trackedHeadFiles = new Set(snapshot.headFiles.map(toForwardSlashes));
93823
94249
  const expectedHeadFiles = new Set(trackedHeadFiles);
93824
- for (const filePath of params.options.includePaths) {
94250
+ for (const filePath of baselineIncludePaths) {
93825
94251
  const normalizedFilePath = toForwardSlashes(filePath);
93826
94252
  if (!baseFiles.has(normalizedFilePath)) expectedHeadFiles.add(normalizedFilePath);
93827
94253
  }
@@ -93858,8 +94284,9 @@ const runBaselineComparison = async (params) => {
93858
94284
  resolveLocalGithubViewerPermission: false,
93859
94285
  suppressScanSummary: true,
93860
94286
  supplyChainManifestChanged: params.options.supplyChainManifestChanged,
93861
- deadlineEpochMs: params.deadlineEpochMs ?? void 0
93862
- }, {}).pipe(provide(baseLayers), provideService$2(PerFileLintCacheEnabled, false), provideService$2(SidecarLintCacheEnabled, false), provideService$2(Console, silentConsole))));
94287
+ deadlineEpochMs: params.deadlineEpochMs ?? void 0,
94288
+ signal: params.oxlintRuntime.abortSignal
94289
+ }, {}).pipe(provide(baseLayers), provideService$2(PerFileLintCacheEnabled, false), provideService$2(SidecarLintCacheEnabled, false), provideService$2(Console, silentConsole))), { signal: params.oxlintRuntime.abortSignal });
93863
94290
  if (baseOutput.didLintFail || countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0) return null;
93864
94291
  const hasUnscannedUntrackedSourceFiles = filterSourceFiles(snapshot.untrackedFiles.map(toForwardSlashes)).some((filePath) => !analyzedHeadFiles.has(filePath));
93865
94292
  const delta = computeDiagnosticDelta({
@@ -93887,6 +94314,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
93887
94314
  const isDiffMode = options.includePaths.length > 0;
93888
94315
  const resolvedNodeBinaryPath = await resolveOxlintNode(options.lint, options.scoreOnly || options.silent);
93889
94316
  const lintBindingMissing = options.lint && !resolvedNodeBinaryPath;
94317
+ await yieldToEventLoop();
93890
94318
  const cacheKey = buildScanResultCacheKey({
93891
94319
  projectDirectory: directory,
93892
94320
  version: VERSION,
@@ -93894,19 +94322,14 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
93894
94322
  options,
93895
94323
  userConfig,
93896
94324
  hasConfigOverride,
93897
- configSourceDirectory
94325
+ configSourceDirectory,
94326
+ invocationState: oxlintRuntime.scanResultCacheInvocationState
93898
94327
  });
93899
94328
  const scanResultCache = cacheKey === null ? null : createScanResultCache(directory);
93900
- const cachedPayload = cacheKey === null ? null : scanResultCache?.lookup(cacheKey) ?? null;
94329
+ const cachedPayload = cacheKey === null || scanResultCache === null ? null : scanResultCache.lookup(cacheKey);
93901
94330
  if (cachedPayload) {
93902
94331
  recordSentryProjectContext(cachedPayload.project, rootSentrySpan, { concurrentScan: options.concurrentScan });
93903
94332
  recordCount(METRIC.projectDetected, 1);
93904
- await renderCachedProjectDetection({
93905
- payload: cachedPayload,
93906
- options,
93907
- userConfig,
93908
- isDiffMode
93909
- });
93910
94333
  const baselineDegraded = Boolean(options.baseline) && isDiffMode && cachedPayload.baselineDelta === void 0;
93911
94334
  const result = await renderAndRecordScan({
93912
94335
  payload: cachedPayload,
@@ -93940,6 +94363,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
93940
94363
  });
93941
94364
  const program = runInspect({
93942
94365
  directory,
94366
+ precomputedSourceFileCount: options.precomputedSourceFileCount,
93943
94367
  includePaths: options.includePaths,
93944
94368
  customRulesOnly: options.customRulesOnly,
93945
94369
  respectInlineDisables: options.respectInlineDisables,
@@ -93956,26 +94380,19 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
93956
94380
  resolveLocalGithubViewerPermission: !options.noScore,
93957
94381
  suppressScanSummary: options.suppressRendering,
93958
94382
  supplyChainManifestChanged: options.supplyChainManifestChanged,
93959
- concurrentScan: options.concurrentScan,
93960
- deadlineEpochMs: deadlineEpochMs ?? void 0
93961
- }, { beforeLint: (projectInfo, lintIncludePaths) => gen(function* () {
94383
+ deadlineEpochMs: deadlineEpochMs ?? void 0,
94384
+ signal: oxlintRuntime.abortSignal,
94385
+ excludedProjectDirectories: options.excludedProjectDirectories,
94386
+ retainExcludedProjectDeadCodeDiagnostics: options.retainExcludedProjectDeadCodeDiagnostics
94387
+ }, { beforeLint: (projectInfo) => sync(() => {
93962
94388
  recordSentryProjectContext(projectInfo, rootSentrySpan, { concurrentScan: options.concurrentScan });
93963
94389
  recordCount(METRIC.projectDetected, 1);
93964
- if (options.scoreOnly || options.suppressRendering) return;
93965
- const lintSourceFileCount = lintIncludePaths?.length ?? projectInfo.sourceFileCount;
93966
- yield* printProjectDetection({
93967
- projectInfo,
93968
- userConfig,
93969
- isDiffMode,
93970
- includePaths: options.includePaths,
93971
- lintSourceFileCount
93972
- });
93973
94390
  }) });
93974
- const output = await runPromise(restoreLegacyThrow(applyObservability(options.silent ? program.pipe(provide(layers), provideService$2(Console, silentConsole)) : program.pipe(provide(layers)), rootSentrySpan)));
94391
+ const output = await runPromise(restoreLegacyThrow(applyObservability(options.silent ? program.pipe(provide(layers), provideService$2(Console, silentConsole)) : program.pipe(provide(layers)), rootSentrySpan)), { signal: oxlintRuntime.abortSignal });
93975
94392
  const didLintFail = lintBindingMissing || output.didLintFail;
93976
94393
  const lintFailureReason = lintBindingMissing ? `oxlint native binding not found for Node ${process.version}; expected one matching ${OXLINT_NODE_REQUIREMENT}` : output.lintFailureReason;
93977
- 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`)));
93978
- else runConsole(error$1(highlighter.error(lintFailureReason)));
94394
+ if (!options.scoreOnly && !options.uiLayers && !lintBindingMissing && output.didLintFail && lintFailureReason !== null) if (output.lintFailureReasonKind === "native-binding-missing") runSync(log(highlighter.gray(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`)));
94395
+ else runSync(error$1(highlighter.error(lintFailureReason)));
93979
94396
  let inspectDiagnostics = output.diagnostics;
93980
94397
  let baselineDelta;
93981
94398
  if (options.baseline && isDiffMode && !didLintFail && countIncompleteLintFiles(output.lintPartialFailures) === 0) {
@@ -94025,6 +94442,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
94025
94442
  lintFailureReasonKind: lintBindingMissing ? "native-binding-missing" : output.lintFailureReasonKind,
94026
94443
  supplyChainOverlapTimedOut: output.supplyChainOverlapTimedOut,
94027
94444
  securityScanFailed: output.securityScanFailed,
94445
+ securityScanFailureReason: output.securityScanFailureReason,
94028
94446
  suppressedRuleCounts: output.suppressedRuleCounts
94029
94447
  };
94030
94448
  if (cacheKey !== null && scanResultCache !== null && shouldStoreScanPayload(payload) && !baselineDegraded) scanResultCache.store(cacheKey, payload);
@@ -94050,16 +94468,6 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
94050
94468
  return result;
94051
94469
  };
94052
94470
  const runMaybeSilent = (effect, silent) => silent ? effect.pipe(provideService$2(Console, silentConsole)) : effect;
94053
- const renderCachedProjectDetection = async (input) => {
94054
- if (input.options.scoreOnly || input.options.suppressRendering) return;
94055
- await runPromise(runMaybeSilent(printProjectDetection({
94056
- projectInfo: input.payload.project,
94057
- userConfig: input.userConfig,
94058
- isDiffMode: input.isDiffMode,
94059
- includePaths: input.options.includePaths,
94060
- lintSourceFileCount: input.payload.scannedFileCount
94061
- }), input.options.silent));
94062
- };
94063
94471
  const renderAndRecordScan = async (input) => {
94064
94472
  const result = await runPromise(runMaybeSilent(finalizeAndRender({
94065
94473
  options: input.options,
@@ -94075,6 +94483,7 @@ const renderAndRecordScan = async (input) => {
94075
94483
  deadCodeFailureReason: input.payload.deadCodeFailureReason,
94076
94484
  supplyChainOverlapTimedOut: input.payload.supplyChainOverlapTimedOut,
94077
94485
  securityScanFailed: input.payload.securityScanFailed ?? false,
94486
+ securityScanFailureReason: input.payload.securityScanFailureReason ?? null,
94078
94487
  directory: input.payload.directory,
94079
94488
  scannedFileCount: input.payload.scannedFileCount,
94080
94489
  scannedFilePaths: input.payload.scannedFilePaths,
@@ -94126,7 +94535,7 @@ const renderAndRecordScan = async (input) => {
94126
94535
  return result;
94127
94536
  };
94128
94537
  const finalizeAndRender = (input) => gen(function* () {
94129
- 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;
94538
+ 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;
94130
94539
  const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
94131
94540
  didLintFail,
94132
94541
  lintFailureReason,
@@ -94134,10 +94543,15 @@ const finalizeAndRender = (input) => gen(function* () {
94134
94543
  didDeadCodeFail,
94135
94544
  deadCodeFailureReason,
94136
94545
  supplyChainOverlapTimedOut,
94137
- securityScanFailed
94546
+ securityScanFailed,
94547
+ securityScanFailureReason
94138
94548
  });
94139
94549
  const hasSkippedChecks = skippedChecks.length > 0;
94140
- const noScoreMessage = buildNoScoreMessage(options.noScore, options.scoreDisabledMessage);
94550
+ const noScoreMessage = buildNoScoreMessage({
94551
+ isScoreDisabled: options.noScore,
94552
+ isAnalysisIncomplete: hasIncompleteScoreAnalysis(skippedChecks),
94553
+ disabledMessage: options.scoreDisabledMessage
94554
+ });
94141
94555
  const buildResult = () => ({
94142
94556
  diagnostics: [...diagnostics],
94143
94557
  score,
@@ -94183,9 +94597,11 @@ const finalizeAndRender = (input) => gen(function* () {
94183
94597
  if (hasSkippedChecks) {
94184
94598
  const skippedLabel = skippedChecks.join(" and ");
94185
94599
  yield* warn$1(highlighter.warn(`No issues detected, but ${skippedLabel} checks failed — results are incomplete.`));
94186
- } else if (options.categoryFilters.size > 0) yield* log(highlighter.success(`No issues found in category ${formatCategorySelection(options.categoryFilters)}!`));
94187
- else if (demotedDiagnosticCount > 0) yield* log(highlighter.success(`No issues found! (${demotedDiagnosticCount} demoted from the ${options.outputSurface} surface — see config.surfaces.)`));
94188
- else yield* log(highlighter.success("No issues found!"));
94600
+ } else yield* log(highlighter.success(buildEmptyReportMessage({
94601
+ categoryFilters: options.categoryFilters,
94602
+ demotedDiagnosticCount,
94603
+ outputSurface: options.outputSurface
94604
+ })));
94189
94605
  yield* log("");
94190
94606
  yield* pause;
94191
94607
  if (hasSkippedChecks) {
@@ -94265,6 +94681,32 @@ const materializeStagedFiles = async (input) => {
94265
94681
  };
94266
94682
  };
94267
94683
  //#endregion
94684
+ //#region src/cli/utils/deduplicate-project-scans.ts
94685
+ const deduplicateProjectScans = (projectScans) => {
94686
+ const uniqueProjectScans = [];
94687
+ const seenDirectories = /* @__PURE__ */ new Set();
94688
+ for (const projectScan of projectScans) {
94689
+ if (seenDirectories.has(projectScan.directory)) continue;
94690
+ seenDirectories.add(projectScan.directory);
94691
+ uniqueProjectScans.push(projectScan);
94692
+ }
94693
+ return uniqueProjectScans;
94694
+ };
94695
+ //#endregion
94696
+ //#region src/cli/utils/collect-project-source-file-counts.ts
94697
+ const collectProjectSourceFileCounts = async (rootDirectory, projectDirectories) => {
94698
+ const abortController = new AbortController();
94699
+ const unregisterAbortController = activeScanAbortRegistry.register(abortController);
94700
+ try {
94701
+ return await collectSourceFileCountsByDirectory(rootDirectory, projectDirectories, abortController.signal);
94702
+ } finally {
94703
+ unregisterAbortController();
94704
+ }
94705
+ };
94706
+ //#endregion
94707
+ //#region src/cli/utils/format-skipped-projects-message.ts
94708
+ const formatSkippedProjectsMessage = (projectCount) => `${projectCount} ${projectCount === 1 ? "project was" : "projects were"} skipped because the max scan duration was reached.`;
94709
+ //#endregion
94268
94710
  //#region src/cli/utils/is-share-opted-out.ts
94269
94711
  const isShareOptedOut = (scans, flagNoScore) => Boolean(flagNoScore) || scans.some((scan) => Boolean(scan.config?.noScore) || scan.config?.share === false);
94270
94712
  //#endregion
@@ -94276,7 +94718,7 @@ const buildHandoffPayload = (input) => {
94276
94718
  try {
94277
94719
  outputDirectory = writeDiagnosticsDirectory([...input.diagnostics], input.outputDirectory);
94278
94720
  } catch {}
94279
- const lines = [`Fix the top ${topGroups.length} React Doctor ${topGroups.length === 1 ? "issue" : "issues"} in ${input.projectName} on this pass — leave the rest for a follow-up.`, ""];
94721
+ const lines = [`Review and fix the top ${topGroups.length} React Doctor ${topGroups.length === 1 ? "finding" : "findings"} in ${input.projectName}. Leave the rest for a follow-up.`, ""];
94280
94722
  topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
94281
94723
  const representative = ruleDiagnostics[0];
94282
94724
  const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
@@ -94304,7 +94746,7 @@ const buildHandoffPayload = (input) => {
94304
94746
  const ruleSummaries = deferredMigrationBuckets.map((bucket) => `${bucket.title} (${bucket.fileCount} files)`).join(", ");
94305
94747
  lines.push(`Some of the rest are migration-scale (span dozens of files): ${ruleSummaries}. For each, fix a representative sample, confirm the recipe holds, and get the code owner's sign-off before changing the rest in one pass.`, "");
94306
94748
  }
94307
- lines.push("Then work through the rest from the full results above.");
94749
+ lines.push("Stop after this pass. Summarize the remaining findings for a follow-up.");
94308
94750
  return lines.join("\n");
94309
94751
  };
94310
94752
  //#endregion
@@ -94514,20 +94956,20 @@ const resolveOpenCommand = (url) => {
94514
94956
  args: [url]
94515
94957
  };
94516
94958
  };
94517
- const openUrl = (url) => {
94959
+ const openUrl = (url) => new Promise((resolve) => {
94518
94960
  try {
94519
94961
  const { command, args } = resolveOpenCommand(url);
94520
94962
  const child = spawn(command, args, {
94521
94963
  detached: true,
94522
94964
  stdio: "ignore"
94523
94965
  });
94524
- child.on("error", () => {});
94966
+ child.once("spawn", () => resolve(true));
94967
+ child.once("error", () => resolve(false));
94525
94968
  child.unref();
94526
- return true;
94527
94969
  } catch {
94528
- return false;
94970
+ resolve(false);
94529
94971
  }
94530
- };
94972
+ });
94531
94973
  //#endregion
94532
94974
  //#region src/cli/utils/ask-add-to-github-actions.ts
94533
94975
  const CI_YES_CHOICE = "ci-yes";
@@ -94568,7 +95010,7 @@ const askAddToGitHubActions = async (prompt = prompts) => {
94568
95010
  if (ciChoice === void 0) return "cancel";
94569
95011
  if (ciChoice === CI_YES_CHOICE) return "yes";
94570
95012
  if (ciChoice === CI_NO_CHOICE) return "no";
94571
- const opened = openUrl(GITHUB_ACTIONS_SETUP_URL);
95013
+ const opened = await openUrl(GITHUB_ACTIONS_SETUP_URL);
94572
95014
  cliLogger.log(opened ? `Opened ${highlighter.info(GITHUB_ACTIONS_SETUP_URL)} in your browser.` : `Visit ${highlighter.info(GITHUB_ACTIONS_SETUP_URL)} to learn more.`);
94573
95015
  cliLogger.break();
94574
95016
  }
@@ -95665,7 +96107,7 @@ const runInstallReactDoctor = async (options = {}) => {
95665
96107
  const shouldInstallGitHook = gitHookPath != null && (Boolean(options.yes) || !didSkipOptionalSetup && selectedSetupActions.includes(SETUP_OPTION_GIT_HOOK));
95666
96108
  const shouldInstallAgentHooks = Boolean(options.agentHooks) || !didSkipOptionalSetup && selectedSetupActions.includes(SETUP_OPTION_AGENT_HOOKS);
95667
96109
  if (!options.dryRun) {
95668
- if (shouldInstallGitHook && gitHookTarget !== null && gitHookTarget !== void 0) installReactDoctorGitHookStep(gitHookTarget);
96110
+ if (shouldInstallGitHook && gitHookTarget != null) installReactDoctorGitHookStep(gitHookTarget);
95669
96111
  if (shouldInstallAgentHooks) installReactDoctorAgentHooksStep(projectRoot, selectedAgents);
95670
96112
  }
95671
96113
  if (options.dryRun) {
@@ -96283,6 +96725,27 @@ const resolveCliInspectOptions = (flags, userConfig) => {
96283
96725
  };
96284
96726
  };
96285
96727
  //#endregion
96728
+ //#region src/cli/utils/resolve-workspace-dead-code-owner.ts
96729
+ const resolveWorkspaceDeadCodeOwner = (input) => {
96730
+ if (!input.isRootDeadCodeEnabled) return null;
96731
+ const resolvedRootDirectory = Path.resolve(input.rootDirectory);
96732
+ return input.projectDirectories.find((projectDirectory) => Path.resolve(projectDirectory) === resolvedRootDirectory) ?? null;
96733
+ };
96734
+ //#endregion
96735
+ //#region src/cli/utils/retry-missing-project-scores.ts
96736
+ const retryMissingProjectScores = async (projectScans) => mapWithConcurrency(projectScans, 64, async (projectScan) => {
96737
+ if (!(projectScan.result.score === null && !projectScan.isScoreDisabled && !hasIncompleteScoreAnalysis(projectScan.result.skippedChecks))) return projectScan;
96738
+ const score = await calculateScore(filterScansForSurface([projectScan], "score"));
96739
+ recordCount(METRIC.scanScoreRetry, 1, { succeeded: score !== null });
96740
+ return score === null ? projectScan : {
96741
+ ...projectScan,
96742
+ result: {
96743
+ ...projectScan.result,
96744
+ score
96745
+ }
96746
+ };
96747
+ });
96748
+ //#endregion
96286
96749
  //#region src/cli/utils/resolve-project-relative-directory.ts
96287
96750
  /**
96288
96751
  * The scanned project's directory relative to the diff root, forward-slashed —
@@ -96343,6 +96806,16 @@ const resolveProjectChangedLineRanges = (rootDirectory, projectDirectory, ranges
96343
96806
  });
96344
96807
  };
96345
96808
  //#endregion
96809
+ //#region src/cli/utils/resolve-project-scan.ts
96810
+ const resolveProjectScan = async (rootScanTarget, projectDirectory) => {
96811
+ const projectScanTarget = projectDirectory === rootScanTarget.resolvedDirectory ? rootScanTarget : await resolveScanTarget(projectDirectory, { allowAmbiguous: true });
96812
+ return {
96813
+ directory: projectScanTarget.resolvedDirectory,
96814
+ config: projectDirectory === rootScanTarget.resolvedDirectory ? rootScanTarget.userConfig : mergeReactDoctorConfigs(rootScanTarget.userConfig, projectScanTarget.userConfig ?? void 0),
96815
+ configSourceDirectory: projectScanTarget.userConfig?.plugins === void 0 ? rootScanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory
96816
+ };
96817
+ };
96818
+ //#endregion
96346
96819
  //#region src/cli/utils/build-diagnostic-issue-url.ts
96347
96820
  const formatRuleIdentifier = (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`;
96348
96821
  const buildDiagnosticIssueBody = (input) => {
@@ -96485,7 +96958,7 @@ const resolveExplainTargetDirectory = async (filePath, context) => {
96485
96958
  if (context.projectFlag) {
96486
96959
  const matchedDirectories = await selectProjects(context.resolvedDirectory, context.projectFlag, true);
96487
96960
  if (matchedDirectories.length === 0) return context.resolvedDirectory;
96488
- if (matchedDirectories.length > 1) throw new Error(`--explain takes a single project; --project resolved to ${matchedDirectories.length} projects.`);
96961
+ if (matchedDirectories.length > 1) throw new Error(`why takes a single project; --project resolved to ${matchedDirectories.length} projects.`);
96489
96962
  return matchedDirectories[0];
96490
96963
  }
96491
96964
  return findOwningProjectDirectory(context.resolvedDirectory, filePath);
@@ -96704,6 +97177,15 @@ const filterCompletedScansByCategories = (completedScans, categoryFilters) => {
96704
97177
  }
96705
97178
  }));
96706
97179
  };
97180
+ const reportSkippedProjects = (input) => {
97181
+ input.skippedProjects.sort((left, right) => left.directory.localeCompare(right.directory));
97182
+ if (input.skippedProjects.length === 0) return;
97183
+ recordCount(METRIC.scanProjectSkipped, input.skippedProjects.length, { reason: "max-duration" });
97184
+ if (!input.isQuiet) {
97185
+ cliLogger.warn(formatSkippedProjectsMessage(input.skippedProjects.length));
97186
+ cliLogger.break();
97187
+ }
97188
+ };
96707
97189
  /**
96708
97190
  * Post-scan finalization shared by the staged-arm and project-loop
96709
97191
  * paths of `inspectAction`: emit the JSON report (when in JSON mode)
@@ -96718,7 +97200,7 @@ const filterCompletedScansByCategories = (completedScans, categoryFilters) => {
96718
97200
  */
96719
97201
  const finalizeScans = (input) => {
96720
97202
  const baselineDeltas = input.completedScans.flatMap((scan) => scan.result.baselineDelta ? [scan.result.baselineDelta] : []);
96721
- const baselineComputed = input.completedScans.length > 0 && input.completedScans.every((scan) => scan.result.baselineDelta !== void 0);
97203
+ const baselineComputed = input.skippedProjects.length === 0 && input.completedScans.length > 0 && input.completedScans.every((scan) => scan.result.baselineDelta !== void 0);
96722
97204
  const baselineDegraded = input.baselineIntended && !baselineComputed;
96723
97205
  const mode = baselineDegraded ? "diff" : input.mode;
96724
97206
  const isReactDetected = input.completedScans.some((scan) => hasReactRuntime(scan.result.project));
@@ -96739,6 +97221,7 @@ const finalizeScans = (input) => {
96739
97221
  mode,
96740
97222
  diff: input.diff,
96741
97223
  scans: jsonCompletedScans,
97224
+ skippedProjects: input.skippedProjects,
96742
97225
  totalElapsedMilliseconds: performance$1.now() - input.startTime,
96743
97226
  baseline,
96744
97227
  baselineDegraded
@@ -96859,16 +97342,16 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
96859
97342
  return;
96860
97343
  }
96861
97344
  const buildProjectScanContext = async (projectDirectory) => {
96862
- const projectScanTarget = projectDirectory === resolvedDirectory ? scanTarget : await resolveScanTarget(projectDirectory, { allowAmbiguous: true });
96863
- const scanDirectory = projectScanTarget.resolvedDirectory;
97345
+ const projectScan = await resolveProjectScan(scanTarget, projectDirectory);
97346
+ const scanDirectory = projectScan.directory;
96864
97347
  const treeRelativeDirectory = resolveProjectRelativeDirectory(resolvedDirectory, scanDirectory);
96865
97348
  if (treeRelativeDirectory === null) return null;
96866
97349
  return {
96867
97350
  projectDirectory,
96868
97351
  scanDirectory,
96869
97352
  treeRelativeDirectory,
96870
- projectConfig: projectDirectory === resolvedDirectory ? userConfig : mergeReactDoctorConfigs(userConfig, projectScanTarget.userConfig ?? void 0),
96871
- projectConfigSourceDirectory: projectScanTarget.userConfig?.plugins === void 0 ? scanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory
97353
+ projectConfig: projectScan.config,
97354
+ projectConfigSourceDirectory: projectScan.configSourceDirectory
96872
97355
  };
96873
97356
  };
96874
97357
  const projectScanContexts = [];
@@ -96955,6 +97438,7 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
96955
97438
  includePaths: resolveProjectSourceFilePaths(resolvedDirectory, projectScan.scanDirectory, projectScan.stagedFiles.filter((stagedFile) => materializedStagedFiles.has(stagedFile)))
96956
97439
  })).filter((projectRun) => projectRun.includePaths.length > 0);
96957
97440
  const isMultiProject = stagedProjectRuns.length > 1;
97441
+ const skippedProjects = [];
96958
97442
  if (stagedProjectRuns.length === 0) {
96959
97443
  snapshot.cleanup();
96960
97444
  reportNothingToScan({
@@ -96970,6 +97454,13 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
96970
97454
  }
96971
97455
  const scanStagedProject = async (projectRun) => {
96972
97456
  const { projectScan, includePaths } = projectRun;
97457
+ if (scanDeadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(scanDeadlineEpochMs) === 0) {
97458
+ skippedProjects.push({
97459
+ directory: projectScan.scanDirectory,
97460
+ reason: "max-duration"
97461
+ });
97462
+ return null;
97463
+ }
96973
97464
  const projectTempDirectory = Path.join(snapshot.tempDirectory, projectScan.treeRelativeDirectory);
96974
97465
  const scanResult = await inspectProject(projectTempDirectory, {
96975
97466
  ...scanOptions,
@@ -97010,7 +97501,11 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97010
97501
  });
97011
97502
  const completedScans = stagedBatch.completedScans;
97012
97503
  try {
97013
- if (!isQuiet && isMultiProject) {
97504
+ reportSkippedProjects({
97505
+ skippedProjects,
97506
+ isQuiet
97507
+ });
97508
+ if (!isQuiet && isMultiProject && completedScans.length > 0) {
97014
97509
  const showShareLink = !isShareOptedOut(completedScans, scanOptions.noScore) && !scanOptions.isCi;
97015
97510
  await runPromise(printMultiProjectSummary({
97016
97511
  completedScans,
@@ -97028,6 +97523,7 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97028
97523
  }
97029
97524
  finalizeScans({
97030
97525
  completedScans,
97526
+ skippedProjects,
97031
97527
  mode: "staged",
97032
97528
  diff: null,
97033
97529
  baselineIntended: false,
@@ -97103,12 +97599,28 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97103
97599
  }
97104
97600
  cliLogger.break();
97105
97601
  }
97106
- const isMultiProject = projectDirectories.length > 1;
97107
- const scanProject = async (projectDirectory) => {
97108
- const projectScanTarget = projectDirectory === resolvedDirectory ? scanTarget : await resolveScanTarget(projectDirectory, { allowAmbiguous: true });
97109
- const scanDirectory = projectScanTarget.resolvedDirectory;
97110
- const projectConfig = projectDirectory === resolvedDirectory ? userConfig : mergeReactDoctorConfigs(userConfig, projectScanTarget.userConfig ?? void 0);
97111
- const projectConfigSourceDirectory = projectScanTarget.userConfig?.plugins === void 0 ? scanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory;
97602
+ const projectScans = deduplicateProjectScans(await Promise.all(projectDirectories.map((projectDirectory) => resolveProjectScan(scanTarget, projectDirectory))));
97603
+ const isMultiProject = projectScans.length > 1;
97604
+ const rootProjectScan = projectScans.find((projectScan) => Path.resolve(projectScan.directory) === Path.resolve(resolvedDirectory));
97605
+ const workspaceDeadCodeOwner = resolveWorkspaceDeadCodeOwner({
97606
+ rootDirectory: resolvedDirectory,
97607
+ projectDirectories: projectScans.map((projectScan) => projectScan.directory),
97608
+ isRootDeadCodeEnabled: scanOptions.deadCode ?? rootProjectScan?.config?.deadCode ?? true
97609
+ });
97610
+ if (workspaceDeadCodeOwner !== null) recordCount(METRIC.scanWorkspaceDeadCodeShared, 1, { projectCount: projectScans.length });
97611
+ const precomputedSourceFileCounts = isMultiProject && !isDiffMode ? await collectProjectSourceFileCounts(resolvedDirectory, projectScans.map((projectScan) => projectScan.directory)) : null;
97612
+ const skippedProjects = [];
97613
+ const scanProject = async (projectScan) => {
97614
+ if (scanDeadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(scanDeadlineEpochMs) === 0) {
97615
+ skippedProjects.push({
97616
+ directory: projectScan.directory,
97617
+ reason: "max-duration"
97618
+ });
97619
+ return null;
97620
+ }
97621
+ const scanDirectory = projectScan.directory;
97622
+ const projectConfig = projectScan.config;
97623
+ const ownsWorkspaceDeadCode = scanDirectory === workspaceDeadCodeOwner;
97112
97624
  const supplyChainEnabled = flags.supplyChain ?? projectConfig?.supplyChain?.enabled !== false;
97113
97625
  let includePaths;
97114
97626
  let supplyChainManifestChanged = false;
@@ -97132,12 +97644,16 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97132
97644
  if (!isQuiet && !isMultiProject) cliLogger.dim(" ");
97133
97645
  const scanResult = await inspectProject(scanDirectory, {
97134
97646
  ...scanOptions,
97647
+ deadCode: workspaceDeadCodeOwner === null ? scanOptions.deadCode : ownsWorkspaceDeadCode,
97648
+ precomputedSourceFileCount: precomputedSourceFileCounts?.get(scanDirectory),
97135
97649
  deadlineEpochMs: scanDeadlineEpochMs,
97136
97650
  includePaths,
97137
97651
  configOverride: projectConfig,
97138
- configSourceDirectory: projectConfigSourceDirectory ?? void 0,
97652
+ configSourceDirectory: projectScan.configSourceDirectory ?? void 0,
97139
97653
  suppressRendering: isMultiProject,
97140
97654
  concurrentScan: isMultiProject,
97655
+ excludedProjectDirectories: projectScans.filter((candidateProjectScan) => isPathInsideDirectory(candidateProjectScan.directory, scanDirectory)).map((candidateProjectScan) => candidateProjectScan.directory),
97656
+ retainExcludedProjectDeadCodeDiagnostics: ownsWorkspaceDeadCode,
97141
97657
  baseline: baselineRef !== null && projectBaselineBaseFiles !== null && projectBaselineHeadFiles !== null ? {
97142
97658
  ref: baselineRef,
97143
97659
  baseFiles: projectBaselineBaseFiles,
@@ -97154,12 +97670,19 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97154
97670
  };
97155
97671
  };
97156
97672
  const projectBatch = await runProjectScanBatch({
97157
- projects: projectDirectories,
97673
+ projects: projectScans,
97158
97674
  isQuiet,
97159
97675
  isSilent: scanOptions.silent === true,
97160
97676
  scanProject
97161
97677
  });
97162
- const completedScans = projectBatch.completedScans;
97678
+ const completedScans = await retryMissingProjectScores(projectBatch.completedScans.map((completedScan) => ({
97679
+ ...completedScan,
97680
+ isScoreDisabled: scanOptions.noScore ?? completedScan.config?.noScore ?? false
97681
+ })));
97682
+ reportSkippedProjects({
97683
+ skippedProjects,
97684
+ isQuiet
97685
+ });
97163
97686
  if (!isQuiet && isMultiProject && completedScans.length > 0) {
97164
97687
  const showShareLink = !isShareOptedOut(completedScans, scanOptions.noScore) && !scanOptions.isCi;
97165
97688
  await runPromise(printMultiProjectSummary({
@@ -97177,6 +97700,7 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97177
97700
  if (flags.outputDir && !didScansWriteDump) await runPromise(printDiagnosticsDump(selectedSurfaceDiagnostics, flags.outputDir, false, isQuiet ? "stderr" : "stdout"));
97178
97701
  finalizeScans({
97179
97702
  completedScans,
97703
+ skippedProjects,
97180
97704
  mode: baselineRef ? "baseline" : isDiffMode ? "diff" : "full",
97181
97705
  diff: isDiffMode ? diffInfo : null,
97182
97706
  baselineIntended: scope === "changed" && diffInfo !== null && !diffInfo.isCurrentChanges,
@@ -97261,6 +97785,56 @@ const installAction = async (options, command) => {
97261
97785
  }
97262
97786
  };
97263
97787
  //#endregion
97788
+ //#region src/cli/utils/should-use-tui.ts
97789
+ const shouldUseTui = (input) => {
97790
+ if (input.isNonInteractiveEnvironment || !input.stdinIsTty || !input.stdoutIsTty || !input.supportsRawMode || input.nodeMajorVersion < 22 || input.terminalName === "dumb") return false;
97791
+ const flags = input.flags;
97792
+ let requiresStableConfiguredScope = false;
97793
+ if (input.userConfig?.scope !== void 0) requiresStableConfiguredScope = input.userConfig.scope !== "full";
97794
+ else if (input.userConfig?.diff !== void 0) requiresStableConfiguredScope = input.userConfig.diff !== false && input.userConfig.diff !== "false";
97795
+ else if (input.userConfig?.base !== void 0) requiresStableConfiguredScope = true;
97796
+ return !(flags.verbose === true || flags.outputDir !== void 0 || flags.score === true || flags.json === true || flags.jsonCompact === true || flags.jsonOut !== void 0 || flags.staged === true || flags.scope !== void 0 || flags.base !== void 0 || flags.includeUntracked === true || flags.diff !== void 0 || flags.changedFilesFrom !== void 0 || flags.debug === true || flags.failOn !== void 0 || requiresStableConfiguredScope);
97797
+ };
97798
+ //#endregion
97799
+ //#region src/cli/commands/scan.ts
97800
+ const runScanCommand = async (input) => {
97801
+ if (input.flags.cache === false) process.env.REACT_DOCTOR_NO_CACHE = "1";
97802
+ const nodeMajorVersion = Number(process.versions.node.split(".")[0]);
97803
+ const tuiEnvironment = {
97804
+ flags: input.flags,
97805
+ isNonInteractiveEnvironment: isNonInteractiveEnvironment(),
97806
+ nodeMajorVersion,
97807
+ stdinIsTty: process.stdin.isTTY === true,
97808
+ stdoutIsTty: process.stdout.isTTY === true,
97809
+ supportsRawMode: typeof process.stdin.setRawMode === "function",
97810
+ terminalName: process.env.TERM
97811
+ };
97812
+ if (!shouldUseTui(tuiEnvironment)) {
97813
+ await inspectAction(input.directory, input.flags, input.invocationCommand);
97814
+ return;
97815
+ }
97816
+ await runProjectMigrations(path.resolve(input.directory));
97817
+ const scanTarget = await resolveScanTarget(input.directory, { allowAmbiguous: true });
97818
+ if (!shouldUseTui({
97819
+ ...tuiEnvironment,
97820
+ userConfig: scanTarget.userConfig
97821
+ })) {
97822
+ await inspectAction(input.directory, input.flags, input.invocationCommand);
97823
+ return;
97824
+ }
97825
+ recordCount(METRIC.cliInvoked, 1, { command: input.invocationCommand });
97826
+ const { runScanApp } = await import("./run-scan-app-QbtG43NL.js");
97827
+ const { shouldFail } = await runScanApp({
97828
+ directory: input.directory,
97829
+ scanTarget,
97830
+ options: resolveCliInspectOptions(input.flags, null),
97831
+ projectFlag: input.flags.project,
97832
+ skipPrompts: input.flags.yes ?? false,
97833
+ blocking: input.flags.blocking
97834
+ });
97835
+ if (shouldFail) process.exitCode = 1;
97836
+ };
97837
+ //#endregion
97264
97838
  //#region src/cli/utils/rule-catalog.ts
97265
97839
  const buildRuleCatalog = () => REACT_DOCTOR_RULES.map((entry) => ({
97266
97840
  key: entry.key,
@@ -97901,13 +98475,39 @@ const ensureWindowsUtf8Console = () => {
97901
98475
  } catch {}
97902
98476
  };
97903
98477
  //#endregion
98478
+ //#region src/cli/utils/active-tui-renderer.ts
98479
+ let activeTuiRenderer = null;
98480
+ const registerActiveTuiRenderer = (renderer) => {
98481
+ activeTuiRenderer = renderer;
98482
+ return () => {
98483
+ if (activeTuiRenderer === renderer) activeTuiRenderer = null;
98484
+ };
98485
+ };
98486
+ const clearActiveTuiRenderer = () => {
98487
+ const renderer = activeTuiRenderer;
98488
+ activeTuiRenderer = null;
98489
+ renderer?.clear();
98490
+ };
98491
+ //#endregion
97904
98492
  //#region src/cli/utils/exit-gracefully.ts
98493
+ let didStartExiting = false;
97905
98494
  const exitGracefully = () => {
98495
+ if (didStartExiting) return process.exit(130);
98496
+ didStartExiting = true;
98497
+ activeScanAbortRegistry.abortAll();
98498
+ clearActiveTuiRenderer();
97906
98499
  try {
97907
98500
  if (isJsonModeActive()) writeJsonErrorReport(/* @__PURE__ */ new Error("Scan cancelled by user (SIGINT/SIGTERM)"));
97908
- else console.log("\nCancelled.\n");
98501
+ else console.log([
98502
+ "",
98503
+ "Cancelled.",
98504
+ "",
98505
+ buildSectionDivider(),
98506
+ "",
98507
+ ...buildFooterLinkLines({ shareUrl: null })
98508
+ ].join("\n"));
97909
98509
  } catch {}
97910
- process.exit(130);
98510
+ flushSentry().finally(() => process.exit(130));
97911
98511
  };
97912
98512
  //#endregion
97913
98513
  //#region src/cli/utils/guard-stdin.ts
@@ -98025,6 +98625,7 @@ const ROOT_FLAG_SPEC = {
98025
98625
  "--json-compact",
98026
98626
  "--lint",
98027
98627
  "--no-color",
98628
+ "--no-cache",
98028
98629
  "--no-dead-code",
98029
98630
  "--no-lint",
98030
98631
  "--no-parallel",
@@ -98148,13 +98749,18 @@ const COMMAND_FLAG_SPECS = new Map([
98148
98749
  longOptionsWithoutValues: new Set([
98149
98750
  "--color",
98150
98751
  "--help",
98752
+ "--no-cache",
98151
98753
  "--no-color",
98152
98754
  "--no-dead-code",
98153
98755
  "--no-score",
98154
98756
  "--no-supply-chain",
98155
98757
  "--yes"
98156
98758
  ]),
98157
- longOptionsWithRequiredValues: new Set(["--blocking", "--project"]),
98759
+ longOptionsWithRequiredValues: new Set([
98760
+ "--blocking",
98761
+ "--max-duration",
98762
+ "--project"
98763
+ ]),
98158
98764
  longOptionsWithOptionalValues: /* @__PURE__ */ new Set(),
98159
98765
  shortOptionsWithoutValues: new Set(["-h", "-y"]),
98160
98766
  shortOptionsWithRequiredValues: new Set(["-p"])
@@ -98296,6 +98902,7 @@ ${highlighter.dim("Scope:")}
98296
98902
  Dead-code, supply-chain, external lint-config, custom-plugin, and health-score passes are skipped.
98297
98903
  Standard scan flags such as ${highlighter.info("--scope")}, ${highlighter.info("--project")}, ${highlighter.info("--verbose")}, and ${highlighter.info("--json")} still work.
98298
98904
  `;
98905
+ 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)";
98299
98906
  const renderCiHelpEpilog = () => `
98300
98907
  ${highlighter.dim("Examples:")}
98301
98908
  ${formatExampleLines([
@@ -98315,8 +98922,12 @@ ${highlighter.dim("Learn more:")}
98315
98922
  ${highlighter.info(CI_URL)}
98316
98923
  `;
98317
98924
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
98318
- 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);
98319
- program.action(inspectAction);
98925
+ 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);
98926
+ program.action((directory = ".", flags) => runScanCommand({
98927
+ directory,
98928
+ flags,
98929
+ invocationCommand: "inspect"
98930
+ }));
98320
98931
  program.command("design [directory]").description("Run only the focused UI design diagnostics").addHelpText("after", renderDesignHelpEpilog).action((directory, _options, command) => designAction(directory ?? ".", command.optsWithGlobals()));
98321
98932
  program.command("why <location>").description("Explain why a rule fired (or why a suppression didn't apply) at a file:line").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple)").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").action((location, options) => whyAction(location, options));
98322
98933
  program.command("install").alias("setup").description("Install the react-doctor skill into your coding agents and optional git hook").option("-y, --yes", "skip prompts, install for all detected agents").option("--dry-run", "show what would be installed without writing files").option("--agent-hooks", "install native non-blocking agent hooks for Claude Code and Cursor").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderInstallHelpEpilog).action(installAction);
@@ -98337,38 +98948,11 @@ rules.command("category <category> <severity>").description("Set the severity fo
98337
98948
  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()));
98338
98949
  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()));
98339
98950
  program.command("experimental-lsp", { hidden: false }).description("[experimental] run the React Doctor language server over stdio (for editors)").allowUnknownOption().action(() => {});
98340
- 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("--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(async (directory = ".", _localOptions, command) => {
98341
- const options = command.optsWithGlobals();
98342
- const deadCode = options.deadCode === false ? false : void 0;
98343
- const noScore = options.score === false ? true : void 0;
98344
- const supplyChain = options.supplyChain === false ? false : void 0;
98345
- const nodeMajorVersion = Number(process.versions.node.split(".")[0]);
98346
- if (process.stdout.isTTY !== true || process.stdin.isTTY !== true || isNonInteractiveEnvironment() || nodeMajorVersion < 22) {
98347
- await inspectAction(directory, {
98348
- deadCode,
98349
- score: options.score === false ? false : void 0,
98350
- supplyChain,
98351
- project: options.project,
98352
- yes: options.yes,
98353
- blocking: options.blocking
98354
- });
98355
- return;
98356
- }
98357
- recordCount(METRIC.cliInvoked, 1, { command: "experimental-tui" });
98358
- const { runScanApp } = await import("./run-scan-app-DHRvqfkX.js");
98359
- const { shouldFail } = await runScanApp({
98360
- directory,
98361
- options: {
98362
- deadCode,
98363
- noScore,
98364
- supplyChain
98365
- },
98366
- projectFlag: options.project,
98367
- skipPrompts: options.yes ?? false,
98368
- blocking: options.blocking
98369
- });
98370
- if (shouldFail) process.exitCode = 1;
98371
- });
98951
+ 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({
98952
+ directory,
98953
+ flags: command.optsWithGlobals(),
98954
+ invocationCommand: "experimental-tui"
98955
+ }));
98372
98956
  process.stdout.on("error", (error) => {
98373
98957
  if (error.code === "EPIPE") process.exit(0);
98374
98958
  });
@@ -98395,7 +98979,7 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
98395
98979
  handleError(error, { sentryEventId });
98396
98980
  });
98397
98981
  //#endregion
98398
- export { toForwardSlashes as A, getCategoryImpact as B, buildRulePriorityMap as C, resolveBlockingLevel as D, formatLearnMoreLine as E, TUI_REPORT_DETAIL_WIDTH_FRACTION as F, runPromise as G, mapWithConcurrency as H, isCiEnvironment as I, succeed$3 as J, sync as K, DIAGNOSTIC_CATEGORY_BUCKETS as L, isUnicodeSupported as M, METRIC as N, isReactDoctorWorkflowInstalled as O, TUI_REPORT_COMPACT_MAX_ROWS as P, Progress as R, hasLintHardFailure as S, formatFixRecipeLine as T, mergeReactDoctorConfigs as U, highlighter as V, resolveScanTarget as W, canAnimateOnboarding as _, resolveCliInspectOptions as a, buildCodeFrame as b, detectLaunchableAgents as c, launchCliAgent as d, isShareOptedOut as f, doctorFace as g, easeOutCubic as h, selectProjects as i, findNearestPackageDirectory as j, recordCount as k, CLI_AGENT_BINARIES as l, printFooter as m, shouldFailScanGate as n, countUniqueScannedFiles as o, createInvocationInspect as p, void_ as q, discoverWorkspacePackages as r, setUpGitHubActions as s, exitGracefully as t, copyToClipboard as u, computeProjectedScore as v, buildSortedRuleGroups as w, buildNoScoreMessage as x, formatElapsedTime as y, Reporter as z };
98982
+ 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 };
98399
98983
 
98400
98984
  //# sourceMappingURL=cli.js.map
98401
- //# debugId=707bd0ef-8490-5120-ba82-f65ad0b490e1
98985
+ //# debugId=d76ac03b-d6e7-5598-a5a0-ce6e9210ddaf