react-doctor 0.5.8-dev.c2ce298 → 0.5.8-dev.d241e51

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]="fa4e432e-2004-53cc-816a-025aebf1c752")}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]="20ae6ec9-b785-55cb-96d6-9c8b9fe3a35b")}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;
@@ -34517,7 +34522,7 @@ const tokenizeRuleList = (ruleList) => {
34517
34522
  const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34518
34523
  const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34519
34524
  const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34520
- 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}\`).`;
34521
34526
  const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
34522
34527
  const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34523
34528
  const candidates = [{
@@ -34997,6 +35002,7 @@ const collectStringSet = (values) => {
34997
35002
  if (!Array.isArray(values)) return /* @__PURE__ */ new Set();
34998
35003
  return new Set(values.filter((value) => typeof value === "string"));
34999
35004
  };
35005
+ const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
35000
35006
  /**
35001
35007
  * Pre-compiles every stateful filter and returns a single
35002
35008
  * `apply(diagnostic)` closure that runs (in order):
@@ -35030,11 +35036,28 @@ const buildDiagnosticPipeline = (input) => {
35030
35036
  const compiledOverrides = compileIgnoreOverrides(userConfig);
35031
35037
  const textComponentNames = collectStringSet(userConfig?.textComponents);
35032
35038
  const rawTextWrapperComponentNames = collectStringSet(userConfig?.rawTextWrapperComponents);
35039
+ const runtimeGlobalNames = collectStringSet(userConfig?.runtimeGlobals);
35033
35040
  const hasTextComponents = textComponentNames.size > 0;
35034
35041
  const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
35042
+ const hasRuntimeGlobals = runtimeGlobalNames.size > 0;
35035
35043
  const fileLinesCache = /* @__PURE__ */ new Map();
35036
35044
  const fileContextCache = /* @__PURE__ */ new Map();
35037
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
+ };
35038
35061
  const isLibraryFile = (filePath) => {
35039
35062
  let cached = libraryFileCache.get(filePath);
35040
35063
  if (cached === void 0) {
@@ -35083,50 +35106,60 @@ const buildDiagnosticPipeline = (input) => {
35083
35106
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
35084
35107
  return false;
35085
35108
  };
35086
- return { apply: (diagnostic) => {
35087
- if (shouldAutoSuppress(diagnostic)) return null;
35088
- let current = diagnostic;
35089
- let explicitSeverityOverride;
35090
- let explicitRuleOverride;
35091
- if (severityControls) {
35092
- const { ruleKey, category } = getDiagnosticRuleIdentity(current);
35093
- explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey }, severityControls);
35094
- explicitSeverityOverride = resolveRuleSeverityOverride({
35095
- ruleKey,
35096
- category
35097
- }, severityControls);
35098
- if (explicitSeverityOverride === "off") return null;
35099
- if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
35100
- }
35101
- if (explicitRuleOverride === void 0) {
35102
- if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
35103
- }
35104
- if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
35105
- if (userConfig) {
35106
- if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return null;
35107
- if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
35108
- if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return null;
35109
- if (isRnRawTextSuppressedByConfig(current)) return null;
35110
- }
35111
- if (respectInlineDisables && current.line > 0) {
35112
- const lines = getFileLines(current.filePath);
35113
- if (lines) {
35114
- const ruleIdentifier = `${current.plugin}/${current.rule}`;
35115
- const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
35116
- if (evaluation.isSuppressed) return null;
35117
- if (evaluation.nearMissHint) current = {
35118
- ...current,
35119
- suppressionHint: evaluation.nearMissHint
35120
- };
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);
35121
35130
  }
35122
- }
35123
- const fileContext = getFileContext(current.filePath);
35124
- if (fileContext !== "production") current = {
35125
- ...current,
35126
- fileContext
35127
- };
35128
- return current;
35129
- } };
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
+ };
35130
35163
  };
35131
35164
  const OxlintUnavailableKind = Literals(["binary-not-found", "native-binding-missing"]);
35132
35165
  var OxlintUnavailable = class extends TaggedErrorClass()("OxlintUnavailable", {
@@ -35368,7 +35401,7 @@ const resolveScanConcurrency = (requested) => {
35368
35401
  if (!Number.isFinite(requested) || requested < 1) return 1;
35369
35402
  return Math.min(Math.floor(requested), 32);
35370
35403
  };
35371
- const readSystemFacts = () => ({
35404
+ const readSystemFacts$1 = () => ({
35372
35405
  availableCores: os.availableParallelism(),
35373
35406
  totalMemoryBytes: os.totalmem(),
35374
35407
  cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
@@ -35389,7 +35422,7 @@ const readSystemFacts = () => ({
35389
35422
  * `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
35390
35423
  * limited, and ceiling cases without mocking `os` or the filesystem.
35391
35424
  */
35392
- const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
35425
+ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
35393
35426
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
35394
35427
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
35395
35428
  return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
@@ -35430,13 +35463,7 @@ var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { de
35430
35463
  * without recompiling react-doctor. Tests override via
35431
35464
  * `Layer.succeed(SupplyChainOverlapTimeoutMs, ...)`.
35432
35465
  */
35433
- var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => {
35434
- const raw = process.env["REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS"];
35435
- if (raw === void 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35436
- const parsed = Number(raw);
35437
- if (!Number.isFinite(parsed) || parsed <= 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35438
- return parsed;
35439
- } }) {};
35466
+ var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS", SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS) }) {};
35440
35467
  /**
35441
35468
  * Hard cap on combined stdout+stderr bytes per oxlint batch. The
35442
35469
  * subprocess gets SIGKILL'd if it produces more; the recovery path
@@ -35580,6 +35607,13 @@ const BOOLEAN_FIELD_NAMES = [
35580
35607
  "adoptExistingLintConfig"
35581
35608
  ];
35582
35609
  const STRING_FIELD_NAMES = ["rootDir"];
35610
+ const STRING_ARRAY_FIELD_NAMES = [
35611
+ "projects",
35612
+ "textComponents",
35613
+ "rawTextWrapperComponents",
35614
+ "runtimeGlobals",
35615
+ "serverAuthFunctionNames"
35616
+ ];
35583
35617
  const SURFACE_CONTROL_FIELD_NAMES = [
35584
35618
  "includeTags",
35585
35619
  "excludeTags",
@@ -35681,6 +35715,7 @@ const validateConfigTypes = (config) => {
35681
35715
  const validated = { ...config };
35682
35716
  for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
35683
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));
35684
35719
  applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
35685
35720
  for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
35686
35721
  applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
@@ -36915,7 +36950,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
36915
36950
  }
36916
36951
  return true;
36917
36952
  };
36918
- const checkSecurityScan = (rootDirectory, options = {}) => {
36953
+ const yieldToEventLoop = () => new Promise((resolve) => {
36954
+ setImmediate(resolve);
36955
+ });
36956
+ const createSecurityScanSession = (rootDirectory, options) => {
36919
36957
  const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
36920
36958
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
36921
36959
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
@@ -36930,7 +36968,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36930
36968
  committedFilesOnly: rule.committedFilesOnly === true
36931
36969
  }];
36932
36970
  });
36933
- if (enabledScanRules.length === 0) return [];
36971
+ if (enabledScanRules.length === 0) return null;
36934
36972
  const diagnostics = [];
36935
36973
  const seen = /* @__PURE__ */ new Set();
36936
36974
  const gitIgnoredCache = /* @__PURE__ */ new Map();
@@ -36942,15 +36980,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36942
36980
  }
36943
36981
  return status === true;
36944
36982
  };
36945
- for (const file of collectSecurityScanFiles(rootDirectory)) for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36946
- if (committedFilesOnly && isFileGitIgnored(file)) continue;
36947
- const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36948
- const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36949
- if (seen.has(key)) continue;
36950
- seen.add(key);
36951
- 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
+ }
36952
37009
  }
36953
- return diagnostics;
37010
+ return session.diagnostics;
36954
37011
  };
36955
37012
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
36956
37013
  let p = process || {}, argv = p.argv || [], env = p.env || {};
@@ -37167,6 +37224,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
37167
37224
  return [...seen].filter((pattern) => pattern.length > 0);
37168
37225
  };
37169
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
+ };
37170
37295
  /**
37171
37296
  * Resolves a path to its canonical, symlink-free form, falling back to
37172
37297
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -37191,6 +37316,11 @@ const toCanonicalPath = (filePath) => {
37191
37316
  };
37192
37317
  const DEAD_CODE_PLUGIN = "deslop";
37193
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
+ ]);
37194
37324
  const DEAD_CODE_WORKER_SCRIPT = `
37195
37325
  const inputChunks = [];
37196
37326
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -37457,14 +37587,17 @@ const checkDeadCode = async (options) => {
37457
37587
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37458
37588
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37459
37589
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37460
- const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37461
- rootDirectory,
37462
- entryPatterns,
37463
- tsConfigPath: resolveTsConfigPath(rootDirectory),
37464
- ignorePatterns,
37465
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37466
- parseConcurrency: options.parseConcurrency
37467
- }), 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());
37468
37601
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
37469
37602
  const diagnostics = [];
37470
37603
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -37493,6 +37626,7 @@ const checkDeadCode = async (options) => {
37493
37626
  });
37494
37627
  }
37495
37628
  for (const unusedDependency of result.unusedDependencies) {
37629
+ if (REACT_DOCTOR_TOOLCHAIN_PACKAGES.has(unusedDependency.name)) continue;
37496
37630
  const label = unusedDependency.isDevDependency ? "devDependency" : "dependency";
37497
37631
  diagnostics.push({
37498
37632
  filePath: "package.json",
@@ -37671,7 +37805,6 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37671
37805
  return yield* tryPromise({
37672
37806
  try: (signal) => checkDeadCode({
37673
37807
  rootDirectory: input.rootDirectory,
37674
- userConfig: input.userConfig,
37675
37808
  parseConcurrency: input.parseConcurrency,
37676
37809
  workerTimeoutMs: input.workerTimeoutMs,
37677
37810
  abortSignal: signal
@@ -37794,6 +37927,11 @@ const isSafeGitRevision = (candidate) => {
37794
37927
  if (candidate.includes("..") || candidate.includes("@{")) return false;
37795
37928
  return SAFE_GIT_REVISION_PATTERN.test(candidate);
37796
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
+ };
37797
37935
  /**
37798
37936
  * Splits a git revision range into its endpoints: three-dot `A...B`
37799
37937
  * (symmetric, merge-base) or two-dot `A..B` (direct). Returns `null`
@@ -37865,43 +38003,47 @@ var Git = class Git extends Service()("react-doctor/Git") {
37865
38003
  * reason: GitInvocationFailed })` so the rest of the codebase
37866
38004
  * sees a single failure channel.
37867
38005
  */
37868
- const runCommand = (input) => scoped(gen(function* () {
37869
- const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37870
- cwd: input.directory,
37871
- env: input.env,
37872
- extendEnv: true
37873
- }));
37874
- const maxStdoutBytes = input.maxStdoutBytes;
37875
- const stdoutByteCount = yield* make$13(0);
37876
- const [stdout, stderr, status] = yield* all([
37877
- 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({
37878
- args: [...input.args],
37879
- directory: input.directory,
37880
- cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
37881
- }) })) : void_)))))),
37882
- mkString(decodeText(handle.stderr)),
37883
- handle.exitCode
37884
- ], { concurrency: 3 });
37885
- return {
37886
- status,
37887
- stdout,
37888
- stderr
37889
- };
37890
- })).pipe(catchTag$1("PlatformError", (cause) => {
37891
- if (input.command !== "git") return succeed$2({
38006
+ const runCommand = (input) => {
38007
+ const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
37892
38008
  status: 127,
37893
38009
  stdout: "",
37894
38010
  stderr: String(cause)
37895
- });
37896
- return new ReactDoctorError({ reason: new GitInvocationFailed({
38011
+ }) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37897
38012
  args: [...input.args],
37898
38013
  directory: input.directory,
37899
38014
  cause
37900
- }) });
37901
- }), withSpan("git.exec", { attributes: {
37902
- "git.command": input.command,
37903
- "git.subcommand": input.args[0] ?? ""
37904
- } }));
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
+ };
37905
38047
  const runGit = (directory, args) => runCommand({
37906
38048
  command: "git",
37907
38049
  args,
@@ -37934,7 +38076,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37934
38076
  "rev-parse",
37935
38077
  "--verify",
37936
38078
  branch
37937
- ]).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)));
37938
38080
  const headSha = (directory) => runGit(directory, ["rev-parse", "HEAD"]).pipe(map$3((result) => result.status === 0 ? trimOrNull(result.stdout) : null));
37939
38081
  const mergeBase = (input) => isSafeGitRevision(input.ref) ? runGit(input.directory, [
37940
38082
  "merge-base",
@@ -38044,6 +38186,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38044
38186
  if (resolvedCurrentBranch === null && explicitBaseBranch === void 0) return null;
38045
38187
  const baseBranch = explicitBaseBranch ?? (yield* defaultBranch(directory));
38046
38188
  if (baseBranch === null) return null;
38189
+ if (!isSafeGitRevision(baseBranch)) return null;
38047
38190
  if (explicitBaseBranch !== void 0) {
38048
38191
  if (!(yield* branchExists(directory, explicitBaseBranch))) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchMissing({ branch: explicitBaseBranch }) }));
38049
38192
  }
@@ -38148,7 +38291,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38148
38291
  ]);
38149
38292
  if (result.status !== 0) return null;
38150
38293
  return parseChangedLineRanges(result.stdout);
38151
- }).pipe(withSpan("Git.changedLineRanges"))
38294
+ }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
38152
38295
  });
38153
38296
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
38154
38297
  /**
@@ -38394,7 +38537,7 @@ const normalizeConfigForHash = (config) => {
38394
38537
  if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
38395
38538
  return clone;
38396
38539
  };
38397
- 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");
38398
38541
  /**
38399
38542
  * Loads a plugin module via the local require resolver and extracts
38400
38543
  * `(name, ruleNames)` from either `module.exports.meta + rules` or
@@ -38672,24 +38815,38 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38672
38815
  updatedAtMs: Date.now(),
38673
38816
  files: Object.fromEntries(cappedEntries)
38674
38817
  };
38675
- const keptHashes = Object.entries(rulesets).sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8).map(([hash]) => hash);
38676
- const prunedRulesets = {};
38677
- for (const hash of keptHashes) prunedRulesets[hash] = rulesets[hash];
38678
38818
  atomicWriteJson(cacheFilePath, {
38679
38819
  version: 1,
38680
- 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))
38681
38821
  });
38682
38822
  }
38683
38823
  };
38684
38824
  };
38685
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
+ };
38686
38843
  const TOOLCHAIN_PACKAGE_SPECIFIERS = [
38687
38844
  "oxlint/package.json",
38688
38845
  "oxlint-plugin-react-doctor/package.json",
38689
38846
  "eslint-plugin-react-hooks/package.json"
38690
38847
  ];
38691
- const resolveOxlintToolchainVersions = () => {
38692
- const versions = [`node=${process.version}`];
38848
+ const resolveOxlintToolchainVersions = (nodeBinaryPath = process.execPath) => {
38849
+ const versions = [`node=${resolveChildNodeVersion(nodeBinaryPath)}`];
38693
38850
  for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
38694
38851
  const packageJson = bundledRequire(specifier);
38695
38852
  const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
@@ -39223,11 +39380,12 @@ const REACT_COMPILER_TITLE = "React Compiler can't optimize this";
39223
39380
  const REACT_COMPILER_TODO_TITLE = "React Compiler doesn't support this syntax";
39224
39381
  const REACT_COMPILER_IMPACT = "This component misses React Compiler's automatic memoization & re-renders more than it should";
39225
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`.";
39226
39384
  const REACT_COMPILER_GENERIC_MESSAGE = `${REACT_COMPILER_IMPACT}. ${REACT_COMPILER_ACTION}`;
39227
- const buildReactCompilerMessage = (reasonSummary) => {
39385
+ const buildReactCompilerMessage = (reasonSummary, action = REACT_COMPILER_ACTION) => {
39228
39386
  const normalizedSummary = reasonSummary.replace(TRAILING_PERIOD_PATTERN, "");
39229
- if (!normalizedSummary) return REACT_COMPILER_GENERIC_MESSAGE;
39230
- return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${REACT_COMPILER_ACTION}`;
39387
+ if (!normalizedSummary) return `${REACT_COMPILER_IMPACT}. ${action}`;
39388
+ return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${action}`;
39231
39389
  };
39232
39390
  const PLUGIN_CATEGORY_MAP = {
39233
39391
  react: "Bugs",
@@ -39276,7 +39434,7 @@ const resolveCleanedDiagnostic = (message, help, plugin, rule, project) => {
39276
39434
  const [reasonSummary = "", ...reasonDetailLines] = bailoutReason.split("\n");
39277
39435
  const reasonDetail = reasonDetailLines.join("\n").trim();
39278
39436
  return {
39279
- message: buildReactCompilerMessage(reasonSummary.trim()),
39437
+ message: buildReactCompilerMessage(reasonSummary.trim(), rule === "incompatible-library" ? REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION : REACT_COMPILER_ACTION),
39280
39438
  help: appendReanimatedSharedValueHint(reasonDetail || help, rule, project)
39281
39439
  };
39282
39440
  }
@@ -39544,21 +39702,27 @@ const spawnLintBatches = async (input) => {
39544
39702
  const allDiagnostics = [];
39545
39703
  const droppedFiles = [];
39546
39704
  let firstDropReason = null;
39547
- const splitDeadlineMs = Date.now() + splitTotalBudgetMs;
39548
- const spawnLintBatch = async (batch, depth) => {
39705
+ const spawnLintBatch = async (batch, depth, splitBudget) => {
39549
39706
  const batchArgs = [...baseArgs, ...batch];
39550
39707
  try {
39551
39708
  return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
39552
39709
  } catch (error) {
39553
39710
  if (!isSplittableReactDoctorError(error)) throw error;
39554
- const splitBudgetExhausted = Date.now() >= splitDeadlineMs || depth >= splitMaxDepth;
39555
- 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) {
39556
39715
  droppedFiles.push(...batch);
39557
- 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
+ }
39558
39722
  return [];
39559
39723
  }
39560
39724
  const splitIndex = Math.ceil(batch.length / 2);
39561
- 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)];
39562
39726
  }
39563
39727
  };
39564
39728
  let startedFileCount = 0;
@@ -39575,7 +39739,7 @@ const spawnLintBatches = async (input) => {
39575
39739
  try {
39576
39740
  const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
39577
39741
  startedFileCount += batch.length;
39578
- const batchDiagnostics = await spawnLintBatch(batch, 0);
39742
+ const batchDiagnostics = await spawnLintBatch(batch, 0, { deadlineMs: null });
39579
39743
  scannedFileCount += batch.length;
39580
39744
  if (onFileProgress) {
39581
39745
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
@@ -39649,7 +39813,14 @@ const resolveReactDoctorCacheDir = (projectDirectory) => {
39649
39813
  if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
39650
39814
  const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
39651
39815
  if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
39652
- 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
+ }
39653
39824
  };
39654
39825
  const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
39655
39826
  /**
@@ -39817,7 +39988,7 @@ const runOxlint = async (options) => {
39817
39988
  extendsPaths: [],
39818
39989
  ruleSelection: "cacheable"
39819
39990
  }),
39820
- toolchainVersions: resolveOxlintToolchainVersions(),
39991
+ toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
39821
39992
  ignorePatterns: combinedPatterns,
39822
39993
  tsconfigContent
39823
39994
  });
@@ -39831,7 +40002,7 @@ const runOxlint = async (options) => {
39831
40002
  missFiles.push(candidateFile);
39832
40003
  continue;
39833
40004
  }
39834
- const cacheKey = `${candidateFile.replaceAll("\\", "/")}${contentHash}`;
40005
+ const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
39835
40006
  cacheKeyByFile.set(candidateFile, cacheKey);
39836
40007
  const cachedDiagnostics = cache.lookup(cacheKey);
39837
40008
  if (cachedDiagnostics === null) missFiles.push(candidateFile);
@@ -40147,7 +40318,7 @@ const ScoreApiResponseSchema = Struct({
40147
40318
  })))
40148
40319
  });
40149
40320
  const parseScoreResult = (value) => getOrNull(decodeUnknownOption(ScoreApiResponseSchema)(value));
40150
- 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);
40151
40322
  const isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
40152
40323
  const describeFailure = (error) => {
40153
40324
  if (isAbortError(error)) return `timed out after ${FETCH_TIMEOUT_MS / 1e3}s`;
@@ -40451,7 +40622,10 @@ const ALERT_SEVERITY_RANK = {
40451
40622
  medium: 2,
40452
40623
  low: 1
40453
40624
  };
40454
- 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
+ };
40455
40629
  const displaySeverity = (severity) => {
40456
40630
  const normalized = sanitizeTerminalText(severity.toLowerCase());
40457
40631
  return normalized === "middle" ? "medium" : normalized;
@@ -40468,7 +40642,7 @@ const ALERT_TYPE_LABELS = {
40468
40642
  typosquat: "possible typosquat"
40469
40643
  };
40470
40644
  const humanizeAlertType = (type) => type.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase().trim();
40471
- 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));
40472
40646
  const summarizeAlertNote = (note) => {
40473
40647
  const collapsed = sanitizeTerminalText(note.replace(/\s+/g, " ").trim());
40474
40648
  const firstSentence = collapsed.split(/(?<=\.)\s/)[0] || collapsed;
@@ -40563,7 +40737,10 @@ const checkSupplyChain = (input) => gen(function* () {
40563
40737
  * outage.
40564
40738
  */
40565
40739
  var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
40566
- 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"))) }));
40567
40744
  static layerOf = (diagnostics) => succeed$3(SupplyChain, SupplyChain.of({ run: () => fromIterable$1(diagnostics) }));
40568
40745
  };
40569
40746
  const getObjectProperty = (value, propertyName) => {
@@ -40625,7 +40802,10 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40625
40802
  * diagnostics).
40626
40803
  * 2. beforeLint hook (e.g. CLI renders the project-detection block)
40627
40804
  * 3. environment checks (reduced-motion + pnpm hardening +
40628
- * 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.
40629
40809
  * 4. The supply-chain check (Socket.dev) is forked onto a background
40630
40810
  * fiber so its ~100% network-bound time overlaps the ~100%
40631
40811
  * CPU/subprocess-bound lint pass below, collapsing two serial
@@ -40645,7 +40825,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40645
40825
  * order, so terminal output is identical either way; supply-chain
40646
40826
  * rides alongside without a spinner.
40647
40827
  * 6. Join the supply-chain fiber, then assemble the diagnostics in a
40648
- * 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
40649
40829
  * byte-identical regardless of which fiber settled first. The
40650
40830
  * viewer-permission fiber is joined later, during score-metadata
40651
40831
  * assembly (it feeds score metadata, not diagnostics). The per-element
@@ -40706,12 +40886,13 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40706
40886
  ...checkPnpmHardening(scanDirectory),
40707
40887
  ...checkReactServerComponentsAdvisory(scanDirectory, project),
40708
40888
  ...checkExpoProject(scanDirectory, project),
40709
- ...checkReactNativeProject(scanDirectory, project),
40710
- ...checkSecurityScan(scanDirectory, {
40711
- project,
40712
- ignoredTags: input.ignoredTags
40713
- })
40889
+ ...checkReactNativeProject(scanDirectory, project)
40714
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")));
40715
40896
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
40716
40897
  const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
40717
40898
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
@@ -40749,7 +40930,6 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40749
40930
  const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
40750
40931
  const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
40751
40932
  rootDirectory: scanDirectory,
40752
- userConfig: resolvedConfig.config,
40753
40933
  parseConcurrency: deadCodeParseConcurrency,
40754
40934
  workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
40755
40935
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
@@ -40847,9 +41027,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40847
41027
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
40848
41028
  const supplyChainResult = yield* join(supplyChainFiber);
40849
41029
  const supplyChainCollected = supplyChainResult.diagnostics;
41030
+ const securityScanCollected = yield* join(securityScanFiber);
40850
41031
  yield* reporterService.finalize;
40851
41032
  const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
40852
41033
  ...envCollected,
41034
+ ...securityScanCollected,
40853
41035
  ...supplyChainCollected,
40854
41036
  ...lintCollected,
40855
41037
  ...deadCodeCollected
@@ -40875,6 +41057,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40875
41057
  metadata: scoreMetadata
40876
41058
  });
40877
41059
  const lintPartialFailures = yield* get$2(partialFailuresRef);
41060
+ const securityScanFailed = yield* get$2(securityScanFailedRef);
40878
41061
  return {
40879
41062
  project,
40880
41063
  userConfig: resolvedConfig.config,
@@ -40895,8 +41078,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40895
41078
  scanElapsedMilliseconds,
40896
41079
  scanConcurrency,
40897
41080
  supplyChainOverlapTimedOut: supplyChainResult.timedOut,
41081
+ securityScanFailed,
40898
41082
  lintCacheHitFileCount,
40899
- lintCacheTotalFileCount
41083
+ lintCacheTotalFileCount,
41084
+ suppressedRuleCounts: transform.summarizeSuppressions()
40900
41085
  };
40901
41086
  }).pipe(withSpan("runInspect", { attributes: {
40902
41087
  "inspect.directory": input.directory,
@@ -41301,6 +41486,61 @@ const clearAutoSuppressionCaches = () => {};
41301
41486
  * untouched so the loader sees the same plain object either way.
41302
41487
  */
41303
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
+ };
41304
41544
  const unionValues = (baseValues, overrideValues) => [...new Set([...baseValues, ...overrideValues])];
41305
41545
  const mergeIgnores = (baseIgnore, overrideIgnore) => {
41306
41546
  const mergedIgnore = {
@@ -41345,6 +41585,12 @@ const mergeReactDoctorConfigs = (baseConfig, overrideConfig) => {
41345
41585
  };
41346
41586
  //#endregion
41347
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
+ };
41348
41594
  const buildDiagnoseLayer = (config, configOverrideTarget) => {
41349
41595
  const configLayer = configOverrideTarget === void 0 ? Config.layerNode : Config.layerOf({
41350
41596
  config,
@@ -41382,6 +41628,7 @@ const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
41382
41628
  };
41383
41629
  };
41384
41630
  const diagnoseDirectory = async (directory, options) => {
41631
+ warnIfAiTrainingEnvironment();
41385
41632
  const startTime = globalThis.performance.now();
41386
41633
  const scanTarget = await resolveScanTarget(directory);
41387
41634
  return outputToDiagnoseResult(await runPromise(restoreLegacyThrow(buildInspectProgram(scanTarget, options).pipe(provide(buildDiagnoseLayer(scanTarget.userConfig)), provide(layerOtlp)))), globalThis.performance.now() - startTime);
@@ -41428,6 +41675,7 @@ const diagnoseProject = async (projectDefinition, baseOptions, batchConfig) => {
41428
41675
  }
41429
41676
  };
41430
41677
  const diagnoseProjectBatch = async (input) => {
41678
+ warnIfAiTrainingEnvironment();
41431
41679
  const startTime = globalThis.performance.now();
41432
41680
  const { projects, concurrency, config: batchConfig, ...baseOptions } = input;
41433
41681
  const projectResults = await mapWithConcurrency(projects, concurrency ?? 4, (projectDefinition) => diagnoseProject(projectDefinition, baseOptions, batchConfig));
@@ -41472,4 +41720,4 @@ const toJsonReport = (result, options) => buildJsonReport({
41472
41720
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
41473
41721
 
41474
41722
  //# sourceMappingURL=index.js.map
41475
- //# debugId=fa4e432e-2004-53cc-816a-025aebf1c752
41723
+ //# debugId=20ae6ec9-b785-55cb-96d6-9c8b9fe3a35b