react-doctor 0.5.8-dev.441e6af → 0.5.8-dev.5774deb

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.
Files changed (4) hide show
  1. package/dist/cli.js +137 -36
  2. package/dist/index.js +131 -32
  3. package/dist/lsp.js +131 -32
  4. package/package.json +4 -4
package/dist/cli.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]="7e6ea254-9fcd-5076-8bd6-01ad63633c24")}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]="498477a7-1f41-5ecb-9999-c48cfef57259")}catch(e){}}();
3
3
  import { createRequire } from "node:module";
4
4
  import * as NodeChildProcess from "node:child_process";
5
5
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
@@ -9041,7 +9041,7 @@ const composePassthrough = /* @__PURE__ */ dual(2, (left, right) => (input) => {
9041
9041
  * @since 2.0.0
9042
9042
  */
9043
9043
  const Scheduler = /* @__PURE__ */ Reference("effect/Scheduler", { defaultValue: () => new MixedScheduler() });
9044
- const setImmediate = "setImmediate" in globalThis ? (f) => {
9044
+ const setImmediate$1 = "setImmediate" in globalThis ? (f) => {
9045
9045
  const timer = globalThis.setImmediate(f);
9046
9046
  return () => globalThis.clearImmediate(timer);
9047
9047
  } : (f) => {
@@ -9085,7 +9085,7 @@ var PriorityBuckets = class {
9085
9085
  var MixedScheduler = class {
9086
9086
  executionMode;
9087
9087
  setImmediate;
9088
- constructor(executionMode = "async", setImmediateFn = setImmediate) {
9088
+ constructor(executionMode = "async", setImmediateFn = setImmediate$1) {
9089
9089
  this.executionMode = executionMode;
9090
9090
  this.setImmediate = setImmediateFn;
9091
9091
  }
@@ -9110,7 +9110,7 @@ var MixedSchedulerDispatcher = class {
9110
9110
  tasks = /* @__PURE__ */ new PriorityBuckets();
9111
9111
  running = void 0;
9112
9112
  setImmediate;
9113
- constructor(setImmediateFn = setImmediate) {
9113
+ constructor(setImmediateFn = setImmediate$1) {
9114
9114
  this.setImmediate = setImmediateFn;
9115
9115
  }
9116
9116
  /**
@@ -36947,6 +36947,7 @@ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
36947
36947
  const LINT_PHASE_TIMEOUT_MS = 3e5;
36948
36948
  const SCAN_TOTAL_DEADLINE_MS = 9e5;
36949
36949
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
36950
+ const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
36950
36951
  const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
36951
36952
  const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
36952
36953
  const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
@@ -38557,7 +38558,7 @@ const resolveScanConcurrency = (requested) => {
38557
38558
  if (!Number.isFinite(requested) || requested < 1) return 1;
38558
38559
  return Math.min(Math.floor(requested), 32);
38559
38560
  };
38560
- const readSystemFacts = () => ({
38561
+ const readSystemFacts$1 = () => ({
38561
38562
  availableCores: os.availableParallelism(),
38562
38563
  totalMemoryBytes: os.totalmem(),
38563
38564
  cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
@@ -38578,7 +38579,7 @@ const readSystemFacts = () => ({
38578
38579
  * `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
38579
38580
  * limited, and ceiling cases without mocking `os` or the filesystem.
38580
38581
  */
38581
- const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
38582
+ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
38582
38583
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
38583
38584
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
38584
38585
  return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
@@ -40139,7 +40140,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
40139
40140
  }
40140
40141
  return true;
40141
40142
  };
40142
- const checkSecurityScan = (rootDirectory, options = {}) => {
40143
+ const yieldToEventLoop = () => new Promise((resolve) => {
40144
+ setImmediate(resolve);
40145
+ });
40146
+ const createSecurityScanSession = (rootDirectory, options) => {
40143
40147
  const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
40144
40148
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
40145
40149
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
@@ -40154,7 +40158,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
40154
40158
  committedFilesOnly: rule.committedFilesOnly === true
40155
40159
  }];
40156
40160
  });
40157
- if (enabledScanRules.length === 0) return [];
40161
+ if (enabledScanRules.length === 0) return null;
40158
40162
  const diagnostics = [];
40159
40163
  const seen = /* @__PURE__ */ new Set();
40160
40164
  const gitIgnoredCache = /* @__PURE__ */ new Map();
@@ -40166,15 +40170,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
40166
40170
  }
40167
40171
  return status === true;
40168
40172
  };
40169
- for (const file of collectSecurityScanFiles(rootDirectory)) for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
40170
- if (committedFilesOnly && isFileGitIgnored(file)) continue;
40171
- const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
40172
- const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
40173
- if (seen.has(key)) continue;
40174
- seen.add(key);
40175
- diagnostics.push(diagnostic);
40173
+ const scanFile = (file) => {
40174
+ for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
40175
+ if (committedFilesOnly && isFileGitIgnored(file)) continue;
40176
+ const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
40177
+ const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
40178
+ if (seen.has(key)) continue;
40179
+ seen.add(key);
40180
+ diagnostics.push(diagnostic);
40181
+ }
40182
+ };
40183
+ return {
40184
+ scanFile,
40185
+ diagnostics
40186
+ };
40187
+ };
40188
+ const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
40189
+ const session = createSecurityScanSession(rootDirectory, options);
40190
+ if (session === null) return [];
40191
+ let filesSinceYield = 0;
40192
+ for (const file of collectSecurityScanFiles(rootDirectory)) {
40193
+ session.scanFile(file);
40194
+ filesSinceYield += 1;
40195
+ if (filesSinceYield >= 16) {
40196
+ filesSinceYield = 0;
40197
+ await yieldToEventLoop();
40198
+ }
40176
40199
  }
40177
- return diagnostics;
40200
+ return session.diagnostics;
40178
40201
  };
40179
40202
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
40180
40203
  let p = process || {}, argv = p.argv || [], env = p.env || {};
@@ -40407,6 +40430,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
40407
40430
  return [...seen].filter((pattern) => pattern.length > 0);
40408
40431
  };
40409
40432
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
40433
+ const readSystemFacts = () => ({
40434
+ availableCores: os.availableParallelism(),
40435
+ totalMemoryBytes: os.totalmem(),
40436
+ cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
40437
+ });
40438
+ /**
40439
+ * How many real deslop dead-code child processes may run at once, across the
40440
+ * concurrent per-project `runInspect` fibers of one CLI run. The cap is the
40441
+ * smaller of the core count and the number of `DEAD_CODE_WORKER_MEM_BUDGET_BYTES`
40442
+ * workers that fit in available memory, floored at 1.
40443
+ *
40444
+ * On a roomy dev box / CI runner this resolves high enough that every
40445
+ * concurrently-scanned project still spawns its own worker (no serialization vs
40446
+ * the prior uncapped behavior); on a memory-constrained runner it collapses
40447
+ * toward 1, so the `withDeadCodeWorkerSlot` semaphore serializes the spawns
40448
+ * instead of oversubscribing memory with N simultaneous children — the global
40449
+ * cap the per-project spawn path lacked.
40450
+ *
40451
+ * Mirrors `resolveAutoScanConcurrency` (lint), but budgets memory per the
40452
+ * heavier dead-code worker. `facts` is injectable for tests.
40453
+ */
40454
+ const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
40455
+ const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
40456
+ const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
40457
+ return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
40458
+ };
40459
+ let availableSlots = -1;
40460
+ const waiters = [];
40461
+ const releaseSlot = () => {
40462
+ const nextWaiter = waiters.shift();
40463
+ if (nextWaiter !== void 0) nextWaiter();
40464
+ else availableSlots += 1;
40465
+ };
40466
+ /**
40467
+ * Runs `task` once a dead-code worker slot is free, releasing the slot when the
40468
+ * task settles (success or failure). With a high cap (roomy machine) every
40469
+ * caller proceeds immediately; with a low cap (constrained runner) callers
40470
+ * queue and run as slots free.
40471
+ *
40472
+ * `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
40473
+ * this caller is queued, the call rejects without acquiring a slot or running
40474
+ * `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
40475
+ * then spawn a child only to tear it down. A queued caller that aborts removes
40476
+ * its own waiter so a later release never hands a slot to a dead request.
40477
+ */
40478
+ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
40479
+ if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
40480
+ if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
40481
+ if (availableSlots > 0) availableSlots -= 1;
40482
+ else await new Promise((resolve, reject) => {
40483
+ const waiter = () => {
40484
+ abortSignal?.removeEventListener("abort", onAbort);
40485
+ resolve();
40486
+ };
40487
+ const onAbort = () => {
40488
+ const queuedIndex = waiters.indexOf(waiter);
40489
+ if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
40490
+ reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
40491
+ };
40492
+ waiters.push(waiter);
40493
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
40494
+ });
40495
+ try {
40496
+ return await task();
40497
+ } finally {
40498
+ releaseSlot();
40499
+ }
40500
+ };
40410
40501
  /**
40411
40502
  * Resolves a path to its canonical, symlink-free form, falling back to
40412
40503
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -40697,14 +40788,17 @@ const checkDeadCode = async (options) => {
40697
40788
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
40698
40789
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
40699
40790
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
40700
- const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
40701
- rootDirectory,
40702
- entryPatterns,
40703
- tsConfigPath: resolveTsConfigPath(rootDirectory),
40704
- ignorePatterns,
40705
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
40706
- parseConcurrency: options.parseConcurrency
40707
- }), options.workerTimeoutMs ?? 12e4, options.abortSignal));
40791
+ const spawnAndRun = () => {
40792
+ return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
40793
+ rootDirectory,
40794
+ entryPatterns,
40795
+ tsConfigPath: resolveTsConfigPath(rootDirectory),
40796
+ ignorePatterns,
40797
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
40798
+ parseConcurrency: options.parseConcurrency
40799
+ }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
40800
+ };
40801
+ const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
40708
40802
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
40709
40803
  const diagnostics = [];
40710
40804
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -43868,7 +43962,10 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
43868
43962
  * diagnostics).
43869
43963
  * 2. beforeLint hook (e.g. CLI renders the project-detection block)
43870
43964
  * 3. environment checks (reduced-motion + pnpm hardening +
43871
- * expo/react-native + security scan), collected synchronously
43965
+ * expo/react-native), collected synchronously. The heavier
43966
+ * content-regex security scan is forked instead (like supply-chain
43967
+ * below) and joined before the concat, so its CPU overlaps lint
43968
+ * rather than blocking the event loop before it.
43872
43969
  * 4. The supply-chain check (Socket.dev) is forked onto a background
43873
43970
  * fiber so its ~100% network-bound time overlaps the ~100%
43874
43971
  * CPU/subprocess-bound lint pass below, collapsing two serial
@@ -43888,7 +43985,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
43888
43985
  * order, so terminal output is identical either way; supply-chain
43889
43986
  * rides alongside without a spinner.
43890
43987
  * 6. Join the supply-chain fiber, then assemble the diagnostics in a
43891
- * FIXED order (env, supply-chain, lint, dead-code) so the output is
43988
+ * FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
43892
43989
  * byte-identical regardless of which fiber settled first. The
43893
43990
  * viewer-permission fiber is joined later, during score-metadata
43894
43991
  * assembly (it feeds score metadata, not diagnostics). The per-element
@@ -43949,12 +44046,12 @@ const runInspect = (input, hooks = {}) => gen(function* () {
43949
44046
  ...checkPnpmHardening(scanDirectory),
43950
44047
  ...checkReactServerComponentsAdvisory(scanDirectory, project),
43951
44048
  ...checkExpoProject(scanDirectory, project),
43952
- ...checkReactNativeProject(scanDirectory, project),
43953
- ...checkSecurityScan(scanDirectory, {
43954
- project,
43955
- ignoredTags: input.ignoredTags
43956
- })
44049
+ ...checkReactNativeProject(scanDirectory, project)
43957
44050
  ])));
44051
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(promise(() => checkSecurityScanCooperative(scanDirectory, {
44052
+ project,
44053
+ ignoredTags: input.ignoredTags
44054
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)))))).pipe(withSpan("SecurityScan.run")));
43958
44055
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
43959
44056
  const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
43960
44057
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
@@ -44090,9 +44187,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44090
44187
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
44091
44188
  const supplyChainResult = yield* join(supplyChainFiber);
44092
44189
  const supplyChainCollected = supplyChainResult.diagnostics;
44190
+ const securityScanCollected = yield* join(securityScanFiber);
44093
44191
  yield* reporterService.finalize;
44094
44192
  const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
44095
44193
  ...envCollected,
44194
+ ...securityScanCollected,
44096
44195
  ...supplyChainCollected,
44097
44196
  ...lintCollected,
44098
44197
  ...deadCodeCollected
@@ -44926,7 +45025,7 @@ const makeNoopConsole = () => ({
44926
45025
  });
44927
45026
  //#endregion
44928
45027
  //#region src/cli/utils/version.ts
44929
- const VERSION = "0.5.8-dev.441e6af";
45028
+ const VERSION = "0.5.8-dev.5774deb";
44930
45029
  //#endregion
44931
45030
  //#region src/cli/utils/json-mode.ts
44932
45031
  let context = null;
@@ -45290,13 +45389,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
45290
45389
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
45291
45390
  * standard `SENTRY_RELEASE` override.
45292
45391
  */
45293
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.441e6af`;
45392
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.5774deb`;
45294
45393
  /**
45295
45394
  * Deployment environment shown in Sentry's environment filter. Defaults to
45296
45395
  * `production` for tagged releases and `development` for dev/unbuilt versions,
45297
45396
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
45298
45397
  */
45299
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.441e6af") ? "development" : "production");
45398
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.5774deb") ? "development" : "production");
45300
45399
  /**
45301
45400
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
45302
45401
  * (set to `0` to disable tracing) and falls back to
@@ -51089,7 +51188,8 @@ const getStagedSourceFiles = async (directory) => {
51089
51188
  return [...await runPromise(gen(function* () {
51090
51189
  return yield* (yield* StagedFiles).discoverSourceFiles(directory);
51091
51190
  }).pipe(provide(stagedFilesLayer)))];
51092
- } catch {
51191
+ } catch (error) {
51192
+ cliLogger.warn(`Failed to discover staged files: ${error instanceof Error ? error.message : String(error)}`);
51093
51193
  return [];
51094
51194
  }
51095
51195
  };
@@ -53976,6 +54076,7 @@ const resolveRequestedProjects = (requestedNames, workspacePackages, rootDirecto
53976
54076
  return requestedNames.map((requestedName) => {
53977
54077
  const matched = workspacePackages.find((workspacePackage) => workspacePackage.name === requestedName || Path.basename(workspacePackage.directory) === requestedName);
53978
54078
  if (matched) return matched.directory;
54079
+ if (Path.basename(rootDirectory) === requestedName) return rootDirectory;
53979
54080
  const candidateDirectory = Path.resolve(rootDirectory, requestedName);
53980
54081
  if (isDirectory(candidateDirectory)) {
53981
54082
  recordCount(METRIC.projectPathSelected);
@@ -55481,4 +55582,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
55481
55582
  export {};
55482
55583
 
55483
55584
  //# sourceMappingURL=cli.js.map
55484
- //# debugId=7e6ea254-9fcd-5076-8bd6-01ad63633c24
55585
+ //# debugId=498477a7-1f41-5ecb-9999-c48cfef57259
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="dccb6d63-63d4-5e91-af21-2756a08fe93a")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b9d1a98e-f5ec-5357-a08b-f84864fdfa20")}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";
@@ -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
  /**
@@ -33752,6 +33752,7 @@ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
33752
33752
  const LINT_PHASE_TIMEOUT_MS = 3e5;
33753
33753
  const SCAN_TOTAL_DEADLINE_MS = 9e5;
33754
33754
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
33755
+ const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
33755
33756
  const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
33756
33757
  const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
33757
33758
  const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
@@ -35369,7 +35370,7 @@ const resolveScanConcurrency = (requested) => {
35369
35370
  if (!Number.isFinite(requested) || requested < 1) return 1;
35370
35371
  return Math.min(Math.floor(requested), 32);
35371
35372
  };
35372
- const readSystemFacts = () => ({
35373
+ const readSystemFacts$1 = () => ({
35373
35374
  availableCores: os.availableParallelism(),
35374
35375
  totalMemoryBytes: os.totalmem(),
35375
35376
  cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
@@ -35390,7 +35391,7 @@ const readSystemFacts = () => ({
35390
35391
  * `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
35391
35392
  * limited, and ceiling cases without mocking `os` or the filesystem.
35392
35393
  */
35393
- const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
35394
+ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
35394
35395
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
35395
35396
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
35396
35397
  return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
@@ -36923,7 +36924,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
36923
36924
  }
36924
36925
  return true;
36925
36926
  };
36926
- const checkSecurityScan = (rootDirectory, options = {}) => {
36927
+ const yieldToEventLoop = () => new Promise((resolve) => {
36928
+ setImmediate(resolve);
36929
+ });
36930
+ const createSecurityScanSession = (rootDirectory, options) => {
36927
36931
  const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
36928
36932
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
36929
36933
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
@@ -36938,7 +36942,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36938
36942
  committedFilesOnly: rule.committedFilesOnly === true
36939
36943
  }];
36940
36944
  });
36941
- if (enabledScanRules.length === 0) return [];
36945
+ if (enabledScanRules.length === 0) return null;
36942
36946
  const diagnostics = [];
36943
36947
  const seen = /* @__PURE__ */ new Set();
36944
36948
  const gitIgnoredCache = /* @__PURE__ */ new Map();
@@ -36950,15 +36954,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36950
36954
  }
36951
36955
  return status === true;
36952
36956
  };
36953
- for (const file of collectSecurityScanFiles(rootDirectory)) for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36954
- if (committedFilesOnly && isFileGitIgnored(file)) continue;
36955
- const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36956
- const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36957
- if (seen.has(key)) continue;
36958
- seen.add(key);
36959
- diagnostics.push(diagnostic);
36957
+ const scanFile = (file) => {
36958
+ for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36959
+ if (committedFilesOnly && isFileGitIgnored(file)) continue;
36960
+ const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36961
+ const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36962
+ if (seen.has(key)) continue;
36963
+ seen.add(key);
36964
+ diagnostics.push(diagnostic);
36965
+ }
36966
+ };
36967
+ return {
36968
+ scanFile,
36969
+ diagnostics
36970
+ };
36971
+ };
36972
+ const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
36973
+ const session = createSecurityScanSession(rootDirectory, options);
36974
+ if (session === null) return [];
36975
+ let filesSinceYield = 0;
36976
+ for (const file of collectSecurityScanFiles(rootDirectory)) {
36977
+ session.scanFile(file);
36978
+ filesSinceYield += 1;
36979
+ if (filesSinceYield >= 16) {
36980
+ filesSinceYield = 0;
36981
+ await yieldToEventLoop();
36982
+ }
36960
36983
  }
36961
- return diagnostics;
36984
+ return session.diagnostics;
36962
36985
  };
36963
36986
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
36964
36987
  let p = process || {}, argv = p.argv || [], env = p.env || {};
@@ -37175,6 +37198,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
37175
37198
  return [...seen].filter((pattern) => pattern.length > 0);
37176
37199
  };
37177
37200
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
37201
+ const readSystemFacts = () => ({
37202
+ availableCores: os.availableParallelism(),
37203
+ totalMemoryBytes: os.totalmem(),
37204
+ cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
37205
+ });
37206
+ /**
37207
+ * How many real deslop dead-code child processes may run at once, across the
37208
+ * concurrent per-project `runInspect` fibers of one CLI run. The cap is the
37209
+ * smaller of the core count and the number of `DEAD_CODE_WORKER_MEM_BUDGET_BYTES`
37210
+ * workers that fit in available memory, floored at 1.
37211
+ *
37212
+ * On a roomy dev box / CI runner this resolves high enough that every
37213
+ * concurrently-scanned project still spawns its own worker (no serialization vs
37214
+ * the prior uncapped behavior); on a memory-constrained runner it collapses
37215
+ * toward 1, so the `withDeadCodeWorkerSlot` semaphore serializes the spawns
37216
+ * instead of oversubscribing memory with N simultaneous children — the global
37217
+ * cap the per-project spawn path lacked.
37218
+ *
37219
+ * Mirrors `resolveAutoScanConcurrency` (lint), but budgets memory per the
37220
+ * heavier dead-code worker. `facts` is injectable for tests.
37221
+ */
37222
+ const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
37223
+ const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
37224
+ const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
37225
+ return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
37226
+ };
37227
+ let availableSlots = -1;
37228
+ const waiters = [];
37229
+ const releaseSlot = () => {
37230
+ const nextWaiter = waiters.shift();
37231
+ if (nextWaiter !== void 0) nextWaiter();
37232
+ else availableSlots += 1;
37233
+ };
37234
+ /**
37235
+ * Runs `task` once a dead-code worker slot is free, releasing the slot when the
37236
+ * task settles (success or failure). With a high cap (roomy machine) every
37237
+ * caller proceeds immediately; with a low cap (constrained runner) callers
37238
+ * queue and run as slots free.
37239
+ *
37240
+ * `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
37241
+ * this caller is queued, the call rejects without acquiring a slot or running
37242
+ * `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
37243
+ * then spawn a child only to tear it down. A queued caller that aborts removes
37244
+ * its own waiter so a later release never hands a slot to a dead request.
37245
+ */
37246
+ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
37247
+ if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
37248
+ if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
37249
+ if (availableSlots > 0) availableSlots -= 1;
37250
+ else await new Promise((resolve, reject) => {
37251
+ const waiter = () => {
37252
+ abortSignal?.removeEventListener("abort", onAbort);
37253
+ resolve();
37254
+ };
37255
+ const onAbort = () => {
37256
+ const queuedIndex = waiters.indexOf(waiter);
37257
+ if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
37258
+ reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
37259
+ };
37260
+ waiters.push(waiter);
37261
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
37262
+ });
37263
+ try {
37264
+ return await task();
37265
+ } finally {
37266
+ releaseSlot();
37267
+ }
37268
+ };
37178
37269
  /**
37179
37270
  * Resolves a path to its canonical, symlink-free form, falling back to
37180
37271
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -37465,14 +37556,17 @@ const checkDeadCode = async (options) => {
37465
37556
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37466
37557
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37467
37558
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37468
- const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37469
- rootDirectory,
37470
- entryPatterns,
37471
- tsConfigPath: resolveTsConfigPath(rootDirectory),
37472
- ignorePatterns,
37473
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37474
- parseConcurrency: options.parseConcurrency
37475
- }), options.workerTimeoutMs ?? 12e4, options.abortSignal));
37559
+ const spawnAndRun = () => {
37560
+ return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37561
+ rootDirectory,
37562
+ entryPatterns,
37563
+ tsConfigPath: resolveTsConfigPath(rootDirectory),
37564
+ ignorePatterns,
37565
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37566
+ parseConcurrency: options.parseConcurrency
37567
+ }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
37568
+ };
37569
+ const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
37476
37570
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
37477
37571
  const diagnostics = [];
37478
37572
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -40636,7 +40730,10 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40636
40730
  * diagnostics).
40637
40731
  * 2. beforeLint hook (e.g. CLI renders the project-detection block)
40638
40732
  * 3. environment checks (reduced-motion + pnpm hardening +
40639
- * expo/react-native + security scan), collected synchronously
40733
+ * expo/react-native), collected synchronously. The heavier
40734
+ * content-regex security scan is forked instead (like supply-chain
40735
+ * below) and joined before the concat, so its CPU overlaps lint
40736
+ * rather than blocking the event loop before it.
40640
40737
  * 4. The supply-chain check (Socket.dev) is forked onto a background
40641
40738
  * fiber so its ~100% network-bound time overlaps the ~100%
40642
40739
  * CPU/subprocess-bound lint pass below, collapsing two serial
@@ -40656,7 +40753,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40656
40753
  * order, so terminal output is identical either way; supply-chain
40657
40754
  * rides alongside without a spinner.
40658
40755
  * 6. Join the supply-chain fiber, then assemble the diagnostics in a
40659
- * FIXED order (env, supply-chain, lint, dead-code) so the output is
40756
+ * FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
40660
40757
  * byte-identical regardless of which fiber settled first. The
40661
40758
  * viewer-permission fiber is joined later, during score-metadata
40662
40759
  * assembly (it feeds score metadata, not diagnostics). The per-element
@@ -40717,12 +40814,12 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40717
40814
  ...checkPnpmHardening(scanDirectory),
40718
40815
  ...checkReactServerComponentsAdvisory(scanDirectory, project),
40719
40816
  ...checkExpoProject(scanDirectory, project),
40720
- ...checkReactNativeProject(scanDirectory, project),
40721
- ...checkSecurityScan(scanDirectory, {
40722
- project,
40723
- ignoredTags: input.ignoredTags
40724
- })
40817
+ ...checkReactNativeProject(scanDirectory, project)
40725
40818
  ])));
40819
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(promise(() => checkSecurityScanCooperative(scanDirectory, {
40820
+ project,
40821
+ ignoredTags: input.ignoredTags
40822
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)))))).pipe(withSpan("SecurityScan.run")));
40726
40823
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
40727
40824
  const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
40728
40825
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
@@ -40858,9 +40955,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40858
40955
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
40859
40956
  const supplyChainResult = yield* join(supplyChainFiber);
40860
40957
  const supplyChainCollected = supplyChainResult.diagnostics;
40958
+ const securityScanCollected = yield* join(securityScanFiber);
40861
40959
  yield* reporterService.finalize;
40862
40960
  const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
40863
40961
  ...envCollected,
40962
+ ...securityScanCollected,
40864
40963
  ...supplyChainCollected,
40865
40964
  ...lintCollected,
40866
40965
  ...deadCodeCollected
@@ -41483,4 +41582,4 @@ const toJsonReport = (result, options) => buildJsonReport({
41483
41582
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
41484
41583
 
41485
41584
  //# sourceMappingURL=index.js.map
41486
- //# debugId=dccb6d63-63d4-5e91-af21-2756a08fe93a
41585
+ //# debugId=b9d1a98e-f5ec-5357-a08b-f84864fdfa20
package/dist/lsp.js CHANGED
@@ -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
  /**
@@ -33811,6 +33811,7 @@ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
33811
33811
  const LINT_PHASE_TIMEOUT_MS = 3e5;
33812
33812
  const SCAN_TOTAL_DEADLINE_MS = 9e5;
33813
33813
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
33814
+ const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
33814
33815
  const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
33815
33816
  const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
33816
33817
  const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
@@ -35402,7 +35403,7 @@ const resolveScanConcurrency = (requested) => {
35402
35403
  if (!Number.isFinite(requested) || requested < 1) return 1;
35403
35404
  return Math.min(Math.floor(requested), 32);
35404
35405
  };
35405
- const readSystemFacts = () => ({
35406
+ const readSystemFacts$1 = () => ({
35406
35407
  availableCores: os.availableParallelism(),
35407
35408
  totalMemoryBytes: os.totalmem(),
35408
35409
  cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
@@ -35423,7 +35424,7 @@ const readSystemFacts = () => ({
35423
35424
  * `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
35424
35425
  * limited, and ceiling cases without mocking `os` or the filesystem.
35425
35426
  */
35426
- const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
35427
+ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
35427
35428
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
35428
35429
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
35429
35430
  return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
@@ -36908,7 +36909,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
36908
36909
  }
36909
36910
  return true;
36910
36911
  };
36911
- const checkSecurityScan = (rootDirectory, options = {}) => {
36912
+ const yieldToEventLoop = () => new Promise((resolve) => {
36913
+ setImmediate(resolve);
36914
+ });
36915
+ const createSecurityScanSession = (rootDirectory, options) => {
36912
36916
  const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
36913
36917
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
36914
36918
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
@@ -36923,7 +36927,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36923
36927
  committedFilesOnly: rule.committedFilesOnly === true
36924
36928
  }];
36925
36929
  });
36926
- if (enabledScanRules.length === 0) return [];
36930
+ if (enabledScanRules.length === 0) return null;
36927
36931
  const diagnostics = [];
36928
36932
  const seen = /* @__PURE__ */ new Set();
36929
36933
  const gitIgnoredCache = /* @__PURE__ */ new Map();
@@ -36935,15 +36939,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36935
36939
  }
36936
36940
  return status === true;
36937
36941
  };
36938
- for (const file of collectSecurityScanFiles(rootDirectory)) for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36939
- if (committedFilesOnly && isFileGitIgnored(file)) continue;
36940
- const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36941
- const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36942
- if (seen.has(key)) continue;
36943
- seen.add(key);
36944
- diagnostics.push(diagnostic);
36942
+ const scanFile = (file) => {
36943
+ for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36944
+ if (committedFilesOnly && isFileGitIgnored(file)) continue;
36945
+ const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36946
+ const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36947
+ if (seen.has(key)) continue;
36948
+ seen.add(key);
36949
+ diagnostics.push(diagnostic);
36950
+ }
36951
+ };
36952
+ return {
36953
+ scanFile,
36954
+ diagnostics
36955
+ };
36956
+ };
36957
+ const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
36958
+ const session = createSecurityScanSession(rootDirectory, options);
36959
+ if (session === null) return [];
36960
+ let filesSinceYield = 0;
36961
+ for (const file of collectSecurityScanFiles(rootDirectory)) {
36962
+ session.scanFile(file);
36963
+ filesSinceYield += 1;
36964
+ if (filesSinceYield >= 16) {
36965
+ filesSinceYield = 0;
36966
+ await yieldToEventLoop();
36967
+ }
36945
36968
  }
36946
- return diagnostics;
36969
+ return session.diagnostics;
36947
36970
  };
36948
36971
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
36949
36972
  let p = process || {}, argv = p.argv || [], env = p.env || {};
@@ -37160,6 +37183,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
37160
37183
  return [...seen].filter((pattern) => pattern.length > 0);
37161
37184
  };
37162
37185
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
37186
+ const readSystemFacts = () => ({
37187
+ availableCores: os.availableParallelism(),
37188
+ totalMemoryBytes: os.totalmem(),
37189
+ cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
37190
+ });
37191
+ /**
37192
+ * How many real deslop dead-code child processes may run at once, across the
37193
+ * concurrent per-project `runInspect` fibers of one CLI run. The cap is the
37194
+ * smaller of the core count and the number of `DEAD_CODE_WORKER_MEM_BUDGET_BYTES`
37195
+ * workers that fit in available memory, floored at 1.
37196
+ *
37197
+ * On a roomy dev box / CI runner this resolves high enough that every
37198
+ * concurrently-scanned project still spawns its own worker (no serialization vs
37199
+ * the prior uncapped behavior); on a memory-constrained runner it collapses
37200
+ * toward 1, so the `withDeadCodeWorkerSlot` semaphore serializes the spawns
37201
+ * instead of oversubscribing memory with N simultaneous children — the global
37202
+ * cap the per-project spawn path lacked.
37203
+ *
37204
+ * Mirrors `resolveAutoScanConcurrency` (lint), but budgets memory per the
37205
+ * heavier dead-code worker. `facts` is injectable for tests.
37206
+ */
37207
+ const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
37208
+ const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
37209
+ const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
37210
+ return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
37211
+ };
37212
+ let availableSlots = -1;
37213
+ const waiters = [];
37214
+ const releaseSlot = () => {
37215
+ const nextWaiter = waiters.shift();
37216
+ if (nextWaiter !== void 0) nextWaiter();
37217
+ else availableSlots += 1;
37218
+ };
37219
+ /**
37220
+ * Runs `task` once a dead-code worker slot is free, releasing the slot when the
37221
+ * task settles (success or failure). With a high cap (roomy machine) every
37222
+ * caller proceeds immediately; with a low cap (constrained runner) callers
37223
+ * queue and run as slots free.
37224
+ *
37225
+ * `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
37226
+ * this caller is queued, the call rejects without acquiring a slot or running
37227
+ * `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
37228
+ * then spawn a child only to tear it down. A queued caller that aborts removes
37229
+ * its own waiter so a later release never hands a slot to a dead request.
37230
+ */
37231
+ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
37232
+ if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
37233
+ if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
37234
+ if (availableSlots > 0) availableSlots -= 1;
37235
+ else await new Promise((resolve, reject) => {
37236
+ const waiter = () => {
37237
+ abortSignal?.removeEventListener("abort", onAbort);
37238
+ resolve();
37239
+ };
37240
+ const onAbort = () => {
37241
+ const queuedIndex = waiters.indexOf(waiter);
37242
+ if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
37243
+ reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
37244
+ };
37245
+ waiters.push(waiter);
37246
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
37247
+ });
37248
+ try {
37249
+ return await task();
37250
+ } finally {
37251
+ releaseSlot();
37252
+ }
37253
+ };
37163
37254
  /**
37164
37255
  * Resolves a path to its canonical, symlink-free form, falling back to
37165
37256
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -37450,14 +37541,17 @@ const checkDeadCode = async (options) => {
37450
37541
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37451
37542
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37452
37543
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37453
- const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37454
- rootDirectory,
37455
- entryPatterns,
37456
- tsConfigPath: resolveTsConfigPath(rootDirectory),
37457
- ignorePatterns,
37458
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37459
- parseConcurrency: options.parseConcurrency
37460
- }), options.workerTimeoutMs ?? 12e4, options.abortSignal));
37544
+ const spawnAndRun = () => {
37545
+ return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37546
+ rootDirectory,
37547
+ entryPatterns,
37548
+ tsConfigPath: resolveTsConfigPath(rootDirectory),
37549
+ ignorePatterns,
37550
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37551
+ parseConcurrency: options.parseConcurrency
37552
+ }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
37553
+ };
37554
+ const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
37461
37555
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
37462
37556
  const diagnostics = [];
37463
37557
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -40621,7 +40715,10 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40621
40715
  * diagnostics).
40622
40716
  * 2. beforeLint hook (e.g. CLI renders the project-detection block)
40623
40717
  * 3. environment checks (reduced-motion + pnpm hardening +
40624
- * expo/react-native + security scan), collected synchronously
40718
+ * expo/react-native), collected synchronously. The heavier
40719
+ * content-regex security scan is forked instead (like supply-chain
40720
+ * below) and joined before the concat, so its CPU overlaps lint
40721
+ * rather than blocking the event loop before it.
40625
40722
  * 4. The supply-chain check (Socket.dev) is forked onto a background
40626
40723
  * fiber so its ~100% network-bound time overlaps the ~100%
40627
40724
  * CPU/subprocess-bound lint pass below, collapsing two serial
@@ -40641,7 +40738,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40641
40738
  * order, so terminal output is identical either way; supply-chain
40642
40739
  * rides alongside without a spinner.
40643
40740
  * 6. Join the supply-chain fiber, then assemble the diagnostics in a
40644
- * FIXED order (env, supply-chain, lint, dead-code) so the output is
40741
+ * FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
40645
40742
  * byte-identical regardless of which fiber settled first. The
40646
40743
  * viewer-permission fiber is joined later, during score-metadata
40647
40744
  * assembly (it feeds score metadata, not diagnostics). The per-element
@@ -40702,12 +40799,12 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40702
40799
  ...checkPnpmHardening(scanDirectory),
40703
40800
  ...checkReactServerComponentsAdvisory(scanDirectory, project),
40704
40801
  ...checkExpoProject(scanDirectory, project),
40705
- ...checkReactNativeProject(scanDirectory, project),
40706
- ...checkSecurityScan(scanDirectory, {
40707
- project,
40708
- ignoredTags: input.ignoredTags
40709
- })
40802
+ ...checkReactNativeProject(scanDirectory, project)
40710
40803
  ])));
40804
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(promise(() => checkSecurityScanCooperative(scanDirectory, {
40805
+ project,
40806
+ ignoredTags: input.ignoredTags
40807
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)))))).pipe(withSpan("SecurityScan.run")));
40711
40808
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
40712
40809
  const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
40713
40810
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
@@ -40843,9 +40940,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40843
40940
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
40844
40941
  const supplyChainResult = yield* join(supplyChainFiber);
40845
40942
  const supplyChainCollected = supplyChainResult.diagnostics;
40943
+ const securityScanCollected = yield* join(securityScanFiber);
40846
40944
  yield* reporterService.finalize;
40847
40945
  const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
40848
40946
  ...envCollected,
40947
+ ...securityScanCollected,
40849
40948
  ...supplyChainCollected,
40850
40949
  ...lintCollected,
40851
40950
  ...deadCodeCollected
@@ -43264,5 +43363,5 @@ const startLanguageServer = () => {
43264
43363
  };
43265
43364
  //#endregion
43266
43365
  export { startLanguageServer };
43267
- !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]="d12f3c5d-14fd-59ba-9d8a-7e3f589c88ad")}catch(e){}}();
43268
- //# debugId=d12f3c5d-14fd-59ba-9d8a-7e3f589c88ad
43366
+ !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]="bb5309e5-ce90-551d-bf0a-7db0ea302f24")}catch(e){}}();
43367
+ //# debugId=bb5309e5-ce90-551d-bf0a-7db0ea302f24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.5.8-dev.441e6af",
3
+ "version": "0.5.8-dev.5774deb",
4
4
  "description": "Your agent writes bad React. This catches it",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -63,8 +63,8 @@
63
63
  "vscode-languageserver": "^9.0.1",
64
64
  "vscode-languageserver-textdocument": "^1.0.12",
65
65
  "vscode-uri": "^3.1.0",
66
- "deslop-js": "0.5.8",
67
- "oxlint-plugin-react-doctor": "0.5.8-dev.441e6af"
66
+ "oxlint-plugin-react-doctor": "0.5.8-dev.5774deb",
67
+ "deslop-js": "0.5.8"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@types/babel__code-frame": "^7.27.0",
@@ -72,8 +72,8 @@
72
72
  "@xterm/headless": "^6.0.0",
73
73
  "commander": "^14.0.3",
74
74
  "ora": "^9.4.0",
75
- "@react-doctor/core": "0.5.8",
76
75
  "@react-doctor/api": "0.5.8",
76
+ "@react-doctor/core": "0.5.8",
77
77
  "@react-doctor/language-server": "0.5.8"
78
78
  },
79
79
  "engines": {