react-doctor 0.5.8-dev.7f9e7f4 → 0.5.8-dev.8232e96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lsp.js CHANGED
@@ -6,7 +6,7 @@ import os from "node:os";
6
6
  import * as Path from "node:path";
7
7
  import path from "node:path";
8
8
  import * as NodeChildProcess from "node:child_process";
9
- import { spawn, spawnSync } from "node:child_process";
9
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
10
10
  import * as ts from "typescript";
11
11
  import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
12
12
  import { parseJSON5 } from "confbox";
@@ -6021,7 +6021,7 @@ const composePassthrough = /* @__PURE__ */ dual(2, (left, right) => (input) => {
6021
6021
  * @since 2.0.0
6022
6022
  */
6023
6023
  const Scheduler = /* @__PURE__ */ Reference("effect/Scheduler", { defaultValue: () => new MixedScheduler() });
6024
- const setImmediate = "setImmediate" in globalThis ? (f) => {
6024
+ const setImmediate$1 = "setImmediate" in globalThis ? (f) => {
6025
6025
  const timer = globalThis.setImmediate(f);
6026
6026
  return () => globalThis.clearImmediate(timer);
6027
6027
  } : (f) => {
@@ -6065,7 +6065,7 @@ var PriorityBuckets = class {
6065
6065
  var MixedScheduler = class {
6066
6066
  executionMode;
6067
6067
  setImmediate;
6068
- constructor(executionMode = "async", setImmediateFn = setImmediate) {
6068
+ constructor(executionMode = "async", setImmediateFn = setImmediate$1) {
6069
6069
  this.executionMode = executionMode;
6070
6070
  this.setImmediate = setImmediateFn;
6071
6071
  }
@@ -6090,7 +6090,7 @@ var MixedSchedulerDispatcher = class {
6090
6090
  tasks = /* @__PURE__ */ new PriorityBuckets();
6091
6091
  running = void 0;
6092
6092
  setImmediate;
6093
- constructor(setImmediateFn = setImmediate) {
6093
+ constructor(setImmediateFn = setImmediate$1) {
6094
6094
  this.setImmediate = setImmediateFn;
6095
6095
  }
6096
6096
  /**
@@ -19400,6 +19400,15 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
19400
19400
  ok: Boolean$1,
19401
19401
  directory: String$1,
19402
19402
  mode: JsonReportMode,
19403
+ /**
19404
+ * True when a `changed` (compare) run was intended but the base couldn't be
19405
+ * resolved — a shallow CI checkout with no merge base, or a failed base/head
19406
+ * lint. The report then lists every finding in the changed files (mode
19407
+ * downgrades to `diff`, the `baseline` block is dropped, the CI gate is
19408
+ * skipped) rather than only the ones the change introduced. Absent on a
19409
+ * successful `baseline` run (`schemaVersion: 2`) and on non-compare scopes.
19410
+ */
19411
+ baselineDegraded: optional(Boolean$1),
19403
19412
  diff: NullOr(JsonReportDiffInfo),
19404
19413
  projects: ArraySchema(JsonReportProjectEntry),
19405
19414
  diagnostics: ArraySchema(Diagnostic),
@@ -32816,12 +32825,31 @@ const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.m?js$/i;
32816
32825
  const MINIFIED_SNIFF_BYTES = 65536;
32817
32826
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
32818
32827
  const IGNORED_DIRECTORIES = new Set([
32828
+ ".angular",
32829
+ ".astro",
32830
+ ".cache",
32831
+ ".contentlayer",
32832
+ ".direnv",
32833
+ ".docusaurus",
32834
+ ".expo",
32835
+ ".firebase",
32819
32836
  ".git",
32837
+ ".gradle",
32838
+ ".hg",
32820
32839
  ".next",
32821
32840
  ".nuxt",
32841
+ ".nx",
32822
32842
  ".output",
32843
+ ".parcel-cache",
32844
+ ".serverless",
32823
32845
  ".svelte-kit",
32846
+ ".svn",
32847
+ ".terraform",
32824
32848
  ".turbo",
32849
+ ".venv",
32850
+ ".vercel",
32851
+ ".wrangler",
32852
+ ".yarn",
32825
32853
  "build",
32826
32854
  "coverage",
32827
32855
  "dist",
@@ -32829,6 +32857,142 @@ const IGNORED_DIRECTORIES = new Set([
32829
32857
  "out",
32830
32858
  "storybook-static"
32831
32859
  ]);
32860
+ const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
32861
+ const MILLISECONDS_PER_SECOND = 1e3;
32862
+ const SCORE_API_URL = "https://www.react.doctor/api/score";
32863
+ const FETCH_TIMEOUT_MS = 1e4;
32864
+ const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
32865
+ const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
32866
+ const SPAWN_ARGS_MAX_LENGTH_CHARS_POSIX = 15e5;
32867
+ const SPAWN_ARGS_MAX_LENGTH_CHARS_DARWIN = 8e5;
32868
+ const NODE_VERSION_PROBE_TIMEOUT_MS = 5e3;
32869
+ const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
32870
+ const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
32871
+ const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
32872
+ const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
32873
+ const ES_TARGET_YEAR_BY_NAME = {
32874
+ es3: 1999,
32875
+ es5: 2009,
32876
+ es6: 2015,
32877
+ es2015: 2015,
32878
+ es2016: 2016,
32879
+ es2017: 2017,
32880
+ es2018: 2018,
32881
+ es2019: 2019,
32882
+ es2020: 2020,
32883
+ es2021: 2021,
32884
+ es2022: 2022,
32885
+ es2023: 2023,
32886
+ es2024: 2024,
32887
+ es2025: 2025,
32888
+ esnext: 9999
32889
+ };
32890
+ /**
32891
+ * tsconfig filenames probed when resolving a project's TypeScript
32892
+ * compiler options — the root config first, then a monorepo base config.
32893
+ */
32894
+ const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
32895
+ /**
32896
+ * Project-config files that `StagedFiles.materialize` copies into
32897
+ * the temp directory alongside staged sources so oxlint resolves
32898
+ * `tsconfig` / `package.json` / lint configs the same way it would
32899
+ * in the working tree. Hoisted out of the staged-files helper so
32900
+ * the constant lives next to the rest of the IO budget knobs.
32901
+ */
32902
+ const STAGED_FILES_PROJECT_CONFIG_FILENAMES = [
32903
+ "tsconfig.json",
32904
+ "tsconfig.base.json",
32905
+ "package.json",
32906
+ "doctor.config.ts",
32907
+ "doctor.config.mts",
32908
+ "doctor.config.cts",
32909
+ "doctor.config.js",
32910
+ "doctor.config.mjs",
32911
+ "doctor.config.cjs",
32912
+ "doctor.config.json",
32913
+ "doctor.config.jsonc",
32914
+ "oxlint.json",
32915
+ ".oxlintrc.json"
32916
+ ];
32917
+ const CONFIG_FINGERPRINT_FILENAMES = [
32918
+ "doctor.config.ts",
32919
+ "doctor.config.mts",
32920
+ "doctor.config.cts",
32921
+ "doctor.config.js",
32922
+ "doctor.config.mjs",
32923
+ "doctor.config.cjs",
32924
+ "doctor.config.json",
32925
+ "doctor.config.jsonc",
32926
+ "react-doctor.config.json",
32927
+ "package.json",
32928
+ "pnpm-workspace.yaml",
32929
+ "tsconfig.json",
32930
+ "tsconfig.base.json",
32931
+ ".oxlintrc.json",
32932
+ ".eslintrc.json",
32933
+ "pnpm-lock.yaml",
32934
+ "package-lock.json",
32935
+ "yarn.lock",
32936
+ "bun.lock",
32937
+ "bun.lockb",
32938
+ ".gitignore"
32939
+ ];
32940
+ const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
32941
+ const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
32942
+ const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
32943
+ const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
32944
+ const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
32945
+ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
32946
+ const LINT_PHASE_TIMEOUT_MS = 3e5;
32947
+ const SCAN_TOTAL_DEADLINE_MS = 9e5;
32948
+ const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
32949
+ const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
32950
+ const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
32951
+ const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
32952
+ const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
32953
+ const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
32954
+ const REACT_SERVER_DOM_PACKAGES = [
32955
+ "react-server-dom-webpack",
32956
+ "react-server-dom-parcel",
32957
+ "react-server-dom-turbopack"
32958
+ ];
32959
+ const REACT_BLOG_RSC_ADVISORY_URL = "https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components";
32960
+ const VERCEL_NEXTJS_SECURITY_RELEASE_URL = "https://vercel.com/changelog/next-js-may-2026-security-release";
32961
+ const DIAGNOSTIC_CATEGORY_BUCKETS = [
32962
+ "Security",
32963
+ "Bugs",
32964
+ "Performance",
32965
+ "Accessibility",
32966
+ "Maintainability"
32967
+ ];
32968
+ const APP_ONLY_RULE_KEYS = new Set([
32969
+ "react-hooks-js/static-components",
32970
+ "react-doctor/no-render-prop-children",
32971
+ "react-doctor/prefer-explicit-variants"
32972
+ ]);
32973
+ const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
32974
+ const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
32975
+ const ROOT_CAUSE_GROUPABLE_RULE_KEYS = new Set([
32976
+ "react-doctor/no-derived-state",
32977
+ "react-doctor/no-derived-state-effect",
32978
+ "react-doctor/no-derived-useState",
32979
+ "react-doctor/no-adjust-state-on-prop-change",
32980
+ "react-doctor/no-reset-all-state-on-prop-change"
32981
+ ]);
32982
+ const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
32983
+ const CONFIG_CACHE_TTL_MS = 300 * 1e3;
32984
+ const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
32985
+ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
32986
+ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
32987
+ const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
32988
+ const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
32989
+ const SUPPLY_CHAIN_PLUGIN = "socket";
32990
+ const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
32991
+ const SUPPLY_CHAIN_CATEGORY = "Security";
32992
+ const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
32993
+ const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
32994
+ const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
32995
+ const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
32832
32996
  const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath);
32833
32997
  const isMinifiedSource = (absolutePath) => {
32834
32998
  let fileDescriptor;
@@ -32885,23 +33049,38 @@ const readDirectoryEntries = (directoryPath) => {
32885
33049
  throw error;
32886
33050
  }
32887
33051
  };
32888
- const countSourceFilesViaFilesystem = (rootDirectory) => {
32889
- let count = 0;
33052
+ function* walkSourceTreeFiles(rootDirectory) {
32890
33053
  const stack = [rootDirectory];
32891
33054
  while (stack.length > 0) {
32892
33055
  const currentDirectory = stack.pop();
32893
- const entries = readDirectoryEntries(currentDirectory);
32894
- for (const entry of entries) {
33056
+ if (currentDirectory === void 0) continue;
33057
+ for (const entry of readDirectoryEntries(currentDirectory)) {
33058
+ const absolutePath = Path.join(currentDirectory, entry.name);
32895
33059
  if (entry.isDirectory()) {
32896
- if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(Path.join(currentDirectory, entry.name));
33060
+ if (!IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
32897
33061
  continue;
32898
33062
  }
32899
- if (entry.isFile() && isLintableSourceFile(entry.name) && !isLargeMinifiedFile(Path.join(currentDirectory, entry.name))) count++;
33063
+ if (entry.isFile()) yield {
33064
+ absolutePath,
33065
+ name: entry.name
33066
+ };
32900
33067
  }
32901
33068
  }
32902
- return count;
33069
+ }
33070
+ const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
33071
+ const entries = [];
33072
+ for (const relativePath of relativePaths) {
33073
+ const absolutePath = Path.resolve(rootDirectory, relativePath);
33074
+ const sizeBytes = statSourceFileSize(absolutePath);
33075
+ if (isLargeMinifiedFile(absolutePath, sizeBytes)) continue;
33076
+ entries.push({
33077
+ path: relativePath,
33078
+ sizeBytes: sizeBytes ?? 0
33079
+ });
33080
+ }
33081
+ return entries;
32903
33082
  };
32904
- const countSourceFilesViaGit = (rootDirectory) => {
33083
+ const listSourceFilesViaGit = (rootDirectory) => {
32905
33084
  const result = spawnSync("git", [
32906
33085
  "ls-files",
32907
33086
  "-z",
@@ -32914,9 +33093,16 @@ const countSourceFilesViaGit = (rootDirectory) => {
32914
33093
  maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
32915
33094
  });
32916
33095
  if (result.error || result.status !== 0) return null;
32917
- return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath) && !isLargeMinifiedFile(Path.resolve(rootDirectory, filePath))).length;
33096
+ return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath));
33097
+ };
33098
+ const listSourceFilesViaFilesystem = (rootDirectory) => {
33099
+ const filePaths = [];
33100
+ for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) if (isLintableSourceFile(name)) filePaths.push(Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
33101
+ return filePaths;
32918
33102
  };
32919
- const countSourceFiles = (rootDirectory) => countSourceFilesViaGit(rootDirectory) ?? countSourceFilesViaFilesystem(rootDirectory);
33103
+ const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
33104
+ const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
33105
+ const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
32920
33106
  const cachedPackageJsons = /* @__PURE__ */ new Map();
32921
33107
  const clearPackageJsonCache = () => {
32922
33108
  cachedPackageJsons.clear();
@@ -33725,136 +33911,6 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
33725
33911
  if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
33726
33912
  return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
33727
33913
  };
33728
- const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
33729
- const MILLISECONDS_PER_SECOND = 1e3;
33730
- const SCORE_API_URL = "https://www.react.doctor/api/score";
33731
- const FETCH_TIMEOUT_MS = 1e4;
33732
- const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
33733
- const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
33734
- const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
33735
- const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
33736
- const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
33737
- const ES_TARGET_YEAR_BY_NAME = {
33738
- es3: 1999,
33739
- es5: 2009,
33740
- es6: 2015,
33741
- es2015: 2015,
33742
- es2016: 2016,
33743
- es2017: 2017,
33744
- es2018: 2018,
33745
- es2019: 2019,
33746
- es2020: 2020,
33747
- es2021: 2021,
33748
- es2022: 2022,
33749
- es2023: 2023,
33750
- es2024: 2024,
33751
- es2025: 2025,
33752
- esnext: 9999
33753
- };
33754
- /**
33755
- * tsconfig filenames probed when resolving a project's TypeScript
33756
- * compiler options — the root config first, then a monorepo base config.
33757
- */
33758
- const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
33759
- /**
33760
- * Project-config files that `StagedFiles.materialize` copies into
33761
- * the temp directory alongside staged sources so oxlint resolves
33762
- * `tsconfig` / `package.json` / lint configs the same way it would
33763
- * in the working tree. Hoisted out of the staged-files helper so
33764
- * the constant lives next to the rest of the IO budget knobs.
33765
- */
33766
- const STAGED_FILES_PROJECT_CONFIG_FILENAMES = [
33767
- "tsconfig.json",
33768
- "tsconfig.base.json",
33769
- "package.json",
33770
- "doctor.config.ts",
33771
- "doctor.config.mts",
33772
- "doctor.config.cts",
33773
- "doctor.config.js",
33774
- "doctor.config.mjs",
33775
- "doctor.config.cjs",
33776
- "doctor.config.json",
33777
- "doctor.config.jsonc",
33778
- "oxlint.json",
33779
- ".oxlintrc.json"
33780
- ];
33781
- const CONFIG_FINGERPRINT_FILENAMES = [
33782
- "doctor.config.ts",
33783
- "doctor.config.mts",
33784
- "doctor.config.cts",
33785
- "doctor.config.js",
33786
- "doctor.config.mjs",
33787
- "doctor.config.cjs",
33788
- "doctor.config.json",
33789
- "doctor.config.jsonc",
33790
- "react-doctor.config.json",
33791
- "package.json",
33792
- "pnpm-workspace.yaml",
33793
- "tsconfig.json",
33794
- "tsconfig.base.json",
33795
- ".oxlintrc.json",
33796
- ".eslintrc.json",
33797
- "pnpm-lock.yaml",
33798
- "package-lock.json",
33799
- "yarn.lock",
33800
- "bun.lock",
33801
- "bun.lockb",
33802
- ".gitignore"
33803
- ];
33804
- const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
33805
- const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
33806
- const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
33807
- const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
33808
- const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
33809
- const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
33810
- const LINT_PHASE_TIMEOUT_MS = 3e5;
33811
- const SCAN_TOTAL_DEADLINE_MS = 9e5;
33812
- const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
33813
- const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
33814
- const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
33815
- const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
33816
- const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
33817
- const REACT_SERVER_DOM_PACKAGES = [
33818
- "react-server-dom-webpack",
33819
- "react-server-dom-parcel",
33820
- "react-server-dom-turbopack"
33821
- ];
33822
- const REACT_BLOG_RSC_ADVISORY_URL = "https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components";
33823
- const VERCEL_NEXTJS_SECURITY_RELEASE_URL = "https://vercel.com/changelog/next-js-may-2026-security-release";
33824
- const DIAGNOSTIC_CATEGORY_BUCKETS = [
33825
- "Security",
33826
- "Bugs",
33827
- "Performance",
33828
- "Accessibility",
33829
- "Maintainability"
33830
- ];
33831
- const APP_ONLY_RULE_KEYS = new Set([
33832
- "react-hooks-js/static-components",
33833
- "react-doctor/no-render-prop-children",
33834
- "react-doctor/prefer-explicit-variants"
33835
- ]);
33836
- const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
33837
- const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
33838
- const ROOT_CAUSE_GROUPABLE_RULE_KEYS = new Set([
33839
- "react-doctor/no-derived-state",
33840
- "react-doctor/no-derived-state-effect",
33841
- "react-doctor/no-derived-useState",
33842
- "react-doctor/no-adjust-state-on-prop-change",
33843
- "react-doctor/no-reset-all-state-on-prop-change"
33844
- ]);
33845
- const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
33846
- const CONFIG_CACHE_TTL_MS = 300 * 1e3;
33847
- const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
33848
- const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
33849
- const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
33850
- const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
33851
- const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
33852
- const SUPPLY_CHAIN_PLUGIN = "socket";
33853
- const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
33854
- const SUPPLY_CHAIN_CATEGORY = "Security";
33855
- const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
33856
- const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
33857
- const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
33858
33914
  const TSCONFIG_FILENAME = "tsconfig.json";
33859
33915
  const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
33860
33916
  const ensureJsonExtension = (filePath) => Path.extname(filePath) === "" ? `${filePath}.json` : filePath;
@@ -34546,7 +34602,10 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
34546
34602
  NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
34547
34603
  }
34548
34604
  const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
34549
- const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
34605
+ const canonicalizeRuleKey = (ruleKey) => {
34606
+ const nativeRuleKey = LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey];
34607
+ return typeof nativeRuleKey === "string" ? nativeRuleKey : ruleKey;
34608
+ };
34550
34609
  const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
34551
34610
  const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
34552
34611
  const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
@@ -34573,7 +34632,7 @@ const tokenizeRuleList = (ruleList) => {
34573
34632
  const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34574
34633
  const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34575
34634
  const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34576
- const buildHint = (tool, token, ruleId) => `oxlint matches plugin rules only by their full name, so \`${token}\` in your ${tool}-disable comment does not silence \`${ruleId}\` — change it to \`${ruleId}\`.`;
34635
+ const buildHint = (tool, token, ruleId) => `oxlint matches plugin rules only by their full name, so \`${token}\` in your ${tool}-disable comment does not silence \`${ruleId}\` — add \`${ruleId}\` to the comment (e.g. \`${token}, ${ruleId}\`).`;
34577
34636
  const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
34578
34637
  const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34579
34638
  const candidates = [{
@@ -35049,6 +35108,7 @@ const collectStringSet = (values) => {
35049
35108
  if (!Array.isArray(values)) return /* @__PURE__ */ new Set();
35050
35109
  return new Set(values.filter((value) => typeof value === "string"));
35051
35110
  };
35111
+ const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
35052
35112
  /**
35053
35113
  * Pre-compiles every stateful filter and returns a single
35054
35114
  * `apply(diagnostic)` closure that runs (in order):
@@ -35082,11 +35142,28 @@ const buildDiagnosticPipeline = (input) => {
35082
35142
  const compiledOverrides = compileIgnoreOverrides(userConfig);
35083
35143
  const textComponentNames = collectStringSet(userConfig?.textComponents);
35084
35144
  const rawTextWrapperComponentNames = collectStringSet(userConfig?.rawTextWrapperComponents);
35145
+ const runtimeGlobalNames = collectStringSet(userConfig?.runtimeGlobals);
35085
35146
  const hasTextComponents = textComponentNames.size > 0;
35086
35147
  const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
35148
+ const hasRuntimeGlobals = runtimeGlobalNames.size > 0;
35087
35149
  const fileLinesCache = /* @__PURE__ */ new Map();
35088
35150
  const fileContextCache = /* @__PURE__ */ new Map();
35089
35151
  const libraryFileCache = /* @__PURE__ */ new Map();
35152
+ const suppressions = /* @__PURE__ */ new Map();
35153
+ const suppress = (diagnostic, source) => {
35154
+ const { ruleKey } = getDiagnosticRuleIdentity(diagnostic);
35155
+ const suppressionKey = `${ruleKey}\u0000${source}`;
35156
+ const existing = suppressions.get(suppressionKey);
35157
+ suppressions.set(suppressionKey, existing ? {
35158
+ ...existing,
35159
+ count: existing.count + 1
35160
+ } : {
35161
+ rule: ruleKey,
35162
+ source,
35163
+ count: 1
35164
+ });
35165
+ return null;
35166
+ };
35090
35167
  const isLibraryFile = (filePath) => {
35091
35168
  let cached = libraryFileCache.get(filePath);
35092
35169
  if (cached === void 0) {
@@ -35135,50 +35212,60 @@ const buildDiagnosticPipeline = (input) => {
35135
35212
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
35136
35213
  return false;
35137
35214
  };
35138
- return { apply: (diagnostic) => {
35139
- if (shouldAutoSuppress(diagnostic)) return null;
35140
- let current = diagnostic;
35141
- let explicitSeverityOverride;
35142
- let explicitRuleOverride;
35143
- if (severityControls) {
35144
- const { ruleKey, category } = getDiagnosticRuleIdentity(current);
35145
- explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey }, severityControls);
35146
- explicitSeverityOverride = resolveRuleSeverityOverride({
35147
- ruleKey,
35148
- category
35149
- }, severityControls);
35150
- if (explicitSeverityOverride === "off") return null;
35151
- if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
35152
- }
35153
- if (explicitRuleOverride === void 0) {
35154
- if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
35155
- }
35156
- if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
35157
- if (userConfig) {
35158
- if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return null;
35159
- if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
35160
- if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return null;
35161
- if (isRnRawTextSuppressedByConfig(current)) return null;
35162
- }
35163
- if (respectInlineDisables && current.line > 0) {
35164
- const lines = getFileLines(current.filePath);
35165
- if (lines) {
35166
- const ruleIdentifier = `${current.plugin}/${current.rule}`;
35167
- const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
35168
- if (evaluation.isSuppressed) return null;
35169
- if (evaluation.nearMissHint) current = {
35170
- ...current,
35171
- suppressionHint: evaluation.nearMissHint
35172
- };
35215
+ const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
35216
+ if (!hasRuntimeGlobals) return false;
35217
+ if (diagnostic.rule !== "jsx-no-undef") return false;
35218
+ const identifierMatch = JSX_NO_UNDEF_IDENTIFIER_PATTERN.exec(diagnostic.message);
35219
+ return identifierMatch !== null && runtimeGlobalNames.has(identifierMatch[1]);
35220
+ };
35221
+ return {
35222
+ apply: (diagnostic) => {
35223
+ if (shouldAutoSuppress(diagnostic)) return null;
35224
+ let current = diagnostic;
35225
+ let explicitSeverityOverride;
35226
+ let explicitRuleOverride;
35227
+ if (severityControls) {
35228
+ const { ruleKey, category } = getDiagnosticRuleIdentity(current);
35229
+ explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey }, severityControls);
35230
+ explicitSeverityOverride = resolveRuleSeverityOverride({
35231
+ ruleKey,
35232
+ category
35233
+ }, severityControls);
35234
+ if (explicitSeverityOverride === "off") return suppress(current, "config");
35235
+ if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
35173
35236
  }
35174
- }
35175
- const fileContext = getFileContext(current.filePath);
35176
- if (fileContext !== "production") current = {
35177
- ...current,
35178
- fileContext
35179
- };
35180
- return current;
35181
- } };
35237
+ if (explicitRuleOverride === void 0) {
35238
+ if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
35239
+ }
35240
+ if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
35241
+ if (userConfig) {
35242
+ if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return suppress(current, "config");
35243
+ if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
35244
+ if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return suppress(current, "override");
35245
+ if (isRnRawTextSuppressedByConfig(current)) return null;
35246
+ if (isJsxNoUndefSuppressedByConfig(current)) return null;
35247
+ }
35248
+ if (respectInlineDisables && current.line > 0) {
35249
+ const lines = getFileLines(current.filePath);
35250
+ if (lines) {
35251
+ const ruleIdentifier = `${current.plugin}/${current.rule}`;
35252
+ const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
35253
+ if (evaluation.isSuppressed) return suppress(current, "inline");
35254
+ if (evaluation.nearMissHint) current = {
35255
+ ...current,
35256
+ suppressionHint: evaluation.nearMissHint
35257
+ };
35258
+ }
35259
+ }
35260
+ const fileContext = getFileContext(current.filePath);
35261
+ if (fileContext !== "production") current = {
35262
+ ...current,
35263
+ fileContext
35264
+ };
35265
+ return current;
35266
+ },
35267
+ summarizeSuppressions: () => [...suppressions.values()]
35268
+ };
35182
35269
  };
35183
35270
  const OxlintUnavailableKind = Literals(["binary-not-found", "native-binding-missing"]);
35184
35271
  var OxlintUnavailable = class extends TaggedErrorClass()("OxlintUnavailable", {
@@ -35398,7 +35485,7 @@ const resolveScanConcurrency = (requested) => {
35398
35485
  if (!Number.isFinite(requested) || requested < 1) return 1;
35399
35486
  return Math.min(Math.floor(requested), 32);
35400
35487
  };
35401
- const readSystemFacts = () => ({
35488
+ const readSystemFacts$1 = () => ({
35402
35489
  availableCores: os.availableParallelism(),
35403
35490
  totalMemoryBytes: os.totalmem(),
35404
35491
  cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
@@ -35419,7 +35506,7 @@ const readSystemFacts = () => ({
35419
35506
  * `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
35420
35507
  * limited, and ceiling cases without mocking `os` or the filesystem.
35421
35508
  */
35422
- const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
35509
+ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
35423
35510
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
35424
35511
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
35425
35512
  return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
@@ -35460,13 +35547,7 @@ var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { de
35460
35547
  * without recompiling react-doctor. Tests override via
35461
35548
  * `Layer.succeed(SupplyChainOverlapTimeoutMs, ...)`.
35462
35549
  */
35463
- var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => {
35464
- const raw = process.env["REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS"];
35465
- if (raw === void 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35466
- const parsed = Number(raw);
35467
- if (!Number.isFinite(parsed) || parsed <= 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35468
- return parsed;
35469
- } }) {};
35550
+ var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS", SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS) }) {};
35470
35551
  /**
35471
35552
  * Hard cap on combined stdout+stderr bytes per oxlint batch. The
35472
35553
  * subprocess gets SIGKILL'd if it produces more; the recovery path
@@ -35610,6 +35691,13 @@ const BOOLEAN_FIELD_NAMES = [
35610
35691
  "adoptExistingLintConfig"
35611
35692
  ];
35612
35693
  const STRING_FIELD_NAMES = ["rootDir"];
35694
+ const STRING_ARRAY_FIELD_NAMES = [
35695
+ "projects",
35696
+ "textComponents",
35697
+ "rawTextWrapperComponents",
35698
+ "runtimeGlobals",
35699
+ "serverAuthFunctionNames"
35700
+ ];
35613
35701
  const SURFACE_CONTROL_FIELD_NAMES = [
35614
35702
  "includeTags",
35615
35703
  "excludeTags",
@@ -35711,6 +35799,7 @@ const validateConfigTypes = (config) => {
35711
35799
  const validated = { ...config };
35712
35800
  for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
35713
35801
  for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
35802
+ for (const fieldName of STRING_ARRAY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateStringArrayField(fieldName, value));
35714
35803
  applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
35715
35804
  for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
35716
35805
  applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
@@ -36695,14 +36784,17 @@ const checkReactServerComponentsAdvisory = (scanDirectory, project) => {
36695
36784
  return diagnostics;
36696
36785
  };
36697
36786
  const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
36698
- const REDUCED_MOTION_FILE_GLOBS = [
36699
- "*.ts",
36700
- "*.tsx",
36701
- "*.js",
36702
- "*.jsx",
36703
- "*.css",
36704
- "*.scss"
36705
- ];
36787
+ const REDUCED_MOTION_PATTERN = new RegExp(REDUCED_MOTION_GREP_PATTERN);
36788
+ const REDUCED_MOTION_FILE_EXTENSIONS = new Set([
36789
+ ".ts",
36790
+ ".tsx",
36791
+ ".js",
36792
+ ".jsx",
36793
+ ".css",
36794
+ ".scss"
36795
+ ]);
36796
+ const REDUCED_MOTION_FILE_GLOBS = [...REDUCED_MOTION_FILE_EXTENSIONS].map((extension) => `*${extension}`);
36797
+ const GIT_GREP_NO_MATCH_STATUS = 1;
36706
36798
  const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
36707
36799
  filePath: "package.json",
36708
36800
  plugin: "react-doctor",
@@ -36714,6 +36806,19 @@ const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
36714
36806
  column: 0,
36715
36807
  category: "Accessibility"
36716
36808
  };
36809
+ const hasReducedMotionHandlingViaFilesystem = (rootDirectory) => {
36810
+ for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
36811
+ if (!REDUCED_MOTION_FILE_EXTENSIONS.has(Path.extname(name))) continue;
36812
+ let content;
36813
+ try {
36814
+ content = NFS.readFileSync(absolutePath, "utf-8");
36815
+ } catch {
36816
+ continue;
36817
+ }
36818
+ if (REDUCED_MOTION_PATTERN.test(content)) return true;
36819
+ }
36820
+ return false;
36821
+ };
36717
36822
  const checkReducedMotion = (rootDirectory) => {
36718
36823
  const packageJsonPath = Path.join(rootDirectory, "package.json");
36719
36824
  if (!isFile(packageJsonPath)) return [];
@@ -36732,22 +36837,21 @@ const checkReducedMotion = (rootDirectory) => {
36732
36837
  const result = spawnSync("git", [
36733
36838
  "grep",
36734
36839
  "--untracked",
36735
- "-ql",
36736
- "-E",
36840
+ "-lE",
36737
36841
  REDUCED_MOTION_GREP_PATTERN,
36738
36842
  "--",
36739
36843
  ...REDUCED_MOTION_FILE_GLOBS
36740
36844
  ], {
36741
36845
  cwd: rootDirectory,
36846
+ encoding: "utf-8",
36742
36847
  stdio: [
36743
36848
  "ignore",
36744
36849
  "pipe",
36745
36850
  "pipe"
36746
36851
  ]
36747
36852
  });
36748
- if (result.error) return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
36749
- if (result.status === 0) return [];
36750
- return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
36853
+ if (!(!result.error && (result.status === 0 || result.status === GIT_GREP_NO_MATCH_STATUS))) return hasReducedMotionHandlingViaFilesystem(rootDirectory) ? [] : [MISSING_REDUCED_MOTION_DIAGNOSTIC];
36854
+ return result.status === 0 && result.stdout.split("\n").some((filePath) => filePath.length > 0 && !hasIgnoredPathSegment(filePath)) ? [] : [MISSING_REDUCED_MOTION_DIAGNOSTIC];
36751
36855
  };
36752
36856
  const buildSecurityScanDiagnostic = (finding, entry, relativePath) => ({
36753
36857
  filePath: relativePath,
@@ -36897,7 +37001,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
36897
37001
  }
36898
37002
  return true;
36899
37003
  };
36900
- const checkSecurityScan = (rootDirectory, options = {}) => {
37004
+ const yieldToEventLoop = () => new Promise((resolve) => {
37005
+ setImmediate(resolve);
37006
+ });
37007
+ const createSecurityScanSession = (rootDirectory, options) => {
36901
37008
  const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
36902
37009
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
36903
37010
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
@@ -36912,7 +37019,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36912
37019
  committedFilesOnly: rule.committedFilesOnly === true
36913
37020
  }];
36914
37021
  });
36915
- if (enabledScanRules.length === 0) return [];
37022
+ if (enabledScanRules.length === 0) return null;
36916
37023
  const diagnostics = [];
36917
37024
  const seen = /* @__PURE__ */ new Set();
36918
37025
  const gitIgnoredCache = /* @__PURE__ */ new Map();
@@ -36924,15 +37031,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36924
37031
  }
36925
37032
  return status === true;
36926
37033
  };
36927
- for (const file of collectSecurityScanFiles(rootDirectory)) for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36928
- if (committedFilesOnly && isFileGitIgnored(file)) continue;
36929
- const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36930
- const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36931
- if (seen.has(key)) continue;
36932
- seen.add(key);
36933
- diagnostics.push(diagnostic);
37034
+ const scanFile = (file) => {
37035
+ for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
37036
+ if (committedFilesOnly && isFileGitIgnored(file)) continue;
37037
+ const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
37038
+ const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
37039
+ if (seen.has(key)) continue;
37040
+ seen.add(key);
37041
+ diagnostics.push(diagnostic);
37042
+ }
37043
+ };
37044
+ return {
37045
+ scanFile,
37046
+ diagnostics
37047
+ };
37048
+ };
37049
+ const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
37050
+ const session = createSecurityScanSession(rootDirectory, options);
37051
+ if (session === null) return [];
37052
+ let filesSinceYield = 0;
37053
+ for (const file of collectSecurityScanFiles(rootDirectory)) {
37054
+ session.scanFile(file);
37055
+ filesSinceYield += 1;
37056
+ if (filesSinceYield >= 16) {
37057
+ filesSinceYield = 0;
37058
+ await yieldToEventLoop();
37059
+ }
36934
37060
  }
36935
- return diagnostics;
37061
+ return session.diagnostics;
36936
37062
  };
36937
37063
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
36938
37064
  let p = process || {}, argv = p.argv || [], env = p.env || {};
@@ -37149,6 +37275,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
37149
37275
  return [...seen].filter((pattern) => pattern.length > 0);
37150
37276
  };
37151
37277
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
37278
+ const readSystemFacts = () => ({
37279
+ availableCores: os.availableParallelism(),
37280
+ totalMemoryBytes: os.totalmem(),
37281
+ cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
37282
+ });
37283
+ /**
37284
+ * How many real deslop dead-code child processes may run at once, across the
37285
+ * concurrent per-project `runInspect` fibers of one CLI run. The cap is the
37286
+ * smaller of the core count and the number of `DEAD_CODE_WORKER_MEM_BUDGET_BYTES`
37287
+ * workers that fit in available memory, floored at 1.
37288
+ *
37289
+ * On a roomy dev box / CI runner this resolves high enough that every
37290
+ * concurrently-scanned project still spawns its own worker (no serialization vs
37291
+ * the prior uncapped behavior); on a memory-constrained runner it collapses
37292
+ * toward 1, so the `withDeadCodeWorkerSlot` semaphore serializes the spawns
37293
+ * instead of oversubscribing memory with N simultaneous children — the global
37294
+ * cap the per-project spawn path lacked.
37295
+ *
37296
+ * Mirrors `resolveAutoScanConcurrency` (lint), but budgets memory per the
37297
+ * heavier dead-code worker. `facts` is injectable for tests.
37298
+ */
37299
+ const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
37300
+ const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
37301
+ const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
37302
+ return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
37303
+ };
37304
+ let availableSlots = -1;
37305
+ const waiters = [];
37306
+ const releaseSlot = () => {
37307
+ const nextWaiter = waiters.shift();
37308
+ if (nextWaiter !== void 0) nextWaiter();
37309
+ else availableSlots += 1;
37310
+ };
37311
+ /**
37312
+ * Runs `task` once a dead-code worker slot is free, releasing the slot when the
37313
+ * task settles (success or failure). With a high cap (roomy machine) every
37314
+ * caller proceeds immediately; with a low cap (constrained runner) callers
37315
+ * queue and run as slots free.
37316
+ *
37317
+ * `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
37318
+ * this caller is queued, the call rejects without acquiring a slot or running
37319
+ * `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
37320
+ * then spawn a child only to tear it down. A queued caller that aborts removes
37321
+ * its own waiter so a later release never hands a slot to a dead request.
37322
+ */
37323
+ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
37324
+ if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
37325
+ if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
37326
+ if (availableSlots > 0) availableSlots -= 1;
37327
+ else await new Promise((resolve, reject) => {
37328
+ const waiter = () => {
37329
+ abortSignal?.removeEventListener("abort", onAbort);
37330
+ resolve();
37331
+ };
37332
+ const onAbort = () => {
37333
+ const queuedIndex = waiters.indexOf(waiter);
37334
+ if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
37335
+ reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
37336
+ };
37337
+ waiters.push(waiter);
37338
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
37339
+ });
37340
+ try {
37341
+ return await task();
37342
+ } finally {
37343
+ releaseSlot();
37344
+ }
37345
+ };
37152
37346
  /**
37153
37347
  * Resolves a path to its canonical, symlink-free form, falling back to
37154
37348
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -37173,6 +37367,11 @@ const toCanonicalPath = (filePath) => {
37173
37367
  };
37174
37368
  const DEAD_CODE_PLUGIN = "deslop";
37175
37369
  const DEAD_CODE_CATEGORY = "Maintainability";
37370
+ const REACT_DOCTOR_TOOLCHAIN_PACKAGES = new Set([
37371
+ "react-doctor",
37372
+ "eslint-plugin-react-doctor",
37373
+ "oxlint-plugin-react-doctor"
37374
+ ]);
37176
37375
  const DEAD_CODE_WORKER_SCRIPT = `
37177
37376
  const inputChunks = [];
37178
37377
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -37439,14 +37638,17 @@ const checkDeadCode = async (options) => {
37439
37638
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37440
37639
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37441
37640
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37442
- const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37443
- rootDirectory,
37444
- entryPatterns,
37445
- tsConfigPath: resolveTsConfigPath(rootDirectory),
37446
- ignorePatterns,
37447
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37448
- parseConcurrency: options.parseConcurrency
37449
- }), options.workerTimeoutMs ?? 12e4, options.abortSignal));
37641
+ const spawnAndRun = () => {
37642
+ return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37643
+ rootDirectory,
37644
+ entryPatterns,
37645
+ tsConfigPath: resolveTsConfigPath(rootDirectory),
37646
+ ignorePatterns,
37647
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37648
+ parseConcurrency: options.parseConcurrency
37649
+ }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
37650
+ };
37651
+ const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
37450
37652
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
37451
37653
  const diagnostics = [];
37452
37654
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -37475,6 +37677,7 @@ const checkDeadCode = async (options) => {
37475
37677
  });
37476
37678
  }
37477
37679
  for (const unusedDependency of result.unusedDependencies) {
37680
+ if (REACT_DOCTOR_TOOLCHAIN_PACKAGES.has(unusedDependency.name)) continue;
37478
37681
  const label = unusedDependency.isDevDependency ? "devDependency" : "dependency";
37479
37682
  diagnostics.push({
37480
37683
  filePath: "package.json",
@@ -37544,6 +37747,7 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
37544
37747
  return true;
37545
37748
  };
37546
37749
  const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
37750
+ const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs - Date.now(), 0);
37547
37751
  /**
37548
37752
  * Budget for the dead-code phase, scaled to the work. deslop's graph build is
37549
37753
  * CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
@@ -37562,53 +37766,6 @@ const resolveDeadCodeTimeout = (input) => {
37562
37766
  phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
37563
37767
  };
37564
37768
  };
37565
- const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
37566
- const entries = [];
37567
- for (const relativePath of relativePaths) {
37568
- const absolutePath = Path.resolve(rootDirectory, relativePath);
37569
- const sizeBytes = statSourceFileSize(absolutePath);
37570
- if (isLargeMinifiedFile(absolutePath, sizeBytes)) continue;
37571
- entries.push({
37572
- path: relativePath,
37573
- sizeBytes: sizeBytes ?? 0
37574
- });
37575
- }
37576
- return entries;
37577
- };
37578
- const listSourceFilesViaGit = (rootDirectory) => {
37579
- const result = spawnSync("git", [
37580
- "ls-files",
37581
- "-z",
37582
- "--cached",
37583
- "--others",
37584
- "--exclude-standard"
37585
- ], {
37586
- cwd: rootDirectory,
37587
- encoding: "utf-8",
37588
- maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
37589
- });
37590
- if (result.error || result.status !== 0) return null;
37591
- return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath));
37592
- };
37593
- const listSourceFilesViaFilesystem = (rootDirectory) => {
37594
- const filePaths = [];
37595
- const stack = [rootDirectory];
37596
- while (stack.length > 0) {
37597
- const currentDirectory = stack.pop();
37598
- const entries = readDirectoryEntries(currentDirectory);
37599
- for (const entry of entries) {
37600
- const absolutePath = Path.join(currentDirectory, entry.name);
37601
- if (entry.isDirectory()) {
37602
- if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
37603
- continue;
37604
- }
37605
- if (entry.isFile() && isLintableSourceFile(entry.name)) filePaths.push(Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
37606
- }
37607
- }
37608
- return filePaths;
37609
- };
37610
- const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
37611
- const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
37612
37769
  const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
37613
37770
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
37614
37771
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
@@ -37653,7 +37810,6 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37653
37810
  return yield* tryPromise({
37654
37811
  try: (signal) => checkDeadCode({
37655
37812
  rootDirectory: input.rootDirectory,
37656
- userConfig: input.userConfig,
37657
37813
  parseConcurrency: input.parseConcurrency,
37658
37814
  workerTimeoutMs: input.workerTimeoutMs,
37659
37815
  abortSignal: signal
@@ -37776,6 +37932,11 @@ const isSafeGitRevision = (candidate) => {
37776
37932
  if (candidate.includes("..") || candidate.includes("@{")) return false;
37777
37933
  return SAFE_GIT_REVISION_PATTERN.test(candidate);
37778
37934
  };
37935
+ const resolveSpawnArgsLengthCap = () => {
37936
+ if (process.platform === "win32") return SPAWN_ARGS_MAX_LENGTH_CHARS;
37937
+ if (process.platform === "darwin") return SPAWN_ARGS_MAX_LENGTH_CHARS_DARWIN;
37938
+ return SPAWN_ARGS_MAX_LENGTH_CHARS_POSIX;
37939
+ };
37779
37940
  /**
37780
37941
  * Splits a git revision range into its endpoints: three-dot `A...B`
37781
37942
  * (symmetric, merge-base) or two-dot `A..B` (direct). Returns `null`
@@ -37847,43 +38008,47 @@ var Git = class Git extends Service()("react-doctor/Git") {
37847
38008
  * reason: GitInvocationFailed })` so the rest of the codebase
37848
38009
  * sees a single failure channel.
37849
38010
  */
37850
- const runCommand = (input) => scoped(gen(function* () {
37851
- const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37852
- cwd: input.directory,
37853
- env: input.env,
37854
- extendEnv: true
37855
- }));
37856
- const maxStdoutBytes = input.maxStdoutBytes;
37857
- const stdoutByteCount = yield* make$13(0);
37858
- const [stdout, stderr, status] = yield* all([
37859
- mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37860
- args: [...input.args],
37861
- directory: input.directory,
37862
- cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
37863
- }) })) : void_)))))),
37864
- mkString(decodeText(handle.stderr)),
37865
- handle.exitCode
37866
- ], { concurrency: 3 });
37867
- return {
37868
- status,
37869
- stdout,
37870
- stderr
37871
- };
37872
- })).pipe(catchTag$1("PlatformError", (cause) => {
37873
- if (input.command !== "git") return succeed$2({
38011
+ const runCommand = (input) => {
38012
+ const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
37874
38013
  status: 127,
37875
38014
  stdout: "",
37876
38015
  stderr: String(cause)
37877
- });
37878
- return new ReactDoctorError({ reason: new GitInvocationFailed({
38016
+ }) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37879
38017
  args: [...input.args],
37880
38018
  directory: input.directory,
37881
38019
  cause
37882
- }) });
37883
- }), withSpan("git.exec", { attributes: {
37884
- "git.command": input.command,
37885
- "git.subcommand": input.args[0] ?? ""
37886
- } }));
38020
+ }) }));
38021
+ return scoped(gen(function* () {
38022
+ if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
38023
+ const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
38024
+ const spawnArgsLengthCap = resolveSpawnArgsLengthCap();
38025
+ if (argvLengthChars > spawnArgsLengthCap) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${spawnArgsLengthCap})`);
38026
+ const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
38027
+ cwd: input.directory,
38028
+ env: input.env,
38029
+ extendEnv: true
38030
+ }));
38031
+ const maxStdoutBytes = input.maxStdoutBytes;
38032
+ const stdoutByteCount = yield* make$13(0);
38033
+ const [stdout, stderr, status] = yield* all([
38034
+ mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
38035
+ args: [...input.args],
38036
+ directory: input.directory,
38037
+ cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
38038
+ }) })) : void_)))))),
38039
+ mkString(decodeText(handle.stderr)),
38040
+ handle.exitCode
38041
+ ], { concurrency: 3 });
38042
+ return {
38043
+ status,
38044
+ stdout,
38045
+ stderr
38046
+ };
38047
+ })).pipe(catchTag$1("PlatformError", foldSpawnFailure), withSpan("git.exec", { attributes: {
38048
+ "git.command": input.command,
38049
+ "git.subcommand": input.args[0] ?? ""
38050
+ } }));
38051
+ };
37887
38052
  const runGit = (directory, args) => runCommand({
37888
38053
  command: "git",
37889
38054
  args,
@@ -37916,7 +38081,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37916
38081
  "rev-parse",
37917
38082
  "--verify",
37918
38083
  branch
37919
- ]).pipe(map$3((result) => result.status === 0));
38084
+ ]).pipe(map$3((result) => result.status === 0), catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(false) : fail$4(error)));
37920
38085
  const headSha = (directory) => runGit(directory, ["rev-parse", "HEAD"]).pipe(map$3((result) => result.status === 0 ? trimOrNull(result.stdout) : null));
37921
38086
  const mergeBase = (input) => isSafeGitRevision(input.ref) ? runGit(input.directory, [
37922
38087
  "merge-base",
@@ -38026,6 +38191,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38026
38191
  if (resolvedCurrentBranch === null && explicitBaseBranch === void 0) return null;
38027
38192
  const baseBranch = explicitBaseBranch ?? (yield* defaultBranch(directory));
38028
38193
  if (baseBranch === null) return null;
38194
+ if (!isSafeGitRevision(baseBranch)) return null;
38029
38195
  if (explicitBaseBranch !== void 0) {
38030
38196
  if (!(yield* branchExists(directory, explicitBaseBranch))) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchMissing({ branch: explicitBaseBranch }) }));
38031
38197
  }
@@ -38130,7 +38296,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38130
38296
  ]);
38131
38297
  if (result.status !== 0) return null;
38132
38298
  return parseChangedLineRanges(result.stdout);
38133
- }).pipe(withSpan("Git.changedLineRanges"))
38299
+ }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
38134
38300
  });
38135
38301
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
38136
38302
  /**
@@ -38296,12 +38462,12 @@ const findFilesWithDisableDirectivesViaGit = async (rootDirectory, includePaths)
38296
38462
  return null;
38297
38463
  }
38298
38464
  if (grepResult === null) return null;
38299
- return grepResult.stdout.split("\n").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath));
38465
+ return grepResult.stdout.split("\n").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath));
38300
38466
  };
38301
38467
  const findFilesWithDisableDirectivesViaFilesystem = (rootDirectory, includePaths) => {
38302
38468
  const matches = [];
38303
38469
  const checkFile = (relativePath) => {
38304
- if (!isLintableSourceFile(relativePath)) return;
38470
+ if (!isLintableSourceFile(relativePath) || hasIgnoredPathSegment(relativePath)) return;
38305
38471
  const absolutePath = Path.join(rootDirectory, relativePath);
38306
38472
  let content;
38307
38473
  try {
@@ -38315,22 +38481,7 @@ const findFilesWithDisableDirectivesViaFilesystem = (rootDirectory, includePaths
38315
38481
  for (const candidate of includePaths) checkFile(candidate);
38316
38482
  return matches;
38317
38483
  }
38318
- const stack = [rootDirectory];
38319
- while (stack.length > 0) {
38320
- const current = stack.pop();
38321
- if (current === void 0) continue;
38322
- const entries = readDirectoryEntries(current);
38323
- for (const entry of entries) {
38324
- if (entry.isDirectory()) {
38325
- if (entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
38326
- stack.push(Path.join(current, entry.name));
38327
- continue;
38328
- }
38329
- if (!entry.isFile()) continue;
38330
- const absolute = Path.join(current, entry.name);
38331
- checkFile(Path.relative(rootDirectory, absolute));
38332
- }
38333
- }
38484
+ for (const { absolutePath } of walkSourceTreeFiles(rootDirectory)) checkFile(Path.relative(rootDirectory, absolutePath));
38334
38485
  return matches;
38335
38486
  };
38336
38487
  const findFilesWithDisableDirectives = async (rootDirectory, includePaths) => await findFilesWithDisableDirectivesViaGit(rootDirectory, includePaths) ?? findFilesWithDisableDirectivesViaFilesystem(rootDirectory, includePaths);
@@ -38376,7 +38527,7 @@ const normalizeConfigForHash = (config) => {
38376
38527
  if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
38377
38528
  return clone;
38378
38529
  };
38379
- const computeRulesetHash = (input) => crypto.createHash("sha1").update(JSON.stringify(normalizeConfigForHash(input.config))).update("\0").update([...input.toolchainVersions].join("\0")).update("\0").update([...input.ignorePatterns].join("\n")).update(" ").update(input.tsconfigContent ?? "").digest("hex");
38530
+ const computeRulesetHash = (input) => crypto.createHash("sha1").update(JSON.stringify(normalizeConfigForHash(input.config))).update("\0").update([...input.toolchainVersions].join("\0")).update("\0").update([...input.ignorePatterns].join("\n")).update("\0").update(input.tsconfigContent ?? "").digest("hex");
38380
38531
  /**
38381
38532
  * Loads a plugin module via the local require resolver and extracts
38382
38533
  * `(name, ruleNames)` from either `module.exports.meta + rules` or
@@ -38547,12 +38698,12 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38547
38698
  if (customRulesOnly && registryEntry.originallyExternal) continue;
38548
38699
  if (rule.framework !== "global" && !rule.requires) continue;
38549
38700
  if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) continue;
38550
- const explicitSeverity = resolveRuleSeverityOverride({
38701
+ const explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey: registryEntry.key }, severityControls);
38702
+ if (rule.defaultEnabled === false && explicitRuleOverride === void 0) continue;
38703
+ const severity = resolveRuleSeverityOverride({
38551
38704
  ruleKey: registryEntry.key,
38552
38705
  category: rule.category
38553
- }, severityControls);
38554
- if (rule.defaultEnabled === false && explicitSeverity === void 0) continue;
38555
- const severity = explicitSeverity ?? resolveCompilerCleanupBucketSeverity(registryEntry.key, severityControls) ?? rule.severity;
38706
+ }, severityControls) ?? resolveCompilerCleanupBucketSeverity(registryEntry.key, severityControls) ?? rule.severity;
38556
38707
  if (severity === "off") continue;
38557
38708
  enabledReactDoctorRules[registryEntry.key] = severity;
38558
38709
  }
@@ -38654,24 +38805,38 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38654
38805
  updatedAtMs: Date.now(),
38655
38806
  files: Object.fromEntries(cappedEntries)
38656
38807
  };
38657
- const keptHashes = Object.entries(rulesets).sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8).map(([hash]) => hash);
38658
- const prunedRulesets = {};
38659
- for (const hash of keptHashes) prunedRulesets[hash] = rulesets[hash];
38660
38808
  atomicWriteJson(cacheFilePath, {
38661
38809
  version: 1,
38662
- rulesets: prunedRulesets
38810
+ rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
38663
38811
  });
38664
38812
  }
38665
38813
  };
38666
38814
  };
38667
38815
  const bundledRequire = createRequire(import.meta.url);
38816
+ const childNodeVersionByBinaryPath = /* @__PURE__ */ new Map();
38817
+ const resolveChildNodeVersion = (nodeBinaryPath) => {
38818
+ if (nodeBinaryPath === process.execPath) return process.version;
38819
+ const cachedVersion = childNodeVersionByBinaryPath.get(nodeBinaryPath);
38820
+ if (cachedVersion !== void 0) return cachedVersion;
38821
+ let version;
38822
+ try {
38823
+ version = execFileSync(nodeBinaryPath, ["--version"], {
38824
+ encoding: "utf8",
38825
+ timeout: NODE_VERSION_PROBE_TIMEOUT_MS
38826
+ }).trim();
38827
+ } catch {
38828
+ version = `unknown:${nodeBinaryPath}`;
38829
+ }
38830
+ childNodeVersionByBinaryPath.set(nodeBinaryPath, version);
38831
+ return version;
38832
+ };
38668
38833
  const TOOLCHAIN_PACKAGE_SPECIFIERS = [
38669
38834
  "oxlint/package.json",
38670
38835
  "oxlint-plugin-react-doctor/package.json",
38671
38836
  "eslint-plugin-react-hooks/package.json"
38672
38837
  ];
38673
- const resolveOxlintToolchainVersions = () => {
38674
- const versions = [`node=${process.version}`];
38838
+ const resolveOxlintToolchainVersions = (nodeBinaryPath = process.execPath) => {
38839
+ const versions = [`node=${resolveChildNodeVersion(nodeBinaryPath)}`];
38675
38840
  for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
38676
38841
  const packageJson = bundledRequire(specifier);
38677
38842
  const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
@@ -39205,11 +39370,12 @@ const REACT_COMPILER_TITLE = "React Compiler can't optimize this";
39205
39370
  const REACT_COMPILER_TODO_TITLE = "React Compiler doesn't support this syntax";
39206
39371
  const REACT_COMPILER_IMPACT = "This component misses React Compiler's automatic memoization & re-renders more than it should";
39207
39372
  const REACT_COMPILER_ACTION = "Rewrite the flagged code so the compiler can optimize it.";
39373
+ const REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION = "It's how the library works, not a bug in your code. Memoize values you pass from it into other memoized components, or suppress it with `// react-doctor-disable-next-line react-hooks-js/incompatible-library`.";
39208
39374
  const REACT_COMPILER_GENERIC_MESSAGE = `${REACT_COMPILER_IMPACT}. ${REACT_COMPILER_ACTION}`;
39209
- const buildReactCompilerMessage = (reasonSummary) => {
39375
+ const buildReactCompilerMessage = (reasonSummary, action = REACT_COMPILER_ACTION) => {
39210
39376
  const normalizedSummary = reasonSummary.replace(TRAILING_PERIOD_PATTERN, "");
39211
- if (!normalizedSummary) return REACT_COMPILER_GENERIC_MESSAGE;
39212
- return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${REACT_COMPILER_ACTION}`;
39377
+ if (!normalizedSummary) return `${REACT_COMPILER_IMPACT}. ${action}`;
39378
+ return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${action}`;
39213
39379
  };
39214
39380
  const PLUGIN_CATEGORY_MAP = {
39215
39381
  react: "Bugs",
@@ -39258,7 +39424,7 @@ const resolveCleanedDiagnostic = (message, help, plugin, rule, project) => {
39258
39424
  const [reasonSummary = "", ...reasonDetailLines] = bailoutReason.split("\n");
39259
39425
  const reasonDetail = reasonDetailLines.join("\n").trim();
39260
39426
  return {
39261
- message: buildReactCompilerMessage(reasonSummary.trim()),
39427
+ message: buildReactCompilerMessage(reasonSummary.trim(), rule === "incompatible-library" ? REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION : REACT_COMPILER_ACTION),
39262
39428
  help: appendReanimatedSharedValueHint(reasonDetail || help, rule, project)
39263
39429
  };
39264
39430
  }
@@ -39519,28 +39685,46 @@ const isParallelismRelatedSpawnError = (error) => {
39519
39685
  * loop with a slimmer config in that case.
39520
39686
  */
39521
39687
  const spawnLintBatches = async (input) => {
39522
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal } = input;
39688
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal, deadlineEpochMs } = input;
39523
39689
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
39524
39690
  const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
39525
39691
  const runBatchPass = async (concurrency) => {
39526
39692
  const allDiagnostics = [];
39527
39693
  const droppedFiles = [];
39694
+ const deadlineSkippedFiles = [];
39695
+ const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0;
39528
39696
  let firstDropReason = null;
39529
- const splitDeadlineMs = Date.now() + splitTotalBudgetMs;
39530
- const spawnLintBatch = async (batch, depth) => {
39697
+ const spawnLintBatch = async (batch, depth, batchState) => {
39698
+ if (isPastDeadline()) {
39699
+ deadlineSkippedFiles.push(...batch);
39700
+ batchState.deadlineSkippedFileCount += batch.length;
39701
+ return [];
39702
+ }
39531
39703
  const batchArgs = [...baseArgs, ...batch];
39532
39704
  try {
39533
39705
  return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
39534
39706
  } catch (error) {
39535
39707
  if (!isSplittableReactDoctorError(error)) throw error;
39536
- const splitBudgetExhausted = Date.now() >= splitDeadlineMs || depth >= splitMaxDepth;
39537
- if (batch.length <= 1 || splitBudgetExhausted) {
39708
+ if (isPastDeadline()) {
39709
+ deadlineSkippedFiles.push(...batch);
39710
+ batchState.deadlineSkippedFileCount += batch.length;
39711
+ return [];
39712
+ }
39713
+ batchState.deadlineMs ??= Date.now() + splitTotalBudgetMs;
39714
+ const isBudgetElapsed = Date.now() >= batchState.deadlineMs;
39715
+ const isDepthCapReached = depth >= splitMaxDepth;
39716
+ if (batch.length <= 1 || isBudgetElapsed || isDepthCapReached) {
39538
39717
  droppedFiles.push(...batch);
39539
- if (firstDropReason === null) firstDropReason = splitBudgetExhausted && batch.length > 1 ? `${error.message} (split budget exhausted after ${splitMaxDepth} levels / ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s)` : error.message;
39718
+ if (firstDropReason === null) {
39719
+ let limitHint = "";
39720
+ if (isDepthCapReached) limitHint = ` (split depth cap of ${splitMaxDepth} levels reached)`;
39721
+ else if (isBudgetElapsed) limitHint = ` (split budget of ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s exhausted at depth ${depth})`;
39722
+ firstDropReason = batch.length > 1 ? `${error.message}${limitHint}` : error.message;
39723
+ }
39540
39724
  return [];
39541
39725
  }
39542
39726
  const splitIndex = Math.ceil(batch.length / 2);
39543
- return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1)];
39727
+ return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1, batchState), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1, batchState)];
39544
39728
  }
39545
39729
  };
39546
39730
  let startedFileCount = 0;
@@ -39556,9 +39740,17 @@ const spawnLintBatches = async (input) => {
39556
39740
  progressTimer?.unref?.();
39557
39741
  try {
39558
39742
  const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
39743
+ if (isPastDeadline()) {
39744
+ deadlineSkippedFiles.push(...batch);
39745
+ return [];
39746
+ }
39559
39747
  startedFileCount += batch.length;
39560
- const batchDiagnostics = await spawnLintBatch(batch, 0);
39561
- scannedFileCount += batch.length;
39748
+ const batchState = {
39749
+ deadlineMs: null,
39750
+ deadlineSkippedFileCount: 0
39751
+ };
39752
+ const batchDiagnostics = await spawnLintBatch(batch, 0, batchState);
39753
+ scannedFileCount += batch.length - batchState.deadlineSkippedFileCount;
39562
39754
  if (onFileProgress) {
39563
39755
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
39564
39756
  onFileProgress(displayedFileCount, totalFileCount);
@@ -39569,12 +39761,13 @@ const spawnLintBatches = async (input) => {
39569
39761
  } finally {
39570
39762
  if (progressTimer !== null) clearInterval(progressTimer);
39571
39763
  }
39572
- if (droppedFiles.length > 0 && onPartialFailure) {
39573
- const previewFiles = droppedFiles.slice(0, 3).join(", ");
39574
- const remainderHint = droppedFiles.length > 3 ? `, +${droppedFiles.length - 3} more` : "";
39575
- const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
39576
- onPartialFailure(`${droppedFiles.length} file(s) failed to lint and were skipped (${previewFiles}${remainderHint})${reasonHint}`);
39577
- }
39764
+ const reportSkippedFiles = (skippedFiles, buildMessage) => {
39765
+ if (skippedFiles.length === 0 || !onPartialFailure) return;
39766
+ onPartialFailure(buildMessage(`${skippedFiles.slice(0, 3).join(", ")}${skippedFiles.length > 3 ? `, +${skippedFiles.length - 3} more` : ""}`));
39767
+ };
39768
+ const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
39769
+ reportSkippedFiles(droppedFiles, (fileListPreview) => `${droppedFiles.length} file(s) failed to lint and were skipped (${fileListPreview})${reasonHint}`);
39770
+ reportSkippedFiles(deadlineSkippedFiles, (fileListPreview) => `${deadlineSkippedFiles.length} file(s) skipped — max scan duration reached before they were linted (${fileListPreview})`);
39578
39771
  return allDiagnostics;
39579
39772
  };
39580
39773
  let diagnostics;
@@ -39631,7 +39824,14 @@ const resolveReactDoctorCacheDir = (projectDirectory) => {
39631
39824
  if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
39632
39825
  const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
39633
39826
  if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
39634
- return Path.join(os.tmpdir(), "react-doctor-cache", projectCacheSubdir(projectDirectory));
39827
+ return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
39828
+ };
39829
+ const resolveUserCacheScope = () => {
39830
+ try {
39831
+ return String(process.getuid?.() ?? os.userInfo().username);
39832
+ } catch {
39833
+ return "shared";
39834
+ }
39635
39835
  };
39636
39836
  const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
39637
39837
  /**
@@ -39771,7 +39971,8 @@ const runOxlint = async (options) => {
39771
39971
  spawnTimeoutMs,
39772
39972
  outputMaxBytes,
39773
39973
  concurrency: options.concurrency,
39774
- signal: options.signal
39974
+ signal: options.signal,
39975
+ deadlineEpochMs: options.deadlineEpochMs
39775
39976
  });
39776
39977
  writeOxlintConfig(passConfigPath, buildConfigForPass({}));
39777
39978
  try {
@@ -39799,7 +40000,7 @@ const runOxlint = async (options) => {
39799
40000
  extendsPaths: [],
39800
40001
  ruleSelection: "cacheable"
39801
40002
  }),
39802
- toolchainVersions: resolveOxlintToolchainVersions(),
40003
+ toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
39803
40004
  ignorePatterns: combinedPatterns,
39804
40005
  tsconfigContent
39805
40006
  });
@@ -39813,7 +40014,7 @@ const runOxlint = async (options) => {
39813
40014
  missFiles.push(candidateFile);
39814
40015
  continue;
39815
40016
  }
39816
- const cacheKey = `${candidateFile.replaceAll("\\", "/")}${contentHash}`;
40017
+ const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
39817
40018
  cacheKeyByFile.set(candidateFile, cacheKey);
39818
40019
  const cachedDiagnostics = cache.lookup(cacheKey);
39819
40020
  if (cachedDiagnostics === null) missFiles.push(candidateFile);
@@ -39870,7 +40071,8 @@ const runOxlint = async (options) => {
39870
40071
  spawnTimeoutMs,
39871
40072
  outputMaxBytes,
39872
40073
  concurrency: options.concurrency,
39873
- signal: options.signal
40074
+ signal: options.signal,
40075
+ deadlineEpochMs: options.deadlineEpochMs
39874
40076
  });
39875
40077
  writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
39876
40078
  try {
@@ -39974,7 +40176,8 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
39974
40176
  outputMaxBytes,
39975
40177
  concurrency,
39976
40178
  signal,
39977
- lintBatchOrdering
40179
+ lintBatchOrdering,
40180
+ deadlineEpochMs: input.deadlineEpochMs
39978
40181
  }),
39979
40182
  catch: ensureReactDoctorError
39980
40183
  });
@@ -40129,7 +40332,7 @@ const ScoreApiResponseSchema = Struct({
40129
40332
  })))
40130
40333
  });
40131
40334
  const parseScoreResult = (value) => getOrNull(decodeUnknownOption(ScoreApiResponseSchema)(value));
40132
- const stripLocalFileFields = (diagnostics) => diagnostics.map(({ filePath: _filePath, fileContext: _fileContext, ...rest }) => rest);
40335
+ const stripLocalFileFields = (diagnostics) => diagnostics.map(({ filePath: _filePath, fileContext: _fileContext, fixGroupId: _fixGroupId, ...rest }) => rest);
40133
40336
  const isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
40134
40337
  const describeFailure = (error) => {
40135
40338
  if (isAbortError(error)) return `timed out after ${FETCH_TIMEOUT_MS / 1e3}s`;
@@ -40433,7 +40636,10 @@ const ALERT_SEVERITY_RANK = {
40433
40636
  medium: 2,
40434
40637
  low: 1
40435
40638
  };
40436
- const severityRank = (severity) => ALERT_SEVERITY_RANK[severity.toLowerCase()] ?? 0;
40639
+ const severityRank = (severity) => {
40640
+ const normalized = severity.toLowerCase();
40641
+ return Object.hasOwn(ALERT_SEVERITY_RANK, normalized) ? ALERT_SEVERITY_RANK[normalized] : 0;
40642
+ };
40437
40643
  const displaySeverity = (severity) => {
40438
40644
  const normalized = sanitizeTerminalText(severity.toLowerCase());
40439
40645
  return normalized === "middle" ? "medium" : normalized;
@@ -40450,7 +40656,7 @@ const ALERT_TYPE_LABELS = {
40450
40656
  typosquat: "possible typosquat"
40451
40657
  };
40452
40658
  const humanizeAlertType = (type) => type.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase().trim();
40453
- const friendlyAlertType = (type) => ALERT_TYPE_LABELS[type] ?? sanitizeTerminalText(humanizeAlertType(type));
40659
+ const friendlyAlertType = (type) => Object.hasOwn(ALERT_TYPE_LABELS, type) ? ALERT_TYPE_LABELS[type] : sanitizeTerminalText(humanizeAlertType(type));
40454
40660
  const summarizeAlertNote = (note) => {
40455
40661
  const collapsed = sanitizeTerminalText(note.replace(/\s+/g, " ").trim());
40456
40662
  const firstSentence = collapsed.split(/(?<=\.)\s/)[0] || collapsed;
@@ -40545,7 +40751,10 @@ const checkSupplyChain = (input) => gen(function* () {
40545
40751
  * outage.
40546
40752
  */
40547
40753
  var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
40548
- static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(checkSupplyChain(input).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
40754
+ static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(flatMap$2(SupplyChainOverlapTimeoutMs, (totalTimeoutMs) => checkSupplyChain({
40755
+ ...input,
40756
+ totalTimeoutMs
40757
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
40549
40758
  static layerOf = (diagnostics) => succeed$3(SupplyChain, SupplyChain.of({ run: () => fromIterable$1(diagnostics) }));
40550
40759
  };
40551
40760
  const getObjectProperty = (value, propertyName) => {
@@ -40607,7 +40816,10 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40607
40816
  * diagnostics).
40608
40817
  * 2. beforeLint hook (e.g. CLI renders the project-detection block)
40609
40818
  * 3. environment checks (reduced-motion + pnpm hardening +
40610
- * expo/react-native + security scan), collected synchronously
40819
+ * expo/react-native), collected synchronously. The heavier
40820
+ * content-regex security scan is forked instead (like supply-chain
40821
+ * below) and joined before the concat, so its CPU overlaps lint
40822
+ * rather than blocking the event loop before it.
40611
40823
  * 4. The supply-chain check (Socket.dev) is forked onto a background
40612
40824
  * fiber so its ~100% network-bound time overlaps the ~100%
40613
40825
  * CPU/subprocess-bound lint pass below, collapsing two serial
@@ -40627,7 +40839,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40627
40839
  * order, so terminal output is identical either way; supply-chain
40628
40840
  * rides alongside without a spinner.
40629
40841
  * 6. Join the supply-chain fiber, then assemble the diagnostics in a
40630
- * FIXED order (env, supply-chain, lint, dead-code) so the output is
40842
+ * FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
40631
40843
  * byte-identical regardless of which fiber settled first. The
40632
40844
  * viewer-permission fiber is joined later, during score-metadata
40633
40845
  * assembly (it feeds score metadata, not diagnostics). The per-element
@@ -40688,12 +40900,13 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40688
40900
  ...checkPnpmHardening(scanDirectory),
40689
40901
  ...checkReactServerComponentsAdvisory(scanDirectory, project),
40690
40902
  ...checkExpoProject(scanDirectory, project),
40691
- ...checkReactNativeProject(scanDirectory, project),
40692
- ...checkSecurityScan(scanDirectory, {
40693
- project,
40694
- ignoredTags: input.ignoredTags
40695
- })
40903
+ ...checkReactNativeProject(scanDirectory, project)
40696
40904
  ])));
40905
+ const securityScanFailedRef = yield* make$13(false);
40906
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise(() => checkSecurityScanCooperative(scanDirectory, {
40907
+ project,
40908
+ ignoredTags: input.ignoredTags
40909
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), catch_$1(() => set(securityScanFailedRef, true).pipe(as(empty$4))))))).pipe(withSpan("SecurityScan.run")));
40697
40910
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
40698
40911
  const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
40699
40912
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
@@ -40724,6 +40937,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40724
40937
  const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
40725
40938
  const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
40726
40939
  const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
40940
+ const capToDeadline = (phaseTimeoutMs) => input.deadlineEpochMs === void 0 ? phaseTimeoutMs : Math.min(phaseTimeoutMs, remainingDeadlineBudgetMs(input.deadlineEpochMs));
40727
40941
  const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
40728
40942
  const deadCodeOverlapMode = yield* DeadCodeOverlap;
40729
40943
  const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
@@ -40731,7 +40945,6 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40731
40945
  const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
40732
40946
  const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
40733
40947
  rootDirectory: scanDirectory,
40734
- userConfig: resolvedConfig.config,
40735
40948
  parseConcurrency: deadCodeParseConcurrency,
40736
40949
  workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
40737
40950
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
@@ -40754,7 +40967,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40754
40967
  });
40755
40968
  const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
40756
40969
  workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
40757
- phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs)
40970
+ phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs))
40758
40971
  })) : null;
40759
40972
  const scanProgress = yield* progressService.start("Scanning...");
40760
40973
  const scanStartTime = Date.now();
@@ -40779,7 +40992,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40779
40992
  onCacheStats: (cacheHitFileCount, totalConsideredFileCount) => {
40780
40993
  lintCacheHitFileCount = cacheHitFileCount;
40781
40994
  lintCacheTotalFileCount = totalConsideredFileCount;
40782
- }
40995
+ },
40996
+ deadlineEpochMs: input.deadlineEpochMs
40783
40997
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
40784
40998
  yield* set(lintFailure, {
40785
40999
  didFail: true,
@@ -40806,7 +41020,13 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40806
41020
  let deadCodeCollected = [];
40807
41021
  if (lintFailureState.didFail) {
40808
41022
  if (deadCodeFiber !== null) yield* interrupt(deadCodeFiber);
40809
- } else if (shouldRunDeadCode) {
41023
+ } else if (shouldRunDeadCode) if (input.deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(input.deadlineEpochMs) === 0) {
41024
+ if (deadCodeFiber !== null) yield* interrupt(deadCodeFiber);
41025
+ yield* set(deadCodeFailure, {
41026
+ didFail: true,
41027
+ reason: "Dead-code analysis skipped — max scan duration reached."
41028
+ });
41029
+ } else {
40810
41030
  yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
40811
41031
  const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
40812
41032
  sourceFileCount: totalFileCount,
@@ -40815,7 +41035,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40815
41035
  });
40816
41036
  deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
40817
41037
  workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
40818
- phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs)
41038
+ phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs))
40819
41039
  });
40820
41040
  }
40821
41041
  const deadCodeFailureState = lintFailureState.didFail ? {
@@ -40829,9 +41049,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40829
41049
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
40830
41050
  const supplyChainResult = yield* join(supplyChainFiber);
40831
41051
  const supplyChainCollected = supplyChainResult.diagnostics;
41052
+ const securityScanCollected = yield* join(securityScanFiber);
40832
41053
  yield* reporterService.finalize;
40833
41054
  const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
40834
41055
  ...envCollected,
41056
+ ...securityScanCollected,
40835
41057
  ...supplyChainCollected,
40836
41058
  ...lintCollected,
40837
41059
  ...deadCodeCollected
@@ -40851,12 +41073,13 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40851
41073
  };
40852
41074
  const scoreSurface = input.scoreSurface ?? "score";
40853
41075
  const scoreDiagnostics = filterDiagnosticsForSurface([...finalDiagnostics], scoreSurface, resolvedConfig.config);
40854
- const score = lintFailureState.didFail ? null : yield* scoreService.compute({
41076
+ const score = lintFailureState.didFail || deadCodeFailureState.didFail ? null : yield* scoreService.compute({
40855
41077
  diagnostics: scoreDiagnostics,
40856
41078
  isCi: input.isCi,
40857
41079
  metadata: scoreMetadata
40858
41080
  });
40859
41081
  const lintPartialFailures = yield* get$2(partialFailuresRef);
41082
+ const securityScanFailed = yield* get$2(securityScanFailedRef);
40860
41083
  return {
40861
41084
  project,
40862
41085
  userConfig: resolvedConfig.config,
@@ -40877,8 +41100,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40877
41100
  scanElapsedMilliseconds,
40878
41101
  scanConcurrency,
40879
41102
  supplyChainOverlapTimedOut: supplyChainResult.timedOut,
41103
+ securityScanFailed,
40880
41104
  lintCacheHitFileCount,
40881
- lintCacheTotalFileCount
41105
+ lintCacheTotalFileCount,
41106
+ suppressedRuleCounts: transform.summarizeSuppressions()
40882
41107
  };
40883
41108
  }).pipe(withSpan("runInspect", { attributes: {
40884
41109
  "inspect.directory": input.directory,
@@ -42903,6 +43128,7 @@ const SENTRY_FLUSH_TIMEOUT_MS = 2e3;
42903
43128
  const METRIC = {
42904
43129
  cliInvoked: "cli.invoked",
42905
43130
  cliError: "cli.error",
43131
+ cliEnvironmentError: "cli.env_error",
42906
43132
  projectDetected: "project.detected",
42907
43133
  projectPathSelected: "project.path_selected",
42908
43134
  projectConfigSelected: "project.config_selected",
@@ -42915,6 +43141,8 @@ const METRIC = {
42915
43141
  scanCheckSkipped: "scan.check_skipped",
42916
43142
  baselineDegraded: "baseline.degraded",
42917
43143
  ruleFired: "rule.fired",
43144
+ ruleDisabled: "rule.disabled",
43145
+ ruleSuppressed: "rule.suppressed",
42918
43146
  lintFailed: "lint.failed",
42919
43147
  deadCodeFailed: "deadcode.failed",
42920
43148
  scoreUnavailable: "score.unavailable",
@@ -42927,12 +43155,17 @@ const METRIC = {
42927
43155
  installWorkflow: "install.workflow",
42928
43156
  installAgentHooks: "install.agent_hooks",
42929
43157
  installDependency: "install.dependency",
43158
+ ciScaffolded: "ci.scaffolded",
43159
+ ciUpgraded: "ci.upgraded",
43160
+ ciConfigured: "ci.configured",
42930
43161
  rulesChanged: "rules.changed",
42931
43162
  rulesQueried: "rules.queried",
42932
43163
  lspSessionStarted: "lsp.session.started",
42933
43164
  lspScanCompleted: "lsp.scan.completed",
42934
43165
  lspScanDuration: "lsp.scan.duration",
42935
- lspScanDiagnostics: "lsp.scan.diagnostics"
43166
+ lspScanDiagnostics: "lsp.scan.diagnostics",
43167
+ aiTrainingWarningShown: "ai.training.warning_shown",
43168
+ jsonOutUsed: "json.out_used"
42936
43169
  };
42937
43170
  //#endregion
42938
43171
  //#region src/cli/utils/to-category-key.ts
@@ -43249,5 +43482,5 @@ const startLanguageServer = () => {
43249
43482
  };
43250
43483
  //#endregion
43251
43484
  export { startLanguageServer };
43252
- !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]="9ee72ccc-703c-588f-b7e4-c92657144721")}catch(e){}}();
43253
- //# debugId=9ee72ccc-703c-588f-b7e4-c92657144721
43485
+ !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]="6811672b-baa6-5b7c-b676-5121c592d4a0")}catch(e){}}();
43486
+ //# debugId=6811672b-baa6-5b7c-b676-5121c592d4a0