react-doctor 0.5.8-dev.970babc → 0.5.8-dev.988ce57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="dccb6d63-63d4-5e91-af21-2756a08fe93a")}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]="62cf4740-7938-5db0-8b80-dc81b60af36a")}catch(e){}}();
3
3
  import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
4
4
  import { createRequire } from "node:module";
5
5
  import * as NFS from "node:fs";
@@ -7,7 +7,7 @@ import fs from "node:fs";
7
7
  import * as Path from "node:path";
8
8
  import path from "node:path";
9
9
  import * as NodeChildProcess from "node:child_process";
10
- import { spawn, spawnSync } from "node:child_process";
10
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
11
11
  import * as ts from "typescript";
12
12
  import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
13
13
  import * as OS from "node:os";
@@ -5996,7 +5996,7 @@ const composePassthrough = /* @__PURE__ */ dual(2, (left, right) => (input) => {
5996
5996
  * @since 2.0.0
5997
5997
  */
5998
5998
  const Scheduler = /* @__PURE__ */ Reference("effect/Scheduler", { defaultValue: () => new MixedScheduler() });
5999
- const setImmediate = "setImmediate" in globalThis ? (f) => {
5999
+ const setImmediate$1 = "setImmediate" in globalThis ? (f) => {
6000
6000
  const timer = globalThis.setImmediate(f);
6001
6001
  return () => globalThis.clearImmediate(timer);
6002
6002
  } : (f) => {
@@ -6040,7 +6040,7 @@ var PriorityBuckets = class {
6040
6040
  var MixedScheduler = class {
6041
6041
  executionMode;
6042
6042
  setImmediate;
6043
- constructor(executionMode = "async", setImmediateFn = setImmediate) {
6043
+ constructor(executionMode = "async", setImmediateFn = setImmediate$1) {
6044
6044
  this.executionMode = executionMode;
6045
6045
  this.setImmediate = setImmediateFn;
6046
6046
  }
@@ -6065,7 +6065,7 @@ var MixedSchedulerDispatcher = class {
6065
6065
  tasks = /* @__PURE__ */ new PriorityBuckets();
6066
6066
  running = void 0;
6067
6067
  setImmediate;
6068
- constructor(setImmediateFn = setImmediate) {
6068
+ constructor(setImmediateFn = setImmediate$1) {
6069
6069
  this.setImmediate = setImmediateFn;
6070
6070
  }
6071
6071
  /**
@@ -19364,6 +19364,15 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
19364
19364
  ok: Boolean$1,
19365
19365
  directory: String$1,
19366
19366
  mode: JsonReportMode,
19367
+ /**
19368
+ * True when a `changed` (compare) run was intended but the base couldn't be
19369
+ * resolved — a shallow CI checkout with no merge base, or a failed base/head
19370
+ * lint. The report then lists every finding in the changed files (mode
19371
+ * downgrades to `diff`, the `baseline` block is dropped, the CI gate is
19372
+ * skipped) rather than only the ones the change introduced. Absent on a
19373
+ * successful `baseline` run (`schemaVersion: 2`) and on non-compare scopes.
19374
+ */
19375
+ baselineDegraded: optional(Boolean$1),
19367
19376
  diff: NullOr(JsonReportDiffInfo),
19368
19377
  projects: ArraySchema(JsonReportProjectEntry),
19369
19378
  diagnostics: ArraySchema(Diagnostic),
@@ -32780,12 +32789,31 @@ const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.m?js$/i;
32780
32789
  const MINIFIED_SNIFF_BYTES = 65536;
32781
32790
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
32782
32791
  const IGNORED_DIRECTORIES = new Set([
32792
+ ".angular",
32793
+ ".astro",
32794
+ ".cache",
32795
+ ".contentlayer",
32796
+ ".direnv",
32797
+ ".docusaurus",
32798
+ ".expo",
32799
+ ".firebase",
32783
32800
  ".git",
32801
+ ".gradle",
32802
+ ".hg",
32784
32803
  ".next",
32785
32804
  ".nuxt",
32805
+ ".nx",
32786
32806
  ".output",
32807
+ ".parcel-cache",
32808
+ ".serverless",
32787
32809
  ".svelte-kit",
32810
+ ".svn",
32811
+ ".terraform",
32788
32812
  ".turbo",
32813
+ ".venv",
32814
+ ".vercel",
32815
+ ".wrangler",
32816
+ ".yarn",
32789
32817
  "build",
32790
32818
  "coverage",
32791
32819
  "dist",
@@ -32793,6 +32821,119 @@ const IGNORED_DIRECTORIES = new Set([
32793
32821
  "out",
32794
32822
  "storybook-static"
32795
32823
  ]);
32824
+ const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
32825
+ const MILLISECONDS_PER_SECOND = 1e3;
32826
+ const SCORE_API_URL = "https://www.react.doctor/api/score";
32827
+ const FETCH_TIMEOUT_MS = 1e4;
32828
+ const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
32829
+ const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
32830
+ const SPAWN_ARGS_MAX_LENGTH_CHARS_POSIX = 15e5;
32831
+ const SPAWN_ARGS_MAX_LENGTH_CHARS_DARWIN = 8e5;
32832
+ const NODE_VERSION_PROBE_TIMEOUT_MS = 5e3;
32833
+ const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
32834
+ const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
32835
+ const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
32836
+ const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
32837
+ const ES_TARGET_YEAR_BY_NAME = {
32838
+ es3: 1999,
32839
+ es5: 2009,
32840
+ es6: 2015,
32841
+ es2015: 2015,
32842
+ es2016: 2016,
32843
+ es2017: 2017,
32844
+ es2018: 2018,
32845
+ es2019: 2019,
32846
+ es2020: 2020,
32847
+ es2021: 2021,
32848
+ es2022: 2022,
32849
+ es2023: 2023,
32850
+ es2024: 2024,
32851
+ es2025: 2025,
32852
+ esnext: 9999
32853
+ };
32854
+ /**
32855
+ * tsconfig filenames probed when resolving a project's TypeScript
32856
+ * compiler options — the root config first, then a monorepo base config.
32857
+ */
32858
+ const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
32859
+ /**
32860
+ * Project-config files that `StagedFiles.materialize` copies into
32861
+ * the temp directory alongside staged sources so oxlint resolves
32862
+ * `tsconfig` / `package.json` / lint configs the same way it would
32863
+ * in the working tree. Hoisted out of the staged-files helper so
32864
+ * the constant lives next to the rest of the IO budget knobs.
32865
+ */
32866
+ const STAGED_FILES_PROJECT_CONFIG_FILENAMES = [
32867
+ "tsconfig.json",
32868
+ "tsconfig.base.json",
32869
+ "package.json",
32870
+ "doctor.config.ts",
32871
+ "doctor.config.mts",
32872
+ "doctor.config.cts",
32873
+ "doctor.config.js",
32874
+ "doctor.config.mjs",
32875
+ "doctor.config.cjs",
32876
+ "doctor.config.json",
32877
+ "doctor.config.jsonc",
32878
+ "oxlint.json",
32879
+ ".oxlintrc.json"
32880
+ ];
32881
+ const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
32882
+ const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
32883
+ const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
32884
+ const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
32885
+ const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
32886
+ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
32887
+ const LINT_PHASE_TIMEOUT_MS = 3e5;
32888
+ const SCAN_TOTAL_DEADLINE_MS = 9e5;
32889
+ const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
32890
+ const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
32891
+ const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
32892
+ const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
32893
+ const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
32894
+ const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
32895
+ const REACT_SERVER_DOM_PACKAGES = [
32896
+ "react-server-dom-webpack",
32897
+ "react-server-dom-parcel",
32898
+ "react-server-dom-turbopack"
32899
+ ];
32900
+ const REACT_BLOG_RSC_ADVISORY_URL = "https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components";
32901
+ const VERCEL_NEXTJS_SECURITY_RELEASE_URL = "https://vercel.com/changelog/next-js-may-2026-security-release";
32902
+ const DIAGNOSTIC_CATEGORY_BUCKETS = [
32903
+ "Security",
32904
+ "Bugs",
32905
+ "Performance",
32906
+ "Accessibility",
32907
+ "Maintainability"
32908
+ ];
32909
+ const APP_ONLY_RULE_KEYS = new Set([
32910
+ "react-hooks-js/static-components",
32911
+ "react-doctor/no-render-prop-children",
32912
+ "react-doctor/prefer-explicit-variants"
32913
+ ]);
32914
+ const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
32915
+ const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
32916
+ const ROOT_CAUSE_GROUPABLE_RULE_KEYS = new Set([
32917
+ "react-doctor/no-derived-state",
32918
+ "react-doctor/no-derived-state-effect",
32919
+ "react-doctor/no-derived-useState",
32920
+ "react-doctor/no-adjust-state-on-prop-change",
32921
+ "react-doctor/no-reset-all-state-on-prop-change"
32922
+ ]);
32923
+ const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
32924
+ const CONFIG_CACHE_TTL_MS = 300 * 1e3;
32925
+ const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
32926
+ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
32927
+ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
32928
+ const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
32929
+ const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
32930
+ const SUPPLY_CHAIN_PLUGIN = "socket";
32931
+ const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
32932
+ const SUPPLY_CHAIN_CATEGORY = "Security";
32933
+ const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
32934
+ const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
32935
+ const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
32936
+ const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
32796
32937
  const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath);
32797
32938
  const isMinifiedSource = (absolutePath) => {
32798
32939
  let fileDescriptor;
@@ -32849,23 +32990,38 @@ const readDirectoryEntries = (directoryPath) => {
32849
32990
  throw error;
32850
32991
  }
32851
32992
  };
32852
- const countSourceFilesViaFilesystem = (rootDirectory) => {
32853
- let count = 0;
32993
+ function* walkSourceTreeFiles(rootDirectory) {
32854
32994
  const stack = [rootDirectory];
32855
32995
  while (stack.length > 0) {
32856
32996
  const currentDirectory = stack.pop();
32857
- const entries = readDirectoryEntries(currentDirectory);
32858
- for (const entry of entries) {
32997
+ if (currentDirectory === void 0) continue;
32998
+ for (const entry of readDirectoryEntries(currentDirectory)) {
32999
+ const absolutePath = Path.join(currentDirectory, entry.name);
32859
33000
  if (entry.isDirectory()) {
32860
- if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(Path.join(currentDirectory, entry.name));
33001
+ if (!IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
32861
33002
  continue;
32862
33003
  }
32863
- if (entry.isFile() && isLintableSourceFile(entry.name) && !isLargeMinifiedFile(Path.join(currentDirectory, entry.name))) count++;
33004
+ if (entry.isFile()) yield {
33005
+ absolutePath,
33006
+ name: entry.name
33007
+ };
32864
33008
  }
32865
33009
  }
32866
- return count;
33010
+ }
33011
+ const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
33012
+ const entries = [];
33013
+ for (const relativePath of relativePaths) {
33014
+ const absolutePath = Path.resolve(rootDirectory, relativePath);
33015
+ const sizeBytes = statSourceFileSize(absolutePath);
33016
+ if (isLargeMinifiedFile(absolutePath, sizeBytes)) continue;
33017
+ entries.push({
33018
+ path: relativePath,
33019
+ sizeBytes: sizeBytes ?? 0
33020
+ });
33021
+ }
33022
+ return entries;
32867
33023
  };
32868
- const countSourceFilesViaGit = (rootDirectory) => {
33024
+ const listSourceFilesViaGit = (rootDirectory) => {
32869
33025
  const result = spawnSync("git", [
32870
33026
  "ls-files",
32871
33027
  "-z",
@@ -32878,9 +33034,16 @@ const countSourceFilesViaGit = (rootDirectory) => {
32878
33034
  maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
32879
33035
  });
32880
33036
  if (result.error || result.status !== 0) return null;
32881
- return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath) && !isLargeMinifiedFile(Path.resolve(rootDirectory, filePath))).length;
33037
+ return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath));
33038
+ };
33039
+ const listSourceFilesViaFilesystem = (rootDirectory) => {
33040
+ const filePaths = [];
33041
+ for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) if (isLintableSourceFile(name)) filePaths.push(Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
33042
+ return filePaths;
32882
33043
  };
32883
- const countSourceFiles = (rootDirectory) => countSourceFilesViaGit(rootDirectory) ?? countSourceFilesViaFilesystem(rootDirectory);
33044
+ const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
33045
+ const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
33046
+ const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
32884
33047
  const cachedPackageJsons = /* @__PURE__ */ new Map();
32885
33048
  const clearPackageJsonCache = () => {
32886
33049
  cachedPackageJsons.clear();
@@ -33689,114 +33852,6 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
33689
33852
  if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
33690
33853
  return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
33691
33854
  };
33692
- const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
33693
- const MILLISECONDS_PER_SECOND = 1e3;
33694
- const SCORE_API_URL = "https://www.react.doctor/api/score";
33695
- const FETCH_TIMEOUT_MS = 1e4;
33696
- const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
33697
- const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
33698
- const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
33699
- const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
33700
- const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
33701
- const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
33702
- const ES_TARGET_YEAR_BY_NAME = {
33703
- es3: 1999,
33704
- es5: 2009,
33705
- es6: 2015,
33706
- es2015: 2015,
33707
- es2016: 2016,
33708
- es2017: 2017,
33709
- es2018: 2018,
33710
- es2019: 2019,
33711
- es2020: 2020,
33712
- es2021: 2021,
33713
- es2022: 2022,
33714
- es2023: 2023,
33715
- es2024: 2024,
33716
- es2025: 2025,
33717
- esnext: 9999
33718
- };
33719
- /**
33720
- * tsconfig filenames probed when resolving a project's TypeScript
33721
- * compiler options — the root config first, then a monorepo base config.
33722
- */
33723
- const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
33724
- /**
33725
- * Project-config files that `StagedFiles.materialize` copies into
33726
- * the temp directory alongside staged sources so oxlint resolves
33727
- * `tsconfig` / `package.json` / lint configs the same way it would
33728
- * in the working tree. Hoisted out of the staged-files helper so
33729
- * the constant lives next to the rest of the IO budget knobs.
33730
- */
33731
- const STAGED_FILES_PROJECT_CONFIG_FILENAMES = [
33732
- "tsconfig.json",
33733
- "tsconfig.base.json",
33734
- "package.json",
33735
- "doctor.config.ts",
33736
- "doctor.config.mts",
33737
- "doctor.config.cts",
33738
- "doctor.config.js",
33739
- "doctor.config.mjs",
33740
- "doctor.config.cjs",
33741
- "doctor.config.json",
33742
- "doctor.config.jsonc",
33743
- "oxlint.json",
33744
- ".oxlintrc.json"
33745
- ];
33746
- const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
33747
- const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
33748
- const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
33749
- const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
33750
- const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
33751
- const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
33752
- const LINT_PHASE_TIMEOUT_MS = 3e5;
33753
- const SCAN_TOTAL_DEADLINE_MS = 9e5;
33754
- const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
33755
- const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
33756
- const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
33757
- const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
33758
- const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
33759
- const REACT_SERVER_DOM_PACKAGES = [
33760
- "react-server-dom-webpack",
33761
- "react-server-dom-parcel",
33762
- "react-server-dom-turbopack"
33763
- ];
33764
- const REACT_BLOG_RSC_ADVISORY_URL = "https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components";
33765
- const VERCEL_NEXTJS_SECURITY_RELEASE_URL = "https://vercel.com/changelog/next-js-may-2026-security-release";
33766
- const DIAGNOSTIC_CATEGORY_BUCKETS = [
33767
- "Security",
33768
- "Bugs",
33769
- "Performance",
33770
- "Accessibility",
33771
- "Maintainability"
33772
- ];
33773
- const APP_ONLY_RULE_KEYS = new Set([
33774
- "react-hooks-js/static-components",
33775
- "react-doctor/no-render-prop-children",
33776
- "react-doctor/prefer-explicit-variants"
33777
- ]);
33778
- const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
33779
- const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
33780
- const ROOT_CAUSE_GROUPABLE_RULE_KEYS = new Set([
33781
- "react-doctor/no-derived-state",
33782
- "react-doctor/no-derived-state-effect",
33783
- "react-doctor/no-derived-useState",
33784
- "react-doctor/no-adjust-state-on-prop-change",
33785
- "react-doctor/no-reset-all-state-on-prop-change"
33786
- ]);
33787
- const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
33788
- const CONFIG_CACHE_TTL_MS = 300 * 1e3;
33789
- const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
33790
- const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
33791
- const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
33792
- const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
33793
- const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
33794
- const SUPPLY_CHAIN_PLUGIN = "socket";
33795
- const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
33796
- const SUPPLY_CHAIN_CATEGORY = "Security";
33797
- const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
33798
- const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
33799
- const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
33800
33855
  const TSCONFIG_FILENAME = "tsconfig.json";
33801
33856
  const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
33802
33857
  const ensureJsonExtension = (filePath) => Path.extname(filePath) === "" ? `${filePath}.json` : filePath;
@@ -34518,7 +34573,7 @@ const tokenizeRuleList = (ruleList) => {
34518
34573
  const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34519
34574
  const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34520
34575
  const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34521
- 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}\`.`;
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}\` — add \`${ruleId}\` to the comment (e.g. \`${token}, ${ruleId}\`).`;
34522
34577
  const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
34523
34578
  const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34524
34579
  const candidates = [{
@@ -34998,6 +35053,7 @@ const collectStringSet = (values) => {
34998
35053
  if (!Array.isArray(values)) return /* @__PURE__ */ new Set();
34999
35054
  return new Set(values.filter((value) => typeof value === "string"));
35000
35055
  };
35056
+ const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
35001
35057
  /**
35002
35058
  * Pre-compiles every stateful filter and returns a single
35003
35059
  * `apply(diagnostic)` closure that runs (in order):
@@ -35031,11 +35087,28 @@ const buildDiagnosticPipeline = (input) => {
35031
35087
  const compiledOverrides = compileIgnoreOverrides(userConfig);
35032
35088
  const textComponentNames = collectStringSet(userConfig?.textComponents);
35033
35089
  const rawTextWrapperComponentNames = collectStringSet(userConfig?.rawTextWrapperComponents);
35090
+ const runtimeGlobalNames = collectStringSet(userConfig?.runtimeGlobals);
35034
35091
  const hasTextComponents = textComponentNames.size > 0;
35035
35092
  const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
35093
+ const hasRuntimeGlobals = runtimeGlobalNames.size > 0;
35036
35094
  const fileLinesCache = /* @__PURE__ */ new Map();
35037
35095
  const fileContextCache = /* @__PURE__ */ new Map();
35038
35096
  const libraryFileCache = /* @__PURE__ */ new Map();
35097
+ const suppressions = /* @__PURE__ */ new Map();
35098
+ const suppress = (diagnostic, source) => {
35099
+ const { ruleKey } = getDiagnosticRuleIdentity(diagnostic);
35100
+ const suppressionKey = `${ruleKey}\u0000${source}`;
35101
+ const existing = suppressions.get(suppressionKey);
35102
+ suppressions.set(suppressionKey, existing ? {
35103
+ ...existing,
35104
+ count: existing.count + 1
35105
+ } : {
35106
+ rule: ruleKey,
35107
+ source,
35108
+ count: 1
35109
+ });
35110
+ return null;
35111
+ };
35039
35112
  const isLibraryFile = (filePath) => {
35040
35113
  let cached = libraryFileCache.get(filePath);
35041
35114
  if (cached === void 0) {
@@ -35084,50 +35157,60 @@ const buildDiagnosticPipeline = (input) => {
35084
35157
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
35085
35158
  return false;
35086
35159
  };
35087
- return { apply: (diagnostic) => {
35088
- if (shouldAutoSuppress(diagnostic)) return null;
35089
- let current = diagnostic;
35090
- let explicitSeverityOverride;
35091
- let explicitRuleOverride;
35092
- if (severityControls) {
35093
- const { ruleKey, category } = getDiagnosticRuleIdentity(current);
35094
- explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey }, severityControls);
35095
- explicitSeverityOverride = resolveRuleSeverityOverride({
35096
- ruleKey,
35097
- category
35098
- }, severityControls);
35099
- if (explicitSeverityOverride === "off") return null;
35100
- if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
35101
- }
35102
- if (explicitRuleOverride === void 0) {
35103
- if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
35104
- }
35105
- if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
35106
- if (userConfig) {
35107
- if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return null;
35108
- if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
35109
- if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return null;
35110
- if (isRnRawTextSuppressedByConfig(current)) return null;
35111
- }
35112
- if (respectInlineDisables && current.line > 0) {
35113
- const lines = getFileLines(current.filePath);
35114
- if (lines) {
35115
- const ruleIdentifier = `${current.plugin}/${current.rule}`;
35116
- const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
35117
- if (evaluation.isSuppressed) return null;
35118
- if (evaluation.nearMissHint) current = {
35119
- ...current,
35120
- suppressionHint: evaluation.nearMissHint
35121
- };
35160
+ const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
35161
+ if (!hasRuntimeGlobals) return false;
35162
+ if (diagnostic.rule !== "jsx-no-undef") return false;
35163
+ const identifierMatch = JSX_NO_UNDEF_IDENTIFIER_PATTERN.exec(diagnostic.message);
35164
+ return identifierMatch !== null && runtimeGlobalNames.has(identifierMatch[1]);
35165
+ };
35166
+ return {
35167
+ apply: (diagnostic) => {
35168
+ if (shouldAutoSuppress(diagnostic)) return null;
35169
+ let current = diagnostic;
35170
+ let explicitSeverityOverride;
35171
+ let explicitRuleOverride;
35172
+ if (severityControls) {
35173
+ const { ruleKey, category } = getDiagnosticRuleIdentity(current);
35174
+ explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey }, severityControls);
35175
+ explicitSeverityOverride = resolveRuleSeverityOverride({
35176
+ ruleKey,
35177
+ category
35178
+ }, severityControls);
35179
+ if (explicitSeverityOverride === "off") return suppress(current, "config");
35180
+ if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
35122
35181
  }
35123
- }
35124
- const fileContext = getFileContext(current.filePath);
35125
- if (fileContext !== "production") current = {
35126
- ...current,
35127
- fileContext
35128
- };
35129
- return current;
35130
- } };
35182
+ if (explicitRuleOverride === void 0) {
35183
+ if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
35184
+ }
35185
+ if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
35186
+ if (userConfig) {
35187
+ if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return suppress(current, "config");
35188
+ if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
35189
+ if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return suppress(current, "override");
35190
+ if (isRnRawTextSuppressedByConfig(current)) return null;
35191
+ if (isJsxNoUndefSuppressedByConfig(current)) return null;
35192
+ }
35193
+ if (respectInlineDisables && current.line > 0) {
35194
+ const lines = getFileLines(current.filePath);
35195
+ if (lines) {
35196
+ const ruleIdentifier = `${current.plugin}/${current.rule}`;
35197
+ const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
35198
+ if (evaluation.isSuppressed) return suppress(current, "inline");
35199
+ if (evaluation.nearMissHint) current = {
35200
+ ...current,
35201
+ suppressionHint: evaluation.nearMissHint
35202
+ };
35203
+ }
35204
+ }
35205
+ const fileContext = getFileContext(current.filePath);
35206
+ if (fileContext !== "production") current = {
35207
+ ...current,
35208
+ fileContext
35209
+ };
35210
+ return current;
35211
+ },
35212
+ summarizeSuppressions: () => [...suppressions.values()]
35213
+ };
35131
35214
  };
35132
35215
  const OxlintUnavailableKind = Literals(["binary-not-found", "native-binding-missing"]);
35133
35216
  var OxlintUnavailable = class extends TaggedErrorClass()("OxlintUnavailable", {
@@ -35369,7 +35452,7 @@ const resolveScanConcurrency = (requested) => {
35369
35452
  if (!Number.isFinite(requested) || requested < 1) return 1;
35370
35453
  return Math.min(Math.floor(requested), 32);
35371
35454
  };
35372
- const readSystemFacts = () => ({
35455
+ const readSystemFacts$1 = () => ({
35373
35456
  availableCores: os.availableParallelism(),
35374
35457
  totalMemoryBytes: os.totalmem(),
35375
35458
  cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
@@ -35390,7 +35473,7 @@ const readSystemFacts = () => ({
35390
35473
  * `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
35391
35474
  * limited, and ceiling cases without mocking `os` or the filesystem.
35392
35475
  */
35393
- const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
35476
+ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
35394
35477
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
35395
35478
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
35396
35479
  return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
@@ -35431,13 +35514,7 @@ var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { de
35431
35514
  * without recompiling react-doctor. Tests override via
35432
35515
  * `Layer.succeed(SupplyChainOverlapTimeoutMs, ...)`.
35433
35516
  */
35434
- var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => {
35435
- const raw = process.env["REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS"];
35436
- if (raw === void 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35437
- const parsed = Number(raw);
35438
- if (!Number.isFinite(parsed) || parsed <= 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35439
- return parsed;
35440
- } }) {};
35517
+ var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS", SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS) }) {};
35441
35518
  /**
35442
35519
  * Hard cap on combined stdout+stderr bytes per oxlint batch. The
35443
35520
  * subprocess gets SIGKILL'd if it produces more; the recovery path
@@ -35585,6 +35662,7 @@ const STRING_ARRAY_FIELD_NAMES = [
35585
35662
  "projects",
35586
35663
  "textComponents",
35587
35664
  "rawTextWrapperComponents",
35665
+ "runtimeGlobals",
35588
35666
  "serverAuthFunctionNames"
35589
35667
  ];
35590
35668
  const SURFACE_CONTROL_FIELD_NAMES = [
@@ -36721,14 +36799,17 @@ const checkReactServerComponentsAdvisory = (scanDirectory, project) => {
36721
36799
  return diagnostics;
36722
36800
  };
36723
36801
  const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
36724
- const REDUCED_MOTION_FILE_GLOBS = [
36725
- "*.ts",
36726
- "*.tsx",
36727
- "*.js",
36728
- "*.jsx",
36729
- "*.css",
36730
- "*.scss"
36731
- ];
36802
+ const REDUCED_MOTION_PATTERN = new RegExp(REDUCED_MOTION_GREP_PATTERN);
36803
+ const REDUCED_MOTION_FILE_EXTENSIONS = new Set([
36804
+ ".ts",
36805
+ ".tsx",
36806
+ ".js",
36807
+ ".jsx",
36808
+ ".css",
36809
+ ".scss"
36810
+ ]);
36811
+ const REDUCED_MOTION_FILE_GLOBS = [...REDUCED_MOTION_FILE_EXTENSIONS].map((extension) => `*${extension}`);
36812
+ const GIT_GREP_NO_MATCH_STATUS = 1;
36732
36813
  const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
36733
36814
  filePath: "package.json",
36734
36815
  plugin: "react-doctor",
@@ -36740,6 +36821,19 @@ const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
36740
36821
  column: 0,
36741
36822
  category: "Accessibility"
36742
36823
  };
36824
+ const hasReducedMotionHandlingViaFilesystem = (rootDirectory) => {
36825
+ for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
36826
+ if (!REDUCED_MOTION_FILE_EXTENSIONS.has(Path.extname(name))) continue;
36827
+ let content;
36828
+ try {
36829
+ content = NFS.readFileSync(absolutePath, "utf-8");
36830
+ } catch {
36831
+ continue;
36832
+ }
36833
+ if (REDUCED_MOTION_PATTERN.test(content)) return true;
36834
+ }
36835
+ return false;
36836
+ };
36743
36837
  const checkReducedMotion = (rootDirectory) => {
36744
36838
  const packageJsonPath = Path.join(rootDirectory, "package.json");
36745
36839
  if (!isFile(packageJsonPath)) return [];
@@ -36758,22 +36852,21 @@ const checkReducedMotion = (rootDirectory) => {
36758
36852
  const result = spawnSync("git", [
36759
36853
  "grep",
36760
36854
  "--untracked",
36761
- "-ql",
36762
- "-E",
36855
+ "-lE",
36763
36856
  REDUCED_MOTION_GREP_PATTERN,
36764
36857
  "--",
36765
36858
  ...REDUCED_MOTION_FILE_GLOBS
36766
36859
  ], {
36767
36860
  cwd: rootDirectory,
36861
+ encoding: "utf-8",
36768
36862
  stdio: [
36769
36863
  "ignore",
36770
36864
  "pipe",
36771
36865
  "pipe"
36772
36866
  ]
36773
36867
  });
36774
- if (result.error) return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
36775
- if (result.status === 0) return [];
36776
- return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
36868
+ if (!(!result.error && (result.status === 0 || result.status === GIT_GREP_NO_MATCH_STATUS))) return hasReducedMotionHandlingViaFilesystem(rootDirectory) ? [] : [MISSING_REDUCED_MOTION_DIAGNOSTIC];
36869
+ return result.status === 0 && result.stdout.split("\n").some((filePath) => filePath.length > 0 && !hasIgnoredPathSegment(filePath)) ? [] : [MISSING_REDUCED_MOTION_DIAGNOSTIC];
36777
36870
  };
36778
36871
  const buildSecurityScanDiagnostic = (finding, entry, relativePath) => ({
36779
36872
  filePath: relativePath,
@@ -36923,7 +37016,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
36923
37016
  }
36924
37017
  return true;
36925
37018
  };
36926
- const checkSecurityScan = (rootDirectory, options = {}) => {
37019
+ const yieldToEventLoop = () => new Promise((resolve) => {
37020
+ setImmediate(resolve);
37021
+ });
37022
+ const createSecurityScanSession = (rootDirectory, options) => {
36927
37023
  const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
36928
37024
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
36929
37025
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
@@ -36938,7 +37034,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36938
37034
  committedFilesOnly: rule.committedFilesOnly === true
36939
37035
  }];
36940
37036
  });
36941
- if (enabledScanRules.length === 0) return [];
37037
+ if (enabledScanRules.length === 0) return null;
36942
37038
  const diagnostics = [];
36943
37039
  const seen = /* @__PURE__ */ new Set();
36944
37040
  const gitIgnoredCache = /* @__PURE__ */ new Map();
@@ -36950,15 +37046,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36950
37046
  }
36951
37047
  return status === true;
36952
37048
  };
36953
- for (const file of collectSecurityScanFiles(rootDirectory)) for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36954
- if (committedFilesOnly && isFileGitIgnored(file)) continue;
36955
- const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36956
- const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36957
- if (seen.has(key)) continue;
36958
- seen.add(key);
36959
- diagnostics.push(diagnostic);
37049
+ const scanFile = (file) => {
37050
+ for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
37051
+ if (committedFilesOnly && isFileGitIgnored(file)) continue;
37052
+ const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
37053
+ const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
37054
+ if (seen.has(key)) continue;
37055
+ seen.add(key);
37056
+ diagnostics.push(diagnostic);
37057
+ }
37058
+ };
37059
+ return {
37060
+ scanFile,
37061
+ diagnostics
37062
+ };
37063
+ };
37064
+ const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
37065
+ const session = createSecurityScanSession(rootDirectory, options);
37066
+ if (session === null) return [];
37067
+ let filesSinceYield = 0;
37068
+ for (const file of collectSecurityScanFiles(rootDirectory)) {
37069
+ session.scanFile(file);
37070
+ filesSinceYield += 1;
37071
+ if (filesSinceYield >= 16) {
37072
+ filesSinceYield = 0;
37073
+ await yieldToEventLoop();
37074
+ }
36960
37075
  }
36961
- return diagnostics;
37076
+ return session.diagnostics;
36962
37077
  };
36963
37078
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
36964
37079
  let p = process || {}, argv = p.argv || [], env = p.env || {};
@@ -37175,6 +37290,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
37175
37290
  return [...seen].filter((pattern) => pattern.length > 0);
37176
37291
  };
37177
37292
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
37293
+ const readSystemFacts = () => ({
37294
+ availableCores: os.availableParallelism(),
37295
+ totalMemoryBytes: os.totalmem(),
37296
+ cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
37297
+ });
37298
+ /**
37299
+ * How many real deslop dead-code child processes may run at once, across the
37300
+ * concurrent per-project `runInspect` fibers of one CLI run. The cap is the
37301
+ * smaller of the core count and the number of `DEAD_CODE_WORKER_MEM_BUDGET_BYTES`
37302
+ * workers that fit in available memory, floored at 1.
37303
+ *
37304
+ * On a roomy dev box / CI runner this resolves high enough that every
37305
+ * concurrently-scanned project still spawns its own worker (no serialization vs
37306
+ * the prior uncapped behavior); on a memory-constrained runner it collapses
37307
+ * toward 1, so the `withDeadCodeWorkerSlot` semaphore serializes the spawns
37308
+ * instead of oversubscribing memory with N simultaneous children — the global
37309
+ * cap the per-project spawn path lacked.
37310
+ *
37311
+ * Mirrors `resolveAutoScanConcurrency` (lint), but budgets memory per the
37312
+ * heavier dead-code worker. `facts` is injectable for tests.
37313
+ */
37314
+ const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
37315
+ const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
37316
+ const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
37317
+ return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
37318
+ };
37319
+ let availableSlots = -1;
37320
+ const waiters = [];
37321
+ const releaseSlot = () => {
37322
+ const nextWaiter = waiters.shift();
37323
+ if (nextWaiter !== void 0) nextWaiter();
37324
+ else availableSlots += 1;
37325
+ };
37326
+ /**
37327
+ * Runs `task` once a dead-code worker slot is free, releasing the slot when the
37328
+ * task settles (success or failure). With a high cap (roomy machine) every
37329
+ * caller proceeds immediately; with a low cap (constrained runner) callers
37330
+ * queue and run as slots free.
37331
+ *
37332
+ * `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
37333
+ * this caller is queued, the call rejects without acquiring a slot or running
37334
+ * `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
37335
+ * then spawn a child only to tear it down. A queued caller that aborts removes
37336
+ * its own waiter so a later release never hands a slot to a dead request.
37337
+ */
37338
+ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
37339
+ if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
37340
+ if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
37341
+ if (availableSlots > 0) availableSlots -= 1;
37342
+ else await new Promise((resolve, reject) => {
37343
+ const waiter = () => {
37344
+ abortSignal?.removeEventListener("abort", onAbort);
37345
+ resolve();
37346
+ };
37347
+ const onAbort = () => {
37348
+ const queuedIndex = waiters.indexOf(waiter);
37349
+ if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
37350
+ reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
37351
+ };
37352
+ waiters.push(waiter);
37353
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
37354
+ });
37355
+ try {
37356
+ return await task();
37357
+ } finally {
37358
+ releaseSlot();
37359
+ }
37360
+ };
37178
37361
  /**
37179
37362
  * Resolves a path to its canonical, symlink-free form, falling back to
37180
37363
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -37199,6 +37382,11 @@ const toCanonicalPath = (filePath) => {
37199
37382
  };
37200
37383
  const DEAD_CODE_PLUGIN = "deslop";
37201
37384
  const DEAD_CODE_CATEGORY = "Maintainability";
37385
+ const REACT_DOCTOR_TOOLCHAIN_PACKAGES = new Set([
37386
+ "react-doctor",
37387
+ "eslint-plugin-react-doctor",
37388
+ "oxlint-plugin-react-doctor"
37389
+ ]);
37202
37390
  const DEAD_CODE_WORKER_SCRIPT = `
37203
37391
  const inputChunks = [];
37204
37392
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -37465,14 +37653,17 @@ const checkDeadCode = async (options) => {
37465
37653
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37466
37654
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37467
37655
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37468
- const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37469
- rootDirectory,
37470
- entryPatterns,
37471
- tsConfigPath: resolveTsConfigPath(rootDirectory),
37472
- ignorePatterns,
37473
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37474
- parseConcurrency: options.parseConcurrency
37475
- }), options.workerTimeoutMs ?? 12e4, options.abortSignal));
37656
+ const spawnAndRun = () => {
37657
+ return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37658
+ rootDirectory,
37659
+ entryPatterns,
37660
+ tsConfigPath: resolveTsConfigPath(rootDirectory),
37661
+ ignorePatterns,
37662
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37663
+ parseConcurrency: options.parseConcurrency
37664
+ }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
37665
+ };
37666
+ const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
37476
37667
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
37477
37668
  const diagnostics = [];
37478
37669
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -37501,6 +37692,7 @@ const checkDeadCode = async (options) => {
37501
37692
  });
37502
37693
  }
37503
37694
  for (const unusedDependency of result.unusedDependencies) {
37695
+ if (REACT_DOCTOR_TOOLCHAIN_PACKAGES.has(unusedDependency.name)) continue;
37504
37696
  const label = unusedDependency.isDevDependency ? "devDependency" : "dependency";
37505
37697
  diagnostics.push({
37506
37698
  filePath: "package.json",
@@ -37570,6 +37762,7 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
37570
37762
  return true;
37571
37763
  };
37572
37764
  const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
37765
+ const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs - Date.now(), 0);
37573
37766
  /**
37574
37767
  * Budget for the dead-code phase, scaled to the work. deslop's graph build is
37575
37768
  * CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
@@ -37588,53 +37781,6 @@ const resolveDeadCodeTimeout = (input) => {
37588
37781
  phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
37589
37782
  };
37590
37783
  };
37591
- const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
37592
- const entries = [];
37593
- for (const relativePath of relativePaths) {
37594
- const absolutePath = Path.resolve(rootDirectory, relativePath);
37595
- const sizeBytes = statSourceFileSize(absolutePath);
37596
- if (isLargeMinifiedFile(absolutePath, sizeBytes)) continue;
37597
- entries.push({
37598
- path: relativePath,
37599
- sizeBytes: sizeBytes ?? 0
37600
- });
37601
- }
37602
- return entries;
37603
- };
37604
- const listSourceFilesViaGit = (rootDirectory) => {
37605
- const result = spawnSync("git", [
37606
- "ls-files",
37607
- "-z",
37608
- "--cached",
37609
- "--others",
37610
- "--exclude-standard"
37611
- ], {
37612
- cwd: rootDirectory,
37613
- encoding: "utf-8",
37614
- maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
37615
- });
37616
- if (result.error || result.status !== 0) return null;
37617
- return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath));
37618
- };
37619
- const listSourceFilesViaFilesystem = (rootDirectory) => {
37620
- const filePaths = [];
37621
- const stack = [rootDirectory];
37622
- while (stack.length > 0) {
37623
- const currentDirectory = stack.pop();
37624
- const entries = readDirectoryEntries(currentDirectory);
37625
- for (const entry of entries) {
37626
- const absolutePath = Path.join(currentDirectory, entry.name);
37627
- if (entry.isDirectory()) {
37628
- if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
37629
- continue;
37630
- }
37631
- if (entry.isFile() && isLintableSourceFile(entry.name)) filePaths.push(Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
37632
- }
37633
- }
37634
- return filePaths;
37635
- };
37636
- const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
37637
- const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
37638
37784
  const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
37639
37785
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
37640
37786
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
@@ -37679,7 +37825,6 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37679
37825
  return yield* tryPromise({
37680
37826
  try: (signal) => checkDeadCode({
37681
37827
  rootDirectory: input.rootDirectory,
37682
- userConfig: input.userConfig,
37683
37828
  parseConcurrency: input.parseConcurrency,
37684
37829
  workerTimeoutMs: input.workerTimeoutMs,
37685
37830
  abortSignal: signal
@@ -37802,6 +37947,11 @@ const isSafeGitRevision = (candidate) => {
37802
37947
  if (candidate.includes("..") || candidate.includes("@{")) return false;
37803
37948
  return SAFE_GIT_REVISION_PATTERN.test(candidate);
37804
37949
  };
37950
+ const resolveSpawnArgsLengthCap = () => {
37951
+ if (process.platform === "win32") return SPAWN_ARGS_MAX_LENGTH_CHARS;
37952
+ if (process.platform === "darwin") return SPAWN_ARGS_MAX_LENGTH_CHARS_DARWIN;
37953
+ return SPAWN_ARGS_MAX_LENGTH_CHARS_POSIX;
37954
+ };
37805
37955
  /**
37806
37956
  * Splits a git revision range into its endpoints: three-dot `A...B`
37807
37957
  * (symmetric, merge-base) or two-dot `A..B` (direct). Returns `null`
@@ -37886,7 +38036,8 @@ var Git = class Git extends Service()("react-doctor/Git") {
37886
38036
  return scoped(gen(function* () {
37887
38037
  if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
37888
38038
  const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
37889
- if (argvLengthChars > 24e3) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${SPAWN_ARGS_MAX_LENGTH_CHARS})`);
38039
+ const spawnArgsLengthCap = resolveSpawnArgsLengthCap();
38040
+ if (argvLengthChars > spawnArgsLengthCap) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${spawnArgsLengthCap})`);
37890
38041
  const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37891
38042
  cwd: input.directory,
37892
38043
  env: input.env,
@@ -38055,6 +38206,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38055
38206
  if (resolvedCurrentBranch === null && explicitBaseBranch === void 0) return null;
38056
38207
  const baseBranch = explicitBaseBranch ?? (yield* defaultBranch(directory));
38057
38208
  if (baseBranch === null) return null;
38209
+ if (!isSafeGitRevision(baseBranch)) return null;
38058
38210
  if (explicitBaseBranch !== void 0) {
38059
38211
  if (!(yield* branchExists(directory, explicitBaseBranch))) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchMissing({ branch: explicitBaseBranch }) }));
38060
38212
  }
@@ -38325,12 +38477,12 @@ const findFilesWithDisableDirectivesViaGit = async (rootDirectory, includePaths)
38325
38477
  return null;
38326
38478
  }
38327
38479
  if (grepResult === null) return null;
38328
- return grepResult.stdout.split("\n").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath));
38480
+ return grepResult.stdout.split("\n").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath));
38329
38481
  };
38330
38482
  const findFilesWithDisableDirectivesViaFilesystem = (rootDirectory, includePaths) => {
38331
38483
  const matches = [];
38332
38484
  const checkFile = (relativePath) => {
38333
- if (!isLintableSourceFile(relativePath)) return;
38485
+ if (!isLintableSourceFile(relativePath) || hasIgnoredPathSegment(relativePath)) return;
38334
38486
  const absolutePath = Path.join(rootDirectory, relativePath);
38335
38487
  let content;
38336
38488
  try {
@@ -38344,22 +38496,7 @@ const findFilesWithDisableDirectivesViaFilesystem = (rootDirectory, includePaths
38344
38496
  for (const candidate of includePaths) checkFile(candidate);
38345
38497
  return matches;
38346
38498
  }
38347
- const stack = [rootDirectory];
38348
- while (stack.length > 0) {
38349
- const current = stack.pop();
38350
- if (current === void 0) continue;
38351
- const entries = readDirectoryEntries(current);
38352
- for (const entry of entries) {
38353
- if (entry.isDirectory()) {
38354
- if (entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
38355
- stack.push(Path.join(current, entry.name));
38356
- continue;
38357
- }
38358
- if (!entry.isFile()) continue;
38359
- const absolute = Path.join(current, entry.name);
38360
- checkFile(Path.relative(rootDirectory, absolute));
38361
- }
38362
- }
38499
+ for (const { absolutePath } of walkSourceTreeFiles(rootDirectory)) checkFile(Path.relative(rootDirectory, absolutePath));
38363
38500
  return matches;
38364
38501
  };
38365
38502
  const findFilesWithDisableDirectives = async (rootDirectory, includePaths) => await findFilesWithDisableDirectivesViaGit(rootDirectory, includePaths) ?? findFilesWithDisableDirectivesViaFilesystem(rootDirectory, includePaths);
@@ -38405,7 +38542,7 @@ const normalizeConfigForHash = (config) => {
38405
38542
  if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
38406
38543
  return clone;
38407
38544
  };
38408
- 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");
38545
+ 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");
38409
38546
  /**
38410
38547
  * Loads a plugin module via the local require resolver and extracts
38411
38548
  * `(name, ruleNames)` from either `module.exports.meta + rules` or
@@ -38576,12 +38713,12 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38576
38713
  if (customRulesOnly && registryEntry.originallyExternal) continue;
38577
38714
  if (rule.framework !== "global" && !rule.requires) continue;
38578
38715
  if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) continue;
38579
- const explicitSeverity = resolveRuleSeverityOverride({
38716
+ const explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey: registryEntry.key }, severityControls);
38717
+ if (rule.defaultEnabled === false && explicitRuleOverride === void 0) continue;
38718
+ const severity = resolveRuleSeverityOverride({
38580
38719
  ruleKey: registryEntry.key,
38581
38720
  category: rule.category
38582
- }, severityControls);
38583
- if (rule.defaultEnabled === false && explicitSeverity === void 0) continue;
38584
- const severity = explicitSeverity ?? resolveCompilerCleanupBucketSeverity(registryEntry.key, severityControls) ?? rule.severity;
38721
+ }, severityControls) ?? resolveCompilerCleanupBucketSeverity(registryEntry.key, severityControls) ?? rule.severity;
38585
38722
  if (severity === "off") continue;
38586
38723
  enabledReactDoctorRules[registryEntry.key] = severity;
38587
38724
  }
@@ -38683,24 +38820,38 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38683
38820
  updatedAtMs: Date.now(),
38684
38821
  files: Object.fromEntries(cappedEntries)
38685
38822
  };
38686
- const keptHashes = Object.entries(rulesets).sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8).map(([hash]) => hash);
38687
- const prunedRulesets = {};
38688
- for (const hash of keptHashes) prunedRulesets[hash] = rulesets[hash];
38689
38823
  atomicWriteJson(cacheFilePath, {
38690
38824
  version: 1,
38691
- rulesets: prunedRulesets
38825
+ rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
38692
38826
  });
38693
38827
  }
38694
38828
  };
38695
38829
  };
38696
38830
  const bundledRequire = createRequire(import.meta.url);
38831
+ const childNodeVersionByBinaryPath = /* @__PURE__ */ new Map();
38832
+ const resolveChildNodeVersion = (nodeBinaryPath) => {
38833
+ if (nodeBinaryPath === process.execPath) return process.version;
38834
+ const cachedVersion = childNodeVersionByBinaryPath.get(nodeBinaryPath);
38835
+ if (cachedVersion !== void 0) return cachedVersion;
38836
+ let version;
38837
+ try {
38838
+ version = execFileSync(nodeBinaryPath, ["--version"], {
38839
+ encoding: "utf8",
38840
+ timeout: NODE_VERSION_PROBE_TIMEOUT_MS
38841
+ }).trim();
38842
+ } catch {
38843
+ version = `unknown:${nodeBinaryPath}`;
38844
+ }
38845
+ childNodeVersionByBinaryPath.set(nodeBinaryPath, version);
38846
+ return version;
38847
+ };
38697
38848
  const TOOLCHAIN_PACKAGE_SPECIFIERS = [
38698
38849
  "oxlint/package.json",
38699
38850
  "oxlint-plugin-react-doctor/package.json",
38700
38851
  "eslint-plugin-react-hooks/package.json"
38701
38852
  ];
38702
- const resolveOxlintToolchainVersions = () => {
38703
- const versions = [`node=${process.version}`];
38853
+ const resolveOxlintToolchainVersions = (nodeBinaryPath = process.execPath) => {
38854
+ const versions = [`node=${resolveChildNodeVersion(nodeBinaryPath)}`];
38704
38855
  for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
38705
38856
  const packageJson = bundledRequire(specifier);
38706
38857
  const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
@@ -39234,11 +39385,12 @@ const REACT_COMPILER_TITLE = "React Compiler can't optimize this";
39234
39385
  const REACT_COMPILER_TODO_TITLE = "React Compiler doesn't support this syntax";
39235
39386
  const REACT_COMPILER_IMPACT = "This component misses React Compiler's automatic memoization & re-renders more than it should";
39236
39387
  const REACT_COMPILER_ACTION = "Rewrite the flagged code so the compiler can optimize it.";
39388
+ 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`.";
39237
39389
  const REACT_COMPILER_GENERIC_MESSAGE = `${REACT_COMPILER_IMPACT}. ${REACT_COMPILER_ACTION}`;
39238
- const buildReactCompilerMessage = (reasonSummary) => {
39390
+ const buildReactCompilerMessage = (reasonSummary, action = REACT_COMPILER_ACTION) => {
39239
39391
  const normalizedSummary = reasonSummary.replace(TRAILING_PERIOD_PATTERN, "");
39240
- if (!normalizedSummary) return REACT_COMPILER_GENERIC_MESSAGE;
39241
- return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${REACT_COMPILER_ACTION}`;
39392
+ if (!normalizedSummary) return `${REACT_COMPILER_IMPACT}. ${action}`;
39393
+ return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${action}`;
39242
39394
  };
39243
39395
  const PLUGIN_CATEGORY_MAP = {
39244
39396
  react: "Bugs",
@@ -39287,7 +39439,7 @@ const resolveCleanedDiagnostic = (message, help, plugin, rule, project) => {
39287
39439
  const [reasonSummary = "", ...reasonDetailLines] = bailoutReason.split("\n");
39288
39440
  const reasonDetail = reasonDetailLines.join("\n").trim();
39289
39441
  return {
39290
- message: buildReactCompilerMessage(reasonSummary.trim()),
39442
+ message: buildReactCompilerMessage(reasonSummary.trim(), rule === "incompatible-library" ? REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION : REACT_COMPILER_ACTION),
39291
39443
  help: appendReanimatedSharedValueHint(reasonDetail || help, rule, project)
39292
39444
  };
39293
39445
  }
@@ -39548,28 +39700,46 @@ const isParallelismRelatedSpawnError = (error) => {
39548
39700
  * loop with a slimmer config in that case.
39549
39701
  */
39550
39702
  const spawnLintBatches = async (input) => {
39551
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal } = input;
39703
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal, deadlineEpochMs } = input;
39552
39704
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
39553
39705
  const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
39554
39706
  const runBatchPass = async (concurrency) => {
39555
39707
  const allDiagnostics = [];
39556
39708
  const droppedFiles = [];
39709
+ const deadlineSkippedFiles = [];
39710
+ const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0;
39557
39711
  let firstDropReason = null;
39558
- const splitDeadlineMs = Date.now() + splitTotalBudgetMs;
39559
- const spawnLintBatch = async (batch, depth) => {
39712
+ const spawnLintBatch = async (batch, depth, batchState) => {
39713
+ if (isPastDeadline()) {
39714
+ deadlineSkippedFiles.push(...batch);
39715
+ batchState.deadlineSkippedFileCount += batch.length;
39716
+ return [];
39717
+ }
39560
39718
  const batchArgs = [...baseArgs, ...batch];
39561
39719
  try {
39562
39720
  return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
39563
39721
  } catch (error) {
39564
39722
  if (!isSplittableReactDoctorError(error)) throw error;
39565
- const splitBudgetExhausted = Date.now() >= splitDeadlineMs || depth >= splitMaxDepth;
39566
- if (batch.length <= 1 || splitBudgetExhausted) {
39723
+ if (isPastDeadline()) {
39724
+ deadlineSkippedFiles.push(...batch);
39725
+ batchState.deadlineSkippedFileCount += batch.length;
39726
+ return [];
39727
+ }
39728
+ batchState.deadlineMs ??= Date.now() + splitTotalBudgetMs;
39729
+ const isBudgetElapsed = Date.now() >= batchState.deadlineMs;
39730
+ const isDepthCapReached = depth >= splitMaxDepth;
39731
+ if (batch.length <= 1 || isBudgetElapsed || isDepthCapReached) {
39567
39732
  droppedFiles.push(...batch);
39568
- if (firstDropReason === null) firstDropReason = splitBudgetExhausted && batch.length > 1 ? `${error.message} (split budget exhausted after ${splitMaxDepth} levels / ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s)` : error.message;
39733
+ if (firstDropReason === null) {
39734
+ let limitHint = "";
39735
+ if (isDepthCapReached) limitHint = ` (split depth cap of ${splitMaxDepth} levels reached)`;
39736
+ else if (isBudgetElapsed) limitHint = ` (split budget of ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s exhausted at depth ${depth})`;
39737
+ firstDropReason = batch.length > 1 ? `${error.message}${limitHint}` : error.message;
39738
+ }
39569
39739
  return [];
39570
39740
  }
39571
39741
  const splitIndex = Math.ceil(batch.length / 2);
39572
- return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1)];
39742
+ return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1, batchState), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1, batchState)];
39573
39743
  }
39574
39744
  };
39575
39745
  let startedFileCount = 0;
@@ -39585,9 +39755,17 @@ const spawnLintBatches = async (input) => {
39585
39755
  progressTimer?.unref?.();
39586
39756
  try {
39587
39757
  const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
39758
+ if (isPastDeadline()) {
39759
+ deadlineSkippedFiles.push(...batch);
39760
+ return [];
39761
+ }
39588
39762
  startedFileCount += batch.length;
39589
- const batchDiagnostics = await spawnLintBatch(batch, 0);
39590
- scannedFileCount += batch.length;
39763
+ const batchState = {
39764
+ deadlineMs: null,
39765
+ deadlineSkippedFileCount: 0
39766
+ };
39767
+ const batchDiagnostics = await spawnLintBatch(batch, 0, batchState);
39768
+ scannedFileCount += batch.length - batchState.deadlineSkippedFileCount;
39591
39769
  if (onFileProgress) {
39592
39770
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
39593
39771
  onFileProgress(displayedFileCount, totalFileCount);
@@ -39598,12 +39776,13 @@ const spawnLintBatches = async (input) => {
39598
39776
  } finally {
39599
39777
  if (progressTimer !== null) clearInterval(progressTimer);
39600
39778
  }
39601
- if (droppedFiles.length > 0 && onPartialFailure) {
39602
- const previewFiles = droppedFiles.slice(0, 3).join(", ");
39603
- const remainderHint = droppedFiles.length > 3 ? `, +${droppedFiles.length - 3} more` : "";
39604
- const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
39605
- onPartialFailure(`${droppedFiles.length} file(s) failed to lint and were skipped (${previewFiles}${remainderHint})${reasonHint}`);
39606
- }
39779
+ const reportSkippedFiles = (skippedFiles, buildMessage) => {
39780
+ if (skippedFiles.length === 0 || !onPartialFailure) return;
39781
+ onPartialFailure(buildMessage(`${skippedFiles.slice(0, 3).join(", ")}${skippedFiles.length > 3 ? `, +${skippedFiles.length - 3} more` : ""}`));
39782
+ };
39783
+ const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
39784
+ reportSkippedFiles(droppedFiles, (fileListPreview) => `${droppedFiles.length} file(s) failed to lint and were skipped (${fileListPreview})${reasonHint}`);
39785
+ reportSkippedFiles(deadlineSkippedFiles, (fileListPreview) => `${deadlineSkippedFiles.length} file(s) skipped — max scan duration reached before they were linted (${fileListPreview})`);
39607
39786
  return allDiagnostics;
39608
39787
  };
39609
39788
  let diagnostics;
@@ -39660,7 +39839,14 @@ const resolveReactDoctorCacheDir = (projectDirectory) => {
39660
39839
  if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
39661
39840
  const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
39662
39841
  if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
39663
- return Path.join(os.tmpdir(), "react-doctor-cache", projectCacheSubdir(projectDirectory));
39842
+ return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
39843
+ };
39844
+ const resolveUserCacheScope = () => {
39845
+ try {
39846
+ return String(process.getuid?.() ?? os.userInfo().username);
39847
+ } catch {
39848
+ return "shared";
39849
+ }
39664
39850
  };
39665
39851
  const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
39666
39852
  /**
@@ -39800,7 +39986,8 @@ const runOxlint = async (options) => {
39800
39986
  spawnTimeoutMs,
39801
39987
  outputMaxBytes,
39802
39988
  concurrency: options.concurrency,
39803
- signal: options.signal
39989
+ signal: options.signal,
39990
+ deadlineEpochMs: options.deadlineEpochMs
39804
39991
  });
39805
39992
  writeOxlintConfig(passConfigPath, buildConfigForPass({}));
39806
39993
  try {
@@ -39828,7 +40015,7 @@ const runOxlint = async (options) => {
39828
40015
  extendsPaths: [],
39829
40016
  ruleSelection: "cacheable"
39830
40017
  }),
39831
- toolchainVersions: resolveOxlintToolchainVersions(),
40018
+ toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
39832
40019
  ignorePatterns: combinedPatterns,
39833
40020
  tsconfigContent
39834
40021
  });
@@ -39842,7 +40029,7 @@ const runOxlint = async (options) => {
39842
40029
  missFiles.push(candidateFile);
39843
40030
  continue;
39844
40031
  }
39845
- const cacheKey = `${candidateFile.replaceAll("\\", "/")}${contentHash}`;
40032
+ const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
39846
40033
  cacheKeyByFile.set(candidateFile, cacheKey);
39847
40034
  const cachedDiagnostics = cache.lookup(cacheKey);
39848
40035
  if (cachedDiagnostics === null) missFiles.push(candidateFile);
@@ -39899,7 +40086,8 @@ const runOxlint = async (options) => {
39899
40086
  spawnTimeoutMs,
39900
40087
  outputMaxBytes,
39901
40088
  concurrency: options.concurrency,
39902
- signal: options.signal
40089
+ signal: options.signal,
40090
+ deadlineEpochMs: options.deadlineEpochMs
39903
40091
  });
39904
40092
  writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
39905
40093
  try {
@@ -40003,7 +40191,8 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
40003
40191
  outputMaxBytes,
40004
40192
  concurrency,
40005
40193
  signal,
40006
- lintBatchOrdering
40194
+ lintBatchOrdering,
40195
+ deadlineEpochMs: input.deadlineEpochMs
40007
40196
  }),
40008
40197
  catch: ensureReactDoctorError
40009
40198
  });
@@ -40158,7 +40347,7 @@ const ScoreApiResponseSchema = Struct({
40158
40347
  })))
40159
40348
  });
40160
40349
  const parseScoreResult = (value) => getOrNull(decodeUnknownOption(ScoreApiResponseSchema)(value));
40161
- const stripLocalFileFields = (diagnostics) => diagnostics.map(({ filePath: _filePath, fileContext: _fileContext, ...rest }) => rest);
40350
+ const stripLocalFileFields = (diagnostics) => diagnostics.map(({ filePath: _filePath, fileContext: _fileContext, fixGroupId: _fixGroupId, ...rest }) => rest);
40162
40351
  const isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
40163
40352
  const describeFailure = (error) => {
40164
40353
  if (isAbortError(error)) return `timed out after ${FETCH_TIMEOUT_MS / 1e3}s`;
@@ -40462,7 +40651,10 @@ const ALERT_SEVERITY_RANK = {
40462
40651
  medium: 2,
40463
40652
  low: 1
40464
40653
  };
40465
- const severityRank = (severity) => ALERT_SEVERITY_RANK[severity.toLowerCase()] ?? 0;
40654
+ const severityRank = (severity) => {
40655
+ const normalized = severity.toLowerCase();
40656
+ return Object.hasOwn(ALERT_SEVERITY_RANK, normalized) ? ALERT_SEVERITY_RANK[normalized] : 0;
40657
+ };
40466
40658
  const displaySeverity = (severity) => {
40467
40659
  const normalized = sanitizeTerminalText(severity.toLowerCase());
40468
40660
  return normalized === "middle" ? "medium" : normalized;
@@ -40479,7 +40671,7 @@ const ALERT_TYPE_LABELS = {
40479
40671
  typosquat: "possible typosquat"
40480
40672
  };
40481
40673
  const humanizeAlertType = (type) => type.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase().trim();
40482
- const friendlyAlertType = (type) => ALERT_TYPE_LABELS[type] ?? sanitizeTerminalText(humanizeAlertType(type));
40674
+ const friendlyAlertType = (type) => Object.hasOwn(ALERT_TYPE_LABELS, type) ? ALERT_TYPE_LABELS[type] : sanitizeTerminalText(humanizeAlertType(type));
40483
40675
  const summarizeAlertNote = (note) => {
40484
40676
  const collapsed = sanitizeTerminalText(note.replace(/\s+/g, " ").trim());
40485
40677
  const firstSentence = collapsed.split(/(?<=\.)\s/)[0] || collapsed;
@@ -40574,7 +40766,10 @@ const checkSupplyChain = (input) => gen(function* () {
40574
40766
  * outage.
40575
40767
  */
40576
40768
  var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
40577
- static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(checkSupplyChain(input).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
40769
+ static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(flatMap$2(SupplyChainOverlapTimeoutMs, (totalTimeoutMs) => checkSupplyChain({
40770
+ ...input,
40771
+ totalTimeoutMs
40772
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
40578
40773
  static layerOf = (diagnostics) => succeed$3(SupplyChain, SupplyChain.of({ run: () => fromIterable$1(diagnostics) }));
40579
40774
  };
40580
40775
  const getObjectProperty = (value, propertyName) => {
@@ -40636,7 +40831,10 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40636
40831
  * diagnostics).
40637
40832
  * 2. beforeLint hook (e.g. CLI renders the project-detection block)
40638
40833
  * 3. environment checks (reduced-motion + pnpm hardening +
40639
- * expo/react-native + security scan), collected synchronously
40834
+ * expo/react-native), collected synchronously. The heavier
40835
+ * content-regex security scan is forked instead (like supply-chain
40836
+ * below) and joined before the concat, so its CPU overlaps lint
40837
+ * rather than blocking the event loop before it.
40640
40838
  * 4. The supply-chain check (Socket.dev) is forked onto a background
40641
40839
  * fiber so its ~100% network-bound time overlaps the ~100%
40642
40840
  * CPU/subprocess-bound lint pass below, collapsing two serial
@@ -40656,7 +40854,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40656
40854
  * order, so terminal output is identical either way; supply-chain
40657
40855
  * rides alongside without a spinner.
40658
40856
  * 6. Join the supply-chain fiber, then assemble the diagnostics in a
40659
- * FIXED order (env, supply-chain, lint, dead-code) so the output is
40857
+ * FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
40660
40858
  * byte-identical regardless of which fiber settled first. The
40661
40859
  * viewer-permission fiber is joined later, during score-metadata
40662
40860
  * assembly (it feeds score metadata, not diagnostics). The per-element
@@ -40717,12 +40915,13 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40717
40915
  ...checkPnpmHardening(scanDirectory),
40718
40916
  ...checkReactServerComponentsAdvisory(scanDirectory, project),
40719
40917
  ...checkExpoProject(scanDirectory, project),
40720
- ...checkReactNativeProject(scanDirectory, project),
40721
- ...checkSecurityScan(scanDirectory, {
40722
- project,
40723
- ignoredTags: input.ignoredTags
40724
- })
40918
+ ...checkReactNativeProject(scanDirectory, project)
40725
40919
  ])));
40920
+ const securityScanFailedRef = yield* make$13(false);
40921
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise(() => checkSecurityScanCooperative(scanDirectory, {
40922
+ project,
40923
+ ignoredTags: input.ignoredTags
40924
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), catch_$1(() => set(securityScanFailedRef, true).pipe(as(empty$4))))))).pipe(withSpan("SecurityScan.run")));
40726
40925
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
40727
40926
  const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
40728
40927
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
@@ -40753,6 +40952,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40753
40952
  const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
40754
40953
  const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
40755
40954
  const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
40955
+ const capToDeadline = (phaseTimeoutMs) => input.deadlineEpochMs === void 0 ? phaseTimeoutMs : Math.min(phaseTimeoutMs, remainingDeadlineBudgetMs(input.deadlineEpochMs));
40756
40956
  const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
40757
40957
  const deadCodeOverlapMode = yield* DeadCodeOverlap;
40758
40958
  const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
@@ -40760,7 +40960,6 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40760
40960
  const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
40761
40961
  const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
40762
40962
  rootDirectory: scanDirectory,
40763
- userConfig: resolvedConfig.config,
40764
40963
  parseConcurrency: deadCodeParseConcurrency,
40765
40964
  workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
40766
40965
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
@@ -40783,7 +40982,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40783
40982
  });
40784
40983
  const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
40785
40984
  workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
40786
- phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs)
40985
+ phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs))
40787
40986
  })) : null;
40788
40987
  const scanProgress = yield* progressService.start("Scanning...");
40789
40988
  const scanStartTime = Date.now();
@@ -40808,7 +41007,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40808
41007
  onCacheStats: (cacheHitFileCount, totalConsideredFileCount) => {
40809
41008
  lintCacheHitFileCount = cacheHitFileCount;
40810
41009
  lintCacheTotalFileCount = totalConsideredFileCount;
40811
- }
41010
+ },
41011
+ deadlineEpochMs: input.deadlineEpochMs
40812
41012
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
40813
41013
  yield* set(lintFailure, {
40814
41014
  didFail: true,
@@ -40835,7 +41035,13 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40835
41035
  let deadCodeCollected = [];
40836
41036
  if (lintFailureState.didFail) {
40837
41037
  if (deadCodeFiber !== null) yield* interrupt(deadCodeFiber);
40838
- } else if (shouldRunDeadCode) {
41038
+ } else if (shouldRunDeadCode) if (input.deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(input.deadlineEpochMs) === 0) {
41039
+ if (deadCodeFiber !== null) yield* interrupt(deadCodeFiber);
41040
+ yield* set(deadCodeFailure, {
41041
+ didFail: true,
41042
+ reason: "Dead-code analysis skipped — max scan duration reached."
41043
+ });
41044
+ } else {
40839
41045
  yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
40840
41046
  const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
40841
41047
  sourceFileCount: totalFileCount,
@@ -40844,7 +41050,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40844
41050
  });
40845
41051
  deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
40846
41052
  workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
40847
- phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs)
41053
+ phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs))
40848
41054
  });
40849
41055
  }
40850
41056
  const deadCodeFailureState = lintFailureState.didFail ? {
@@ -40858,9 +41064,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40858
41064
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
40859
41065
  const supplyChainResult = yield* join(supplyChainFiber);
40860
41066
  const supplyChainCollected = supplyChainResult.diagnostics;
41067
+ const securityScanCollected = yield* join(securityScanFiber);
40861
41068
  yield* reporterService.finalize;
40862
41069
  const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
40863
41070
  ...envCollected,
41071
+ ...securityScanCollected,
40864
41072
  ...supplyChainCollected,
40865
41073
  ...lintCollected,
40866
41074
  ...deadCodeCollected
@@ -40880,12 +41088,13 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40880
41088
  };
40881
41089
  const scoreSurface = input.scoreSurface ?? "score";
40882
41090
  const scoreDiagnostics = filterDiagnosticsForSurface([...finalDiagnostics], scoreSurface, resolvedConfig.config);
40883
- const score = lintFailureState.didFail ? null : yield* scoreService.compute({
41091
+ const score = lintFailureState.didFail || deadCodeFailureState.didFail ? null : yield* scoreService.compute({
40884
41092
  diagnostics: scoreDiagnostics,
40885
41093
  isCi: input.isCi,
40886
41094
  metadata: scoreMetadata
40887
41095
  });
40888
41096
  const lintPartialFailures = yield* get$2(partialFailuresRef);
41097
+ const securityScanFailed = yield* get$2(securityScanFailedRef);
40889
41098
  return {
40890
41099
  project,
40891
41100
  userConfig: resolvedConfig.config,
@@ -40906,8 +41115,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40906
41115
  scanElapsedMilliseconds,
40907
41116
  scanConcurrency,
40908
41117
  supplyChainOverlapTimedOut: supplyChainResult.timedOut,
41118
+ securityScanFailed,
40909
41119
  lintCacheHitFileCount,
40910
- lintCacheTotalFileCount
41120
+ lintCacheTotalFileCount,
41121
+ suppressedRuleCounts: transform.summarizeSuppressions()
40911
41122
  };
40912
41123
  }).pipe(withSpan("runInspect", { attributes: {
40913
41124
  "inspect.directory": input.directory,
@@ -41246,6 +41457,7 @@ const buildJsonReport = (input) => {
41246
41457
  return {
41247
41458
  schemaVersion: 1,
41248
41459
  mode: input.mode,
41460
+ ...input.baselineDegraded ? { baselineDegraded: true } : {},
41249
41461
  ...shared
41250
41462
  };
41251
41463
  };
@@ -41312,6 +41524,61 @@ const clearAutoSuppressionCaches = () => {};
41312
41524
  * untouched so the loader sees the same plain object either way.
41313
41525
  */
41314
41526
  const defineConfig = (config) => config;
41527
+ const AI_TRAINING_ENV_VARS = [
41528
+ ["HF_DATASETS_CACHE", "huggingface"],
41529
+ ["HF_HOME", "huggingface"],
41530
+ ["HUGGINGFACE_HUB_CACHE", "huggingface"],
41531
+ ["SPACE_ID", "huggingface-spaces"],
41532
+ ["CUDA_VISIBLE_DEVICES", "cuda"],
41533
+ ["NVIDIA_VISIBLE_DEVICES", "nvidia"],
41534
+ ["ROCR_VISIBLE_DEVICES", "rocm"],
41535
+ ["TPU_NAME", "google-tpu"],
41536
+ ["WANDB_RUN_ID", "wandb"],
41537
+ ["WANDB_SWEEP_ID", "wandb"],
41538
+ ["MLFLOW_RUN_ID", "mlflow"],
41539
+ ["MLFLOW_TRACKING_URI", "mlflow"],
41540
+ ["COMET_EXPERIMENT_KEY", "comet"],
41541
+ ["NEPTUNE_RUN_ID", "neptune"],
41542
+ ["CLEARML_TASK_ID", "clearml"],
41543
+ ["DVC_STAGE", "dvc"],
41544
+ ["RAY_WORKER_PROCESS", "ray"],
41545
+ ["RAY_ADDRESS", "ray"],
41546
+ ["MUJOCO_GL", "mujoco"],
41547
+ ["MUJOCO_PATH", "mujoco"],
41548
+ ["GYM_DISABLE_ENV_CHECKER", "gymnasium"],
41549
+ ["SM_TRAINING_ENV", "sagemaker"],
41550
+ ["TRAINING_JOB_ARN", "sagemaker"],
41551
+ ["SAGEMAKER_BASE_DIR", "sagemaker"],
41552
+ ["AZUREML_RUN_ID", "azure-ml"],
41553
+ ["AZURE_ML_MODEL_DIR", "azure-ml"],
41554
+ ["CLOUD_ML_PROJECT_ID", "vertex-ai"],
41555
+ ["VERTEX_AI_LOG_LEVEL", "vertex-ai"],
41556
+ ["DET_MASTER", "determined-ai"],
41557
+ ["LIGHTNING_USER_ID", "lightning-ai"],
41558
+ ["FLYTE_INTERNAL_EXECUTION_ID", "flyte"],
41559
+ ["ARGO_WORKFLOW_NAME", "argo-workflows"],
41560
+ ["KFP_POD_NAME", "kubeflow-pipelines"],
41561
+ ["KAGGLE_KERNEL_RUN_TYPE", "kaggle"],
41562
+ ["COLAB_BACKEND_VERSION", "google-colab"],
41563
+ ["DATABRICKS_RUNTIME_VERSION", "databricks"],
41564
+ ["DAYTONA_WS_ID", "daytona"],
41565
+ ["DAYTONA_WS_NAME", "daytona"],
41566
+ ["E2B_SANDBOX_ID", "e2b"],
41567
+ ["MODAL_FUNCTION_ID", "modal"],
41568
+ ["MODAL_TASK_ID", "modal"],
41569
+ ["RUNPOD_POD_ID", "runpod"],
41570
+ ["REPLICATE_USERNAME", "replicate"],
41571
+ ["VAST_CONTAINERLABEL", "vast-ai"],
41572
+ ["HARBOR_URL", "harbor"],
41573
+ ["HARBOR_HOSTNAME", "harbor"],
41574
+ ["SWE_BENCH_TASK", "swe-bench"],
41575
+ ["SWEBENCH_TASK", "swe-bench"],
41576
+ ["SWE_AGENT_MODEL", "swe-agent"]
41577
+ ];
41578
+ const detectAiTrainingEnvironment = () => {
41579
+ for (const [envVar, label] of AI_TRAINING_ENV_VARS) if (process.env[envVar]) return label;
41580
+ return null;
41581
+ };
41315
41582
  const unionValues = (baseValues, overrideValues) => [...new Set([...baseValues, ...overrideValues])];
41316
41583
  const mergeIgnores = (baseIgnore, overrideIgnore) => {
41317
41584
  const mergedIgnore = {
@@ -41356,6 +41623,12 @@ const mergeReactDoctorConfigs = (baseConfig, overrideConfig) => {
41356
41623
  };
41357
41624
  //#endregion
41358
41625
  //#region ../api/dist/index.js
41626
+ let didWarnAiTraining = false;
41627
+ const warnIfAiTrainingEnvironment = () => {
41628
+ if (didWarnAiTraining || detectAiTrainingEnvironment() === null) return;
41629
+ didWarnAiTraining = true;
41630
+ console.warn("[react-doctor] Use in an AI or ML pipeline requires written permission under the react-doctor license. Contact founders@million.dev to request access.");
41631
+ };
41359
41632
  const buildDiagnoseLayer = (config, configOverrideTarget) => {
41360
41633
  const configLayer = configOverrideTarget === void 0 ? Config.layerNode : Config.layerOf({
41361
41634
  config,
@@ -41393,6 +41666,7 @@ const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
41393
41666
  };
41394
41667
  };
41395
41668
  const diagnoseDirectory = async (directory, options) => {
41669
+ warnIfAiTrainingEnvironment();
41396
41670
  const startTime = globalThis.performance.now();
41397
41671
  const scanTarget = await resolveScanTarget(directory);
41398
41672
  return outputToDiagnoseResult(await runPromise(restoreLegacyThrow(buildInspectProgram(scanTarget, options).pipe(provide(buildDiagnoseLayer(scanTarget.userConfig)), provide(layerOtlp)))), globalThis.performance.now() - startTime);
@@ -41439,6 +41713,7 @@ const diagnoseProject = async (projectDefinition, baseOptions, batchConfig) => {
41439
41713
  }
41440
41714
  };
41441
41715
  const diagnoseProjectBatch = async (input) => {
41716
+ warnIfAiTrainingEnvironment();
41442
41717
  const startTime = globalThis.performance.now();
41443
41718
  const { projects, concurrency, config: batchConfig, ...baseOptions } = input;
41444
41719
  const projectResults = await mapWithConcurrency(projects, concurrency ?? 4, (projectDefinition) => diagnoseProject(projectDefinition, baseOptions, batchConfig));
@@ -41483,4 +41758,4 @@ const toJsonReport = (result, options) => buildJsonReport({
41483
41758
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
41484
41759
 
41485
41760
  //# sourceMappingURL=index.js.map
41486
- //# debugId=dccb6d63-63d4-5e91-af21-2756a08fe93a
41761
+ //# debugId=62cf4740-7938-5db0-8b80-dc81b60af36a