react-doctor 0.5.8-dev.7f9e7f4 → 0.5.8-dev.80e3093

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]="33508ee6-c977-5b5f-8585-9939928ce74d")}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]="83529d8f-2ca3-50ba-a3b9-44622f913549")}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
  /**
@@ -33694,6 +33694,10 @@ const MILLISECONDS_PER_SECOND = 1e3;
33694
33694
  const SCORE_API_URL = "https://www.react.doctor/api/score";
33695
33695
  const FETCH_TIMEOUT_MS = 1e4;
33696
33696
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
33697
+ const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
33698
+ const SPAWN_ARGS_MAX_LENGTH_CHARS_POSIX = 15e5;
33699
+ const SPAWN_ARGS_MAX_LENGTH_CHARS_DARWIN = 8e5;
33700
+ const NODE_VERSION_PROBE_TIMEOUT_MS = 5e3;
33697
33701
  const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
33698
33702
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
33699
33703
  const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
@@ -33751,6 +33755,7 @@ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
33751
33755
  const LINT_PHASE_TIMEOUT_MS = 3e5;
33752
33756
  const SCAN_TOTAL_DEADLINE_MS = 9e5;
33753
33757
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
33758
+ const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
33754
33759
  const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
33755
33760
  const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
33756
33761
  const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
@@ -34487,7 +34492,10 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
34487
34492
  NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
34488
34493
  }
34489
34494
  const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
34490
- const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
34495
+ const canonicalizeRuleKey = (ruleKey) => {
34496
+ const nativeRuleKey = LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey];
34497
+ return typeof nativeRuleKey === "string" ? nativeRuleKey : ruleKey;
34498
+ };
34491
34499
  const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
34492
34500
  const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
34493
34501
  const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
@@ -34514,7 +34522,7 @@ const tokenizeRuleList = (ruleList) => {
34514
34522
  const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34515
34523
  const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34516
34524
  const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34517
- const buildHint = (tool, token, ruleId) => `oxlint matches plugin rules only by their full name, so \`${token}\` in your ${tool}-disable comment does not silence \`${ruleId}\` — change it to \`${ruleId}\`.`;
34525
+ 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}\`).`;
34518
34526
  const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
34519
34527
  const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34520
34528
  const candidates = [{
@@ -34994,6 +35002,7 @@ const collectStringSet = (values) => {
34994
35002
  if (!Array.isArray(values)) return /* @__PURE__ */ new Set();
34995
35003
  return new Set(values.filter((value) => typeof value === "string"));
34996
35004
  };
35005
+ const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
34997
35006
  /**
34998
35007
  * Pre-compiles every stateful filter and returns a single
34999
35008
  * `apply(diagnostic)` closure that runs (in order):
@@ -35027,11 +35036,28 @@ const buildDiagnosticPipeline = (input) => {
35027
35036
  const compiledOverrides = compileIgnoreOverrides(userConfig);
35028
35037
  const textComponentNames = collectStringSet(userConfig?.textComponents);
35029
35038
  const rawTextWrapperComponentNames = collectStringSet(userConfig?.rawTextWrapperComponents);
35039
+ const runtimeGlobalNames = collectStringSet(userConfig?.runtimeGlobals);
35030
35040
  const hasTextComponents = textComponentNames.size > 0;
35031
35041
  const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
35042
+ const hasRuntimeGlobals = runtimeGlobalNames.size > 0;
35032
35043
  const fileLinesCache = /* @__PURE__ */ new Map();
35033
35044
  const fileContextCache = /* @__PURE__ */ new Map();
35034
35045
  const libraryFileCache = /* @__PURE__ */ new Map();
35046
+ const suppressions = /* @__PURE__ */ new Map();
35047
+ const suppress = (diagnostic, source) => {
35048
+ const { ruleKey } = getDiagnosticRuleIdentity(diagnostic);
35049
+ const suppressionKey = `${ruleKey}\u0000${source}`;
35050
+ const existing = suppressions.get(suppressionKey);
35051
+ suppressions.set(suppressionKey, existing ? {
35052
+ ...existing,
35053
+ count: existing.count + 1
35054
+ } : {
35055
+ rule: ruleKey,
35056
+ source,
35057
+ count: 1
35058
+ });
35059
+ return null;
35060
+ };
35035
35061
  const isLibraryFile = (filePath) => {
35036
35062
  let cached = libraryFileCache.get(filePath);
35037
35063
  if (cached === void 0) {
@@ -35080,50 +35106,60 @@ const buildDiagnosticPipeline = (input) => {
35080
35106
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
35081
35107
  return false;
35082
35108
  };
35083
- return { apply: (diagnostic) => {
35084
- if (shouldAutoSuppress(diagnostic)) return null;
35085
- let current = diagnostic;
35086
- let explicitSeverityOverride;
35087
- let explicitRuleOverride;
35088
- if (severityControls) {
35089
- const { ruleKey, category } = getDiagnosticRuleIdentity(current);
35090
- explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey }, severityControls);
35091
- explicitSeverityOverride = resolveRuleSeverityOverride({
35092
- ruleKey,
35093
- category
35094
- }, severityControls);
35095
- if (explicitSeverityOverride === "off") return null;
35096
- if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
35097
- }
35098
- if (explicitRuleOverride === void 0) {
35099
- if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
35100
- }
35101
- if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
35102
- if (userConfig) {
35103
- if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return null;
35104
- if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
35105
- if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return null;
35106
- if (isRnRawTextSuppressedByConfig(current)) return null;
35107
- }
35108
- if (respectInlineDisables && current.line > 0) {
35109
- const lines = getFileLines(current.filePath);
35110
- if (lines) {
35111
- const ruleIdentifier = `${current.plugin}/${current.rule}`;
35112
- const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
35113
- if (evaluation.isSuppressed) return null;
35114
- if (evaluation.nearMissHint) current = {
35115
- ...current,
35116
- suppressionHint: evaluation.nearMissHint
35117
- };
35109
+ const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
35110
+ if (!hasRuntimeGlobals) return false;
35111
+ if (diagnostic.rule !== "jsx-no-undef") return false;
35112
+ const identifierMatch = JSX_NO_UNDEF_IDENTIFIER_PATTERN.exec(diagnostic.message);
35113
+ return identifierMatch !== null && runtimeGlobalNames.has(identifierMatch[1]);
35114
+ };
35115
+ return {
35116
+ apply: (diagnostic) => {
35117
+ if (shouldAutoSuppress(diagnostic)) return null;
35118
+ let current = diagnostic;
35119
+ let explicitSeverityOverride;
35120
+ let explicitRuleOverride;
35121
+ if (severityControls) {
35122
+ const { ruleKey, category } = getDiagnosticRuleIdentity(current);
35123
+ explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey }, severityControls);
35124
+ explicitSeverityOverride = resolveRuleSeverityOverride({
35125
+ ruleKey,
35126
+ category
35127
+ }, severityControls);
35128
+ if (explicitSeverityOverride === "off") return suppress(current, "config");
35129
+ if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
35118
35130
  }
35119
- }
35120
- const fileContext = getFileContext(current.filePath);
35121
- if (fileContext !== "production") current = {
35122
- ...current,
35123
- fileContext
35124
- };
35125
- return current;
35126
- } };
35131
+ if (explicitRuleOverride === void 0) {
35132
+ if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
35133
+ }
35134
+ if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
35135
+ if (userConfig) {
35136
+ if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return suppress(current, "config");
35137
+ if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
35138
+ if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return suppress(current, "override");
35139
+ if (isRnRawTextSuppressedByConfig(current)) return null;
35140
+ if (isJsxNoUndefSuppressedByConfig(current)) return null;
35141
+ }
35142
+ if (respectInlineDisables && current.line > 0) {
35143
+ const lines = getFileLines(current.filePath);
35144
+ if (lines) {
35145
+ const ruleIdentifier = `${current.plugin}/${current.rule}`;
35146
+ const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
35147
+ if (evaluation.isSuppressed) return suppress(current, "inline");
35148
+ if (evaluation.nearMissHint) current = {
35149
+ ...current,
35150
+ suppressionHint: evaluation.nearMissHint
35151
+ };
35152
+ }
35153
+ }
35154
+ const fileContext = getFileContext(current.filePath);
35155
+ if (fileContext !== "production") current = {
35156
+ ...current,
35157
+ fileContext
35158
+ };
35159
+ return current;
35160
+ },
35161
+ summarizeSuppressions: () => [...suppressions.values()]
35162
+ };
35127
35163
  };
35128
35164
  const OxlintUnavailableKind = Literals(["binary-not-found", "native-binding-missing"]);
35129
35165
  var OxlintUnavailable = class extends TaggedErrorClass()("OxlintUnavailable", {
@@ -35365,7 +35401,7 @@ const resolveScanConcurrency = (requested) => {
35365
35401
  if (!Number.isFinite(requested) || requested < 1) return 1;
35366
35402
  return Math.min(Math.floor(requested), 32);
35367
35403
  };
35368
- const readSystemFacts = () => ({
35404
+ const readSystemFacts$1 = () => ({
35369
35405
  availableCores: os.availableParallelism(),
35370
35406
  totalMemoryBytes: os.totalmem(),
35371
35407
  cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
@@ -35386,7 +35422,7 @@ const readSystemFacts = () => ({
35386
35422
  * `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
35387
35423
  * limited, and ceiling cases without mocking `os` or the filesystem.
35388
35424
  */
35389
- const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
35425
+ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
35390
35426
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
35391
35427
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
35392
35428
  return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
@@ -35427,13 +35463,7 @@ var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { de
35427
35463
  * without recompiling react-doctor. Tests override via
35428
35464
  * `Layer.succeed(SupplyChainOverlapTimeoutMs, ...)`.
35429
35465
  */
35430
- var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => {
35431
- const raw = process.env["REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS"];
35432
- if (raw === void 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35433
- const parsed = Number(raw);
35434
- if (!Number.isFinite(parsed) || parsed <= 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35435
- return parsed;
35436
- } }) {};
35466
+ var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS", SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS) }) {};
35437
35467
  /**
35438
35468
  * Hard cap on combined stdout+stderr bytes per oxlint batch. The
35439
35469
  * subprocess gets SIGKILL'd if it produces more; the recovery path
@@ -35577,6 +35607,13 @@ const BOOLEAN_FIELD_NAMES = [
35577
35607
  "adoptExistingLintConfig"
35578
35608
  ];
35579
35609
  const STRING_FIELD_NAMES = ["rootDir"];
35610
+ const STRING_ARRAY_FIELD_NAMES = [
35611
+ "projects",
35612
+ "textComponents",
35613
+ "rawTextWrapperComponents",
35614
+ "runtimeGlobals",
35615
+ "serverAuthFunctionNames"
35616
+ ];
35580
35617
  const SURFACE_CONTROL_FIELD_NAMES = [
35581
35618
  "includeTags",
35582
35619
  "excludeTags",
@@ -35678,6 +35715,7 @@ const validateConfigTypes = (config) => {
35678
35715
  const validated = { ...config };
35679
35716
  for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
35680
35717
  for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
35718
+ for (const fieldName of STRING_ARRAY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateStringArrayField(fieldName, value));
35681
35719
  applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
35682
35720
  for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
35683
35721
  applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
@@ -36912,7 +36950,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
36912
36950
  }
36913
36951
  return true;
36914
36952
  };
36915
- const checkSecurityScan = (rootDirectory, options = {}) => {
36953
+ const yieldToEventLoop = () => new Promise((resolve) => {
36954
+ setImmediate(resolve);
36955
+ });
36956
+ const createSecurityScanSession = (rootDirectory, options) => {
36916
36957
  const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
36917
36958
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
36918
36959
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
@@ -36927,7 +36968,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36927
36968
  committedFilesOnly: rule.committedFilesOnly === true
36928
36969
  }];
36929
36970
  });
36930
- if (enabledScanRules.length === 0) return [];
36971
+ if (enabledScanRules.length === 0) return null;
36931
36972
  const diagnostics = [];
36932
36973
  const seen = /* @__PURE__ */ new Set();
36933
36974
  const gitIgnoredCache = /* @__PURE__ */ new Map();
@@ -36939,15 +36980,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36939
36980
  }
36940
36981
  return status === true;
36941
36982
  };
36942
- for (const file of collectSecurityScanFiles(rootDirectory)) for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36943
- if (committedFilesOnly && isFileGitIgnored(file)) continue;
36944
- const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36945
- const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36946
- if (seen.has(key)) continue;
36947
- seen.add(key);
36948
- diagnostics.push(diagnostic);
36983
+ const scanFile = (file) => {
36984
+ for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36985
+ if (committedFilesOnly && isFileGitIgnored(file)) continue;
36986
+ const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36987
+ const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36988
+ if (seen.has(key)) continue;
36989
+ seen.add(key);
36990
+ diagnostics.push(diagnostic);
36991
+ }
36992
+ };
36993
+ return {
36994
+ scanFile,
36995
+ diagnostics
36996
+ };
36997
+ };
36998
+ const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
36999
+ const session = createSecurityScanSession(rootDirectory, options);
37000
+ if (session === null) return [];
37001
+ let filesSinceYield = 0;
37002
+ for (const file of collectSecurityScanFiles(rootDirectory)) {
37003
+ session.scanFile(file);
37004
+ filesSinceYield += 1;
37005
+ if (filesSinceYield >= 16) {
37006
+ filesSinceYield = 0;
37007
+ await yieldToEventLoop();
37008
+ }
36949
37009
  }
36950
- return diagnostics;
37010
+ return session.diagnostics;
36951
37011
  };
36952
37012
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
36953
37013
  let p = process || {}, argv = p.argv || [], env = p.env || {};
@@ -37164,6 +37224,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
37164
37224
  return [...seen].filter((pattern) => pattern.length > 0);
37165
37225
  };
37166
37226
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
37227
+ const readSystemFacts = () => ({
37228
+ availableCores: os.availableParallelism(),
37229
+ totalMemoryBytes: os.totalmem(),
37230
+ cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
37231
+ });
37232
+ /**
37233
+ * How many real deslop dead-code child processes may run at once, across the
37234
+ * concurrent per-project `runInspect` fibers of one CLI run. The cap is the
37235
+ * smaller of the core count and the number of `DEAD_CODE_WORKER_MEM_BUDGET_BYTES`
37236
+ * workers that fit in available memory, floored at 1.
37237
+ *
37238
+ * On a roomy dev box / CI runner this resolves high enough that every
37239
+ * concurrently-scanned project still spawns its own worker (no serialization vs
37240
+ * the prior uncapped behavior); on a memory-constrained runner it collapses
37241
+ * toward 1, so the `withDeadCodeWorkerSlot` semaphore serializes the spawns
37242
+ * instead of oversubscribing memory with N simultaneous children — the global
37243
+ * cap the per-project spawn path lacked.
37244
+ *
37245
+ * Mirrors `resolveAutoScanConcurrency` (lint), but budgets memory per the
37246
+ * heavier dead-code worker. `facts` is injectable for tests.
37247
+ */
37248
+ const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
37249
+ const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
37250
+ const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
37251
+ return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
37252
+ };
37253
+ let availableSlots = -1;
37254
+ const waiters = [];
37255
+ const releaseSlot = () => {
37256
+ const nextWaiter = waiters.shift();
37257
+ if (nextWaiter !== void 0) nextWaiter();
37258
+ else availableSlots += 1;
37259
+ };
37260
+ /**
37261
+ * Runs `task` once a dead-code worker slot is free, releasing the slot when the
37262
+ * task settles (success or failure). With a high cap (roomy machine) every
37263
+ * caller proceeds immediately; with a low cap (constrained runner) callers
37264
+ * queue and run as slots free.
37265
+ *
37266
+ * `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
37267
+ * this caller is queued, the call rejects without acquiring a slot or running
37268
+ * `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
37269
+ * then spawn a child only to tear it down. A queued caller that aborts removes
37270
+ * its own waiter so a later release never hands a slot to a dead request.
37271
+ */
37272
+ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
37273
+ if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
37274
+ if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
37275
+ if (availableSlots > 0) availableSlots -= 1;
37276
+ else await new Promise((resolve, reject) => {
37277
+ const waiter = () => {
37278
+ abortSignal?.removeEventListener("abort", onAbort);
37279
+ resolve();
37280
+ };
37281
+ const onAbort = () => {
37282
+ const queuedIndex = waiters.indexOf(waiter);
37283
+ if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
37284
+ reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
37285
+ };
37286
+ waiters.push(waiter);
37287
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
37288
+ });
37289
+ try {
37290
+ return await task();
37291
+ } finally {
37292
+ releaseSlot();
37293
+ }
37294
+ };
37167
37295
  /**
37168
37296
  * Resolves a path to its canonical, symlink-free form, falling back to
37169
37297
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -37188,6 +37316,11 @@ const toCanonicalPath = (filePath) => {
37188
37316
  };
37189
37317
  const DEAD_CODE_PLUGIN = "deslop";
37190
37318
  const DEAD_CODE_CATEGORY = "Maintainability";
37319
+ const REACT_DOCTOR_TOOLCHAIN_PACKAGES = new Set([
37320
+ "react-doctor",
37321
+ "eslint-plugin-react-doctor",
37322
+ "oxlint-plugin-react-doctor"
37323
+ ]);
37191
37324
  const DEAD_CODE_WORKER_SCRIPT = `
37192
37325
  const inputChunks = [];
37193
37326
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -37454,14 +37587,17 @@ const checkDeadCode = async (options) => {
37454
37587
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37455
37588
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37456
37589
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37457
- const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37458
- rootDirectory,
37459
- entryPatterns,
37460
- tsConfigPath: resolveTsConfigPath(rootDirectory),
37461
- ignorePatterns,
37462
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37463
- parseConcurrency: options.parseConcurrency
37464
- }), options.workerTimeoutMs ?? 12e4, options.abortSignal));
37590
+ const spawnAndRun = () => {
37591
+ return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37592
+ rootDirectory,
37593
+ entryPatterns,
37594
+ tsConfigPath: resolveTsConfigPath(rootDirectory),
37595
+ ignorePatterns,
37596
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37597
+ parseConcurrency: options.parseConcurrency
37598
+ }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
37599
+ };
37600
+ const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
37465
37601
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
37466
37602
  const diagnostics = [];
37467
37603
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -37490,6 +37626,7 @@ const checkDeadCode = async (options) => {
37490
37626
  });
37491
37627
  }
37492
37628
  for (const unusedDependency of result.unusedDependencies) {
37629
+ if (REACT_DOCTOR_TOOLCHAIN_PACKAGES.has(unusedDependency.name)) continue;
37493
37630
  const label = unusedDependency.isDevDependency ? "devDependency" : "dependency";
37494
37631
  diagnostics.push({
37495
37632
  filePath: "package.json",
@@ -37668,7 +37805,6 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37668
37805
  return yield* tryPromise({
37669
37806
  try: (signal) => checkDeadCode({
37670
37807
  rootDirectory: input.rootDirectory,
37671
- userConfig: input.userConfig,
37672
37808
  parseConcurrency: input.parseConcurrency,
37673
37809
  workerTimeoutMs: input.workerTimeoutMs,
37674
37810
  abortSignal: signal
@@ -37791,6 +37927,11 @@ const isSafeGitRevision = (candidate) => {
37791
37927
  if (candidate.includes("..") || candidate.includes("@{")) return false;
37792
37928
  return SAFE_GIT_REVISION_PATTERN.test(candidate);
37793
37929
  };
37930
+ const resolveSpawnArgsLengthCap = () => {
37931
+ if (process.platform === "win32") return SPAWN_ARGS_MAX_LENGTH_CHARS;
37932
+ if (process.platform === "darwin") return SPAWN_ARGS_MAX_LENGTH_CHARS_DARWIN;
37933
+ return SPAWN_ARGS_MAX_LENGTH_CHARS_POSIX;
37934
+ };
37794
37935
  /**
37795
37936
  * Splits a git revision range into its endpoints: three-dot `A...B`
37796
37937
  * (symmetric, merge-base) or two-dot `A..B` (direct). Returns `null`
@@ -37862,43 +38003,47 @@ var Git = class Git extends Service()("react-doctor/Git") {
37862
38003
  * reason: GitInvocationFailed })` so the rest of the codebase
37863
38004
  * sees a single failure channel.
37864
38005
  */
37865
- const runCommand = (input) => scoped(gen(function* () {
37866
- const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37867
- cwd: input.directory,
37868
- env: input.env,
37869
- extendEnv: true
37870
- }));
37871
- const maxStdoutBytes = input.maxStdoutBytes;
37872
- const stdoutByteCount = yield* make$13(0);
37873
- const [stdout, stderr, status] = yield* all([
37874
- 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({
37875
- args: [...input.args],
37876
- directory: input.directory,
37877
- cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
37878
- }) })) : void_)))))),
37879
- mkString(decodeText(handle.stderr)),
37880
- handle.exitCode
37881
- ], { concurrency: 3 });
37882
- return {
37883
- status,
37884
- stdout,
37885
- stderr
37886
- };
37887
- })).pipe(catchTag$1("PlatformError", (cause) => {
37888
- if (input.command !== "git") return succeed$2({
38006
+ const runCommand = (input) => {
38007
+ const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
37889
38008
  status: 127,
37890
38009
  stdout: "",
37891
38010
  stderr: String(cause)
37892
- });
37893
- return new ReactDoctorError({ reason: new GitInvocationFailed({
38011
+ }) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37894
38012
  args: [...input.args],
37895
38013
  directory: input.directory,
37896
38014
  cause
37897
- }) });
37898
- }), withSpan("git.exec", { attributes: {
37899
- "git.command": input.command,
37900
- "git.subcommand": input.args[0] ?? ""
37901
- } }));
38015
+ }) }));
38016
+ return scoped(gen(function* () {
38017
+ if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
38018
+ const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
38019
+ const spawnArgsLengthCap = resolveSpawnArgsLengthCap();
38020
+ if (argvLengthChars > spawnArgsLengthCap) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${spawnArgsLengthCap})`);
38021
+ const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
38022
+ cwd: input.directory,
38023
+ env: input.env,
38024
+ extendEnv: true
38025
+ }));
38026
+ const maxStdoutBytes = input.maxStdoutBytes;
38027
+ const stdoutByteCount = yield* make$13(0);
38028
+ const [stdout, stderr, status] = yield* all([
38029
+ 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({
38030
+ args: [...input.args],
38031
+ directory: input.directory,
38032
+ cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
38033
+ }) })) : void_)))))),
38034
+ mkString(decodeText(handle.stderr)),
38035
+ handle.exitCode
38036
+ ], { concurrency: 3 });
38037
+ return {
38038
+ status,
38039
+ stdout,
38040
+ stderr
38041
+ };
38042
+ })).pipe(catchTag$1("PlatformError", foldSpawnFailure), withSpan("git.exec", { attributes: {
38043
+ "git.command": input.command,
38044
+ "git.subcommand": input.args[0] ?? ""
38045
+ } }));
38046
+ };
37902
38047
  const runGit = (directory, args) => runCommand({
37903
38048
  command: "git",
37904
38049
  args,
@@ -37931,7 +38076,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37931
38076
  "rev-parse",
37932
38077
  "--verify",
37933
38078
  branch
37934
- ]).pipe(map$3((result) => result.status === 0));
38079
+ ]).pipe(map$3((result) => result.status === 0), catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(false) : fail$4(error)));
37935
38080
  const headSha = (directory) => runGit(directory, ["rev-parse", "HEAD"]).pipe(map$3((result) => result.status === 0 ? trimOrNull(result.stdout) : null));
37936
38081
  const mergeBase = (input) => isSafeGitRevision(input.ref) ? runGit(input.directory, [
37937
38082
  "merge-base",
@@ -38041,6 +38186,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38041
38186
  if (resolvedCurrentBranch === null && explicitBaseBranch === void 0) return null;
38042
38187
  const baseBranch = explicitBaseBranch ?? (yield* defaultBranch(directory));
38043
38188
  if (baseBranch === null) return null;
38189
+ if (!isSafeGitRevision(baseBranch)) return null;
38044
38190
  if (explicitBaseBranch !== void 0) {
38045
38191
  if (!(yield* branchExists(directory, explicitBaseBranch))) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchMissing({ branch: explicitBaseBranch }) }));
38046
38192
  }
@@ -38145,7 +38291,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38145
38291
  ]);
38146
38292
  if (result.status !== 0) return null;
38147
38293
  return parseChangedLineRanges(result.stdout);
38148
- }).pipe(withSpan("Git.changedLineRanges"))
38294
+ }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
38149
38295
  });
38150
38296
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
38151
38297
  /**
@@ -38391,7 +38537,7 @@ const normalizeConfigForHash = (config) => {
38391
38537
  if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
38392
38538
  return clone;
38393
38539
  };
38394
- const computeRulesetHash = (input) => crypto.createHash("sha1").update(JSON.stringify(normalizeConfigForHash(input.config))).update("\0").update([...input.toolchainVersions].join("\0")).update("\0").update([...input.ignorePatterns].join("\n")).update(" ").update(input.tsconfigContent ?? "").digest("hex");
38540
+ 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");
38395
38541
  /**
38396
38542
  * Loads a plugin module via the local require resolver and extracts
38397
38543
  * `(name, ruleNames)` from either `module.exports.meta + rules` or
@@ -38669,24 +38815,38 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38669
38815
  updatedAtMs: Date.now(),
38670
38816
  files: Object.fromEntries(cappedEntries)
38671
38817
  };
38672
- const keptHashes = Object.entries(rulesets).sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8).map(([hash]) => hash);
38673
- const prunedRulesets = {};
38674
- for (const hash of keptHashes) prunedRulesets[hash] = rulesets[hash];
38675
38818
  atomicWriteJson(cacheFilePath, {
38676
38819
  version: 1,
38677
- rulesets: prunedRulesets
38820
+ rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
38678
38821
  });
38679
38822
  }
38680
38823
  };
38681
38824
  };
38682
38825
  const bundledRequire = createRequire(import.meta.url);
38826
+ const childNodeVersionByBinaryPath = /* @__PURE__ */ new Map();
38827
+ const resolveChildNodeVersion = (nodeBinaryPath) => {
38828
+ if (nodeBinaryPath === process.execPath) return process.version;
38829
+ const cachedVersion = childNodeVersionByBinaryPath.get(nodeBinaryPath);
38830
+ if (cachedVersion !== void 0) return cachedVersion;
38831
+ let version;
38832
+ try {
38833
+ version = execFileSync(nodeBinaryPath, ["--version"], {
38834
+ encoding: "utf8",
38835
+ timeout: NODE_VERSION_PROBE_TIMEOUT_MS
38836
+ }).trim();
38837
+ } catch {
38838
+ version = `unknown:${nodeBinaryPath}`;
38839
+ }
38840
+ childNodeVersionByBinaryPath.set(nodeBinaryPath, version);
38841
+ return version;
38842
+ };
38683
38843
  const TOOLCHAIN_PACKAGE_SPECIFIERS = [
38684
38844
  "oxlint/package.json",
38685
38845
  "oxlint-plugin-react-doctor/package.json",
38686
38846
  "eslint-plugin-react-hooks/package.json"
38687
38847
  ];
38688
- const resolveOxlintToolchainVersions = () => {
38689
- const versions = [`node=${process.version}`];
38848
+ const resolveOxlintToolchainVersions = (nodeBinaryPath = process.execPath) => {
38849
+ const versions = [`node=${resolveChildNodeVersion(nodeBinaryPath)}`];
38690
38850
  for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
38691
38851
  const packageJson = bundledRequire(specifier);
38692
38852
  const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
@@ -39220,11 +39380,12 @@ const REACT_COMPILER_TITLE = "React Compiler can't optimize this";
39220
39380
  const REACT_COMPILER_TODO_TITLE = "React Compiler doesn't support this syntax";
39221
39381
  const REACT_COMPILER_IMPACT = "This component misses React Compiler's automatic memoization & re-renders more than it should";
39222
39382
  const REACT_COMPILER_ACTION = "Rewrite the flagged code so the compiler can optimize it.";
39383
+ 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`.";
39223
39384
  const REACT_COMPILER_GENERIC_MESSAGE = `${REACT_COMPILER_IMPACT}. ${REACT_COMPILER_ACTION}`;
39224
- const buildReactCompilerMessage = (reasonSummary) => {
39385
+ const buildReactCompilerMessage = (reasonSummary, action = REACT_COMPILER_ACTION) => {
39225
39386
  const normalizedSummary = reasonSummary.replace(TRAILING_PERIOD_PATTERN, "");
39226
- if (!normalizedSummary) return REACT_COMPILER_GENERIC_MESSAGE;
39227
- return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${REACT_COMPILER_ACTION}`;
39387
+ if (!normalizedSummary) return `${REACT_COMPILER_IMPACT}. ${action}`;
39388
+ return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${action}`;
39228
39389
  };
39229
39390
  const PLUGIN_CATEGORY_MAP = {
39230
39391
  react: "Bugs",
@@ -39273,7 +39434,7 @@ const resolveCleanedDiagnostic = (message, help, plugin, rule, project) => {
39273
39434
  const [reasonSummary = "", ...reasonDetailLines] = bailoutReason.split("\n");
39274
39435
  const reasonDetail = reasonDetailLines.join("\n").trim();
39275
39436
  return {
39276
- message: buildReactCompilerMessage(reasonSummary.trim()),
39437
+ message: buildReactCompilerMessage(reasonSummary.trim(), rule === "incompatible-library" ? REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION : REACT_COMPILER_ACTION),
39277
39438
  help: appendReanimatedSharedValueHint(reasonDetail || help, rule, project)
39278
39439
  };
39279
39440
  }
@@ -39541,21 +39702,27 @@ const spawnLintBatches = async (input) => {
39541
39702
  const allDiagnostics = [];
39542
39703
  const droppedFiles = [];
39543
39704
  let firstDropReason = null;
39544
- const splitDeadlineMs = Date.now() + splitTotalBudgetMs;
39545
- const spawnLintBatch = async (batch, depth) => {
39705
+ const spawnLintBatch = async (batch, depth, splitBudget) => {
39546
39706
  const batchArgs = [...baseArgs, ...batch];
39547
39707
  try {
39548
39708
  return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
39549
39709
  } catch (error) {
39550
39710
  if (!isSplittableReactDoctorError(error)) throw error;
39551
- const splitBudgetExhausted = Date.now() >= splitDeadlineMs || depth >= splitMaxDepth;
39552
- if (batch.length <= 1 || splitBudgetExhausted) {
39711
+ splitBudget.deadlineMs ??= Date.now() + splitTotalBudgetMs;
39712
+ const isBudgetElapsed = Date.now() >= splitBudget.deadlineMs;
39713
+ const isDepthCapReached = depth >= splitMaxDepth;
39714
+ if (batch.length <= 1 || isBudgetElapsed || isDepthCapReached) {
39553
39715
  droppedFiles.push(...batch);
39554
- if (firstDropReason === null) firstDropReason = splitBudgetExhausted && batch.length > 1 ? `${error.message} (split budget exhausted after ${splitMaxDepth} levels / ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s)` : error.message;
39716
+ if (firstDropReason === null) {
39717
+ let limitHint = "";
39718
+ if (isDepthCapReached) limitHint = ` (split depth cap of ${splitMaxDepth} levels reached)`;
39719
+ else if (isBudgetElapsed) limitHint = ` (split budget of ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s exhausted at depth ${depth})`;
39720
+ firstDropReason = batch.length > 1 ? `${error.message}${limitHint}` : error.message;
39721
+ }
39555
39722
  return [];
39556
39723
  }
39557
39724
  const splitIndex = Math.ceil(batch.length / 2);
39558
- return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1)];
39725
+ return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1, splitBudget), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1, splitBudget)];
39559
39726
  }
39560
39727
  };
39561
39728
  let startedFileCount = 0;
@@ -39572,7 +39739,7 @@ const spawnLintBatches = async (input) => {
39572
39739
  try {
39573
39740
  const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
39574
39741
  startedFileCount += batch.length;
39575
- const batchDiagnostics = await spawnLintBatch(batch, 0);
39742
+ const batchDiagnostics = await spawnLintBatch(batch, 0, { deadlineMs: null });
39576
39743
  scannedFileCount += batch.length;
39577
39744
  if (onFileProgress) {
39578
39745
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
@@ -39646,7 +39813,14 @@ const resolveReactDoctorCacheDir = (projectDirectory) => {
39646
39813
  if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
39647
39814
  const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
39648
39815
  if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
39649
- return Path.join(os.tmpdir(), "react-doctor-cache", projectCacheSubdir(projectDirectory));
39816
+ return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
39817
+ };
39818
+ const resolveUserCacheScope = () => {
39819
+ try {
39820
+ return String(process.getuid?.() ?? os.userInfo().username);
39821
+ } catch {
39822
+ return "shared";
39823
+ }
39650
39824
  };
39651
39825
  const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
39652
39826
  /**
@@ -39814,7 +39988,7 @@ const runOxlint = async (options) => {
39814
39988
  extendsPaths: [],
39815
39989
  ruleSelection: "cacheable"
39816
39990
  }),
39817
- toolchainVersions: resolveOxlintToolchainVersions(),
39991
+ toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
39818
39992
  ignorePatterns: combinedPatterns,
39819
39993
  tsconfigContent
39820
39994
  });
@@ -39828,7 +40002,7 @@ const runOxlint = async (options) => {
39828
40002
  missFiles.push(candidateFile);
39829
40003
  continue;
39830
40004
  }
39831
- const cacheKey = `${candidateFile.replaceAll("\\", "/")}${contentHash}`;
40005
+ const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
39832
40006
  cacheKeyByFile.set(candidateFile, cacheKey);
39833
40007
  const cachedDiagnostics = cache.lookup(cacheKey);
39834
40008
  if (cachedDiagnostics === null) missFiles.push(candidateFile);
@@ -40144,7 +40318,7 @@ const ScoreApiResponseSchema = Struct({
40144
40318
  })))
40145
40319
  });
40146
40320
  const parseScoreResult = (value) => getOrNull(decodeUnknownOption(ScoreApiResponseSchema)(value));
40147
- const stripLocalFileFields = (diagnostics) => diagnostics.map(({ filePath: _filePath, fileContext: _fileContext, ...rest }) => rest);
40321
+ const stripLocalFileFields = (diagnostics) => diagnostics.map(({ filePath: _filePath, fileContext: _fileContext, fixGroupId: _fixGroupId, ...rest }) => rest);
40148
40322
  const isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
40149
40323
  const describeFailure = (error) => {
40150
40324
  if (isAbortError(error)) return `timed out after ${FETCH_TIMEOUT_MS / 1e3}s`;
@@ -40448,7 +40622,10 @@ const ALERT_SEVERITY_RANK = {
40448
40622
  medium: 2,
40449
40623
  low: 1
40450
40624
  };
40451
- const severityRank = (severity) => ALERT_SEVERITY_RANK[severity.toLowerCase()] ?? 0;
40625
+ const severityRank = (severity) => {
40626
+ const normalized = severity.toLowerCase();
40627
+ return Object.hasOwn(ALERT_SEVERITY_RANK, normalized) ? ALERT_SEVERITY_RANK[normalized] : 0;
40628
+ };
40452
40629
  const displaySeverity = (severity) => {
40453
40630
  const normalized = sanitizeTerminalText(severity.toLowerCase());
40454
40631
  return normalized === "middle" ? "medium" : normalized;
@@ -40465,7 +40642,7 @@ const ALERT_TYPE_LABELS = {
40465
40642
  typosquat: "possible typosquat"
40466
40643
  };
40467
40644
  const humanizeAlertType = (type) => type.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase().trim();
40468
- const friendlyAlertType = (type) => ALERT_TYPE_LABELS[type] ?? sanitizeTerminalText(humanizeAlertType(type));
40645
+ const friendlyAlertType = (type) => Object.hasOwn(ALERT_TYPE_LABELS, type) ? ALERT_TYPE_LABELS[type] : sanitizeTerminalText(humanizeAlertType(type));
40469
40646
  const summarizeAlertNote = (note) => {
40470
40647
  const collapsed = sanitizeTerminalText(note.replace(/\s+/g, " ").trim());
40471
40648
  const firstSentence = collapsed.split(/(?<=\.)\s/)[0] || collapsed;
@@ -40560,7 +40737,10 @@ const checkSupplyChain = (input) => gen(function* () {
40560
40737
  * outage.
40561
40738
  */
40562
40739
  var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
40563
- static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(checkSupplyChain(input).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
40740
+ static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(flatMap$2(SupplyChainOverlapTimeoutMs, (totalTimeoutMs) => checkSupplyChain({
40741
+ ...input,
40742
+ totalTimeoutMs
40743
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
40564
40744
  static layerOf = (diagnostics) => succeed$3(SupplyChain, SupplyChain.of({ run: () => fromIterable$1(diagnostics) }));
40565
40745
  };
40566
40746
  const getObjectProperty = (value, propertyName) => {
@@ -40622,7 +40802,10 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40622
40802
  * diagnostics).
40623
40803
  * 2. beforeLint hook (e.g. CLI renders the project-detection block)
40624
40804
  * 3. environment checks (reduced-motion + pnpm hardening +
40625
- * expo/react-native + security scan), collected synchronously
40805
+ * expo/react-native), collected synchronously. The heavier
40806
+ * content-regex security scan is forked instead (like supply-chain
40807
+ * below) and joined before the concat, so its CPU overlaps lint
40808
+ * rather than blocking the event loop before it.
40626
40809
  * 4. The supply-chain check (Socket.dev) is forked onto a background
40627
40810
  * fiber so its ~100% network-bound time overlaps the ~100%
40628
40811
  * CPU/subprocess-bound lint pass below, collapsing two serial
@@ -40642,7 +40825,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40642
40825
  * order, so terminal output is identical either way; supply-chain
40643
40826
  * rides alongside without a spinner.
40644
40827
  * 6. Join the supply-chain fiber, then assemble the diagnostics in a
40645
- * FIXED order (env, supply-chain, lint, dead-code) so the output is
40828
+ * FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
40646
40829
  * byte-identical regardless of which fiber settled first. The
40647
40830
  * viewer-permission fiber is joined later, during score-metadata
40648
40831
  * assembly (it feeds score metadata, not diagnostics). The per-element
@@ -40703,12 +40886,13 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40703
40886
  ...checkPnpmHardening(scanDirectory),
40704
40887
  ...checkReactServerComponentsAdvisory(scanDirectory, project),
40705
40888
  ...checkExpoProject(scanDirectory, project),
40706
- ...checkReactNativeProject(scanDirectory, project),
40707
- ...checkSecurityScan(scanDirectory, {
40708
- project,
40709
- ignoredTags: input.ignoredTags
40710
- })
40889
+ ...checkReactNativeProject(scanDirectory, project)
40711
40890
  ])));
40891
+ const securityScanFailedRef = yield* make$13(false);
40892
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise(() => checkSecurityScanCooperative(scanDirectory, {
40893
+ project,
40894
+ ignoredTags: input.ignoredTags
40895
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), catch_$1(() => set(securityScanFailedRef, true).pipe(as(empty$4))))))).pipe(withSpan("SecurityScan.run")));
40712
40896
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
40713
40897
  const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
40714
40898
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
@@ -40746,7 +40930,6 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40746
40930
  const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
40747
40931
  const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
40748
40932
  rootDirectory: scanDirectory,
40749
- userConfig: resolvedConfig.config,
40750
40933
  parseConcurrency: deadCodeParseConcurrency,
40751
40934
  workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
40752
40935
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
@@ -40844,9 +41027,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40844
41027
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
40845
41028
  const supplyChainResult = yield* join(supplyChainFiber);
40846
41029
  const supplyChainCollected = supplyChainResult.diagnostics;
41030
+ const securityScanCollected = yield* join(securityScanFiber);
40847
41031
  yield* reporterService.finalize;
40848
41032
  const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
40849
41033
  ...envCollected,
41034
+ ...securityScanCollected,
40850
41035
  ...supplyChainCollected,
40851
41036
  ...lintCollected,
40852
41037
  ...deadCodeCollected
@@ -40872,6 +41057,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40872
41057
  metadata: scoreMetadata
40873
41058
  });
40874
41059
  const lintPartialFailures = yield* get$2(partialFailuresRef);
41060
+ const securityScanFailed = yield* get$2(securityScanFailedRef);
40875
41061
  return {
40876
41062
  project,
40877
41063
  userConfig: resolvedConfig.config,
@@ -40892,8 +41078,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40892
41078
  scanElapsedMilliseconds,
40893
41079
  scanConcurrency,
40894
41080
  supplyChainOverlapTimedOut: supplyChainResult.timedOut,
41081
+ securityScanFailed,
40895
41082
  lintCacheHitFileCount,
40896
- lintCacheTotalFileCount
41083
+ lintCacheTotalFileCount,
41084
+ suppressedRuleCounts: transform.summarizeSuppressions()
40897
41085
  };
40898
41086
  }).pipe(withSpan("runInspect", { attributes: {
40899
41087
  "inspect.directory": input.directory,
@@ -41298,6 +41486,61 @@ const clearAutoSuppressionCaches = () => {};
41298
41486
  * untouched so the loader sees the same plain object either way.
41299
41487
  */
41300
41488
  const defineConfig = (config) => config;
41489
+ const AI_TRAINING_ENV_VARS = [
41490
+ ["HF_DATASETS_CACHE", "huggingface"],
41491
+ ["HF_HOME", "huggingface"],
41492
+ ["HUGGINGFACE_HUB_CACHE", "huggingface"],
41493
+ ["SPACE_ID", "huggingface-spaces"],
41494
+ ["CUDA_VISIBLE_DEVICES", "cuda"],
41495
+ ["NVIDIA_VISIBLE_DEVICES", "nvidia"],
41496
+ ["ROCR_VISIBLE_DEVICES", "rocm"],
41497
+ ["TPU_NAME", "google-tpu"],
41498
+ ["WANDB_RUN_ID", "wandb"],
41499
+ ["WANDB_SWEEP_ID", "wandb"],
41500
+ ["MLFLOW_RUN_ID", "mlflow"],
41501
+ ["MLFLOW_TRACKING_URI", "mlflow"],
41502
+ ["COMET_EXPERIMENT_KEY", "comet"],
41503
+ ["NEPTUNE_RUN_ID", "neptune"],
41504
+ ["CLEARML_TASK_ID", "clearml"],
41505
+ ["DVC_STAGE", "dvc"],
41506
+ ["RAY_WORKER_PROCESS", "ray"],
41507
+ ["RAY_ADDRESS", "ray"],
41508
+ ["MUJOCO_GL", "mujoco"],
41509
+ ["MUJOCO_PATH", "mujoco"],
41510
+ ["GYM_DISABLE_ENV_CHECKER", "gymnasium"],
41511
+ ["SM_TRAINING_ENV", "sagemaker"],
41512
+ ["TRAINING_JOB_ARN", "sagemaker"],
41513
+ ["SAGEMAKER_BASE_DIR", "sagemaker"],
41514
+ ["AZUREML_RUN_ID", "azure-ml"],
41515
+ ["AZURE_ML_MODEL_DIR", "azure-ml"],
41516
+ ["CLOUD_ML_PROJECT_ID", "vertex-ai"],
41517
+ ["VERTEX_AI_LOG_LEVEL", "vertex-ai"],
41518
+ ["DET_MASTER", "determined-ai"],
41519
+ ["LIGHTNING_USER_ID", "lightning-ai"],
41520
+ ["FLYTE_INTERNAL_EXECUTION_ID", "flyte"],
41521
+ ["ARGO_WORKFLOW_NAME", "argo-workflows"],
41522
+ ["KFP_POD_NAME", "kubeflow-pipelines"],
41523
+ ["KAGGLE_KERNEL_RUN_TYPE", "kaggle"],
41524
+ ["COLAB_BACKEND_VERSION", "google-colab"],
41525
+ ["DATABRICKS_RUNTIME_VERSION", "databricks"],
41526
+ ["DAYTONA_WS_ID", "daytona"],
41527
+ ["DAYTONA_WS_NAME", "daytona"],
41528
+ ["E2B_SANDBOX_ID", "e2b"],
41529
+ ["MODAL_FUNCTION_ID", "modal"],
41530
+ ["MODAL_TASK_ID", "modal"],
41531
+ ["RUNPOD_POD_ID", "runpod"],
41532
+ ["REPLICATE_USERNAME", "replicate"],
41533
+ ["VAST_CONTAINERLABEL", "vast-ai"],
41534
+ ["HARBOR_URL", "harbor"],
41535
+ ["HARBOR_HOSTNAME", "harbor"],
41536
+ ["SWE_BENCH_TASK", "swe-bench"],
41537
+ ["SWEBENCH_TASK", "swe-bench"],
41538
+ ["SWE_AGENT_MODEL", "swe-agent"]
41539
+ ];
41540
+ const detectAiTrainingEnvironment = () => {
41541
+ for (const [envVar, label] of AI_TRAINING_ENV_VARS) if (process.env[envVar]) return label;
41542
+ return null;
41543
+ };
41301
41544
  const unionValues = (baseValues, overrideValues) => [...new Set([...baseValues, ...overrideValues])];
41302
41545
  const mergeIgnores = (baseIgnore, overrideIgnore) => {
41303
41546
  const mergedIgnore = {
@@ -41342,6 +41585,12 @@ const mergeReactDoctorConfigs = (baseConfig, overrideConfig) => {
41342
41585
  };
41343
41586
  //#endregion
41344
41587
  //#region ../api/dist/index.js
41588
+ let didWarnAiTraining = false;
41589
+ const warnIfAiTrainingEnvironment = () => {
41590
+ if (didWarnAiTraining || detectAiTrainingEnvironment() === null) return;
41591
+ didWarnAiTraining = true;
41592
+ 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.");
41593
+ };
41345
41594
  const buildDiagnoseLayer = (config, configOverrideTarget) => {
41346
41595
  const configLayer = configOverrideTarget === void 0 ? Config.layerNode : Config.layerOf({
41347
41596
  config,
@@ -41379,6 +41628,7 @@ const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
41379
41628
  };
41380
41629
  };
41381
41630
  const diagnoseDirectory = async (directory, options) => {
41631
+ warnIfAiTrainingEnvironment();
41382
41632
  const startTime = globalThis.performance.now();
41383
41633
  const scanTarget = await resolveScanTarget(directory);
41384
41634
  return outputToDiagnoseResult(await runPromise(restoreLegacyThrow(buildInspectProgram(scanTarget, options).pipe(provide(buildDiagnoseLayer(scanTarget.userConfig)), provide(layerOtlp)))), globalThis.performance.now() - startTime);
@@ -41425,6 +41675,7 @@ const diagnoseProject = async (projectDefinition, baseOptions, batchConfig) => {
41425
41675
  }
41426
41676
  };
41427
41677
  const diagnoseProjectBatch = async (input) => {
41678
+ warnIfAiTrainingEnvironment();
41428
41679
  const startTime = globalThis.performance.now();
41429
41680
  const { projects, concurrency, config: batchConfig, ...baseOptions } = input;
41430
41681
  const projectResults = await mapWithConcurrency(projects, concurrency ?? 4, (projectDefinition) => diagnoseProject(projectDefinition, baseOptions, batchConfig));
@@ -41469,4 +41720,4 @@ const toJsonReport = (result, options) => buildJsonReport({
41469
41720
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
41470
41721
 
41471
41722
  //# sourceMappingURL=index.js.map
41472
- //# debugId=33508ee6-c977-5b5f-8585-9939928ce74d
41723
+ //# debugId=83529d8f-2ca3-50ba-a3b9-44622f913549