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/lsp.js CHANGED
@@ -6,7 +6,7 @@ import os from "node:os";
6
6
  import * as Path from "node:path";
7
7
  import path from "node:path";
8
8
  import * as NodeChildProcess from "node:child_process";
9
- import { spawn, spawnSync } from "node:child_process";
9
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
10
10
  import * as ts from "typescript";
11
11
  import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
12
12
  import { parseJSON5 } from "confbox";
@@ -6021,7 +6021,7 @@ const composePassthrough = /* @__PURE__ */ dual(2, (left, right) => (input) => {
6021
6021
  * @since 2.0.0
6022
6022
  */
6023
6023
  const Scheduler = /* @__PURE__ */ Reference("effect/Scheduler", { defaultValue: () => new MixedScheduler() });
6024
- const setImmediate = "setImmediate" in globalThis ? (f) => {
6024
+ const setImmediate$1 = "setImmediate" in globalThis ? (f) => {
6025
6025
  const timer = globalThis.setImmediate(f);
6026
6026
  return () => globalThis.clearImmediate(timer);
6027
6027
  } : (f) => {
@@ -6065,7 +6065,7 @@ var PriorityBuckets = class {
6065
6065
  var MixedScheduler = class {
6066
6066
  executionMode;
6067
6067
  setImmediate;
6068
- constructor(executionMode = "async", setImmediateFn = setImmediate) {
6068
+ constructor(executionMode = "async", setImmediateFn = setImmediate$1) {
6069
6069
  this.executionMode = executionMode;
6070
6070
  this.setImmediate = setImmediateFn;
6071
6071
  }
@@ -6090,7 +6090,7 @@ var MixedSchedulerDispatcher = class {
6090
6090
  tasks = /* @__PURE__ */ new PriorityBuckets();
6091
6091
  running = void 0;
6092
6092
  setImmediate;
6093
- constructor(setImmediateFn = setImmediate) {
6093
+ constructor(setImmediateFn = setImmediate$1) {
6094
6094
  this.setImmediate = setImmediateFn;
6095
6095
  }
6096
6096
  /**
@@ -33730,6 +33730,10 @@ const MILLISECONDS_PER_SECOND = 1e3;
33730
33730
  const SCORE_API_URL = "https://www.react.doctor/api/score";
33731
33731
  const FETCH_TIMEOUT_MS = 1e4;
33732
33732
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
33733
+ const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
33734
+ const SPAWN_ARGS_MAX_LENGTH_CHARS_POSIX = 15e5;
33735
+ const SPAWN_ARGS_MAX_LENGTH_CHARS_DARWIN = 8e5;
33736
+ const NODE_VERSION_PROBE_TIMEOUT_MS = 5e3;
33733
33737
  const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
33734
33738
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
33735
33739
  const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
@@ -33810,6 +33814,7 @@ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
33810
33814
  const LINT_PHASE_TIMEOUT_MS = 3e5;
33811
33815
  const SCAN_TOTAL_DEADLINE_MS = 9e5;
33812
33816
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
33817
+ const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
33813
33818
  const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
33814
33819
  const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
33815
33820
  const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
@@ -34546,7 +34551,10 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
34546
34551
  NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
34547
34552
  }
34548
34553
  const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
34549
- const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
34554
+ const canonicalizeRuleKey = (ruleKey) => {
34555
+ const nativeRuleKey = LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey];
34556
+ return typeof nativeRuleKey === "string" ? nativeRuleKey : ruleKey;
34557
+ };
34550
34558
  const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
34551
34559
  const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
34552
34560
  const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
@@ -34573,7 +34581,7 @@ const tokenizeRuleList = (ruleList) => {
34573
34581
  const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34574
34582
  const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34575
34583
  const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34576
- const buildHint = (tool, token, ruleId) => `oxlint matches plugin rules only by their full name, so \`${token}\` in your ${tool}-disable comment does not silence \`${ruleId}\` — change it to \`${ruleId}\`.`;
34584
+ const buildHint = (tool, token, ruleId) => `oxlint matches plugin rules only by their full name, so \`${token}\` in your ${tool}-disable comment does not silence \`${ruleId}\` — add \`${ruleId}\` to the comment (e.g. \`${token}, ${ruleId}\`).`;
34577
34585
  const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
34578
34586
  const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34579
34587
  const candidates = [{
@@ -35049,6 +35057,7 @@ const collectStringSet = (values) => {
35049
35057
  if (!Array.isArray(values)) return /* @__PURE__ */ new Set();
35050
35058
  return new Set(values.filter((value) => typeof value === "string"));
35051
35059
  };
35060
+ const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
35052
35061
  /**
35053
35062
  * Pre-compiles every stateful filter and returns a single
35054
35063
  * `apply(diagnostic)` closure that runs (in order):
@@ -35082,11 +35091,28 @@ const buildDiagnosticPipeline = (input) => {
35082
35091
  const compiledOverrides = compileIgnoreOverrides(userConfig);
35083
35092
  const textComponentNames = collectStringSet(userConfig?.textComponents);
35084
35093
  const rawTextWrapperComponentNames = collectStringSet(userConfig?.rawTextWrapperComponents);
35094
+ const runtimeGlobalNames = collectStringSet(userConfig?.runtimeGlobals);
35085
35095
  const hasTextComponents = textComponentNames.size > 0;
35086
35096
  const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
35097
+ const hasRuntimeGlobals = runtimeGlobalNames.size > 0;
35087
35098
  const fileLinesCache = /* @__PURE__ */ new Map();
35088
35099
  const fileContextCache = /* @__PURE__ */ new Map();
35089
35100
  const libraryFileCache = /* @__PURE__ */ new Map();
35101
+ const suppressions = /* @__PURE__ */ new Map();
35102
+ const suppress = (diagnostic, source) => {
35103
+ const { ruleKey } = getDiagnosticRuleIdentity(diagnostic);
35104
+ const suppressionKey = `${ruleKey}\u0000${source}`;
35105
+ const existing = suppressions.get(suppressionKey);
35106
+ suppressions.set(suppressionKey, existing ? {
35107
+ ...existing,
35108
+ count: existing.count + 1
35109
+ } : {
35110
+ rule: ruleKey,
35111
+ source,
35112
+ count: 1
35113
+ });
35114
+ return null;
35115
+ };
35090
35116
  const isLibraryFile = (filePath) => {
35091
35117
  let cached = libraryFileCache.get(filePath);
35092
35118
  if (cached === void 0) {
@@ -35135,50 +35161,60 @@ const buildDiagnosticPipeline = (input) => {
35135
35161
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
35136
35162
  return false;
35137
35163
  };
35138
- return { apply: (diagnostic) => {
35139
- if (shouldAutoSuppress(diagnostic)) return null;
35140
- let current = diagnostic;
35141
- let explicitSeverityOverride;
35142
- let explicitRuleOverride;
35143
- if (severityControls) {
35144
- const { ruleKey, category } = getDiagnosticRuleIdentity(current);
35145
- explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey }, severityControls);
35146
- explicitSeverityOverride = resolveRuleSeverityOverride({
35147
- ruleKey,
35148
- category
35149
- }, severityControls);
35150
- if (explicitSeverityOverride === "off") return null;
35151
- if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
35152
- }
35153
- if (explicitRuleOverride === void 0) {
35154
- if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
35155
- }
35156
- if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
35157
- if (userConfig) {
35158
- if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return null;
35159
- if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
35160
- if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return null;
35161
- if (isRnRawTextSuppressedByConfig(current)) return null;
35162
- }
35163
- if (respectInlineDisables && current.line > 0) {
35164
- const lines = getFileLines(current.filePath);
35165
- if (lines) {
35166
- const ruleIdentifier = `${current.plugin}/${current.rule}`;
35167
- const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
35168
- if (evaluation.isSuppressed) return null;
35169
- if (evaluation.nearMissHint) current = {
35170
- ...current,
35171
- suppressionHint: evaluation.nearMissHint
35172
- };
35164
+ const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
35165
+ if (!hasRuntimeGlobals) return false;
35166
+ if (diagnostic.rule !== "jsx-no-undef") return false;
35167
+ const identifierMatch = JSX_NO_UNDEF_IDENTIFIER_PATTERN.exec(diagnostic.message);
35168
+ return identifierMatch !== null && runtimeGlobalNames.has(identifierMatch[1]);
35169
+ };
35170
+ return {
35171
+ apply: (diagnostic) => {
35172
+ if (shouldAutoSuppress(diagnostic)) return null;
35173
+ let current = diagnostic;
35174
+ let explicitSeverityOverride;
35175
+ let explicitRuleOverride;
35176
+ if (severityControls) {
35177
+ const { ruleKey, category } = getDiagnosticRuleIdentity(current);
35178
+ explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey }, severityControls);
35179
+ explicitSeverityOverride = resolveRuleSeverityOverride({
35180
+ ruleKey,
35181
+ category
35182
+ }, severityControls);
35183
+ if (explicitSeverityOverride === "off") return suppress(current, "config");
35184
+ if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
35173
35185
  }
35174
- }
35175
- const fileContext = getFileContext(current.filePath);
35176
- if (fileContext !== "production") current = {
35177
- ...current,
35178
- fileContext
35179
- };
35180
- return current;
35181
- } };
35186
+ if (explicitRuleOverride === void 0) {
35187
+ if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
35188
+ }
35189
+ if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
35190
+ if (userConfig) {
35191
+ if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return suppress(current, "config");
35192
+ if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
35193
+ if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return suppress(current, "override");
35194
+ if (isRnRawTextSuppressedByConfig(current)) return null;
35195
+ if (isJsxNoUndefSuppressedByConfig(current)) return null;
35196
+ }
35197
+ if (respectInlineDisables && current.line > 0) {
35198
+ const lines = getFileLines(current.filePath);
35199
+ if (lines) {
35200
+ const ruleIdentifier = `${current.plugin}/${current.rule}`;
35201
+ const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
35202
+ if (evaluation.isSuppressed) return suppress(current, "inline");
35203
+ if (evaluation.nearMissHint) current = {
35204
+ ...current,
35205
+ suppressionHint: evaluation.nearMissHint
35206
+ };
35207
+ }
35208
+ }
35209
+ const fileContext = getFileContext(current.filePath);
35210
+ if (fileContext !== "production") current = {
35211
+ ...current,
35212
+ fileContext
35213
+ };
35214
+ return current;
35215
+ },
35216
+ summarizeSuppressions: () => [...suppressions.values()]
35217
+ };
35182
35218
  };
35183
35219
  const OxlintUnavailableKind = Literals(["binary-not-found", "native-binding-missing"]);
35184
35220
  var OxlintUnavailable = class extends TaggedErrorClass()("OxlintUnavailable", {
@@ -35398,7 +35434,7 @@ const resolveScanConcurrency = (requested) => {
35398
35434
  if (!Number.isFinite(requested) || requested < 1) return 1;
35399
35435
  return Math.min(Math.floor(requested), 32);
35400
35436
  };
35401
- const readSystemFacts = () => ({
35437
+ const readSystemFacts$1 = () => ({
35402
35438
  availableCores: os.availableParallelism(),
35403
35439
  totalMemoryBytes: os.totalmem(),
35404
35440
  cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
@@ -35419,7 +35455,7 @@ const readSystemFacts = () => ({
35419
35455
  * `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
35420
35456
  * limited, and ceiling cases without mocking `os` or the filesystem.
35421
35457
  */
35422
- const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
35458
+ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
35423
35459
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
35424
35460
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
35425
35461
  return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
@@ -35460,13 +35496,7 @@ var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { de
35460
35496
  * without recompiling react-doctor. Tests override via
35461
35497
  * `Layer.succeed(SupplyChainOverlapTimeoutMs, ...)`.
35462
35498
  */
35463
- var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => {
35464
- const raw = process.env["REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS"];
35465
- if (raw === void 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35466
- const parsed = Number(raw);
35467
- if (!Number.isFinite(parsed) || parsed <= 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35468
- return parsed;
35469
- } }) {};
35499
+ var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS", SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS) }) {};
35470
35500
  /**
35471
35501
  * Hard cap on combined stdout+stderr bytes per oxlint batch. The
35472
35502
  * subprocess gets SIGKILL'd if it produces more; the recovery path
@@ -35610,6 +35640,13 @@ const BOOLEAN_FIELD_NAMES = [
35610
35640
  "adoptExistingLintConfig"
35611
35641
  ];
35612
35642
  const STRING_FIELD_NAMES = ["rootDir"];
35643
+ const STRING_ARRAY_FIELD_NAMES = [
35644
+ "projects",
35645
+ "textComponents",
35646
+ "rawTextWrapperComponents",
35647
+ "runtimeGlobals",
35648
+ "serverAuthFunctionNames"
35649
+ ];
35613
35650
  const SURFACE_CONTROL_FIELD_NAMES = [
35614
35651
  "includeTags",
35615
35652
  "excludeTags",
@@ -35711,6 +35748,7 @@ const validateConfigTypes = (config) => {
35711
35748
  const validated = { ...config };
35712
35749
  for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
35713
35750
  for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
35751
+ for (const fieldName of STRING_ARRAY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateStringArrayField(fieldName, value));
35714
35752
  applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
35715
35753
  for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
35716
35754
  applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
@@ -36897,7 +36935,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
36897
36935
  }
36898
36936
  return true;
36899
36937
  };
36900
- const checkSecurityScan = (rootDirectory, options = {}) => {
36938
+ const yieldToEventLoop = () => new Promise((resolve) => {
36939
+ setImmediate(resolve);
36940
+ });
36941
+ const createSecurityScanSession = (rootDirectory, options) => {
36901
36942
  const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
36902
36943
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
36903
36944
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
@@ -36912,7 +36953,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36912
36953
  committedFilesOnly: rule.committedFilesOnly === true
36913
36954
  }];
36914
36955
  });
36915
- if (enabledScanRules.length === 0) return [];
36956
+ if (enabledScanRules.length === 0) return null;
36916
36957
  const diagnostics = [];
36917
36958
  const seen = /* @__PURE__ */ new Set();
36918
36959
  const gitIgnoredCache = /* @__PURE__ */ new Map();
@@ -36924,15 +36965,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36924
36965
  }
36925
36966
  return status === true;
36926
36967
  };
36927
- for (const file of collectSecurityScanFiles(rootDirectory)) for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36928
- if (committedFilesOnly && isFileGitIgnored(file)) continue;
36929
- const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36930
- const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36931
- if (seen.has(key)) continue;
36932
- seen.add(key);
36933
- diagnostics.push(diagnostic);
36968
+ const scanFile = (file) => {
36969
+ for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36970
+ if (committedFilesOnly && isFileGitIgnored(file)) continue;
36971
+ const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36972
+ const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36973
+ if (seen.has(key)) continue;
36974
+ seen.add(key);
36975
+ diagnostics.push(diagnostic);
36976
+ }
36977
+ };
36978
+ return {
36979
+ scanFile,
36980
+ diagnostics
36981
+ };
36982
+ };
36983
+ const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
36984
+ const session = createSecurityScanSession(rootDirectory, options);
36985
+ if (session === null) return [];
36986
+ let filesSinceYield = 0;
36987
+ for (const file of collectSecurityScanFiles(rootDirectory)) {
36988
+ session.scanFile(file);
36989
+ filesSinceYield += 1;
36990
+ if (filesSinceYield >= 16) {
36991
+ filesSinceYield = 0;
36992
+ await yieldToEventLoop();
36993
+ }
36934
36994
  }
36935
- return diagnostics;
36995
+ return session.diagnostics;
36936
36996
  };
36937
36997
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
36938
36998
  let p = process || {}, argv = p.argv || [], env = p.env || {};
@@ -37149,6 +37209,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
37149
37209
  return [...seen].filter((pattern) => pattern.length > 0);
37150
37210
  };
37151
37211
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
37212
+ const readSystemFacts = () => ({
37213
+ availableCores: os.availableParallelism(),
37214
+ totalMemoryBytes: os.totalmem(),
37215
+ cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
37216
+ });
37217
+ /**
37218
+ * How many real deslop dead-code child processes may run at once, across the
37219
+ * concurrent per-project `runInspect` fibers of one CLI run. The cap is the
37220
+ * smaller of the core count and the number of `DEAD_CODE_WORKER_MEM_BUDGET_BYTES`
37221
+ * workers that fit in available memory, floored at 1.
37222
+ *
37223
+ * On a roomy dev box / CI runner this resolves high enough that every
37224
+ * concurrently-scanned project still spawns its own worker (no serialization vs
37225
+ * the prior uncapped behavior); on a memory-constrained runner it collapses
37226
+ * toward 1, so the `withDeadCodeWorkerSlot` semaphore serializes the spawns
37227
+ * instead of oversubscribing memory with N simultaneous children — the global
37228
+ * cap the per-project spawn path lacked.
37229
+ *
37230
+ * Mirrors `resolveAutoScanConcurrency` (lint), but budgets memory per the
37231
+ * heavier dead-code worker. `facts` is injectable for tests.
37232
+ */
37233
+ const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
37234
+ const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
37235
+ const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
37236
+ return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
37237
+ };
37238
+ let availableSlots = -1;
37239
+ const waiters = [];
37240
+ const releaseSlot = () => {
37241
+ const nextWaiter = waiters.shift();
37242
+ if (nextWaiter !== void 0) nextWaiter();
37243
+ else availableSlots += 1;
37244
+ };
37245
+ /**
37246
+ * Runs `task` once a dead-code worker slot is free, releasing the slot when the
37247
+ * task settles (success or failure). With a high cap (roomy machine) every
37248
+ * caller proceeds immediately; with a low cap (constrained runner) callers
37249
+ * queue and run as slots free.
37250
+ *
37251
+ * `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
37252
+ * this caller is queued, the call rejects without acquiring a slot or running
37253
+ * `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
37254
+ * then spawn a child only to tear it down. A queued caller that aborts removes
37255
+ * its own waiter so a later release never hands a slot to a dead request.
37256
+ */
37257
+ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
37258
+ if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
37259
+ if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
37260
+ if (availableSlots > 0) availableSlots -= 1;
37261
+ else await new Promise((resolve, reject) => {
37262
+ const waiter = () => {
37263
+ abortSignal?.removeEventListener("abort", onAbort);
37264
+ resolve();
37265
+ };
37266
+ const onAbort = () => {
37267
+ const queuedIndex = waiters.indexOf(waiter);
37268
+ if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
37269
+ reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
37270
+ };
37271
+ waiters.push(waiter);
37272
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
37273
+ });
37274
+ try {
37275
+ return await task();
37276
+ } finally {
37277
+ releaseSlot();
37278
+ }
37279
+ };
37152
37280
  /**
37153
37281
  * Resolves a path to its canonical, symlink-free form, falling back to
37154
37282
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -37173,6 +37301,11 @@ const toCanonicalPath = (filePath) => {
37173
37301
  };
37174
37302
  const DEAD_CODE_PLUGIN = "deslop";
37175
37303
  const DEAD_CODE_CATEGORY = "Maintainability";
37304
+ const REACT_DOCTOR_TOOLCHAIN_PACKAGES = new Set([
37305
+ "react-doctor",
37306
+ "eslint-plugin-react-doctor",
37307
+ "oxlint-plugin-react-doctor"
37308
+ ]);
37176
37309
  const DEAD_CODE_WORKER_SCRIPT = `
37177
37310
  const inputChunks = [];
37178
37311
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -37439,14 +37572,17 @@ const checkDeadCode = async (options) => {
37439
37572
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37440
37573
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37441
37574
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37442
- const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37443
- rootDirectory,
37444
- entryPatterns,
37445
- tsConfigPath: resolveTsConfigPath(rootDirectory),
37446
- ignorePatterns,
37447
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37448
- parseConcurrency: options.parseConcurrency
37449
- }), options.workerTimeoutMs ?? 12e4, options.abortSignal));
37575
+ const spawnAndRun = () => {
37576
+ return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37577
+ rootDirectory,
37578
+ entryPatterns,
37579
+ tsConfigPath: resolveTsConfigPath(rootDirectory),
37580
+ ignorePatterns,
37581
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37582
+ parseConcurrency: options.parseConcurrency
37583
+ }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
37584
+ };
37585
+ const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
37450
37586
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
37451
37587
  const diagnostics = [];
37452
37588
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -37475,6 +37611,7 @@ const checkDeadCode = async (options) => {
37475
37611
  });
37476
37612
  }
37477
37613
  for (const unusedDependency of result.unusedDependencies) {
37614
+ if (REACT_DOCTOR_TOOLCHAIN_PACKAGES.has(unusedDependency.name)) continue;
37478
37615
  const label = unusedDependency.isDevDependency ? "devDependency" : "dependency";
37479
37616
  diagnostics.push({
37480
37617
  filePath: "package.json",
@@ -37653,7 +37790,6 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37653
37790
  return yield* tryPromise({
37654
37791
  try: (signal) => checkDeadCode({
37655
37792
  rootDirectory: input.rootDirectory,
37656
- userConfig: input.userConfig,
37657
37793
  parseConcurrency: input.parseConcurrency,
37658
37794
  workerTimeoutMs: input.workerTimeoutMs,
37659
37795
  abortSignal: signal
@@ -37776,6 +37912,11 @@ const isSafeGitRevision = (candidate) => {
37776
37912
  if (candidate.includes("..") || candidate.includes("@{")) return false;
37777
37913
  return SAFE_GIT_REVISION_PATTERN.test(candidate);
37778
37914
  };
37915
+ const resolveSpawnArgsLengthCap = () => {
37916
+ if (process.platform === "win32") return SPAWN_ARGS_MAX_LENGTH_CHARS;
37917
+ if (process.platform === "darwin") return SPAWN_ARGS_MAX_LENGTH_CHARS_DARWIN;
37918
+ return SPAWN_ARGS_MAX_LENGTH_CHARS_POSIX;
37919
+ };
37779
37920
  /**
37780
37921
  * Splits a git revision range into its endpoints: three-dot `A...B`
37781
37922
  * (symmetric, merge-base) or two-dot `A..B` (direct). Returns `null`
@@ -37847,43 +37988,47 @@ var Git = class Git extends Service()("react-doctor/Git") {
37847
37988
  * reason: GitInvocationFailed })` so the rest of the codebase
37848
37989
  * sees a single failure channel.
37849
37990
  */
37850
- const runCommand = (input) => scoped(gen(function* () {
37851
- const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37852
- cwd: input.directory,
37853
- env: input.env,
37854
- extendEnv: true
37855
- }));
37856
- const maxStdoutBytes = input.maxStdoutBytes;
37857
- const stdoutByteCount = yield* make$13(0);
37858
- const [stdout, stderr, status] = yield* all([
37859
- mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37860
- args: [...input.args],
37861
- directory: input.directory,
37862
- cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
37863
- }) })) : void_)))))),
37864
- mkString(decodeText(handle.stderr)),
37865
- handle.exitCode
37866
- ], { concurrency: 3 });
37867
- return {
37868
- status,
37869
- stdout,
37870
- stderr
37871
- };
37872
- })).pipe(catchTag$1("PlatformError", (cause) => {
37873
- if (input.command !== "git") return succeed$2({
37991
+ const runCommand = (input) => {
37992
+ const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
37874
37993
  status: 127,
37875
37994
  stdout: "",
37876
37995
  stderr: String(cause)
37877
- });
37878
- return new ReactDoctorError({ reason: new GitInvocationFailed({
37996
+ }) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37879
37997
  args: [...input.args],
37880
37998
  directory: input.directory,
37881
37999
  cause
37882
- }) });
37883
- }), withSpan("git.exec", { attributes: {
37884
- "git.command": input.command,
37885
- "git.subcommand": input.args[0] ?? ""
37886
- } }));
38000
+ }) }));
38001
+ return scoped(gen(function* () {
38002
+ if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
38003
+ const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
38004
+ const spawnArgsLengthCap = resolveSpawnArgsLengthCap();
38005
+ if (argvLengthChars > spawnArgsLengthCap) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${spawnArgsLengthCap})`);
38006
+ const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
38007
+ cwd: input.directory,
38008
+ env: input.env,
38009
+ extendEnv: true
38010
+ }));
38011
+ const maxStdoutBytes = input.maxStdoutBytes;
38012
+ const stdoutByteCount = yield* make$13(0);
38013
+ const [stdout, stderr, status] = yield* all([
38014
+ 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({
38015
+ args: [...input.args],
38016
+ directory: input.directory,
38017
+ cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
38018
+ }) })) : void_)))))),
38019
+ mkString(decodeText(handle.stderr)),
38020
+ handle.exitCode
38021
+ ], { concurrency: 3 });
38022
+ return {
38023
+ status,
38024
+ stdout,
38025
+ stderr
38026
+ };
38027
+ })).pipe(catchTag$1("PlatformError", foldSpawnFailure), withSpan("git.exec", { attributes: {
38028
+ "git.command": input.command,
38029
+ "git.subcommand": input.args[0] ?? ""
38030
+ } }));
38031
+ };
37887
38032
  const runGit = (directory, args) => runCommand({
37888
38033
  command: "git",
37889
38034
  args,
@@ -37916,7 +38061,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37916
38061
  "rev-parse",
37917
38062
  "--verify",
37918
38063
  branch
37919
- ]).pipe(map$3((result) => result.status === 0));
38064
+ ]).pipe(map$3((result) => result.status === 0), catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(false) : fail$4(error)));
37920
38065
  const headSha = (directory) => runGit(directory, ["rev-parse", "HEAD"]).pipe(map$3((result) => result.status === 0 ? trimOrNull(result.stdout) : null));
37921
38066
  const mergeBase = (input) => isSafeGitRevision(input.ref) ? runGit(input.directory, [
37922
38067
  "merge-base",
@@ -38026,6 +38171,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38026
38171
  if (resolvedCurrentBranch === null && explicitBaseBranch === void 0) return null;
38027
38172
  const baseBranch = explicitBaseBranch ?? (yield* defaultBranch(directory));
38028
38173
  if (baseBranch === null) return null;
38174
+ if (!isSafeGitRevision(baseBranch)) return null;
38029
38175
  if (explicitBaseBranch !== void 0) {
38030
38176
  if (!(yield* branchExists(directory, explicitBaseBranch))) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchMissing({ branch: explicitBaseBranch }) }));
38031
38177
  }
@@ -38130,7 +38276,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38130
38276
  ]);
38131
38277
  if (result.status !== 0) return null;
38132
38278
  return parseChangedLineRanges(result.stdout);
38133
- }).pipe(withSpan("Git.changedLineRanges"))
38279
+ }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
38134
38280
  });
38135
38281
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
38136
38282
  /**
@@ -38376,7 +38522,7 @@ const normalizeConfigForHash = (config) => {
38376
38522
  if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
38377
38523
  return clone;
38378
38524
  };
38379
- const computeRulesetHash = (input) => crypto.createHash("sha1").update(JSON.stringify(normalizeConfigForHash(input.config))).update("\0").update([...input.toolchainVersions].join("\0")).update("\0").update([...input.ignorePatterns].join("\n")).update(" ").update(input.tsconfigContent ?? "").digest("hex");
38525
+ const computeRulesetHash = (input) => crypto.createHash("sha1").update(JSON.stringify(normalizeConfigForHash(input.config))).update("\0").update([...input.toolchainVersions].join("\0")).update("\0").update([...input.ignorePatterns].join("\n")).update("\0").update(input.tsconfigContent ?? "").digest("hex");
38380
38526
  /**
38381
38527
  * Loads a plugin module via the local require resolver and extracts
38382
38528
  * `(name, ruleNames)` from either `module.exports.meta + rules` or
@@ -38654,24 +38800,38 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38654
38800
  updatedAtMs: Date.now(),
38655
38801
  files: Object.fromEntries(cappedEntries)
38656
38802
  };
38657
- const keptHashes = Object.entries(rulesets).sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8).map(([hash]) => hash);
38658
- const prunedRulesets = {};
38659
- for (const hash of keptHashes) prunedRulesets[hash] = rulesets[hash];
38660
38803
  atomicWriteJson(cacheFilePath, {
38661
38804
  version: 1,
38662
- rulesets: prunedRulesets
38805
+ rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
38663
38806
  });
38664
38807
  }
38665
38808
  };
38666
38809
  };
38667
38810
  const bundledRequire = createRequire(import.meta.url);
38811
+ const childNodeVersionByBinaryPath = /* @__PURE__ */ new Map();
38812
+ const resolveChildNodeVersion = (nodeBinaryPath) => {
38813
+ if (nodeBinaryPath === process.execPath) return process.version;
38814
+ const cachedVersion = childNodeVersionByBinaryPath.get(nodeBinaryPath);
38815
+ if (cachedVersion !== void 0) return cachedVersion;
38816
+ let version;
38817
+ try {
38818
+ version = execFileSync(nodeBinaryPath, ["--version"], {
38819
+ encoding: "utf8",
38820
+ timeout: NODE_VERSION_PROBE_TIMEOUT_MS
38821
+ }).trim();
38822
+ } catch {
38823
+ version = `unknown:${nodeBinaryPath}`;
38824
+ }
38825
+ childNodeVersionByBinaryPath.set(nodeBinaryPath, version);
38826
+ return version;
38827
+ };
38668
38828
  const TOOLCHAIN_PACKAGE_SPECIFIERS = [
38669
38829
  "oxlint/package.json",
38670
38830
  "oxlint-plugin-react-doctor/package.json",
38671
38831
  "eslint-plugin-react-hooks/package.json"
38672
38832
  ];
38673
- const resolveOxlintToolchainVersions = () => {
38674
- const versions = [`node=${process.version}`];
38833
+ const resolveOxlintToolchainVersions = (nodeBinaryPath = process.execPath) => {
38834
+ const versions = [`node=${resolveChildNodeVersion(nodeBinaryPath)}`];
38675
38835
  for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
38676
38836
  const packageJson = bundledRequire(specifier);
38677
38837
  const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
@@ -39205,11 +39365,12 @@ const REACT_COMPILER_TITLE = "React Compiler can't optimize this";
39205
39365
  const REACT_COMPILER_TODO_TITLE = "React Compiler doesn't support this syntax";
39206
39366
  const REACT_COMPILER_IMPACT = "This component misses React Compiler's automatic memoization & re-renders more than it should";
39207
39367
  const REACT_COMPILER_ACTION = "Rewrite the flagged code so the compiler can optimize it.";
39368
+ const REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION = "It's how the library works, not a bug in your code. Memoize values you pass from it into other memoized components, or suppress it with `// react-doctor-disable-next-line react-hooks-js/incompatible-library`.";
39208
39369
  const REACT_COMPILER_GENERIC_MESSAGE = `${REACT_COMPILER_IMPACT}. ${REACT_COMPILER_ACTION}`;
39209
- const buildReactCompilerMessage = (reasonSummary) => {
39370
+ const buildReactCompilerMessage = (reasonSummary, action = REACT_COMPILER_ACTION) => {
39210
39371
  const normalizedSummary = reasonSummary.replace(TRAILING_PERIOD_PATTERN, "");
39211
- if (!normalizedSummary) return REACT_COMPILER_GENERIC_MESSAGE;
39212
- return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${REACT_COMPILER_ACTION}`;
39372
+ if (!normalizedSummary) return `${REACT_COMPILER_IMPACT}. ${action}`;
39373
+ return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${action}`;
39213
39374
  };
39214
39375
  const PLUGIN_CATEGORY_MAP = {
39215
39376
  react: "Bugs",
@@ -39258,7 +39419,7 @@ const resolveCleanedDiagnostic = (message, help, plugin, rule, project) => {
39258
39419
  const [reasonSummary = "", ...reasonDetailLines] = bailoutReason.split("\n");
39259
39420
  const reasonDetail = reasonDetailLines.join("\n").trim();
39260
39421
  return {
39261
- message: buildReactCompilerMessage(reasonSummary.trim()),
39422
+ message: buildReactCompilerMessage(reasonSummary.trim(), rule === "incompatible-library" ? REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION : REACT_COMPILER_ACTION),
39262
39423
  help: appendReanimatedSharedValueHint(reasonDetail || help, rule, project)
39263
39424
  };
39264
39425
  }
@@ -39526,21 +39687,27 @@ const spawnLintBatches = async (input) => {
39526
39687
  const allDiagnostics = [];
39527
39688
  const droppedFiles = [];
39528
39689
  let firstDropReason = null;
39529
- const splitDeadlineMs = Date.now() + splitTotalBudgetMs;
39530
- const spawnLintBatch = async (batch, depth) => {
39690
+ const spawnLintBatch = async (batch, depth, splitBudget) => {
39531
39691
  const batchArgs = [...baseArgs, ...batch];
39532
39692
  try {
39533
39693
  return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
39534
39694
  } catch (error) {
39535
39695
  if (!isSplittableReactDoctorError(error)) throw error;
39536
- const splitBudgetExhausted = Date.now() >= splitDeadlineMs || depth >= splitMaxDepth;
39537
- if (batch.length <= 1 || splitBudgetExhausted) {
39696
+ splitBudget.deadlineMs ??= Date.now() + splitTotalBudgetMs;
39697
+ const isBudgetElapsed = Date.now() >= splitBudget.deadlineMs;
39698
+ const isDepthCapReached = depth >= splitMaxDepth;
39699
+ if (batch.length <= 1 || isBudgetElapsed || isDepthCapReached) {
39538
39700
  droppedFiles.push(...batch);
39539
- if (firstDropReason === null) firstDropReason = splitBudgetExhausted && batch.length > 1 ? `${error.message} (split budget exhausted after ${splitMaxDepth} levels / ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s)` : error.message;
39701
+ if (firstDropReason === null) {
39702
+ let limitHint = "";
39703
+ if (isDepthCapReached) limitHint = ` (split depth cap of ${splitMaxDepth} levels reached)`;
39704
+ else if (isBudgetElapsed) limitHint = ` (split budget of ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s exhausted at depth ${depth})`;
39705
+ firstDropReason = batch.length > 1 ? `${error.message}${limitHint}` : error.message;
39706
+ }
39540
39707
  return [];
39541
39708
  }
39542
39709
  const splitIndex = Math.ceil(batch.length / 2);
39543
- return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1)];
39710
+ return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1, splitBudget), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1, splitBudget)];
39544
39711
  }
39545
39712
  };
39546
39713
  let startedFileCount = 0;
@@ -39557,7 +39724,7 @@ const spawnLintBatches = async (input) => {
39557
39724
  try {
39558
39725
  const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
39559
39726
  startedFileCount += batch.length;
39560
- const batchDiagnostics = await spawnLintBatch(batch, 0);
39727
+ const batchDiagnostics = await spawnLintBatch(batch, 0, { deadlineMs: null });
39561
39728
  scannedFileCount += batch.length;
39562
39729
  if (onFileProgress) {
39563
39730
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
@@ -39631,7 +39798,14 @@ const resolveReactDoctorCacheDir = (projectDirectory) => {
39631
39798
  if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
39632
39799
  const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
39633
39800
  if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
39634
- return Path.join(os.tmpdir(), "react-doctor-cache", projectCacheSubdir(projectDirectory));
39801
+ return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
39802
+ };
39803
+ const resolveUserCacheScope = () => {
39804
+ try {
39805
+ return String(process.getuid?.() ?? os.userInfo().username);
39806
+ } catch {
39807
+ return "shared";
39808
+ }
39635
39809
  };
39636
39810
  const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
39637
39811
  /**
@@ -39799,7 +39973,7 @@ const runOxlint = async (options) => {
39799
39973
  extendsPaths: [],
39800
39974
  ruleSelection: "cacheable"
39801
39975
  }),
39802
- toolchainVersions: resolveOxlintToolchainVersions(),
39976
+ toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
39803
39977
  ignorePatterns: combinedPatterns,
39804
39978
  tsconfigContent
39805
39979
  });
@@ -39813,7 +39987,7 @@ const runOxlint = async (options) => {
39813
39987
  missFiles.push(candidateFile);
39814
39988
  continue;
39815
39989
  }
39816
- const cacheKey = `${candidateFile.replaceAll("\\", "/")}${contentHash}`;
39990
+ const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
39817
39991
  cacheKeyByFile.set(candidateFile, cacheKey);
39818
39992
  const cachedDiagnostics = cache.lookup(cacheKey);
39819
39993
  if (cachedDiagnostics === null) missFiles.push(candidateFile);
@@ -40129,7 +40303,7 @@ const ScoreApiResponseSchema = Struct({
40129
40303
  })))
40130
40304
  });
40131
40305
  const parseScoreResult = (value) => getOrNull(decodeUnknownOption(ScoreApiResponseSchema)(value));
40132
- const stripLocalFileFields = (diagnostics) => diagnostics.map(({ filePath: _filePath, fileContext: _fileContext, ...rest }) => rest);
40306
+ const stripLocalFileFields = (diagnostics) => diagnostics.map(({ filePath: _filePath, fileContext: _fileContext, fixGroupId: _fixGroupId, ...rest }) => rest);
40133
40307
  const isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
40134
40308
  const describeFailure = (error) => {
40135
40309
  if (isAbortError(error)) return `timed out after ${FETCH_TIMEOUT_MS / 1e3}s`;
@@ -40433,7 +40607,10 @@ const ALERT_SEVERITY_RANK = {
40433
40607
  medium: 2,
40434
40608
  low: 1
40435
40609
  };
40436
- const severityRank = (severity) => ALERT_SEVERITY_RANK[severity.toLowerCase()] ?? 0;
40610
+ const severityRank = (severity) => {
40611
+ const normalized = severity.toLowerCase();
40612
+ return Object.hasOwn(ALERT_SEVERITY_RANK, normalized) ? ALERT_SEVERITY_RANK[normalized] : 0;
40613
+ };
40437
40614
  const displaySeverity = (severity) => {
40438
40615
  const normalized = sanitizeTerminalText(severity.toLowerCase());
40439
40616
  return normalized === "middle" ? "medium" : normalized;
@@ -40450,7 +40627,7 @@ const ALERT_TYPE_LABELS = {
40450
40627
  typosquat: "possible typosquat"
40451
40628
  };
40452
40629
  const humanizeAlertType = (type) => type.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase().trim();
40453
- const friendlyAlertType = (type) => ALERT_TYPE_LABELS[type] ?? sanitizeTerminalText(humanizeAlertType(type));
40630
+ const friendlyAlertType = (type) => Object.hasOwn(ALERT_TYPE_LABELS, type) ? ALERT_TYPE_LABELS[type] : sanitizeTerminalText(humanizeAlertType(type));
40454
40631
  const summarizeAlertNote = (note) => {
40455
40632
  const collapsed = sanitizeTerminalText(note.replace(/\s+/g, " ").trim());
40456
40633
  const firstSentence = collapsed.split(/(?<=\.)\s/)[0] || collapsed;
@@ -40545,7 +40722,10 @@ const checkSupplyChain = (input) => gen(function* () {
40545
40722
  * outage.
40546
40723
  */
40547
40724
  var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
40548
- static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(checkSupplyChain(input).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
40725
+ static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(flatMap$2(SupplyChainOverlapTimeoutMs, (totalTimeoutMs) => checkSupplyChain({
40726
+ ...input,
40727
+ totalTimeoutMs
40728
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
40549
40729
  static layerOf = (diagnostics) => succeed$3(SupplyChain, SupplyChain.of({ run: () => fromIterable$1(diagnostics) }));
40550
40730
  };
40551
40731
  const getObjectProperty = (value, propertyName) => {
@@ -40607,7 +40787,10 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40607
40787
  * diagnostics).
40608
40788
  * 2. beforeLint hook (e.g. CLI renders the project-detection block)
40609
40789
  * 3. environment checks (reduced-motion + pnpm hardening +
40610
- * expo/react-native + security scan), collected synchronously
40790
+ * expo/react-native), collected synchronously. The heavier
40791
+ * content-regex security scan is forked instead (like supply-chain
40792
+ * below) and joined before the concat, so its CPU overlaps lint
40793
+ * rather than blocking the event loop before it.
40611
40794
  * 4. The supply-chain check (Socket.dev) is forked onto a background
40612
40795
  * fiber so its ~100% network-bound time overlaps the ~100%
40613
40796
  * CPU/subprocess-bound lint pass below, collapsing two serial
@@ -40627,7 +40810,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40627
40810
  * order, so terminal output is identical either way; supply-chain
40628
40811
  * rides alongside without a spinner.
40629
40812
  * 6. Join the supply-chain fiber, then assemble the diagnostics in a
40630
- * FIXED order (env, supply-chain, lint, dead-code) so the output is
40813
+ * FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
40631
40814
  * byte-identical regardless of which fiber settled first. The
40632
40815
  * viewer-permission fiber is joined later, during score-metadata
40633
40816
  * assembly (it feeds score metadata, not diagnostics). The per-element
@@ -40688,12 +40871,13 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40688
40871
  ...checkPnpmHardening(scanDirectory),
40689
40872
  ...checkReactServerComponentsAdvisory(scanDirectory, project),
40690
40873
  ...checkExpoProject(scanDirectory, project),
40691
- ...checkReactNativeProject(scanDirectory, project),
40692
- ...checkSecurityScan(scanDirectory, {
40693
- project,
40694
- ignoredTags: input.ignoredTags
40695
- })
40874
+ ...checkReactNativeProject(scanDirectory, project)
40696
40875
  ])));
40876
+ const securityScanFailedRef = yield* make$13(false);
40877
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise(() => checkSecurityScanCooperative(scanDirectory, {
40878
+ project,
40879
+ ignoredTags: input.ignoredTags
40880
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), catch_$1(() => set(securityScanFailedRef, true).pipe(as(empty$4))))))).pipe(withSpan("SecurityScan.run")));
40697
40881
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
40698
40882
  const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
40699
40883
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
@@ -40731,7 +40915,6 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40731
40915
  const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
40732
40916
  const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
40733
40917
  rootDirectory: scanDirectory,
40734
- userConfig: resolvedConfig.config,
40735
40918
  parseConcurrency: deadCodeParseConcurrency,
40736
40919
  workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
40737
40920
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
@@ -40829,9 +41012,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40829
41012
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
40830
41013
  const supplyChainResult = yield* join(supplyChainFiber);
40831
41014
  const supplyChainCollected = supplyChainResult.diagnostics;
41015
+ const securityScanCollected = yield* join(securityScanFiber);
40832
41016
  yield* reporterService.finalize;
40833
41017
  const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
40834
41018
  ...envCollected,
41019
+ ...securityScanCollected,
40835
41020
  ...supplyChainCollected,
40836
41021
  ...lintCollected,
40837
41022
  ...deadCodeCollected
@@ -40857,6 +41042,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40857
41042
  metadata: scoreMetadata
40858
41043
  });
40859
41044
  const lintPartialFailures = yield* get$2(partialFailuresRef);
41045
+ const securityScanFailed = yield* get$2(securityScanFailedRef);
40860
41046
  return {
40861
41047
  project,
40862
41048
  userConfig: resolvedConfig.config,
@@ -40877,8 +41063,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40877
41063
  scanElapsedMilliseconds,
40878
41064
  scanConcurrency,
40879
41065
  supplyChainOverlapTimedOut: supplyChainResult.timedOut,
41066
+ securityScanFailed,
40880
41067
  lintCacheHitFileCount,
40881
- lintCacheTotalFileCount
41068
+ lintCacheTotalFileCount,
41069
+ suppressedRuleCounts: transform.summarizeSuppressions()
40882
41070
  };
40883
41071
  }).pipe(withSpan("runInspect", { attributes: {
40884
41072
  "inspect.directory": input.directory,
@@ -42903,6 +43091,7 @@ const SENTRY_FLUSH_TIMEOUT_MS = 2e3;
42903
43091
  const METRIC = {
42904
43092
  cliInvoked: "cli.invoked",
42905
43093
  cliError: "cli.error",
43094
+ cliEnvironmentError: "cli.env_error",
42906
43095
  projectDetected: "project.detected",
42907
43096
  projectPathSelected: "project.path_selected",
42908
43097
  projectConfigSelected: "project.config_selected",
@@ -42915,6 +43104,8 @@ const METRIC = {
42915
43104
  scanCheckSkipped: "scan.check_skipped",
42916
43105
  baselineDegraded: "baseline.degraded",
42917
43106
  ruleFired: "rule.fired",
43107
+ ruleDisabled: "rule.disabled",
43108
+ ruleSuppressed: "rule.suppressed",
42918
43109
  lintFailed: "lint.failed",
42919
43110
  deadCodeFailed: "deadcode.failed",
42920
43111
  scoreUnavailable: "score.unavailable",
@@ -42927,12 +43118,17 @@ const METRIC = {
42927
43118
  installWorkflow: "install.workflow",
42928
43119
  installAgentHooks: "install.agent_hooks",
42929
43120
  installDependency: "install.dependency",
43121
+ ciScaffolded: "ci.scaffolded",
43122
+ ciUpgraded: "ci.upgraded",
43123
+ ciConfigured: "ci.configured",
42930
43124
  rulesChanged: "rules.changed",
42931
43125
  rulesQueried: "rules.queried",
42932
43126
  lspSessionStarted: "lsp.session.started",
42933
43127
  lspScanCompleted: "lsp.scan.completed",
42934
43128
  lspScanDuration: "lsp.scan.duration",
42935
- lspScanDiagnostics: "lsp.scan.diagnostics"
43129
+ lspScanDiagnostics: "lsp.scan.diagnostics",
43130
+ aiTrainingWarningShown: "ai.training.warning_shown",
43131
+ jsonOutUsed: "json.out_used"
42936
43132
  };
42937
43133
  //#endregion
42938
43134
  //#region src/cli/utils/to-category-key.ts
@@ -43249,5 +43445,5 @@ const startLanguageServer = () => {
43249
43445
  };
43250
43446
  //#endregion
43251
43447
  export { startLanguageServer };
43252
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="9ee72ccc-703c-588f-b7e4-c92657144721")}catch(e){}}();
43253
- //# debugId=9ee72ccc-703c-588f-b7e4-c92657144721
43448
+ !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]="349b2e37-65e9-5f86-8168-70334a95aeec")}catch(e){}}();
43449
+ //# debugId=349b2e37-65e9-5f86-8168-70334a95aeec