react-doctor 0.9.2-dev.9512488 → 0.9.2-dev.992205a

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/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]="bb982094-8280-5afe-b061-eb103665c55c")}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]="3bff4f9b-31f8-5abe-b7a2-a0ab9f6a85b4")}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";
@@ -8,7 +8,7 @@ import path from "node:path";
8
8
  import * as NFS from "node:fs";
9
9
  import fs, { mkdtempSync, rmSync } from "node:fs";
10
10
  import process$1 from "node:process";
11
- import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
11
+ import { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_DOCTOR_RULE_REGISTRY, REACT_DOCTOR_SCAN_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor/core";
12
12
  import * as Crypto from "node:crypto";
13
13
  import crypto, { createHash, randomUUID } from "node:crypto";
14
14
  import { ResolverFactory } from "oxc-resolver";
@@ -68874,7 +68874,7 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
68874
68874
  const getDiagnosticRuleIdentity = (diagnostic) => ({
68875
68875
  ruleKey: `${diagnostic.plugin}/${diagnostic.rule}`,
68876
68876
  category: diagnostic.category,
68877
- tags: diagnostic.plugin === "react-doctor" ? reactDoctorPlugin.rules[diagnostic.rule]?.tags ?? [] : []
68877
+ tags: diagnostic.plugin === "react-doctor" ? REACT_DOCTOR_RULE_REGISTRY[diagnostic.rule]?.tags ?? [] : []
68878
68878
  });
68879
68879
  const compileIgnoredFilePatterns = (userConfig) => {
68880
68880
  const files = userConfig?.ignore?.files;
@@ -69222,7 +69222,7 @@ const buildDiagnosticPipeline = (input) => {
69222
69222
  };
69223
69223
  const shouldAutoSuppress = (diagnostic) => {
69224
69224
  if (diagnostic.plugin !== "react-doctor") return false;
69225
- const rule = reactDoctorPlugin.rules[diagnostic.rule];
69225
+ const rule = REACT_DOCTOR_RULE_REGISTRY[diagnostic.rule];
69226
69226
  if (!rule?.tags?.includes("test-noise")) return false;
69227
69227
  if (rule.tags.includes("migration-hint")) return false;
69228
69228
  return getFileContext(diagnostic.filePath) !== "production";
@@ -69654,6 +69654,7 @@ var OxlintConcurrency = class extends Reference("react-doctor/OxlintConcurrency"
69654
69654
  if (Number.isInteger(parsed) && parsed > 0) return resolveScanConcurrency(parsed);
69655
69655
  return resolveAutoScanConcurrency();
69656
69656
  } }) {};
69657
+ var OxlintSpawnSlots = class extends Reference("react-doctor/OxlintSpawnSlots", { defaultValue: () => null }) {};
69657
69658
  /**
69658
69659
  * Three-state control for overlapping the dead-code pass with the lint pass —
69659
69660
  * forking dead-code as a child fiber that runs DURING lint instead of strictly
@@ -72004,8 +72005,8 @@ const getCapabilities = (project) => {
72004
72005
  const cached = capabilitiesByProject.get(project);
72005
72006
  if (cached !== void 0) return cached;
72006
72007
  const capabilities = new Set(buildCapabilities(project));
72007
- const packageJson = readPackageJson$1(Path.join(project.rootDirectory, "package.json"));
72008
- const targetBlankOpenerProtection = detectTargetBlankOpenerProtection(project.rootDirectory, packageJson);
72008
+ const capabilityRootDirectory = isFile(Path.join(project.rootDirectory, "package.json")) ? project.rootDirectory : findNearestAncestorPackageJson(project.rootDirectory) ?? project.rootDirectory;
72009
+ const targetBlankOpenerProtection = detectTargetBlankOpenerProtection(capabilityRootDirectory, readPackageJson$1(Path.join(capabilityRootDirectory, "package.json")));
72009
72010
  if (targetBlankOpenerProtection !== void 0) capabilities.add("target-blank-needs-explicit-protection");
72010
72011
  if (targetBlankOpenerProtection === "noreferrer") capabilities.add("target-blank-needs-noreferrer");
72011
72012
  capabilitiesByProject.set(project, capabilities);
@@ -72035,7 +72036,7 @@ const createSecurityScanSession = (rootDirectory, options) => {
72035
72036
  const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
72036
72037
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
72037
72038
  const includedTags = options.includedTags ?? /* @__PURE__ */ new Set();
72038
- const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
72039
+ const enabledScanRules = REACT_DOCTOR_SCAN_RULES.flatMap((entry) => {
72039
72040
  const rule = entry.rule;
72040
72041
  const scan = rule.scan;
72041
72042
  if (typeof scan !== "function") return [];
@@ -72496,47 +72497,55 @@ const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
72496
72497
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
72497
72498
  return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
72498
72499
  };
72499
- let availableSlots = -1;
72500
- const waiters = [];
72501
- const releaseSlot = () => {
72502
- const nextWaiter = waiters.shift();
72503
- if (nextWaiter !== void 0) nextWaiter();
72504
- else availableSlots += 1;
72500
+ const createWorkerSlots = (input) => {
72501
+ let availableSlotCount = input.slotCount;
72502
+ const waiters = [];
72503
+ const releaseSlot = () => {
72504
+ const nextWaiter = waiters.shift();
72505
+ if (nextWaiter === void 0) {
72506
+ availableSlotCount += 1;
72507
+ return;
72508
+ }
72509
+ nextWaiter.abortSignal?.removeEventListener("abort", nextWaiter.onAbort);
72510
+ nextWaiter.resolve();
72511
+ };
72512
+ const acquireSlot = async (abortSignal) => {
72513
+ if (abortSignal?.aborted) throw input.createAbortError();
72514
+ if (availableSlotCount > 0) {
72515
+ availableSlotCount -= 1;
72516
+ return;
72517
+ }
72518
+ await new Promise((resolve, reject) => {
72519
+ const onAbort = () => {
72520
+ const waiterIndex = waiters.indexOf(waiter);
72521
+ if (waiterIndex !== -1) waiters.splice(waiterIndex, 1);
72522
+ reject(input.createAbortError());
72523
+ };
72524
+ const waiter = {
72525
+ resolve,
72526
+ abortSignal,
72527
+ onAbort
72528
+ };
72529
+ waiters.push(waiter);
72530
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
72531
+ });
72532
+ };
72533
+ return { run: async (task, abortSignal) => {
72534
+ await acquireSlot(abortSignal);
72535
+ try {
72536
+ return await task();
72537
+ } finally {
72538
+ releaseSlot();
72539
+ }
72540
+ } };
72505
72541
  };
72506
- /**
72507
- * Runs `task` once a dead-code worker slot is free, releasing the slot when the
72508
- * task settles (success or failure). With a high cap (roomy machine) every
72509
- * caller proceeds immediately; with a low cap (constrained runner) callers
72510
- * queue and run as slots free.
72511
- *
72512
- * `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
72513
- * this caller is queued, the call rejects without acquiring a slot or running
72514
- * `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
72515
- * then spawn a child only to tear it down. A queued caller that aborts removes
72516
- * its own waiter so a later release never hands a slot to a dead request.
72517
- */
72542
+ let deadCodeWorkerSlots = null;
72518
72543
  const withDeadCodeWorkerSlot = async (task, abortSignal) => {
72519
- if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
72520
- if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
72521
- if (availableSlots > 0) availableSlots -= 1;
72522
- else await new Promise((resolve, reject) => {
72523
- const waiter = () => {
72524
- abortSignal?.removeEventListener("abort", onAbort);
72525
- resolve();
72526
- };
72527
- const onAbort = () => {
72528
- const queuedIndex = waiters.indexOf(waiter);
72529
- if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
72530
- reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
72531
- };
72532
- waiters.push(waiter);
72533
- abortSignal?.addEventListener("abort", onAbort, { once: true });
72544
+ deadCodeWorkerSlots ??= createWorkerSlots({
72545
+ slotCount: resolveDeadCodeConcurrency(),
72546
+ createAbortError: () => /* @__PURE__ */ new Error("Dead-code worker aborted.")
72534
72547
  });
72535
- try {
72536
- return await task();
72537
- } finally {
72538
- releaseSlot();
72539
- }
72548
+ return deadCodeWorkerSlots.run(task, abortSignal);
72540
72549
  };
72541
72550
  const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
72542
72551
  const resolveReactDoctorCacheDir = (projectDirectory) => {
@@ -74063,12 +74072,12 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
74063
74072
  const settingsRootDirectory = resolveSettingsRootDirectory(project.rootDirectory);
74064
74073
  const enabledReactDoctorRules = {};
74065
74074
  for (const registryEntry of REACT_DOCTOR_RULES) {
74066
- const rule = reactDoctorPlugin.rules[registryEntry.id];
74075
+ const rule = REACT_DOCTOR_RULE_REGISTRY[registryEntry.id];
74067
74076
  if (!rule) continue;
74068
74077
  if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
74069
74078
  if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
74070
74079
  if (ruleSelection === "sidecar" && sidecarRuleIdFilter !== void 0 && !sidecarRuleIdFilter.has(registryEntry.id)) continue;
74071
- if (rule.scan !== void 0) continue;
74080
+ if (rule.isScanRule) continue;
74072
74081
  if (customRulesOnly && registryEntry.originallyExternal) continue;
74073
74082
  if (rule.framework !== "global" && !rule.requires) continue;
74074
74083
  if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen, includedTags)) continue;
@@ -75379,7 +75388,7 @@ const PLUGIN_CATEGORY_MAP = {
75379
75388
  };
75380
75389
  const lookupOwnString = (record, key) => Object.hasOwn(record, key) ? record[key] : void 0;
75381
75390
  const getRuleRecommendation = (ruleName, project) => {
75382
- const rule = reactDoctorPlugin.rules[ruleName];
75391
+ const rule = REACT_DOCTOR_RULE_REGISTRY[ruleName];
75383
75392
  if (!rule) return void 0;
75384
75393
  if (rule.recommendationFor) {
75385
75394
  const capabilities = getCapabilities(project);
@@ -75388,8 +75397,8 @@ const getRuleRecommendation = (ruleName, project) => {
75388
75397
  }
75389
75398
  return rule.recommendation;
75390
75399
  };
75391
- const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.category;
75392
- const getRuleTitle = (ruleName) => reactDoctorPlugin.rules[ruleName]?.title;
75400
+ const getRuleCategory = (ruleName) => REACT_DOCTOR_RULE_REGISTRY[ruleName]?.category;
75401
+ const getRuleTitle = (ruleName) => REACT_DOCTOR_RULE_REGISTRY[ruleName]?.title;
75393
75402
  const resolveDiagnosticTitle = (plugin, rule) => {
75394
75403
  if (plugin !== "react-hooks-js") return getRuleTitle(rule);
75395
75404
  if (rule === "error-boundaries") return REACT_ERROR_BOUNDARY_TITLE;
@@ -75444,7 +75453,7 @@ const resolveDiagnosticCategory = (plugin, rule) => {
75444
75453
  if (plugin === "react-hooks-js" && rule === "error-boundaries") return "Bugs";
75445
75454
  return getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Bugs";
75446
75455
  };
75447
- const resolveMatchByOccurrence = (rule, category) => OCCURRENCE_MATCHED_CATEGORIES.has(category) || Boolean(reactDoctorPlugin.rules[rule]?.matchByOccurrence);
75456
+ const resolveMatchByOccurrence = (rule, category) => OCCURRENCE_MATCHED_CATEGORIES.has(category) || Boolean(REACT_DOCTOR_RULE_REGISTRY[rule]?.matchByOccurrence);
75448
75457
  /**
75449
75458
  * Maps oxlint's non-primary labels (`labels[1..]`) into related source
75450
75459
  * locations. Editors surface these as a diagnostic's
@@ -75535,7 +75544,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory, sourcePathByLintPath,
75535
75544
  const preparedSourceMap = resolvePreparedSourceMap(diagnostic.filename);
75536
75545
  if (preparedSourceMap === void 0) return [diagnostic];
75537
75546
  const { plugin, rule } = parseRuleCode(diagnostic.code);
75538
- if (plugin !== "react-doctor" || !reactDoctorPlugin.rules[rule]?.tags?.includes("design")) return [];
75547
+ if (plugin !== "react-doctor" || !REACT_DOCTOR_RULE_REGISTRY[rule]?.tags?.includes("design")) return [];
75539
75548
  const labels = mapPreparedSourceLabels(diagnostic.labels, preparedSourceMap);
75540
75549
  const sourceFilename = resolveMappedSourceFilename(diagnostic.filename);
75541
75550
  return labels === null || sourceFilename === void 0 ? [] : [{
@@ -75621,11 +75630,12 @@ const SANITIZED_ENV = buildOxlintChildEnv(process.env);
75621
75630
  * The first three are splittable (the caller's binary-split retry
75622
75631
  * shrinks the batch and re-spawns); the fourth isn't.
75623
75632
  */
75624
- const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES, abortSignal) => new Promise((resolve, reject) => {
75633
+ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES, abortSignal, onSpawn) => new Promise((resolve, reject) => {
75625
75634
  if (abortSignal?.aborted) {
75626
75635
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
75627
75636
  return;
75628
75637
  }
75638
+ onSpawn?.();
75629
75639
  const child = spawn(nodeBinaryPath, buildProfiledNodeArguments({
75630
75640
  argumentsList: args,
75631
75641
  cpuProfileDirectory: SANITIZED_ENV.REACT_DOCTOR_CPU_PROFILE_DIR,
@@ -75785,7 +75795,21 @@ const spawnLintBatches = async (input) => {
75785
75795
  }
75786
75796
  const batchArgs = [...baseArgs, ...batch];
75787
75797
  try {
75788
- return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory, sourcePathByLintPath, sourceMapByLintPath);
75798
+ const spawnBatch = () => {
75799
+ if (isPastDeadline()) {
75800
+ deadlineSkippedFiles.push(...batch);
75801
+ batchState.deadlineSkippedFileCount += batch.length;
75802
+ return Promise.resolve(null);
75803
+ }
75804
+ return spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal, () => {
75805
+ if (batchState.didStart) return;
75806
+ batchState.didStart = true;
75807
+ startedFileCount += batchState.initialFileCount;
75808
+ });
75809
+ };
75810
+ const stdout = input.spawnSlots === void 0 ? await spawnBatch() : await input.spawnSlots.run(spawnBatch, signal);
75811
+ if (stdout === null) return [];
75812
+ return parseOxlintOutput(stdout, project, rootDirectory, sourcePathByLintPath, sourceMapByLintPath);
75789
75813
  } catch (error) {
75790
75814
  if (!isSplittableReactDoctorError(error)) throw error;
75791
75815
  if (isPastDeadline()) {
@@ -75829,14 +75853,15 @@ const spawnLintBatches = async (input) => {
75829
75853
  deadlineSkippedFiles.push(...batch);
75830
75854
  return [];
75831
75855
  }
75832
- startedFileCount += batch.length;
75833
75856
  const batchState = {
75834
75857
  deadlineMs: null,
75835
- deadlineSkippedFileCount: 0
75858
+ deadlineSkippedFileCount: 0,
75859
+ didStart: false,
75860
+ initialFileCount: batch.length
75836
75861
  };
75837
75862
  const batchDiagnostics = await spawnLintBatch(batch, 0, batchState);
75838
75863
  scannedFileCount += batch.length - batchState.deadlineSkippedFileCount;
75839
- if (passOnFileProgress) {
75864
+ if (passOnFileProgress && batchState.didStart) {
75840
75865
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
75841
75866
  passOnFileProgress(displayedFileCount, totalFileCount);
75842
75867
  }
@@ -75920,8 +75945,8 @@ const validateRuleRegistration = () => {
75920
75945
  for (const fullKey of ALL_REACT_DOCTOR_RULE_KEYS) {
75921
75946
  const ruleName = fullKey.replace(/^react-doctor\//, "");
75922
75947
  if (!getRuleCategory(ruleName)) missingCategory.push(fullKey);
75923
- if (!reactDoctorPlugin.rules[ruleName]?.recommendation) missingHelp.push(fullKey);
75924
- if (FRAMEWORK_SPECIFIC_RULE_KEYS.has(fullKey) && !reactDoctorPlugin.rules[ruleName]?.requires) missingMetadata.push(fullKey);
75948
+ if (!REACT_DOCTOR_RULE_REGISTRY[ruleName]?.recommendation) missingHelp.push(fullKey);
75949
+ if (FRAMEWORK_SPECIFIC_RULE_KEYS.has(fullKey) && !REACT_DOCTOR_RULE_REGISTRY[ruleName]?.requires) missingMetadata.push(fullKey);
75925
75950
  }
75926
75951
  if (missingCategory.length === 0 && missingHelp.length === 0 && missingMetadata.length === 0) return;
75927
75952
  const detail = [
@@ -76562,6 +76587,7 @@ const runOxlint = async (options) => {
76562
76587
  spawnTimeoutMs,
76563
76588
  outputMaxBytes,
76564
76589
  concurrency: options.concurrency,
76590
+ spawnSlots: options.spawnSlots,
76565
76591
  signal: options.signal,
76566
76592
  deadlineEpochMs: options.deadlineEpochMs
76567
76593
  });
@@ -76764,6 +76790,7 @@ const runOxlint = async (options) => {
76764
76790
  spawnTimeoutMs,
76765
76791
  outputMaxBytes,
76766
76792
  concurrency: options.concurrency,
76793
+ spawnSlots: options.spawnSlots,
76767
76794
  signal: options.signal,
76768
76795
  deadlineEpochMs: options.deadlineEpochMs
76769
76796
  });
@@ -76858,6 +76885,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
76858
76885
  const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
76859
76886
  const outputMaxBytes = yield* OxlintOutputMaxBytes;
76860
76887
  const concurrency = yield* OxlintConcurrency;
76888
+ const spawnSlots = yield* OxlintSpawnSlots;
76861
76889
  const lintBatchOrdering = yield* LintBatchOrdering;
76862
76890
  const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
76863
76891
  const sidecarLintCacheEnabled = yield* SidecarLintCacheEnabled;
@@ -76887,6 +76915,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
76887
76915
  onSidecarStats: input.onSidecarStats,
76888
76916
  spawnTimeoutMs,
76889
76917
  outputMaxBytes,
76918
+ spawnSlots: spawnSlots ?? void 0,
76890
76919
  concurrency,
76891
76920
  signal,
76892
76921
  lintBatchOrdering,
@@ -78549,6 +78578,11 @@ const computeConfigFingerprint = (projectDirectory, version) => {
78549
78578
  }
78550
78579
  return crypto.createHash("sha1").update(parts.join("|")).digest("hex");
78551
78580
  };
78581
+ const createLintPhaseAbortError = () => new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) });
78582
+ const createOxlintSpawnSlots = (concurrency) => createWorkerSlots({
78583
+ slotCount: resolveScanConcurrency(concurrency),
78584
+ createAbortError: createLintPhaseAbortError
78585
+ });
78552
78586
  const AI_TRAINING_ENV_VARS = [
78553
78587
  ["HF_DATASETS_CACHE", "huggingface"],
78554
78588
  ["HF_HOME", "huggingface"],
@@ -78628,7 +78662,7 @@ const groupBy = (items, keyFn) => {
78628
78662
  * recipe, so advertising "fetch the fix recipe" for them sends agents to
78629
78663
  * a 404. Gate the directive on this predicate.
78630
78664
  */
78631
- const hasPublishedFixRecipe = (diagnostic) => diagnostic.plugin === "react-doctor" && Object.hasOwn(reactDoctorPlugin.rules, diagnostic.rule);
78665
+ const hasPublishedFixRecipe = (diagnostic) => diagnostic.plugin === "react-doctor" && Object.hasOwn(REACT_DOCTOR_RULE_REGISTRY, diagnostic.rule);
78632
78666
  const unionValues = (baseValues, overrideValues) => [...new Set([...baseValues, ...overrideValues])];
78633
78667
  const mergeIgnores = (baseIgnore, overrideIgnore) => {
78634
78668
  const mergedIgnore = {
@@ -78976,7 +79010,7 @@ const makeNoopConsole = () => ({
78976
79010
  });
78977
79011
  //#endregion
78978
79012
  //#region src/cli/utils/version.ts
78979
- const VERSION = "0.9.2-dev.9512488";
79013
+ const VERSION = "0.9.2-dev.992205a";
78980
79014
  //#endregion
78981
79015
  //#region src/cli/utils/json-mode.ts
78982
79016
  let context = null;
@@ -79334,13 +79368,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
79334
79368
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
79335
79369
  * standard `SENTRY_RELEASE` override.
79336
79370
  */
79337
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.9.2-dev.9512488`;
79371
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.9.2-dev.992205a`;
79338
79372
  /**
79339
79373
  * Deployment environment shown in Sentry's environment filter. Defaults to
79340
79374
  * `production` for tagged releases and `development` for dev/unbuilt versions,
79341
79375
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
79342
79376
  */
79343
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.9.2-dev.9512488") ? "development" : "production");
79377
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.9.2-dev.992205a") ? "development" : "production");
79344
79378
  /**
79345
79379
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
79346
79380
  * (set to `0` to disable tracing) and falls back to
@@ -91194,7 +91228,8 @@ const buildRuntimeLayers = (input) => {
91194
91228
  resolvedDirectory: input.directory,
91195
91229
  configSourceDirectory: input.configSourceDirectory
91196
91230
  }) : Config.layerNode, Files.layerNode, Git.layerNode, linterLayer, LintPartialFailures.layerLive, deadCodeLayer, progressLayer, reporterLayer, scoreLayer, supplyChainLayer);
91197
- return input.oxlintConcurrency === void 0 ? baseLayers : mergeAll$1(baseLayers, succeed$3(OxlintConcurrency, input.oxlintConcurrency));
91231
+ const layersWithConcurrency = input.oxlintConcurrency === void 0 ? baseLayers : mergeAll$1(baseLayers, succeed$3(OxlintConcurrency, input.oxlintConcurrency));
91232
+ return input.oxlintSpawnSlots === void 0 ? layersWithConcurrency : mergeAll$1(layersWithConcurrency, succeed$3(OxlintSpawnSlots, input.oxlintSpawnSlots));
91198
91233
  };
91199
91234
  //#endregion
91200
91235
  //#region src/cli/utils/with-sentry-run-span.ts
@@ -93698,7 +93733,7 @@ const buildRunEventConfig = (options, userConfig, hasCustomConfig, resolvedWorke
93698
93733
  userConfig
93699
93734
  };
93700
93735
  };
93701
- const inspect = async (directory, inputOptions = {}) => {
93736
+ const inspectWithOxlintRuntime = async (directory, inputOptions, oxlintRuntime) => {
93702
93737
  const startTime = performance$1.now();
93703
93738
  const deadlineEpochMs = inputOptions.deadlineEpochMs ?? (inputOptions.maxDurationMs != null ? Date.now() + inputOptions.maxDurationMs : null);
93704
93739
  const isConcurrentScan = inputOptions.concurrentScan === true;
@@ -93724,7 +93759,7 @@ const inspect = async (directory, inputOptions = {}) => {
93724
93759
  try {
93725
93760
  const result = await withSentryRunSpan(async (rootSentrySpan) => {
93726
93761
  try {
93727
- return await runInspectWithRuntime(scanDirectory, options, userConfig, hasConfigOverride, configSourceDirectory, startTime, deadlineEpochMs, rootSentrySpan);
93762
+ return await runInspectWithRuntime(scanDirectory, options, userConfig, hasConfigOverride, configSourceDirectory, startTime, deadlineEpochMs, rootSentrySpan, oxlintRuntime);
93728
93763
  } catch (error) {
93729
93764
  recordRunEvent(rootSentrySpan, {
93730
93765
  ...buildRunEventConfig(options, userConfig, userConfig !== null),
@@ -93740,6 +93775,15 @@ const inspect = async (directory, inputOptions = {}) => {
93740
93775
  if (ownsSpinnerSilence) setSpinnerSilent(wasSpinnerSilent);
93741
93776
  }
93742
93777
  };
93778
+ const createInvocationInspect = (requestedOxlintConcurrency) => {
93779
+ const concurrency = resolveScanConcurrency(requestedOxlintConcurrency ?? runSync(OxlintConcurrency));
93780
+ const oxlintRuntime = {
93781
+ concurrency,
93782
+ spawnSlots: createOxlintSpawnSlots(concurrency)
93783
+ };
93784
+ return (directory, inputOptions = {}) => inspectWithOxlintRuntime(directory, inputOptions, oxlintRuntime);
93785
+ };
93786
+ const inspect = async (directory, inputOptions = {}) => createInvocationInspect(inputOptions.concurrency)(directory, inputOptions);
93743
93787
  const countIncompleteLintFiles = (lintPartialFailures) => countDroppedLintFiles(lintPartialFailures) + countDeadlineSkippedFiles(lintPartialFailures);
93744
93788
  /**
93745
93789
  * Runs a second, lint-only scan over the changed files as they existed at the
@@ -93793,7 +93837,8 @@ const runBaselineComparison = async (params) => {
93793
93837
  shouldRunSupplyChain: params.options.supplyChain,
93794
93838
  shouldComputeScore: false,
93795
93839
  shouldShowProgressSpinners: false,
93796
- oxlintConcurrency: params.options.concurrency
93840
+ oxlintConcurrency: params.oxlintRuntime.concurrency,
93841
+ oxlintSpawnSlots: params.oxlintRuntime.spawnSlots
93797
93842
  });
93798
93843
  const baseOutput = await runPromise(restoreLegacyThrow(runInspect({
93799
93844
  directory: snapshot.tempDirectory,
@@ -93838,7 +93883,7 @@ const runBaselineComparison = async (params) => {
93838
93883
  snapshot.cleanup();
93839
93884
  }
93840
93885
  };
93841
- const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOverride, configSourceDirectory, startTime, deadlineEpochMs, rootSentrySpan) => {
93886
+ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOverride, configSourceDirectory, startTime, deadlineEpochMs, rootSentrySpan, oxlintRuntime) => {
93842
93887
  const isDiffMode = options.includePaths.length > 0;
93843
93888
  const resolvedNodeBinaryPath = await resolveOxlintNode(options.lint, options.scoreOnly || options.silent);
93844
93889
  const lintBindingMissing = options.lint && !resolvedNodeBinaryPath;
@@ -93888,7 +93933,8 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
93888
93933
  shouldRunSupplyChain: options.supplyChain,
93889
93934
  shouldComputeScore: !options.noScore,
93890
93935
  shouldShowProgressSpinners,
93891
- oxlintConcurrency: options.concurrency,
93936
+ oxlintConcurrency: oxlintRuntime.concurrency,
93937
+ oxlintSpawnSlots: oxlintRuntime.spawnSlots,
93892
93938
  reporterLayer: options.uiLayers?.reporter,
93893
93939
  progressLayer: options.uiLayers?.progress
93894
93940
  });
@@ -93945,7 +93991,8 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
93945
93991
  baseFiles: options.baseline.baseFiles,
93946
93992
  headFiles: options.baseline.headFiles,
93947
93993
  headAnalyzedFiles: output.analyzedFiles,
93948
- deadlineEpochMs
93994
+ deadlineEpochMs,
93995
+ oxlintRuntime
93949
93996
  });
93950
93997
  if (comparison) {
93951
93998
  inspectDiagnostics = comparison.displayDiagnostics;
@@ -96778,6 +96825,7 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
96778
96825
  if (!isQuiet) if (!flags.verbose && canAnimateOnboarding(process.stdout)) await runPromise(playWelcomeScene({ speedMultiplier: !isOnboardingForced() && hasCompletedOnboarding() ? 2 : 1 }));
96779
96826
  else runSync(printBrandedHeader);
96780
96827
  const scanOptions = resolveCliInspectOptions(flags, userConfig);
96828
+ const inspectProject = createInvocationInspect(scanOptions.concurrency);
96781
96829
  const scanDeadlineEpochMs = scanOptions.maxDurationMs !== void 0 ? Date.now() + scanOptions.maxDurationMs : void 0;
96782
96830
  const categoryFilters = new Set(scanOptions.categoryFilters ?? []);
96783
96831
  const skipPrompts = shouldSkipPrompts({
@@ -96923,7 +96971,7 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
96923
96971
  const scanStagedProject = async (projectRun) => {
96924
96972
  const { projectScan, includePaths } = projectRun;
96925
96973
  const projectTempDirectory = Path.join(snapshot.tempDirectory, projectScan.treeRelativeDirectory);
96926
- const scanResult = await inspect(projectTempDirectory, {
96974
+ const scanResult = await inspectProject(projectTempDirectory, {
96927
96975
  ...scanOptions,
96928
96976
  deadlineEpochMs: scanDeadlineEpochMs,
96929
96977
  includePaths: [...includePaths],
@@ -97082,7 +97130,7 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97082
97130
  if (supplyChainManifestChanged) includePaths.push("package.json");
97083
97131
  }
97084
97132
  if (!isQuiet && !isMultiProject) cliLogger.dim(" ");
97085
- const scanResult = await inspect(scanDirectory, {
97133
+ const scanResult = await inspectProject(scanDirectory, {
97086
97134
  ...scanOptions,
97087
97135
  deadlineEpochMs: scanDeadlineEpochMs,
97088
97136
  includePaths,
@@ -98307,7 +98355,7 @@ program.command("experimental-tui [directory]", { hidden: true }).description("[
98307
98355
  return;
98308
98356
  }
98309
98357
  recordCount(METRIC.cliInvoked, 1, { command: "experimental-tui" });
98310
- const { runScanApp } = await import("./run-scan-app-C9MJnVwv.js");
98358
+ const { runScanApp } = await import("./run-scan-app-DHRvqfkX.js");
98311
98359
  const { shouldFail } = await runScanApp({
98312
98360
  directory,
98313
98361
  options: {
@@ -98347,7 +98395,7 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
98347
98395
  handleError(error, { sentryEventId });
98348
98396
  });
98349
98397
  //#endregion
98350
- export { toForwardSlashes as A, getCategoryImpact as B, buildRulePriorityMap as C, resolveBlockingLevel as D, formatLearnMoreLine as E, TUI_REPORT_DETAIL_WIDTH_FRACTION as F, runPromise as G, mapWithConcurrency as H, isCiEnvironment as I, succeed$3 as J, sync as K, DIAGNOSTIC_CATEGORY_BUCKETS as L, isUnicodeSupported as M, METRIC as N, isReactDoctorWorkflowInstalled as O, TUI_REPORT_COMPACT_MAX_ROWS as P, Progress as R, hasLintHardFailure as S, formatFixRecipeLine as T, mergeReactDoctorConfigs as U, highlighter as V, resolveScanTarget as W, canAnimateOnboarding as _, resolveCliInspectOptions as a, buildCodeFrame as b, detectLaunchableAgents as c, launchCliAgent as d, isShareOptedOut as f, doctorFace as g, easeOutCubic as h, selectProjects as i, findNearestPackageDirectory as j, recordCount as k, CLI_AGENT_BINARIES as l, printFooter as m, shouldFailScanGate as n, countUniqueScannedFiles as o, inspect as p, void_ as q, discoverWorkspacePackages as r, setUpGitHubActions as s, exitGracefully as t, copyToClipboard as u, computeProjectedScore as v, buildSortedRuleGroups as w, buildNoScoreMessage as x, formatElapsedTime as y, Reporter as z };
98398
+ export { toForwardSlashes as A, getCategoryImpact as B, buildRulePriorityMap as C, resolveBlockingLevel as D, formatLearnMoreLine as E, TUI_REPORT_DETAIL_WIDTH_FRACTION as F, runPromise as G, mapWithConcurrency as H, isCiEnvironment as I, succeed$3 as J, sync as K, DIAGNOSTIC_CATEGORY_BUCKETS as L, isUnicodeSupported as M, METRIC as N, isReactDoctorWorkflowInstalled as O, TUI_REPORT_COMPACT_MAX_ROWS as P, Progress as R, hasLintHardFailure as S, formatFixRecipeLine as T, mergeReactDoctorConfigs as U, highlighter as V, resolveScanTarget as W, canAnimateOnboarding as _, resolveCliInspectOptions as a, buildCodeFrame as b, detectLaunchableAgents as c, launchCliAgent as d, isShareOptedOut as f, doctorFace as g, easeOutCubic as h, selectProjects as i, findNearestPackageDirectory as j, recordCount as k, CLI_AGENT_BINARIES as l, printFooter as m, shouldFailScanGate as n, countUniqueScannedFiles as o, createInvocationInspect as p, void_ as q, discoverWorkspacePackages as r, setUpGitHubActions as s, exitGracefully as t, copyToClipboard as u, computeProjectedScore as v, buildSortedRuleGroups as w, buildNoScoreMessage as x, formatElapsedTime as y, Reporter as z };
98351
98399
 
98352
98400
  //# sourceMappingURL=cli.js.map
98353
- //# debugId=bb982094-8280-5afe-b061-eb103665c55c
98401
+ //# debugId=3bff4f9b-31f8-5abe-b7a2-a0ab9f6a85b4
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { FrameworkToken } from "oxlint-plugin-react-doctor";
1
+ import { FrameworkToken } from "oxlint-plugin-react-doctor/core";
2
2
 
3
3
  //#region ../../node_modules/.pnpm/effect@4.0.0-beta.70/node_modules/effect/dist/Types.d.ts
4
4
  /**
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
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]="18c61424-4926-5cae-8f2a-6001534ea755")}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]="61d9689f-8ba5-5ca3-a0c2-09557d3f7028")}catch(e){}}();
3
3
  import { i as __toESM$1, r as __require, t as __commonJSMin$1 } from "./chunk-ChKA3wi6.js";
4
4
  import { createRequire } from "node:module";
5
- import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
5
+ import { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_DOCTOR_RULE_REGISTRY, REACT_DOCTOR_SCAN_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor/core";
6
6
  import * as Crypto from "node:crypto";
7
7
  import crypto, { createHash } from "node:crypto";
8
8
  import * as NFS from "node:fs";
@@ -65673,7 +65673,7 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
65673
65673
  const getDiagnosticRuleIdentity = (diagnostic) => ({
65674
65674
  ruleKey: `${diagnostic.plugin}/${diagnostic.rule}`,
65675
65675
  category: diagnostic.category,
65676
- tags: diagnostic.plugin === "react-doctor" ? reactDoctorPlugin.rules[diagnostic.rule]?.tags ?? [] : []
65676
+ tags: diagnostic.plugin === "react-doctor" ? REACT_DOCTOR_RULE_REGISTRY[diagnostic.rule]?.tags ?? [] : []
65677
65677
  });
65678
65678
  const compileIgnoredFilePatterns = (userConfig) => {
65679
65679
  const files = userConfig?.ignore?.files;
@@ -66025,7 +66025,7 @@ const buildDiagnosticPipeline = (input) => {
66025
66025
  };
66026
66026
  const shouldAutoSuppress = (diagnostic) => {
66027
66027
  if (diagnostic.plugin !== "react-doctor") return false;
66028
- const rule = reactDoctorPlugin.rules[diagnostic.rule];
66028
+ const rule = REACT_DOCTOR_RULE_REGISTRY[diagnostic.rule];
66029
66029
  if (!rule?.tags?.includes("test-noise")) return false;
66030
66030
  if (rule.tags.includes("migration-hint")) return false;
66031
66031
  return getFileContext(diagnostic.filePath) !== "production";
@@ -66457,6 +66457,7 @@ var OxlintConcurrency = class extends Reference("react-doctor/OxlintConcurrency"
66457
66457
  if (Number.isInteger(parsed) && parsed > 0) return resolveScanConcurrency(parsed);
66458
66458
  return resolveAutoScanConcurrency();
66459
66459
  } }) {};
66460
+ var OxlintSpawnSlots = class extends Reference("react-doctor/OxlintSpawnSlots", { defaultValue: () => null }) {};
66460
66461
  /**
66461
66462
  * Three-state control for overlapping the dead-code pass with the lint pass —
66462
66463
  * forking dead-code as a child fiber that runs DURING lint instead of strictly
@@ -68779,8 +68780,8 @@ const getCapabilities = (project) => {
68779
68780
  const cached = capabilitiesByProject.get(project);
68780
68781
  if (cached !== void 0) return cached;
68781
68782
  const capabilities = new Set(buildCapabilities(project));
68782
- const packageJson = readPackageJson(Path.join(project.rootDirectory, "package.json"));
68783
- const targetBlankOpenerProtection = detectTargetBlankOpenerProtection(project.rootDirectory, packageJson);
68783
+ const capabilityRootDirectory = isFile(Path.join(project.rootDirectory, "package.json")) ? project.rootDirectory : findNearestAncestorPackageJson(project.rootDirectory) ?? project.rootDirectory;
68784
+ const targetBlankOpenerProtection = detectTargetBlankOpenerProtection(capabilityRootDirectory, readPackageJson(Path.join(capabilityRootDirectory, "package.json")));
68784
68785
  if (targetBlankOpenerProtection !== void 0) capabilities.add("target-blank-needs-explicit-protection");
68785
68786
  if (targetBlankOpenerProtection === "noreferrer") capabilities.add("target-blank-needs-noreferrer");
68786
68787
  capabilitiesByProject.set(project, capabilities);
@@ -68810,7 +68811,7 @@ const createSecurityScanSession = (rootDirectory, options) => {
68810
68811
  const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
68811
68812
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
68812
68813
  const includedTags = options.includedTags ?? /* @__PURE__ */ new Set();
68813
- const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
68814
+ const enabledScanRules = REACT_DOCTOR_SCAN_RULES.flatMap((entry) => {
68814
68815
  const rule = entry.rule;
68815
68816
  const scan = rule.scan;
68816
68817
  if (typeof scan !== "function") return [];
@@ -69255,47 +69256,55 @@ const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
69255
69256
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
69256
69257
  return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
69257
69258
  };
69258
- let availableSlots = -1;
69259
- const waiters = [];
69260
- const releaseSlot = () => {
69261
- const nextWaiter = waiters.shift();
69262
- if (nextWaiter !== void 0) nextWaiter();
69263
- else availableSlots += 1;
69259
+ const createWorkerSlots = (input) => {
69260
+ let availableSlotCount = input.slotCount;
69261
+ const waiters = [];
69262
+ const releaseSlot = () => {
69263
+ const nextWaiter = waiters.shift();
69264
+ if (nextWaiter === void 0) {
69265
+ availableSlotCount += 1;
69266
+ return;
69267
+ }
69268
+ nextWaiter.abortSignal?.removeEventListener("abort", nextWaiter.onAbort);
69269
+ nextWaiter.resolve();
69270
+ };
69271
+ const acquireSlot = async (abortSignal) => {
69272
+ if (abortSignal?.aborted) throw input.createAbortError();
69273
+ if (availableSlotCount > 0) {
69274
+ availableSlotCount -= 1;
69275
+ return;
69276
+ }
69277
+ await new Promise((resolve, reject) => {
69278
+ const onAbort = () => {
69279
+ const waiterIndex = waiters.indexOf(waiter);
69280
+ if (waiterIndex !== -1) waiters.splice(waiterIndex, 1);
69281
+ reject(input.createAbortError());
69282
+ };
69283
+ const waiter = {
69284
+ resolve,
69285
+ abortSignal,
69286
+ onAbort
69287
+ };
69288
+ waiters.push(waiter);
69289
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
69290
+ });
69291
+ };
69292
+ return { run: async (task, abortSignal) => {
69293
+ await acquireSlot(abortSignal);
69294
+ try {
69295
+ return await task();
69296
+ } finally {
69297
+ releaseSlot();
69298
+ }
69299
+ } };
69264
69300
  };
69265
- /**
69266
- * Runs `task` once a dead-code worker slot is free, releasing the slot when the
69267
- * task settles (success or failure). With a high cap (roomy machine) every
69268
- * caller proceeds immediately; with a low cap (constrained runner) callers
69269
- * queue and run as slots free.
69270
- *
69271
- * `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
69272
- * this caller is queued, the call rejects without acquiring a slot or running
69273
- * `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
69274
- * then spawn a child only to tear it down. A queued caller that aborts removes
69275
- * its own waiter so a later release never hands a slot to a dead request.
69276
- */
69301
+ let deadCodeWorkerSlots = null;
69277
69302
  const withDeadCodeWorkerSlot = async (task, abortSignal) => {
69278
- if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
69279
- if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
69280
- if (availableSlots > 0) availableSlots -= 1;
69281
- else await new Promise((resolve, reject) => {
69282
- const waiter = () => {
69283
- abortSignal?.removeEventListener("abort", onAbort);
69284
- resolve();
69285
- };
69286
- const onAbort = () => {
69287
- const queuedIndex = waiters.indexOf(waiter);
69288
- if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
69289
- reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
69290
- };
69291
- waiters.push(waiter);
69292
- abortSignal?.addEventListener("abort", onAbort, { once: true });
69303
+ deadCodeWorkerSlots ??= createWorkerSlots({
69304
+ slotCount: resolveDeadCodeConcurrency(),
69305
+ createAbortError: () => /* @__PURE__ */ new Error("Dead-code worker aborted.")
69293
69306
  });
69294
- try {
69295
- return await task();
69296
- } finally {
69297
- releaseSlot();
69298
- }
69307
+ return deadCodeWorkerSlots.run(task, abortSignal);
69299
69308
  };
69300
69309
  const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
69301
69310
  const resolveReactDoctorCacheDir = (projectDirectory) => {
@@ -70822,12 +70831,12 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
70822
70831
  const settingsRootDirectory = resolveSettingsRootDirectory(project.rootDirectory);
70823
70832
  const enabledReactDoctorRules = {};
70824
70833
  for (const registryEntry of REACT_DOCTOR_RULES) {
70825
- const rule = reactDoctorPlugin.rules[registryEntry.id];
70834
+ const rule = REACT_DOCTOR_RULE_REGISTRY[registryEntry.id];
70826
70835
  if (!rule) continue;
70827
70836
  if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
70828
70837
  if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
70829
70838
  if (ruleSelection === "sidecar" && sidecarRuleIdFilter !== void 0 && !sidecarRuleIdFilter.has(registryEntry.id)) continue;
70830
- if (rule.scan !== void 0) continue;
70839
+ if (rule.isScanRule) continue;
70831
70840
  if (customRulesOnly && registryEntry.originallyExternal) continue;
70832
70841
  if (rule.framework !== "global" && !rule.requires) continue;
70833
70842
  if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen, includedTags)) continue;
@@ -72138,7 +72147,7 @@ const PLUGIN_CATEGORY_MAP = {
72138
72147
  };
72139
72148
  const lookupOwnString = (record, key) => Object.hasOwn(record, key) ? record[key] : void 0;
72140
72149
  const getRuleRecommendation = (ruleName, project) => {
72141
- const rule = reactDoctorPlugin.rules[ruleName];
72150
+ const rule = REACT_DOCTOR_RULE_REGISTRY[ruleName];
72142
72151
  if (!rule) return void 0;
72143
72152
  if (rule.recommendationFor) {
72144
72153
  const capabilities = getCapabilities(project);
@@ -72147,8 +72156,8 @@ const getRuleRecommendation = (ruleName, project) => {
72147
72156
  }
72148
72157
  return rule.recommendation;
72149
72158
  };
72150
- const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.category;
72151
- const getRuleTitle = (ruleName) => reactDoctorPlugin.rules[ruleName]?.title;
72159
+ const getRuleCategory = (ruleName) => REACT_DOCTOR_RULE_REGISTRY[ruleName]?.category;
72160
+ const getRuleTitle = (ruleName) => REACT_DOCTOR_RULE_REGISTRY[ruleName]?.title;
72152
72161
  const resolveDiagnosticTitle = (plugin, rule) => {
72153
72162
  if (plugin !== "react-hooks-js") return getRuleTitle(rule);
72154
72163
  if (rule === "error-boundaries") return REACT_ERROR_BOUNDARY_TITLE;
@@ -72203,7 +72212,7 @@ const resolveDiagnosticCategory = (plugin, rule) => {
72203
72212
  if (plugin === "react-hooks-js" && rule === "error-boundaries") return "Bugs";
72204
72213
  return getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Bugs";
72205
72214
  };
72206
- const resolveMatchByOccurrence = (rule, category) => OCCURRENCE_MATCHED_CATEGORIES.has(category) || Boolean(reactDoctorPlugin.rules[rule]?.matchByOccurrence);
72215
+ const resolveMatchByOccurrence = (rule, category) => OCCURRENCE_MATCHED_CATEGORIES.has(category) || Boolean(REACT_DOCTOR_RULE_REGISTRY[rule]?.matchByOccurrence);
72207
72216
  /**
72208
72217
  * Maps oxlint's non-primary labels (`labels[1..]`) into related source
72209
72218
  * locations. Editors surface these as a diagnostic's
@@ -72294,7 +72303,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory, sourcePathByLintPath,
72294
72303
  const preparedSourceMap = resolvePreparedSourceMap(diagnostic.filename);
72295
72304
  if (preparedSourceMap === void 0) return [diagnostic];
72296
72305
  const { plugin, rule } = parseRuleCode(diagnostic.code);
72297
- if (plugin !== "react-doctor" || !reactDoctorPlugin.rules[rule]?.tags?.includes("design")) return [];
72306
+ if (plugin !== "react-doctor" || !REACT_DOCTOR_RULE_REGISTRY[rule]?.tags?.includes("design")) return [];
72298
72307
  const labels = mapPreparedSourceLabels(diagnostic.labels, preparedSourceMap);
72299
72308
  const sourceFilename = resolveMappedSourceFilename(diagnostic.filename);
72300
72309
  return labels === null || sourceFilename === void 0 ? [] : [{
@@ -72380,11 +72389,12 @@ const SANITIZED_ENV = buildOxlintChildEnv(process.env);
72380
72389
  * The first three are splittable (the caller's binary-split retry
72381
72390
  * shrinks the batch and re-spawns); the fourth isn't.
72382
72391
  */
72383
- const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES, abortSignal) => new Promise((resolve, reject) => {
72392
+ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES, abortSignal, onSpawn) => new Promise((resolve, reject) => {
72384
72393
  if (abortSignal?.aborted) {
72385
72394
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
72386
72395
  return;
72387
72396
  }
72397
+ onSpawn?.();
72388
72398
  const child = spawn(nodeBinaryPath, buildProfiledNodeArguments({
72389
72399
  argumentsList: args,
72390
72400
  cpuProfileDirectory: SANITIZED_ENV.REACT_DOCTOR_CPU_PROFILE_DIR,
@@ -72544,7 +72554,21 @@ const spawnLintBatches = async (input) => {
72544
72554
  }
72545
72555
  const batchArgs = [...baseArgs, ...batch];
72546
72556
  try {
72547
- return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory, sourcePathByLintPath, sourceMapByLintPath);
72557
+ const spawnBatch = () => {
72558
+ if (isPastDeadline()) {
72559
+ deadlineSkippedFiles.push(...batch);
72560
+ batchState.deadlineSkippedFileCount += batch.length;
72561
+ return Promise.resolve(null);
72562
+ }
72563
+ return spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal, () => {
72564
+ if (batchState.didStart) return;
72565
+ batchState.didStart = true;
72566
+ startedFileCount += batchState.initialFileCount;
72567
+ });
72568
+ };
72569
+ const stdout = input.spawnSlots === void 0 ? await spawnBatch() : await input.spawnSlots.run(spawnBatch, signal);
72570
+ if (stdout === null) return [];
72571
+ return parseOxlintOutput(stdout, project, rootDirectory, sourcePathByLintPath, sourceMapByLintPath);
72548
72572
  } catch (error) {
72549
72573
  if (!isSplittableReactDoctorError(error)) throw error;
72550
72574
  if (isPastDeadline()) {
@@ -72588,14 +72612,15 @@ const spawnLintBatches = async (input) => {
72588
72612
  deadlineSkippedFiles.push(...batch);
72589
72613
  return [];
72590
72614
  }
72591
- startedFileCount += batch.length;
72592
72615
  const batchState = {
72593
72616
  deadlineMs: null,
72594
- deadlineSkippedFileCount: 0
72617
+ deadlineSkippedFileCount: 0,
72618
+ didStart: false,
72619
+ initialFileCount: batch.length
72595
72620
  };
72596
72621
  const batchDiagnostics = await spawnLintBatch(batch, 0, batchState);
72597
72622
  scannedFileCount += batch.length - batchState.deadlineSkippedFileCount;
72598
- if (passOnFileProgress) {
72623
+ if (passOnFileProgress && batchState.didStart) {
72599
72624
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
72600
72625
  passOnFileProgress(displayedFileCount, totalFileCount);
72601
72626
  }
@@ -72679,8 +72704,8 @@ const validateRuleRegistration = () => {
72679
72704
  for (const fullKey of ALL_REACT_DOCTOR_RULE_KEYS) {
72680
72705
  const ruleName = fullKey.replace(/^react-doctor\//, "");
72681
72706
  if (!getRuleCategory(ruleName)) missingCategory.push(fullKey);
72682
- if (!reactDoctorPlugin.rules[ruleName]?.recommendation) missingHelp.push(fullKey);
72683
- if (FRAMEWORK_SPECIFIC_RULE_KEYS.has(fullKey) && !reactDoctorPlugin.rules[ruleName]?.requires) missingMetadata.push(fullKey);
72707
+ if (!REACT_DOCTOR_RULE_REGISTRY[ruleName]?.recommendation) missingHelp.push(fullKey);
72708
+ if (FRAMEWORK_SPECIFIC_RULE_KEYS.has(fullKey) && !REACT_DOCTOR_RULE_REGISTRY[ruleName]?.requires) missingMetadata.push(fullKey);
72684
72709
  }
72685
72710
  if (missingCategory.length === 0 && missingHelp.length === 0 && missingMetadata.length === 0) return;
72686
72711
  const detail = [
@@ -73321,6 +73346,7 @@ const runOxlint = async (options) => {
73321
73346
  spawnTimeoutMs,
73322
73347
  outputMaxBytes,
73323
73348
  concurrency: options.concurrency,
73349
+ spawnSlots: options.spawnSlots,
73324
73350
  signal: options.signal,
73325
73351
  deadlineEpochMs: options.deadlineEpochMs
73326
73352
  });
@@ -73523,6 +73549,7 @@ const runOxlint = async (options) => {
73523
73549
  spawnTimeoutMs,
73524
73550
  outputMaxBytes,
73525
73551
  concurrency: options.concurrency,
73552
+ spawnSlots: options.spawnSlots,
73526
73553
  signal: options.signal,
73527
73554
  deadlineEpochMs: options.deadlineEpochMs
73528
73555
  });
@@ -73617,6 +73644,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
73617
73644
  const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
73618
73645
  const outputMaxBytes = yield* OxlintOutputMaxBytes;
73619
73646
  const concurrency = yield* OxlintConcurrency;
73647
+ const spawnSlots = yield* OxlintSpawnSlots;
73620
73648
  const lintBatchOrdering = yield* LintBatchOrdering;
73621
73649
  const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
73622
73650
  const sidecarLintCacheEnabled = yield* SidecarLintCacheEnabled;
@@ -73646,6 +73674,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
73646
73674
  onSidecarStats: input.onSidecarStats,
73647
73675
  spawnTimeoutMs,
73648
73676
  outputMaxBytes,
73677
+ spawnSlots: spawnSlots ?? void 0,
73649
73678
  concurrency,
73650
73679
  signal,
73651
73680
  lintBatchOrdering,
@@ -75173,6 +75202,11 @@ const filterSourceFiles = (filePaths) => filePaths.filter(isLintableSourceFile);
75173
75202
  * other `clear*` helpers.
75174
75203
  */
75175
75204
  const clearAutoSuppressionCaches = () => {};
75205
+ const createLintPhaseAbortError = () => new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) });
75206
+ const createOxlintSpawnSlots = (concurrency) => createWorkerSlots({
75207
+ slotCount: resolveScanConcurrency(concurrency),
75208
+ createAbortError: createLintPhaseAbortError
75209
+ });
75176
75210
  /**
75177
75211
  * Identity helper for authoring a typed `doctor.config.{ts,js,mjs,cjs}`.
75178
75212
  * Gives editor autocomplete and type-checking for the config object without
@@ -75293,7 +75327,7 @@ const buildDiagnoseLayer = (input) => {
75293
75327
  resolvedDirectory: input.configOverrideTarget.resolvedDirectory,
75294
75328
  configSourceDirectory: input.configOverrideTarget.configSourceDirectory
75295
75329
  });
75296
- return mergeAll$1(Project.layerNode, configLayer, input.shouldRunDeadCode ? DeadCode.layerNode : DeadCode.layerOf([]), Files.layerNode, Git.layerNode, input.shouldRunLint ? Linter.layerOxlint : Linter.layerOf([]), LintPartialFailures.layerLive, Progress.layerNoop, Reporter.layerNoop, Score.layerHttp, input.config?.supplyChain?.enabled !== false ? SupplyChain.layerNode : SupplyChain.layerOf([]));
75330
+ return mergeAll$1(Project.layerNode, configLayer, input.shouldRunDeadCode ? DeadCode.layerNode : DeadCode.layerOf([]), Files.layerNode, Git.layerNode, input.shouldRunLint ? Linter.layerOxlint : Linter.layerOf([]), LintPartialFailures.layerLive, succeed$3(OxlintConcurrency, input.oxlintConcurrency), succeed$3(OxlintSpawnSlots, input.oxlintSpawnSlots), Progress.layerNoop, Reporter.layerNoop, Score.layerHttp, input.config?.supplyChain?.enabled !== false ? SupplyChain.layerNode : SupplyChain.layerOf([]));
75297
75331
  };
75298
75332
  const buildInspectProgram = (scanTarget, options, configOverride) => {
75299
75333
  const effectiveConfig = configOverride ?? scanTarget.userConfig;
@@ -75334,10 +75368,14 @@ const diagnoseDirectory = async (directory, options) => {
75334
75368
  const program = buildInspectProgram(scanTarget, options);
75335
75369
  const shouldRunLint = resolveShouldRunLint(options, scanTarget.userConfig);
75336
75370
  const shouldRunDeadCode = resolveShouldRunDeadCode(options, scanTarget.userConfig);
75371
+ const oxlintConcurrency = runSync(OxlintConcurrency);
75372
+ const oxlintSpawnSlots = createOxlintSpawnSlots(oxlintConcurrency);
75337
75373
  return outputToDiagnoseResult(await runPromise(restoreLegacyThrow(program.pipe(provide(buildDiagnoseLayer({
75338
75374
  config: scanTarget.userConfig,
75339
75375
  shouldRunLint,
75340
- shouldRunDeadCode
75376
+ shouldRunDeadCode,
75377
+ oxlintConcurrency,
75378
+ oxlintSpawnSlots
75341
75379
  })), provide(layerOtlp)))), globalThis.performance.now() - startTime);
75342
75380
  };
75343
75381
  const findWorstScore = (projectResults) => {
@@ -75352,7 +75390,7 @@ const findWorstScore = (projectResults) => {
75352
75390
  }
75353
75391
  return worstResult;
75354
75392
  };
75355
- const diagnoseProject = async (projectDefinition, baseOptions, batchConfig) => {
75393
+ const diagnoseProject = async (projectDefinition, baseOptions, batchConfig, oxlintConcurrency, oxlintSpawnSlots) => {
75356
75394
  const startTime = globalThis.performance.now();
75357
75395
  try {
75358
75396
  const scanTarget = await resolveScanTarget(projectDefinition.directory);
@@ -75371,6 +75409,8 @@ const diagnoseProject = async (projectDefinition, baseOptions, batchConfig) => {
75371
75409
  config: effectiveConfig,
75372
75410
  shouldRunLint,
75373
75411
  shouldRunDeadCode,
75412
+ oxlintConcurrency,
75413
+ oxlintSpawnSlots,
75374
75414
  configOverrideTarget: {
75375
75415
  resolvedDirectory: scanTarget.resolvedDirectory,
75376
75416
  configSourceDirectory: didOverridePlugins ? null : scanTarget.configSourceDirectory
@@ -75378,7 +75418,9 @@ const diagnoseProject = async (projectDefinition, baseOptions, batchConfig) => {
75378
75418
  } : {
75379
75419
  config: effectiveConfig,
75380
75420
  shouldRunLint,
75381
- shouldRunDeadCode
75421
+ shouldRunDeadCode,
75422
+ oxlintConcurrency,
75423
+ oxlintSpawnSlots
75382
75424
  });
75383
75425
  return {
75384
75426
  ok: true,
@@ -75397,7 +75439,9 @@ const diagnoseProjectBatch = async (input) => {
75397
75439
  warnIfAiTrainingEnvironment();
75398
75440
  const startTime = globalThis.performance.now();
75399
75441
  const { projects, concurrency, config: batchConfig, ...baseOptions } = input;
75400
- const projectResults = await mapWithConcurrency(projects, concurrency ?? 4, (projectDefinition) => diagnoseProject(projectDefinition, baseOptions, batchConfig));
75442
+ const oxlintConcurrency = runSync(OxlintConcurrency);
75443
+ const oxlintSpawnSlots = createOxlintSpawnSlots(oxlintConcurrency);
75444
+ const projectResults = await mapWithConcurrency(projects, concurrency ?? 4, (projectDefinition) => diagnoseProject(projectDefinition, baseOptions, batchConfig, oxlintConcurrency, oxlintSpawnSlots));
75401
75445
  const succeededProjects = projectResults.filter((projectResult) => projectResult.ok);
75402
75446
  return {
75403
75447
  projects: projectResults,
@@ -75443,4 +75487,4 @@ const toJsonReport = (result, options) => buildJsonReport({
75443
75487
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
75444
75488
 
75445
75489
  //# sourceMappingURL=index.js.map
75446
- //# debugId=18c61424-4926-5cae-8f2a-6001534ea755
75490
+ //# debugId=61d9689f-8ba5-5ca3-a0c2-09557d3f7028
package/dist/lsp.js CHANGED
@@ -5,7 +5,7 @@ import * as OS from "node:os";
5
5
  import os from "node:os";
6
6
  import * as Path from "node:path";
7
7
  import path from "node:path";
8
- import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
8
+ import { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_DOCTOR_RULE_REGISTRY, REACT_DOCTOR_SCAN_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor/core";
9
9
  import * as Crypto from "node:crypto";
10
10
  import crypto, { createHash } from "node:crypto";
11
11
  import * as NodeChildProcess from "node:child_process";
@@ -65726,7 +65726,7 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
65726
65726
  const getDiagnosticRuleIdentity = (diagnostic) => ({
65727
65727
  ruleKey: `${diagnostic.plugin}/${diagnostic.rule}`,
65728
65728
  category: diagnostic.category,
65729
- tags: diagnostic.plugin === "react-doctor" ? reactDoctorPlugin.rules[diagnostic.rule]?.tags ?? [] : []
65729
+ tags: diagnostic.plugin === "react-doctor" ? REACT_DOCTOR_RULE_REGISTRY[diagnostic.rule]?.tags ?? [] : []
65730
65730
  });
65731
65731
  const compileIgnoredFilePatterns = (userConfig) => {
65732
65732
  const files = userConfig?.ignore?.files;
@@ -66074,7 +66074,7 @@ const buildDiagnosticPipeline = (input) => {
66074
66074
  };
66075
66075
  const shouldAutoSuppress = (diagnostic) => {
66076
66076
  if (diagnostic.plugin !== "react-doctor") return false;
66077
- const rule = reactDoctorPlugin.rules[diagnostic.rule];
66077
+ const rule = REACT_DOCTOR_RULE_REGISTRY[diagnostic.rule];
66078
66078
  if (!rule?.tags?.includes("test-noise")) return false;
66079
66079
  if (rule.tags.includes("migration-hint")) return false;
66080
66080
  return getFileContext(diagnostic.filePath) !== "production";
@@ -66484,6 +66484,7 @@ var OxlintConcurrency = class extends Reference("react-doctor/OxlintConcurrency"
66484
66484
  if (Number.isInteger(parsed) && parsed > 0) return resolveScanConcurrency(parsed);
66485
66485
  return resolveAutoScanConcurrency();
66486
66486
  } }) {};
66487
+ var OxlintSpawnSlots = class extends Reference("react-doctor/OxlintSpawnSlots", { defaultValue: () => null }) {};
66487
66488
  /**
66488
66489
  * Three-state control for overlapping the dead-code pass with the lint pass —
66489
66490
  * forking dead-code as a child fiber that runs DURING lint instead of strictly
@@ -68758,8 +68759,8 @@ const getCapabilities = (project) => {
68758
68759
  const cached = capabilitiesByProject.get(project);
68759
68760
  if (cached !== void 0) return cached;
68760
68761
  const capabilities = new Set(buildCapabilities(project));
68761
- const packageJson = readPackageJson(Path.join(project.rootDirectory, "package.json"));
68762
- const targetBlankOpenerProtection = detectTargetBlankOpenerProtection(project.rootDirectory, packageJson);
68762
+ const capabilityRootDirectory = isFile(Path.join(project.rootDirectory, "package.json")) ? project.rootDirectory : findNearestAncestorPackageJson(project.rootDirectory) ?? project.rootDirectory;
68763
+ const targetBlankOpenerProtection = detectTargetBlankOpenerProtection(capabilityRootDirectory, readPackageJson(Path.join(capabilityRootDirectory, "package.json")));
68763
68764
  if (targetBlankOpenerProtection !== void 0) capabilities.add("target-blank-needs-explicit-protection");
68764
68765
  if (targetBlankOpenerProtection === "noreferrer") capabilities.add("target-blank-needs-noreferrer");
68765
68766
  capabilitiesByProject.set(project, capabilities);
@@ -68789,7 +68790,7 @@ const createSecurityScanSession = (rootDirectory, options) => {
68789
68790
  const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
68790
68791
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
68791
68792
  const includedTags = options.includedTags ?? /* @__PURE__ */ new Set();
68792
- const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
68793
+ const enabledScanRules = REACT_DOCTOR_SCAN_RULES.flatMap((entry) => {
68793
68794
  const rule = entry.rule;
68794
68795
  const scan = rule.scan;
68795
68796
  if (typeof scan !== "function") return [];
@@ -69234,47 +69235,55 @@ const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
69234
69235
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
69235
69236
  return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
69236
69237
  };
69237
- let availableSlots = -1;
69238
- const waiters = [];
69239
- const releaseSlot = () => {
69240
- const nextWaiter = waiters.shift();
69241
- if (nextWaiter !== void 0) nextWaiter();
69242
- else availableSlots += 1;
69238
+ const createWorkerSlots = (input) => {
69239
+ let availableSlotCount = input.slotCount;
69240
+ const waiters = [];
69241
+ const releaseSlot = () => {
69242
+ const nextWaiter = waiters.shift();
69243
+ if (nextWaiter === void 0) {
69244
+ availableSlotCount += 1;
69245
+ return;
69246
+ }
69247
+ nextWaiter.abortSignal?.removeEventListener("abort", nextWaiter.onAbort);
69248
+ nextWaiter.resolve();
69249
+ };
69250
+ const acquireSlot = async (abortSignal) => {
69251
+ if (abortSignal?.aborted) throw input.createAbortError();
69252
+ if (availableSlotCount > 0) {
69253
+ availableSlotCount -= 1;
69254
+ return;
69255
+ }
69256
+ await new Promise((resolve, reject) => {
69257
+ const onAbort = () => {
69258
+ const waiterIndex = waiters.indexOf(waiter);
69259
+ if (waiterIndex !== -1) waiters.splice(waiterIndex, 1);
69260
+ reject(input.createAbortError());
69261
+ };
69262
+ const waiter = {
69263
+ resolve,
69264
+ abortSignal,
69265
+ onAbort
69266
+ };
69267
+ waiters.push(waiter);
69268
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
69269
+ });
69270
+ };
69271
+ return { run: async (task, abortSignal) => {
69272
+ await acquireSlot(abortSignal);
69273
+ try {
69274
+ return await task();
69275
+ } finally {
69276
+ releaseSlot();
69277
+ }
69278
+ } };
69243
69279
  };
69244
- /**
69245
- * Runs `task` once a dead-code worker slot is free, releasing the slot when the
69246
- * task settles (success or failure). With a high cap (roomy machine) every
69247
- * caller proceeds immediately; with a low cap (constrained runner) callers
69248
- * queue and run as slots free.
69249
- *
69250
- * `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
69251
- * this caller is queued, the call rejects without acquiring a slot or running
69252
- * `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
69253
- * then spawn a child only to tear it down. A queued caller that aborts removes
69254
- * its own waiter so a later release never hands a slot to a dead request.
69255
- */
69280
+ let deadCodeWorkerSlots = null;
69256
69281
  const withDeadCodeWorkerSlot = async (task, abortSignal) => {
69257
- if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
69258
- if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
69259
- if (availableSlots > 0) availableSlots -= 1;
69260
- else await new Promise((resolve, reject) => {
69261
- const waiter = () => {
69262
- abortSignal?.removeEventListener("abort", onAbort);
69263
- resolve();
69264
- };
69265
- const onAbort = () => {
69266
- const queuedIndex = waiters.indexOf(waiter);
69267
- if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
69268
- reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
69269
- };
69270
- waiters.push(waiter);
69271
- abortSignal?.addEventListener("abort", onAbort, { once: true });
69282
+ deadCodeWorkerSlots ??= createWorkerSlots({
69283
+ slotCount: resolveDeadCodeConcurrency(),
69284
+ createAbortError: () => /* @__PURE__ */ new Error("Dead-code worker aborted.")
69272
69285
  });
69273
- try {
69274
- return await task();
69275
- } finally {
69276
- releaseSlot();
69277
- }
69286
+ return deadCodeWorkerSlots.run(task, abortSignal);
69278
69287
  };
69279
69288
  const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
69280
69289
  const resolveReactDoctorCacheDir = (projectDirectory) => {
@@ -70801,12 +70810,12 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
70801
70810
  const settingsRootDirectory = resolveSettingsRootDirectory(project.rootDirectory);
70802
70811
  const enabledReactDoctorRules = {};
70803
70812
  for (const registryEntry of REACT_DOCTOR_RULES) {
70804
- const rule = reactDoctorPlugin.rules[registryEntry.id];
70813
+ const rule = REACT_DOCTOR_RULE_REGISTRY[registryEntry.id];
70805
70814
  if (!rule) continue;
70806
70815
  if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
70807
70816
  if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
70808
70817
  if (ruleSelection === "sidecar" && sidecarRuleIdFilter !== void 0 && !sidecarRuleIdFilter.has(registryEntry.id)) continue;
70809
- if (rule.scan !== void 0) continue;
70818
+ if (rule.isScanRule) continue;
70810
70819
  if (customRulesOnly && registryEntry.originallyExternal) continue;
70811
70820
  if (rule.framework !== "global" && !rule.requires) continue;
70812
70821
  if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledWhen, includedTags)) continue;
@@ -72117,7 +72126,7 @@ const PLUGIN_CATEGORY_MAP = {
72117
72126
  };
72118
72127
  const lookupOwnString = (record, key) => Object.hasOwn(record, key) ? record[key] : void 0;
72119
72128
  const getRuleRecommendation = (ruleName, project) => {
72120
- const rule = reactDoctorPlugin.rules[ruleName];
72129
+ const rule = REACT_DOCTOR_RULE_REGISTRY[ruleName];
72121
72130
  if (!rule) return void 0;
72122
72131
  if (rule.recommendationFor) {
72123
72132
  const capabilities = getCapabilities(project);
@@ -72126,8 +72135,8 @@ const getRuleRecommendation = (ruleName, project) => {
72126
72135
  }
72127
72136
  return rule.recommendation;
72128
72137
  };
72129
- const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.category;
72130
- const getRuleTitle = (ruleName) => reactDoctorPlugin.rules[ruleName]?.title;
72138
+ const getRuleCategory = (ruleName) => REACT_DOCTOR_RULE_REGISTRY[ruleName]?.category;
72139
+ const getRuleTitle = (ruleName) => REACT_DOCTOR_RULE_REGISTRY[ruleName]?.title;
72131
72140
  const resolveDiagnosticTitle = (plugin, rule) => {
72132
72141
  if (plugin !== "react-hooks-js") return getRuleTitle(rule);
72133
72142
  if (rule === "error-boundaries") return REACT_ERROR_BOUNDARY_TITLE;
@@ -72182,7 +72191,7 @@ const resolveDiagnosticCategory = (plugin, rule) => {
72182
72191
  if (plugin === "react-hooks-js" && rule === "error-boundaries") return "Bugs";
72183
72192
  return getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Bugs";
72184
72193
  };
72185
- const resolveMatchByOccurrence = (rule, category) => OCCURRENCE_MATCHED_CATEGORIES.has(category) || Boolean(reactDoctorPlugin.rules[rule]?.matchByOccurrence);
72194
+ const resolveMatchByOccurrence = (rule, category) => OCCURRENCE_MATCHED_CATEGORIES.has(category) || Boolean(REACT_DOCTOR_RULE_REGISTRY[rule]?.matchByOccurrence);
72186
72195
  /**
72187
72196
  * Maps oxlint's non-primary labels (`labels[1..]`) into related source
72188
72197
  * locations. Editors surface these as a diagnostic's
@@ -72273,7 +72282,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory, sourcePathByLintPath,
72273
72282
  const preparedSourceMap = resolvePreparedSourceMap(diagnostic.filename);
72274
72283
  if (preparedSourceMap === void 0) return [diagnostic];
72275
72284
  const { plugin, rule } = parseRuleCode(diagnostic.code);
72276
- if (plugin !== "react-doctor" || !reactDoctorPlugin.rules[rule]?.tags?.includes("design")) return [];
72285
+ if (plugin !== "react-doctor" || !REACT_DOCTOR_RULE_REGISTRY[rule]?.tags?.includes("design")) return [];
72277
72286
  const labels = mapPreparedSourceLabels(diagnostic.labels, preparedSourceMap);
72278
72287
  const sourceFilename = resolveMappedSourceFilename(diagnostic.filename);
72279
72288
  return labels === null || sourceFilename === void 0 ? [] : [{
@@ -72359,11 +72368,12 @@ const SANITIZED_ENV = buildOxlintChildEnv(process.env);
72359
72368
  * The first three are splittable (the caller's binary-split retry
72360
72369
  * shrinks the batch and re-spawns); the fourth isn't.
72361
72370
  */
72362
- const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES, abortSignal) => new Promise((resolve, reject) => {
72371
+ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES, abortSignal, onSpawn) => new Promise((resolve, reject) => {
72363
72372
  if (abortSignal?.aborted) {
72364
72373
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
72365
72374
  return;
72366
72375
  }
72376
+ onSpawn?.();
72367
72377
  const child = spawn(nodeBinaryPath, buildProfiledNodeArguments({
72368
72378
  argumentsList: args,
72369
72379
  cpuProfileDirectory: SANITIZED_ENV.REACT_DOCTOR_CPU_PROFILE_DIR,
@@ -72523,7 +72533,21 @@ const spawnLintBatches = async (input) => {
72523
72533
  }
72524
72534
  const batchArgs = [...baseArgs, ...batch];
72525
72535
  try {
72526
- return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory, sourcePathByLintPath, sourceMapByLintPath);
72536
+ const spawnBatch = () => {
72537
+ if (isPastDeadline()) {
72538
+ deadlineSkippedFiles.push(...batch);
72539
+ batchState.deadlineSkippedFileCount += batch.length;
72540
+ return Promise.resolve(null);
72541
+ }
72542
+ return spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal, () => {
72543
+ if (batchState.didStart) return;
72544
+ batchState.didStart = true;
72545
+ startedFileCount += batchState.initialFileCount;
72546
+ });
72547
+ };
72548
+ const stdout = input.spawnSlots === void 0 ? await spawnBatch() : await input.spawnSlots.run(spawnBatch, signal);
72549
+ if (stdout === null) return [];
72550
+ return parseOxlintOutput(stdout, project, rootDirectory, sourcePathByLintPath, sourceMapByLintPath);
72527
72551
  } catch (error) {
72528
72552
  if (!isSplittableReactDoctorError(error)) throw error;
72529
72553
  if (isPastDeadline()) {
@@ -72567,14 +72591,15 @@ const spawnLintBatches = async (input) => {
72567
72591
  deadlineSkippedFiles.push(...batch);
72568
72592
  return [];
72569
72593
  }
72570
- startedFileCount += batch.length;
72571
72594
  const batchState = {
72572
72595
  deadlineMs: null,
72573
- deadlineSkippedFileCount: 0
72596
+ deadlineSkippedFileCount: 0,
72597
+ didStart: false,
72598
+ initialFileCount: batch.length
72574
72599
  };
72575
72600
  const batchDiagnostics = await spawnLintBatch(batch, 0, batchState);
72576
72601
  scannedFileCount += batch.length - batchState.deadlineSkippedFileCount;
72577
- if (passOnFileProgress) {
72602
+ if (passOnFileProgress && batchState.didStart) {
72578
72603
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
72579
72604
  passOnFileProgress(displayedFileCount, totalFileCount);
72580
72605
  }
@@ -72658,8 +72683,8 @@ const validateRuleRegistration = () => {
72658
72683
  for (const fullKey of ALL_REACT_DOCTOR_RULE_KEYS) {
72659
72684
  const ruleName = fullKey.replace(/^react-doctor\//, "");
72660
72685
  if (!getRuleCategory(ruleName)) missingCategory.push(fullKey);
72661
- if (!reactDoctorPlugin.rules[ruleName]?.recommendation) missingHelp.push(fullKey);
72662
- if (FRAMEWORK_SPECIFIC_RULE_KEYS.has(fullKey) && !reactDoctorPlugin.rules[ruleName]?.requires) missingMetadata.push(fullKey);
72686
+ if (!REACT_DOCTOR_RULE_REGISTRY[ruleName]?.recommendation) missingHelp.push(fullKey);
72687
+ if (FRAMEWORK_SPECIFIC_RULE_KEYS.has(fullKey) && !REACT_DOCTOR_RULE_REGISTRY[ruleName]?.requires) missingMetadata.push(fullKey);
72663
72688
  }
72664
72689
  if (missingCategory.length === 0 && missingHelp.length === 0 && missingMetadata.length === 0) return;
72665
72690
  const detail = [
@@ -73300,6 +73325,7 @@ const runOxlint = async (options) => {
73300
73325
  spawnTimeoutMs,
73301
73326
  outputMaxBytes,
73302
73327
  concurrency: options.concurrency,
73328
+ spawnSlots: options.spawnSlots,
73303
73329
  signal: options.signal,
73304
73330
  deadlineEpochMs: options.deadlineEpochMs
73305
73331
  });
@@ -73502,6 +73528,7 @@ const runOxlint = async (options) => {
73502
73528
  spawnTimeoutMs,
73503
73529
  outputMaxBytes,
73504
73530
  concurrency: options.concurrency,
73531
+ spawnSlots: options.spawnSlots,
73505
73532
  signal: options.signal,
73506
73533
  deadlineEpochMs: options.deadlineEpochMs
73507
73534
  });
@@ -73596,6 +73623,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
73596
73623
  const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
73597
73624
  const outputMaxBytes = yield* OxlintOutputMaxBytes;
73598
73625
  const concurrency = yield* OxlintConcurrency;
73626
+ const spawnSlots = yield* OxlintSpawnSlots;
73599
73627
  const lintBatchOrdering = yield* LintBatchOrdering;
73600
73628
  const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
73601
73629
  const sidecarLintCacheEnabled = yield* SidecarLintCacheEnabled;
@@ -73625,6 +73653,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
73625
73653
  onSidecarStats: input.onSidecarStats,
73626
73654
  spawnTimeoutMs,
73627
73655
  outputMaxBytes,
73656
+ spawnSlots: spawnSlots ?? void 0,
73628
73657
  concurrency,
73629
73658
  signal,
73630
73659
  lintBatchOrdering,
@@ -74995,7 +75024,7 @@ const runEditorScan = async (input) => {
74995
75024
  * other `clear*` helpers.
74996
75025
  */
74997
75026
  const clearAutoSuppressionCaches = () => {};
74998
- const lookupOwnRule = (rule) => Object.hasOwn(reactDoctorPlugin.rules, rule) ? reactDoctorPlugin.rules[rule] : void 0;
75027
+ const lookupOwnRule = (rule) => Object.hasOwn(REACT_DOCTOR_RULE_REGISTRY, rule) ? REACT_DOCTOR_RULE_REGISTRY[rule] : void 0;
74999
75028
  /**
75000
75029
  * Returns presentation metadata for `<plugin>/<rule>`, or `null` when the
75001
75030
  * rule is not part of the bundled react-doctor plugin (e.g. an adopted
@@ -77071,5 +77100,5 @@ const startLanguageServer = () => {
77071
77100
  };
77072
77101
  //#endregion
77073
77102
  export { startLanguageServer };
77074
- !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]="7cb1cf13-9787-536b-856a-49d43fec26a4")}catch(e){}}();
77075
- //# debugId=7cb1cf13-9787-536b-856a-49d43fec26a4
77103
+ !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]="ec49e114-f0f3-593d-b3c1-8b1a04d065a6")}catch(e){}}();
77104
+ //# debugId=ec49e114-f0f3-593d-b3c1-8b1a04d065a6
@@ -1,6 +1,6 @@
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]="90f58187-6a1d-56d1-a411-800f7bc51d28")}catch(e){}}();
3
- import { A as toForwardSlashes, B as getCategoryImpact, C as buildRulePriorityMap, D as resolveBlockingLevel, E as formatLearnMoreLine, F as TUI_REPORT_DETAIL_WIDTH_FRACTION, G as runPromise, H as mapWithConcurrency, I as isCiEnvironment, J as succeed, K as sync, L as DIAGNOSTIC_CATEGORY_BUCKETS, M as isUnicodeSupported, N as METRIC, O as isReactDoctorWorkflowInstalled, P as TUI_REPORT_COMPACT_MAX_ROWS, R as Progress, S as hasLintHardFailure, T as formatFixRecipeLine, U as mergeReactDoctorConfigs, V as highlighter, W as resolveScanTarget, _ as canAnimateOnboarding, a as resolveCliInspectOptions, b as buildCodeFrame, c as detectLaunchableAgents, d as launchCliAgent, f as isShareOptedOut, g as doctorFace, h as easeOutCubic, i as selectProjects, j as findNearestPackageDirectory, k as recordCount, l as CLI_AGENT_BINARIES, m as printFooter, n as shouldFailScanGate, o as countUniqueScannedFiles, p as inspect, q as void_, r as discoverWorkspacePackages, s as setUpGitHubActions, t as exitGracefully, u as copyToClipboard, v as computeProjectedScore, w as buildSortedRuleGroups, x as buildNoScoreMessage, y as formatElapsedTime, z as Reporter } from "./cli.js";
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]="898bdb3e-afed-5fca-92a2-f14d1e8ca6fb")}catch(e){}}();
3
+ import { A as toForwardSlashes, B as getCategoryImpact, C as buildRulePriorityMap, D as resolveBlockingLevel, E as formatLearnMoreLine, F as TUI_REPORT_DETAIL_WIDTH_FRACTION, G as runPromise, H as mapWithConcurrency, I as isCiEnvironment, J as succeed, K as sync, L as DIAGNOSTIC_CATEGORY_BUCKETS, M as isUnicodeSupported, N as METRIC, O as isReactDoctorWorkflowInstalled, P as TUI_REPORT_COMPACT_MAX_ROWS, R as Progress, S as hasLintHardFailure, T as formatFixRecipeLine, U as mergeReactDoctorConfigs, V as highlighter, W as resolveScanTarget, _ as canAnimateOnboarding, a as resolveCliInspectOptions, b as buildCodeFrame, c as detectLaunchableAgents, d as launchCliAgent, f as isShareOptedOut, g as doctorFace, h as easeOutCubic, i as selectProjects, j as findNearestPackageDirectory, k as recordCount, l as CLI_AGENT_BINARIES, m as printFooter, n as shouldFailScanGate, o as countUniqueScannedFiles, p as createInvocationInspect, q as void_, r as discoverWorkspacePackages, s as setUpGitHubActions, t as exitGracefully, u as copyToClipboard, v as computeProjectedScore, w as buildSortedRuleGroups, x as buildNoScoreMessage, y as formatElapsedTime, z as Reporter } from "./cli.js";
4
4
  import path from "node:path";
5
5
  import { performance } from "node:perf_hooks";
6
6
  import { getSkillAgentConfig } from "agent-install";
@@ -1610,11 +1610,11 @@ const runMountedScan = async (rootDirectory, presentation, blockingLevel, execut
1610
1610
  throw error;
1611
1611
  }
1612
1612
  };
1613
- const runSingleProjectScan = async (rootScanTarget, projectDirectory, input, blockingLevel) => {
1613
+ const runSingleProjectScan = async (rootScanTarget, projectDirectory, input, blockingLevel, inspectProject) => {
1614
1614
  const projectScan = await resolveProjectScan(rootScanTarget, projectDirectory);
1615
1615
  const presentation = resolveScanPresentation(input, [projectScan]);
1616
1616
  return runMountedScan(projectScan.directory, presentation, blockingLevel, async (context) => {
1617
- const result = await inspect(projectScan.directory, {
1617
+ const result = await inspectProject(projectScan.directory, {
1618
1618
  ...resolveTuiInspectOptions(input, projectScan.config),
1619
1619
  isCi: isCiEnvironment(),
1620
1620
  configOverride: projectScan.config,
@@ -1645,7 +1645,7 @@ const runSingleProjectScan = async (rootScanTarget, projectDirectory, input, blo
1645
1645
  };
1646
1646
  });
1647
1647
  };
1648
- const runMultiProjectScan = async (rootScanTarget, directories, input, blockingLevel) => {
1648
+ const runMultiProjectScan = async (rootScanTarget, directories, input, blockingLevel, inspectProject) => {
1649
1649
  const rootDirectory = rootScanTarget.resolvedDirectory;
1650
1650
  const projectScans = await mapWithConcurrency([...directories], 4, (projectDirectory) => resolveProjectScan(rootScanTarget, projectDirectory));
1651
1651
  return runMountedScan(rootDirectory, resolveScanPresentation(input, projectScans), blockingLevel, async (context) => {
@@ -1653,7 +1653,7 @@ const runMultiProjectScan = async (rootScanTarget, directories, input, blockingL
1653
1653
  let finishedCount = 0;
1654
1654
  context.store.setProgress(`Scanning ${directories.length} projects…`);
1655
1655
  const results = await mapWithConcurrency(projectScans, 4, async (projectScan) => {
1656
- const result = await inspect(projectScan.directory, {
1656
+ const result = await inspectProject(projectScan.directory, {
1657
1657
  ...resolveTuiInspectOptions(input, projectScan.config),
1658
1658
  isCi: isCiEnvironment(),
1659
1659
  configOverride: projectScan.config,
@@ -1720,13 +1720,14 @@ const runScanApp = async (input) => {
1720
1720
  share: input.share ?? scanTarget.userConfig?.share ?? true
1721
1721
  };
1722
1722
  const selectedDirectories = await resolveSelectedDirectories(rootDirectory, resolvedInput);
1723
+ const inspectProject = createInvocationInspect(input.options?.concurrency);
1723
1724
  const blockingLevel = resolveBlockingLevel({ blocking: resolvedInput.blocking }, scanTarget.userConfig);
1724
1725
  if (selectedDirectories.length === 0) return { shouldFail: false };
1725
- if (selectedDirectories.length === 1) return runSingleProjectScan(scanTarget, selectedDirectories[0], resolvedInput, blockingLevel);
1726
- return runMultiProjectScan(scanTarget, selectedDirectories, resolvedInput, blockingLevel);
1726
+ if (selectedDirectories.length === 1) return runSingleProjectScan(scanTarget, selectedDirectories[0], resolvedInput, blockingLevel, inspectProject);
1727
+ return runMultiProjectScan(scanTarget, selectedDirectories, resolvedInput, blockingLevel, inspectProject);
1727
1728
  };
1728
1729
  //#endregion
1729
1730
  export { runScanApp };
1730
1731
 
1731
- //# sourceMappingURL=run-scan-app-C9MJnVwv.js.map
1732
- //# debugId=90f58187-6a1d-56d1-a411-800f7bc51d28
1732
+ //# sourceMappingURL=run-scan-app-DHRvqfkX.js.map
1733
+ //# debugId=898bdb3e-afed-5fca-92a2-f14d1e8ca6fb
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.9.2-dev.9512488",
3
+ "version": "0.9.2-dev.992205a",
4
4
  "description": "Your agent writes bad React. This catches it",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -71,8 +71,8 @@
71
71
  "vscode-languageserver-textdocument": "^1.0.12",
72
72
  "vscode-uri": "^3.1.0",
73
73
  "yaml": "^2.9.0",
74
- "oxlint-plugin-react-doctor": "0.9.2-dev.9512488",
75
- "deslop-js": "0.9.2"
74
+ "deslop-js": "0.9.2",
75
+ "oxlint-plugin-react-doctor": "0.9.2-dev.992205a"
76
76
  },
77
77
  "devDependencies": {
78
78
  "@types/babel__code-frame": "^7.27.0",
@@ -82,9 +82,9 @@
82
82
  "commander": "^14.0.3",
83
83
  "ink-testing-library": "^4.0.0",
84
84
  "ora": "^9.4.0",
85
- "@react-doctor/api": "0.9.2",
85
+ "@react-doctor/language-server": "0.9.2",
86
86
  "@react-doctor/core": "0.9.2",
87
- "@react-doctor/language-server": "0.9.2"
87
+ "@react-doctor/api": "0.9.2"
88
88
  },
89
89
  "engines": {
90
90
  "node": "^20.19.0 || >=22.13.0"