react-doctor 0.5.8-dev.986557d → 0.5.8-dev.99f2417

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -9271,6 +9271,15 @@ interface ReactDoctorConfig {
9271
9271
  * suppresses regardless of sibling content.
9272
9272
  */
9273
9273
  rawTextWrapperComponents?: string[];
9274
+ /**
9275
+ * Capitalized identifiers that exist at runtime but aren't imported in
9276
+ * the file that uses them — react-live's `<LiveProvider scope={...}>`,
9277
+ * Storybook globals, MDX live blocks, or an ambient `declare global` in a
9278
+ * separate `.d.ts`. The single-file `jsx-no-undef` rule can't see those
9279
+ * bindings and would report `<DatePicker />` as an undefined JSX component.
9280
+ * List the injected names here and `jsx-no-undef` treats them as known.
9281
+ */
9282
+ runtimeGlobals?: string[];
9274
9283
  /**
9275
9284
  * Project-level allowlist of function names that the
9276
9285
  * `server-auth-actions` rule treats as an auth check at the top of
@@ -9726,7 +9735,7 @@ interface InspectResult {
9726
9735
  * Per-file lint cache outcome: files served from cache, and total files
9727
9736
  * considered. Both absent when the cache was off or bypassed (audit mode,
9728
9737
  * adopted `extends`, user plugins). The CLI projects these onto the Sentry
9729
- * wide event as `lintCacheHitRatio`.
9738
+ * wide event as `lint.cacheHitRatio`.
9730
9739
  */
9731
9740
  lintCacheHitFileCount?: number | null;
9732
9741
  lintCacheTotalFileCount?: number | null;
@@ -10123,7 +10132,7 @@ declare const summarizeDiagnostics: (diagnostics: Diagnostic[], worstScore?: num
10123
10132
  * untouched so the loader sees the same plain object either way.
10124
10133
  */
10125
10134
  declare const defineConfig: (config: ReactDoctorConfig) => ReactDoctorConfig; //#endregion
10126
- //#region src/utils/group-by.d.ts
10135
+ //#region src/utils/detect-ai-training-environment.d.ts
10127
10136
  //#endregion
10128
10137
  //#region ../api/dist/index.d.ts
10129
10138
  //#region src/diagnose.d.ts
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="33508ee6-c977-5b5f-8585-9939928ce74d")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="0bd202f9-5e22-5625-86dc-458270afbcca")}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
  /**
@@ -33694,6 +33694,7 @@ const MILLISECONDS_PER_SECOND = 1e3;
33694
33694
  const SCORE_API_URL = "https://www.react.doctor/api/score";
33695
33695
  const FETCH_TIMEOUT_MS = 1e4;
33696
33696
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
33697
+ const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
33697
33698
  const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
33698
33699
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
33699
33700
  const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
@@ -33751,6 +33752,7 @@ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
33751
33752
  const LINT_PHASE_TIMEOUT_MS = 3e5;
33752
33753
  const SCAN_TOTAL_DEADLINE_MS = 9e5;
33753
33754
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
33755
+ const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
33754
33756
  const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
33755
33757
  const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
33756
33758
  const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
@@ -34487,7 +34489,10 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
34487
34489
  NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
34488
34490
  }
34489
34491
  const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
34490
- const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
34492
+ const canonicalizeRuleKey = (ruleKey) => {
34493
+ const nativeRuleKey = LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey];
34494
+ return typeof nativeRuleKey === "string" ? nativeRuleKey : ruleKey;
34495
+ };
34491
34496
  const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
34492
34497
  const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
34493
34498
  const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
@@ -34994,6 +34999,7 @@ const collectStringSet = (values) => {
34994
34999
  if (!Array.isArray(values)) return /* @__PURE__ */ new Set();
34995
35000
  return new Set(values.filter((value) => typeof value === "string"));
34996
35001
  };
35002
+ const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
34997
35003
  /**
34998
35004
  * Pre-compiles every stateful filter and returns a single
34999
35005
  * `apply(diagnostic)` closure that runs (in order):
@@ -35027,8 +35033,10 @@ const buildDiagnosticPipeline = (input) => {
35027
35033
  const compiledOverrides = compileIgnoreOverrides(userConfig);
35028
35034
  const textComponentNames = collectStringSet(userConfig?.textComponents);
35029
35035
  const rawTextWrapperComponentNames = collectStringSet(userConfig?.rawTextWrapperComponents);
35036
+ const runtimeGlobalNames = collectStringSet(userConfig?.runtimeGlobals);
35030
35037
  const hasTextComponents = textComponentNames.size > 0;
35031
35038
  const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
35039
+ const hasRuntimeGlobals = runtimeGlobalNames.size > 0;
35032
35040
  const fileLinesCache = /* @__PURE__ */ new Map();
35033
35041
  const fileContextCache = /* @__PURE__ */ new Map();
35034
35042
  const libraryFileCache = /* @__PURE__ */ new Map();
@@ -35080,6 +35088,12 @@ const buildDiagnosticPipeline = (input) => {
35080
35088
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
35081
35089
  return false;
35082
35090
  };
35091
+ const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
35092
+ if (!hasRuntimeGlobals) return false;
35093
+ if (diagnostic.rule !== "jsx-no-undef") return false;
35094
+ const identifierMatch = JSX_NO_UNDEF_IDENTIFIER_PATTERN.exec(diagnostic.message);
35095
+ return identifierMatch !== null && runtimeGlobalNames.has(identifierMatch[1]);
35096
+ };
35083
35097
  return { apply: (diagnostic) => {
35084
35098
  if (shouldAutoSuppress(diagnostic)) return null;
35085
35099
  let current = diagnostic;
@@ -35104,6 +35118,7 @@ const buildDiagnosticPipeline = (input) => {
35104
35118
  if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
35105
35119
  if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return null;
35106
35120
  if (isRnRawTextSuppressedByConfig(current)) return null;
35121
+ if (isJsxNoUndefSuppressedByConfig(current)) return null;
35107
35122
  }
35108
35123
  if (respectInlineDisables && current.line > 0) {
35109
35124
  const lines = getFileLines(current.filePath);
@@ -35365,7 +35380,7 @@ const resolveScanConcurrency = (requested) => {
35365
35380
  if (!Number.isFinite(requested) || requested < 1) return 1;
35366
35381
  return Math.min(Math.floor(requested), 32);
35367
35382
  };
35368
- const readSystemFacts = () => ({
35383
+ const readSystemFacts$1 = () => ({
35369
35384
  availableCores: os.availableParallelism(),
35370
35385
  totalMemoryBytes: os.totalmem(),
35371
35386
  cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
@@ -35386,7 +35401,7 @@ const readSystemFacts = () => ({
35386
35401
  * `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
35387
35402
  * limited, and ceiling cases without mocking `os` or the filesystem.
35388
35403
  */
35389
- const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
35404
+ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
35390
35405
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
35391
35406
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
35392
35407
  return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
@@ -35577,6 +35592,13 @@ const BOOLEAN_FIELD_NAMES = [
35577
35592
  "adoptExistingLintConfig"
35578
35593
  ];
35579
35594
  const STRING_FIELD_NAMES = ["rootDir"];
35595
+ const STRING_ARRAY_FIELD_NAMES = [
35596
+ "projects",
35597
+ "textComponents",
35598
+ "rawTextWrapperComponents",
35599
+ "runtimeGlobals",
35600
+ "serverAuthFunctionNames"
35601
+ ];
35580
35602
  const SURFACE_CONTROL_FIELD_NAMES = [
35581
35603
  "includeTags",
35582
35604
  "excludeTags",
@@ -35678,6 +35700,7 @@ const validateConfigTypes = (config) => {
35678
35700
  const validated = { ...config };
35679
35701
  for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
35680
35702
  for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
35703
+ for (const fieldName of STRING_ARRAY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateStringArrayField(fieldName, value));
35681
35704
  applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
35682
35705
  for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
35683
35706
  applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
@@ -36912,7 +36935,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
36912
36935
  }
36913
36936
  return true;
36914
36937
  };
36915
- const checkSecurityScan = (rootDirectory, options = {}) => {
36938
+ const yieldToEventLoop = () => new Promise((resolve) => {
36939
+ setImmediate(resolve);
36940
+ });
36941
+ const createSecurityScanSession = (rootDirectory, options) => {
36916
36942
  const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
36917
36943
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
36918
36944
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
@@ -36927,7 +36953,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36927
36953
  committedFilesOnly: rule.committedFilesOnly === true
36928
36954
  }];
36929
36955
  });
36930
- if (enabledScanRules.length === 0) return [];
36956
+ if (enabledScanRules.length === 0) return null;
36931
36957
  const diagnostics = [];
36932
36958
  const seen = /* @__PURE__ */ new Set();
36933
36959
  const gitIgnoredCache = /* @__PURE__ */ new Map();
@@ -36939,15 +36965,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36939
36965
  }
36940
36966
  return status === true;
36941
36967
  };
36942
- for (const file of collectSecurityScanFiles(rootDirectory)) for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36943
- if (committedFilesOnly && isFileGitIgnored(file)) continue;
36944
- const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36945
- const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36946
- if (seen.has(key)) continue;
36947
- seen.add(key);
36948
- diagnostics.push(diagnostic);
36968
+ const scanFile = (file) => {
36969
+ for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36970
+ if (committedFilesOnly && isFileGitIgnored(file)) continue;
36971
+ const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36972
+ const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36973
+ if (seen.has(key)) continue;
36974
+ seen.add(key);
36975
+ diagnostics.push(diagnostic);
36976
+ }
36977
+ };
36978
+ return {
36979
+ scanFile,
36980
+ diagnostics
36981
+ };
36982
+ };
36983
+ const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
36984
+ const session = createSecurityScanSession(rootDirectory, options);
36985
+ if (session === null) return [];
36986
+ let filesSinceYield = 0;
36987
+ for (const file of collectSecurityScanFiles(rootDirectory)) {
36988
+ session.scanFile(file);
36989
+ filesSinceYield += 1;
36990
+ if (filesSinceYield >= 16) {
36991
+ filesSinceYield = 0;
36992
+ await yieldToEventLoop();
36993
+ }
36949
36994
  }
36950
- return diagnostics;
36995
+ return session.diagnostics;
36951
36996
  };
36952
36997
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
36953
36998
  let p = process || {}, argv = p.argv || [], env = p.env || {};
@@ -37164,6 +37209,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
37164
37209
  return [...seen].filter((pattern) => pattern.length > 0);
37165
37210
  };
37166
37211
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
37212
+ const readSystemFacts = () => ({
37213
+ availableCores: os.availableParallelism(),
37214
+ totalMemoryBytes: os.totalmem(),
37215
+ cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
37216
+ });
37217
+ /**
37218
+ * How many real deslop dead-code child processes may run at once, across the
37219
+ * concurrent per-project `runInspect` fibers of one CLI run. The cap is the
37220
+ * smaller of the core count and the number of `DEAD_CODE_WORKER_MEM_BUDGET_BYTES`
37221
+ * workers that fit in available memory, floored at 1.
37222
+ *
37223
+ * On a roomy dev box / CI runner this resolves high enough that every
37224
+ * concurrently-scanned project still spawns its own worker (no serialization vs
37225
+ * the prior uncapped behavior); on a memory-constrained runner it collapses
37226
+ * toward 1, so the `withDeadCodeWorkerSlot` semaphore serializes the spawns
37227
+ * instead of oversubscribing memory with N simultaneous children — the global
37228
+ * cap the per-project spawn path lacked.
37229
+ *
37230
+ * Mirrors `resolveAutoScanConcurrency` (lint), but budgets memory per the
37231
+ * heavier dead-code worker. `facts` is injectable for tests.
37232
+ */
37233
+ const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
37234
+ const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
37235
+ const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
37236
+ return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
37237
+ };
37238
+ let availableSlots = -1;
37239
+ const waiters = [];
37240
+ const releaseSlot = () => {
37241
+ const nextWaiter = waiters.shift();
37242
+ if (nextWaiter !== void 0) nextWaiter();
37243
+ else availableSlots += 1;
37244
+ };
37245
+ /**
37246
+ * Runs `task` once a dead-code worker slot is free, releasing the slot when the
37247
+ * task settles (success or failure). With a high cap (roomy machine) every
37248
+ * caller proceeds immediately; with a low cap (constrained runner) callers
37249
+ * queue and run as slots free.
37250
+ *
37251
+ * `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
37252
+ * this caller is queued, the call rejects without acquiring a slot or running
37253
+ * `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
37254
+ * then spawn a child only to tear it down. A queued caller that aborts removes
37255
+ * its own waiter so a later release never hands a slot to a dead request.
37256
+ */
37257
+ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
37258
+ if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
37259
+ if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
37260
+ if (availableSlots > 0) availableSlots -= 1;
37261
+ else await new Promise((resolve, reject) => {
37262
+ const waiter = () => {
37263
+ abortSignal?.removeEventListener("abort", onAbort);
37264
+ resolve();
37265
+ };
37266
+ const onAbort = () => {
37267
+ const queuedIndex = waiters.indexOf(waiter);
37268
+ if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
37269
+ reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
37270
+ };
37271
+ waiters.push(waiter);
37272
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
37273
+ });
37274
+ try {
37275
+ return await task();
37276
+ } finally {
37277
+ releaseSlot();
37278
+ }
37279
+ };
37167
37280
  /**
37168
37281
  * Resolves a path to its canonical, symlink-free form, falling back to
37169
37282
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -37188,6 +37301,11 @@ const toCanonicalPath = (filePath) => {
37188
37301
  };
37189
37302
  const DEAD_CODE_PLUGIN = "deslop";
37190
37303
  const DEAD_CODE_CATEGORY = "Maintainability";
37304
+ const REACT_DOCTOR_TOOLCHAIN_PACKAGES = new Set([
37305
+ "react-doctor",
37306
+ "eslint-plugin-react-doctor",
37307
+ "oxlint-plugin-react-doctor"
37308
+ ]);
37191
37309
  const DEAD_CODE_WORKER_SCRIPT = `
37192
37310
  const inputChunks = [];
37193
37311
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -37454,14 +37572,17 @@ const checkDeadCode = async (options) => {
37454
37572
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37455
37573
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37456
37574
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37457
- const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37458
- rootDirectory,
37459
- entryPatterns,
37460
- tsConfigPath: resolveTsConfigPath(rootDirectory),
37461
- ignorePatterns,
37462
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37463
- parseConcurrency: options.parseConcurrency
37464
- }), options.workerTimeoutMs ?? 12e4, options.abortSignal));
37575
+ const spawnAndRun = () => {
37576
+ return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37577
+ rootDirectory,
37578
+ entryPatterns,
37579
+ tsConfigPath: resolveTsConfigPath(rootDirectory),
37580
+ ignorePatterns,
37581
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37582
+ parseConcurrency: options.parseConcurrency
37583
+ }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
37584
+ };
37585
+ const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
37465
37586
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
37466
37587
  const diagnostics = [];
37467
37588
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -37490,6 +37611,7 @@ const checkDeadCode = async (options) => {
37490
37611
  });
37491
37612
  }
37492
37613
  for (const unusedDependency of result.unusedDependencies) {
37614
+ if (REACT_DOCTOR_TOOLCHAIN_PACKAGES.has(unusedDependency.name)) continue;
37493
37615
  const label = unusedDependency.isDevDependency ? "devDependency" : "dependency";
37494
37616
  diagnostics.push({
37495
37617
  filePath: "package.json",
@@ -37862,43 +37984,46 @@ var Git = class Git extends Service()("react-doctor/Git") {
37862
37984
  * reason: GitInvocationFailed })` so the rest of the codebase
37863
37985
  * sees a single failure channel.
37864
37986
  */
37865
- const runCommand = (input) => scoped(gen(function* () {
37866
- const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37867
- cwd: input.directory,
37868
- env: input.env,
37869
- extendEnv: true
37870
- }));
37871
- const maxStdoutBytes = input.maxStdoutBytes;
37872
- const stdoutByteCount = yield* make$13(0);
37873
- const [stdout, stderr, status] = yield* all([
37874
- mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37875
- args: [...input.args],
37876
- directory: input.directory,
37877
- cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
37878
- }) })) : void_)))))),
37879
- mkString(decodeText(handle.stderr)),
37880
- handle.exitCode
37881
- ], { concurrency: 3 });
37882
- return {
37883
- status,
37884
- stdout,
37885
- stderr
37886
- };
37887
- })).pipe(catchTag$1("PlatformError", (cause) => {
37888
- if (input.command !== "git") return succeed$2({
37987
+ const runCommand = (input) => {
37988
+ const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
37889
37989
  status: 127,
37890
37990
  stdout: "",
37891
37991
  stderr: String(cause)
37892
- });
37893
- return new ReactDoctorError({ reason: new GitInvocationFailed({
37992
+ }) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37894
37993
  args: [...input.args],
37895
37994
  directory: input.directory,
37896
37995
  cause
37897
- }) });
37898
- }), withSpan("git.exec", { attributes: {
37899
- "git.command": input.command,
37900
- "git.subcommand": input.args[0] ?? ""
37901
- } }));
37996
+ }) }));
37997
+ return scoped(gen(function* () {
37998
+ if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
37999
+ const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
38000
+ if (argvLengthChars > 24e3) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${SPAWN_ARGS_MAX_LENGTH_CHARS})`);
38001
+ const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
38002
+ cwd: input.directory,
38003
+ env: input.env,
38004
+ extendEnv: true
38005
+ }));
38006
+ const maxStdoutBytes = input.maxStdoutBytes;
38007
+ const stdoutByteCount = yield* make$13(0);
38008
+ const [stdout, stderr, status] = yield* all([
38009
+ mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
38010
+ args: [...input.args],
38011
+ directory: input.directory,
38012
+ cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
38013
+ }) })) : void_)))))),
38014
+ mkString(decodeText(handle.stderr)),
38015
+ handle.exitCode
38016
+ ], { concurrency: 3 });
38017
+ return {
38018
+ status,
38019
+ stdout,
38020
+ stderr
38021
+ };
38022
+ })).pipe(catchTag$1("PlatformError", foldSpawnFailure), withSpan("git.exec", { attributes: {
38023
+ "git.command": input.command,
38024
+ "git.subcommand": input.args[0] ?? ""
38025
+ } }));
38026
+ };
37902
38027
  const runGit = (directory, args) => runCommand({
37903
38028
  command: "git",
37904
38029
  args,
@@ -37931,7 +38056,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37931
38056
  "rev-parse",
37932
38057
  "--verify",
37933
38058
  branch
37934
- ]).pipe(map$3((result) => result.status === 0));
38059
+ ]).pipe(map$3((result) => result.status === 0), catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(false) : fail$4(error)));
37935
38060
  const headSha = (directory) => runGit(directory, ["rev-parse", "HEAD"]).pipe(map$3((result) => result.status === 0 ? trimOrNull(result.stdout) : null));
37936
38061
  const mergeBase = (input) => isSafeGitRevision(input.ref) ? runGit(input.directory, [
37937
38062
  "merge-base",
@@ -38145,7 +38270,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38145
38270
  ]);
38146
38271
  if (result.status !== 0) return null;
38147
38272
  return parseChangedLineRanges(result.stdout);
38148
- }).pipe(withSpan("Git.changedLineRanges"))
38273
+ }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
38149
38274
  });
38150
38275
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
38151
38276
  /**
@@ -40622,7 +40747,10 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40622
40747
  * diagnostics).
40623
40748
  * 2. beforeLint hook (e.g. CLI renders the project-detection block)
40624
40749
  * 3. environment checks (reduced-motion + pnpm hardening +
40625
- * expo/react-native + security scan), collected synchronously
40750
+ * expo/react-native), collected synchronously. The heavier
40751
+ * content-regex security scan is forked instead (like supply-chain
40752
+ * below) and joined before the concat, so its CPU overlaps lint
40753
+ * rather than blocking the event loop before it.
40626
40754
  * 4. The supply-chain check (Socket.dev) is forked onto a background
40627
40755
  * fiber so its ~100% network-bound time overlaps the ~100%
40628
40756
  * CPU/subprocess-bound lint pass below, collapsing two serial
@@ -40642,7 +40770,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40642
40770
  * order, so terminal output is identical either way; supply-chain
40643
40771
  * rides alongside without a spinner.
40644
40772
  * 6. Join the supply-chain fiber, then assemble the diagnostics in a
40645
- * FIXED order (env, supply-chain, lint, dead-code) so the output is
40773
+ * FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
40646
40774
  * byte-identical regardless of which fiber settled first. The
40647
40775
  * viewer-permission fiber is joined later, during score-metadata
40648
40776
  * assembly (it feeds score metadata, not diagnostics). The per-element
@@ -40703,12 +40831,12 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40703
40831
  ...checkPnpmHardening(scanDirectory),
40704
40832
  ...checkReactServerComponentsAdvisory(scanDirectory, project),
40705
40833
  ...checkExpoProject(scanDirectory, project),
40706
- ...checkReactNativeProject(scanDirectory, project),
40707
- ...checkSecurityScan(scanDirectory, {
40708
- project,
40709
- ignoredTags: input.ignoredTags
40710
- })
40834
+ ...checkReactNativeProject(scanDirectory, project)
40711
40835
  ])));
40836
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(promise(() => checkSecurityScanCooperative(scanDirectory, {
40837
+ project,
40838
+ ignoredTags: input.ignoredTags
40839
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)))))).pipe(withSpan("SecurityScan.run")));
40712
40840
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
40713
40841
  const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
40714
40842
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
@@ -40844,9 +40972,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40844
40972
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
40845
40973
  const supplyChainResult = yield* join(supplyChainFiber);
40846
40974
  const supplyChainCollected = supplyChainResult.diagnostics;
40975
+ const securityScanCollected = yield* join(securityScanFiber);
40847
40976
  yield* reporterService.finalize;
40848
40977
  const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
40849
40978
  ...envCollected,
40979
+ ...securityScanCollected,
40850
40980
  ...supplyChainCollected,
40851
40981
  ...lintCollected,
40852
40982
  ...deadCodeCollected
@@ -41298,6 +41428,61 @@ const clearAutoSuppressionCaches = () => {};
41298
41428
  * untouched so the loader sees the same plain object either way.
41299
41429
  */
41300
41430
  const defineConfig = (config) => config;
41431
+ const AI_TRAINING_ENV_VARS = [
41432
+ ["HF_DATASETS_CACHE", "huggingface"],
41433
+ ["HF_HOME", "huggingface"],
41434
+ ["HUGGINGFACE_HUB_CACHE", "huggingface"],
41435
+ ["SPACE_ID", "huggingface-spaces"],
41436
+ ["CUDA_VISIBLE_DEVICES", "cuda"],
41437
+ ["NVIDIA_VISIBLE_DEVICES", "nvidia"],
41438
+ ["ROCR_VISIBLE_DEVICES", "rocm"],
41439
+ ["TPU_NAME", "google-tpu"],
41440
+ ["WANDB_RUN_ID", "wandb"],
41441
+ ["WANDB_SWEEP_ID", "wandb"],
41442
+ ["MLFLOW_RUN_ID", "mlflow"],
41443
+ ["MLFLOW_TRACKING_URI", "mlflow"],
41444
+ ["COMET_EXPERIMENT_KEY", "comet"],
41445
+ ["NEPTUNE_RUN_ID", "neptune"],
41446
+ ["CLEARML_TASK_ID", "clearml"],
41447
+ ["DVC_STAGE", "dvc"],
41448
+ ["RAY_WORKER_PROCESS", "ray"],
41449
+ ["RAY_ADDRESS", "ray"],
41450
+ ["MUJOCO_GL", "mujoco"],
41451
+ ["MUJOCO_PATH", "mujoco"],
41452
+ ["GYM_DISABLE_ENV_CHECKER", "gymnasium"],
41453
+ ["SM_TRAINING_ENV", "sagemaker"],
41454
+ ["TRAINING_JOB_ARN", "sagemaker"],
41455
+ ["SAGEMAKER_BASE_DIR", "sagemaker"],
41456
+ ["AZUREML_RUN_ID", "azure-ml"],
41457
+ ["AZURE_ML_MODEL_DIR", "azure-ml"],
41458
+ ["CLOUD_ML_PROJECT_ID", "vertex-ai"],
41459
+ ["VERTEX_AI_LOG_LEVEL", "vertex-ai"],
41460
+ ["DET_MASTER", "determined-ai"],
41461
+ ["LIGHTNING_USER_ID", "lightning-ai"],
41462
+ ["FLYTE_INTERNAL_EXECUTION_ID", "flyte"],
41463
+ ["ARGO_WORKFLOW_NAME", "argo-workflows"],
41464
+ ["KFP_POD_NAME", "kubeflow-pipelines"],
41465
+ ["KAGGLE_KERNEL_RUN_TYPE", "kaggle"],
41466
+ ["COLAB_BACKEND_VERSION", "google-colab"],
41467
+ ["DATABRICKS_RUNTIME_VERSION", "databricks"],
41468
+ ["DAYTONA_WS_ID", "daytona"],
41469
+ ["DAYTONA_WS_NAME", "daytona"],
41470
+ ["E2B_SANDBOX_ID", "e2b"],
41471
+ ["MODAL_FUNCTION_ID", "modal"],
41472
+ ["MODAL_TASK_ID", "modal"],
41473
+ ["RUNPOD_POD_ID", "runpod"],
41474
+ ["REPLICATE_USERNAME", "replicate"],
41475
+ ["VAST_CONTAINERLABEL", "vast-ai"],
41476
+ ["HARBOR_URL", "harbor"],
41477
+ ["HARBOR_HOSTNAME", "harbor"],
41478
+ ["SWE_BENCH_TASK", "swe-bench"],
41479
+ ["SWEBENCH_TASK", "swe-bench"],
41480
+ ["SWE_AGENT_MODEL", "swe-agent"]
41481
+ ];
41482
+ const detectAiTrainingEnvironment = () => {
41483
+ for (const [envVar, label] of AI_TRAINING_ENV_VARS) if (process.env[envVar]) return label;
41484
+ return null;
41485
+ };
41301
41486
  const unionValues = (baseValues, overrideValues) => [...new Set([...baseValues, ...overrideValues])];
41302
41487
  const mergeIgnores = (baseIgnore, overrideIgnore) => {
41303
41488
  const mergedIgnore = {
@@ -41342,6 +41527,12 @@ const mergeReactDoctorConfigs = (baseConfig, overrideConfig) => {
41342
41527
  };
41343
41528
  //#endregion
41344
41529
  //#region ../api/dist/index.js
41530
+ let didWarnAiTraining = false;
41531
+ const warnIfAiTrainingEnvironment = () => {
41532
+ if (didWarnAiTraining || detectAiTrainingEnvironment() === null) return;
41533
+ didWarnAiTraining = true;
41534
+ console.warn("[react-doctor] Use in an AI or ML pipeline requires written permission under the react-doctor license. Contact founders@million.dev to request access.");
41535
+ };
41345
41536
  const buildDiagnoseLayer = (config, configOverrideTarget) => {
41346
41537
  const configLayer = configOverrideTarget === void 0 ? Config.layerNode : Config.layerOf({
41347
41538
  config,
@@ -41379,6 +41570,7 @@ const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
41379
41570
  };
41380
41571
  };
41381
41572
  const diagnoseDirectory = async (directory, options) => {
41573
+ warnIfAiTrainingEnvironment();
41382
41574
  const startTime = globalThis.performance.now();
41383
41575
  const scanTarget = await resolveScanTarget(directory);
41384
41576
  return outputToDiagnoseResult(await runPromise(restoreLegacyThrow(buildInspectProgram(scanTarget, options).pipe(provide(buildDiagnoseLayer(scanTarget.userConfig)), provide(layerOtlp)))), globalThis.performance.now() - startTime);
@@ -41425,6 +41617,7 @@ const diagnoseProject = async (projectDefinition, baseOptions, batchConfig) => {
41425
41617
  }
41426
41618
  };
41427
41619
  const diagnoseProjectBatch = async (input) => {
41620
+ warnIfAiTrainingEnvironment();
41428
41621
  const startTime = globalThis.performance.now();
41429
41622
  const { projects, concurrency, config: batchConfig, ...baseOptions } = input;
41430
41623
  const projectResults = await mapWithConcurrency(projects, concurrency ?? 4, (projectDefinition) => diagnoseProject(projectDefinition, baseOptions, batchConfig));
@@ -41469,4 +41662,4 @@ const toJsonReport = (result, options) => buildJsonReport({
41469
41662
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
41470
41663
 
41471
41664
  //# sourceMappingURL=index.js.map
41472
- //# debugId=33508ee6-c977-5b5f-8585-9939928ce74d
41665
+ //# debugId=0bd202f9-5e22-5625-86dc-458270afbcca