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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lsp.js CHANGED
@@ -6021,7 +6021,7 @@ const composePassthrough = /* @__PURE__ */ dual(2, (left, right) => (input) => {
6021
6021
  * @since 2.0.0
6022
6022
  */
6023
6023
  const Scheduler = /* @__PURE__ */ Reference("effect/Scheduler", { defaultValue: () => new MixedScheduler() });
6024
- const setImmediate = "setImmediate" in globalThis ? (f) => {
6024
+ const setImmediate$1 = "setImmediate" in globalThis ? (f) => {
6025
6025
  const timer = globalThis.setImmediate(f);
6026
6026
  return () => globalThis.clearImmediate(timer);
6027
6027
  } : (f) => {
@@ -6065,7 +6065,7 @@ var PriorityBuckets = class {
6065
6065
  var MixedScheduler = class {
6066
6066
  executionMode;
6067
6067
  setImmediate;
6068
- constructor(executionMode = "async", setImmediateFn = setImmediate) {
6068
+ constructor(executionMode = "async", setImmediateFn = setImmediate$1) {
6069
6069
  this.executionMode = executionMode;
6070
6070
  this.setImmediate = setImmediateFn;
6071
6071
  }
@@ -6090,7 +6090,7 @@ var MixedSchedulerDispatcher = class {
6090
6090
  tasks = /* @__PURE__ */ new PriorityBuckets();
6091
6091
  running = void 0;
6092
6092
  setImmediate;
6093
- constructor(setImmediateFn = setImmediate) {
6093
+ constructor(setImmediateFn = setImmediate$1) {
6094
6094
  this.setImmediate = setImmediateFn;
6095
6095
  }
6096
6096
  /**
@@ -33730,6 +33730,7 @@ const MILLISECONDS_PER_SECOND = 1e3;
33730
33730
  const SCORE_API_URL = "https://www.react.doctor/api/score";
33731
33731
  const FETCH_TIMEOUT_MS = 1e4;
33732
33732
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
33733
+ const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
33733
33734
  const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
33734
33735
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
33735
33736
  const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
@@ -33810,6 +33811,7 @@ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
33810
33811
  const LINT_PHASE_TIMEOUT_MS = 3e5;
33811
33812
  const SCAN_TOTAL_DEADLINE_MS = 9e5;
33812
33813
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
33814
+ const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
33813
33815
  const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
33814
33816
  const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
33815
33817
  const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
@@ -35052,6 +35054,7 @@ const collectStringSet = (values) => {
35052
35054
  if (!Array.isArray(values)) return /* @__PURE__ */ new Set();
35053
35055
  return new Set(values.filter((value) => typeof value === "string"));
35054
35056
  };
35057
+ const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
35055
35058
  /**
35056
35059
  * Pre-compiles every stateful filter and returns a single
35057
35060
  * `apply(diagnostic)` closure that runs (in order):
@@ -35085,8 +35088,10 @@ const buildDiagnosticPipeline = (input) => {
35085
35088
  const compiledOverrides = compileIgnoreOverrides(userConfig);
35086
35089
  const textComponentNames = collectStringSet(userConfig?.textComponents);
35087
35090
  const rawTextWrapperComponentNames = collectStringSet(userConfig?.rawTextWrapperComponents);
35091
+ const runtimeGlobalNames = collectStringSet(userConfig?.runtimeGlobals);
35088
35092
  const hasTextComponents = textComponentNames.size > 0;
35089
35093
  const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
35094
+ const hasRuntimeGlobals = runtimeGlobalNames.size > 0;
35090
35095
  const fileLinesCache = /* @__PURE__ */ new Map();
35091
35096
  const fileContextCache = /* @__PURE__ */ new Map();
35092
35097
  const libraryFileCache = /* @__PURE__ */ new Map();
@@ -35138,6 +35143,12 @@ const buildDiagnosticPipeline = (input) => {
35138
35143
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
35139
35144
  return false;
35140
35145
  };
35146
+ const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
35147
+ if (!hasRuntimeGlobals) return false;
35148
+ if (diagnostic.rule !== "jsx-no-undef") return false;
35149
+ const identifierMatch = JSX_NO_UNDEF_IDENTIFIER_PATTERN.exec(diagnostic.message);
35150
+ return identifierMatch !== null && runtimeGlobalNames.has(identifierMatch[1]);
35151
+ };
35141
35152
  return { apply: (diagnostic) => {
35142
35153
  if (shouldAutoSuppress(diagnostic)) return null;
35143
35154
  let current = diagnostic;
@@ -35162,6 +35173,7 @@ const buildDiagnosticPipeline = (input) => {
35162
35173
  if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
35163
35174
  if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return null;
35164
35175
  if (isRnRawTextSuppressedByConfig(current)) return null;
35176
+ if (isJsxNoUndefSuppressedByConfig(current)) return null;
35165
35177
  }
35166
35178
  if (respectInlineDisables && current.line > 0) {
35167
35179
  const lines = getFileLines(current.filePath);
@@ -35401,7 +35413,7 @@ const resolveScanConcurrency = (requested) => {
35401
35413
  if (!Number.isFinite(requested) || requested < 1) return 1;
35402
35414
  return Math.min(Math.floor(requested), 32);
35403
35415
  };
35404
- const readSystemFacts = () => ({
35416
+ const readSystemFacts$1 = () => ({
35405
35417
  availableCores: os.availableParallelism(),
35406
35418
  totalMemoryBytes: os.totalmem(),
35407
35419
  cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
@@ -35422,7 +35434,7 @@ const readSystemFacts = () => ({
35422
35434
  * `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
35423
35435
  * limited, and ceiling cases without mocking `os` or the filesystem.
35424
35436
  */
35425
- const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
35437
+ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
35426
35438
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
35427
35439
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
35428
35440
  return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
@@ -35613,6 +35625,13 @@ const BOOLEAN_FIELD_NAMES = [
35613
35625
  "adoptExistingLintConfig"
35614
35626
  ];
35615
35627
  const STRING_FIELD_NAMES = ["rootDir"];
35628
+ const STRING_ARRAY_FIELD_NAMES = [
35629
+ "projects",
35630
+ "textComponents",
35631
+ "rawTextWrapperComponents",
35632
+ "runtimeGlobals",
35633
+ "serverAuthFunctionNames"
35634
+ ];
35616
35635
  const SURFACE_CONTROL_FIELD_NAMES = [
35617
35636
  "includeTags",
35618
35637
  "excludeTags",
@@ -35714,6 +35733,7 @@ const validateConfigTypes = (config) => {
35714
35733
  const validated = { ...config };
35715
35734
  for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
35716
35735
  for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
35736
+ for (const fieldName of STRING_ARRAY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateStringArrayField(fieldName, value));
35717
35737
  applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
35718
35738
  for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
35719
35739
  applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
@@ -36900,7 +36920,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
36900
36920
  }
36901
36921
  return true;
36902
36922
  };
36903
- const checkSecurityScan = (rootDirectory, options = {}) => {
36923
+ const yieldToEventLoop = () => new Promise((resolve) => {
36924
+ setImmediate(resolve);
36925
+ });
36926
+ const createSecurityScanSession = (rootDirectory, options) => {
36904
36927
  const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
36905
36928
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
36906
36929
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
@@ -36915,7 +36938,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36915
36938
  committedFilesOnly: rule.committedFilesOnly === true
36916
36939
  }];
36917
36940
  });
36918
- if (enabledScanRules.length === 0) return [];
36941
+ if (enabledScanRules.length === 0) return null;
36919
36942
  const diagnostics = [];
36920
36943
  const seen = /* @__PURE__ */ new Set();
36921
36944
  const gitIgnoredCache = /* @__PURE__ */ new Map();
@@ -36927,15 +36950,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36927
36950
  }
36928
36951
  return status === true;
36929
36952
  };
36930
- for (const file of collectSecurityScanFiles(rootDirectory)) for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36931
- if (committedFilesOnly && isFileGitIgnored(file)) continue;
36932
- const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36933
- const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36934
- if (seen.has(key)) continue;
36935
- seen.add(key);
36936
- diagnostics.push(diagnostic);
36953
+ const scanFile = (file) => {
36954
+ for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36955
+ if (committedFilesOnly && isFileGitIgnored(file)) continue;
36956
+ const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36957
+ const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36958
+ if (seen.has(key)) continue;
36959
+ seen.add(key);
36960
+ diagnostics.push(diagnostic);
36961
+ }
36962
+ };
36963
+ return {
36964
+ scanFile,
36965
+ diagnostics
36966
+ };
36967
+ };
36968
+ const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
36969
+ const session = createSecurityScanSession(rootDirectory, options);
36970
+ if (session === null) return [];
36971
+ let filesSinceYield = 0;
36972
+ for (const file of collectSecurityScanFiles(rootDirectory)) {
36973
+ session.scanFile(file);
36974
+ filesSinceYield += 1;
36975
+ if (filesSinceYield >= 16) {
36976
+ filesSinceYield = 0;
36977
+ await yieldToEventLoop();
36978
+ }
36937
36979
  }
36938
- return diagnostics;
36980
+ return session.diagnostics;
36939
36981
  };
36940
36982
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
36941
36983
  let p = process || {}, argv = p.argv || [], env = p.env || {};
@@ -37152,6 +37194,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
37152
37194
  return [...seen].filter((pattern) => pattern.length > 0);
37153
37195
  };
37154
37196
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
37197
+ const readSystemFacts = () => ({
37198
+ availableCores: os.availableParallelism(),
37199
+ totalMemoryBytes: os.totalmem(),
37200
+ cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
37201
+ });
37202
+ /**
37203
+ * How many real deslop dead-code child processes may run at once, across the
37204
+ * concurrent per-project `runInspect` fibers of one CLI run. The cap is the
37205
+ * smaller of the core count and the number of `DEAD_CODE_WORKER_MEM_BUDGET_BYTES`
37206
+ * workers that fit in available memory, floored at 1.
37207
+ *
37208
+ * On a roomy dev box / CI runner this resolves high enough that every
37209
+ * concurrently-scanned project still spawns its own worker (no serialization vs
37210
+ * the prior uncapped behavior); on a memory-constrained runner it collapses
37211
+ * toward 1, so the `withDeadCodeWorkerSlot` semaphore serializes the spawns
37212
+ * instead of oversubscribing memory with N simultaneous children — the global
37213
+ * cap the per-project spawn path lacked.
37214
+ *
37215
+ * Mirrors `resolveAutoScanConcurrency` (lint), but budgets memory per the
37216
+ * heavier dead-code worker. `facts` is injectable for tests.
37217
+ */
37218
+ const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
37219
+ const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
37220
+ const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
37221
+ return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
37222
+ };
37223
+ let availableSlots = -1;
37224
+ const waiters = [];
37225
+ const releaseSlot = () => {
37226
+ const nextWaiter = waiters.shift();
37227
+ if (nextWaiter !== void 0) nextWaiter();
37228
+ else availableSlots += 1;
37229
+ };
37230
+ /**
37231
+ * Runs `task` once a dead-code worker slot is free, releasing the slot when the
37232
+ * task settles (success or failure). With a high cap (roomy machine) every
37233
+ * caller proceeds immediately; with a low cap (constrained runner) callers
37234
+ * queue and run as slots free.
37235
+ *
37236
+ * `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
37237
+ * this caller is queued, the call rejects without acquiring a slot or running
37238
+ * `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
37239
+ * then spawn a child only to tear it down. A queued caller that aborts removes
37240
+ * its own waiter so a later release never hands a slot to a dead request.
37241
+ */
37242
+ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
37243
+ if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
37244
+ if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
37245
+ if (availableSlots > 0) availableSlots -= 1;
37246
+ else await new Promise((resolve, reject) => {
37247
+ const waiter = () => {
37248
+ abortSignal?.removeEventListener("abort", onAbort);
37249
+ resolve();
37250
+ };
37251
+ const onAbort = () => {
37252
+ const queuedIndex = waiters.indexOf(waiter);
37253
+ if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
37254
+ reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
37255
+ };
37256
+ waiters.push(waiter);
37257
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
37258
+ });
37259
+ try {
37260
+ return await task();
37261
+ } finally {
37262
+ releaseSlot();
37263
+ }
37264
+ };
37155
37265
  /**
37156
37266
  * Resolves a path to its canonical, symlink-free form, falling back to
37157
37267
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -37176,6 +37286,11 @@ const toCanonicalPath = (filePath) => {
37176
37286
  };
37177
37287
  const DEAD_CODE_PLUGIN = "deslop";
37178
37288
  const DEAD_CODE_CATEGORY = "Maintainability";
37289
+ const REACT_DOCTOR_TOOLCHAIN_PACKAGES = new Set([
37290
+ "react-doctor",
37291
+ "eslint-plugin-react-doctor",
37292
+ "oxlint-plugin-react-doctor"
37293
+ ]);
37179
37294
  const DEAD_CODE_WORKER_SCRIPT = `
37180
37295
  const inputChunks = [];
37181
37296
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -37442,14 +37557,17 @@ const checkDeadCode = async (options) => {
37442
37557
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37443
37558
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37444
37559
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37445
- const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37446
- rootDirectory,
37447
- entryPatterns,
37448
- tsConfigPath: resolveTsConfigPath(rootDirectory),
37449
- ignorePatterns,
37450
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37451
- parseConcurrency: options.parseConcurrency
37452
- }), options.workerTimeoutMs ?? 12e4, options.abortSignal));
37560
+ const spawnAndRun = () => {
37561
+ return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37562
+ rootDirectory,
37563
+ entryPatterns,
37564
+ tsConfigPath: resolveTsConfigPath(rootDirectory),
37565
+ ignorePatterns,
37566
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37567
+ parseConcurrency: options.parseConcurrency
37568
+ }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
37569
+ };
37570
+ const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
37453
37571
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
37454
37572
  const diagnostics = [];
37455
37573
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -37478,6 +37596,7 @@ const checkDeadCode = async (options) => {
37478
37596
  });
37479
37597
  }
37480
37598
  for (const unusedDependency of result.unusedDependencies) {
37599
+ if (REACT_DOCTOR_TOOLCHAIN_PACKAGES.has(unusedDependency.name)) continue;
37481
37600
  const label = unusedDependency.isDevDependency ? "devDependency" : "dependency";
37482
37601
  diagnostics.push({
37483
37602
  filePath: "package.json",
@@ -37850,43 +37969,46 @@ var Git = class Git extends Service()("react-doctor/Git") {
37850
37969
  * reason: GitInvocationFailed })` so the rest of the codebase
37851
37970
  * sees a single failure channel.
37852
37971
  */
37853
- const runCommand = (input) => scoped(gen(function* () {
37854
- const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37855
- cwd: input.directory,
37856
- env: input.env,
37857
- extendEnv: true
37858
- }));
37859
- const maxStdoutBytes = input.maxStdoutBytes;
37860
- const stdoutByteCount = yield* make$13(0);
37861
- const [stdout, stderr, status] = yield* all([
37862
- 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({
37863
- args: [...input.args],
37864
- directory: input.directory,
37865
- cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
37866
- }) })) : void_)))))),
37867
- mkString(decodeText(handle.stderr)),
37868
- handle.exitCode
37869
- ], { concurrency: 3 });
37870
- return {
37871
- status,
37872
- stdout,
37873
- stderr
37874
- };
37875
- })).pipe(catchTag$1("PlatformError", (cause) => {
37876
- if (input.command !== "git") return succeed$2({
37972
+ const runCommand = (input) => {
37973
+ const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
37877
37974
  status: 127,
37878
37975
  stdout: "",
37879
37976
  stderr: String(cause)
37880
- });
37881
- return new ReactDoctorError({ reason: new GitInvocationFailed({
37977
+ }) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37882
37978
  args: [...input.args],
37883
37979
  directory: input.directory,
37884
37980
  cause
37885
- }) });
37886
- }), withSpan("git.exec", { attributes: {
37887
- "git.command": input.command,
37888
- "git.subcommand": input.args[0] ?? ""
37889
- } }));
37981
+ }) }));
37982
+ return scoped(gen(function* () {
37983
+ if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
37984
+ const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
37985
+ if (argvLengthChars > 24e3) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${SPAWN_ARGS_MAX_LENGTH_CHARS})`);
37986
+ const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37987
+ cwd: input.directory,
37988
+ env: input.env,
37989
+ extendEnv: true
37990
+ }));
37991
+ const maxStdoutBytes = input.maxStdoutBytes;
37992
+ const stdoutByteCount = yield* make$13(0);
37993
+ const [stdout, stderr, status] = yield* all([
37994
+ 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({
37995
+ args: [...input.args],
37996
+ directory: input.directory,
37997
+ cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
37998
+ }) })) : void_)))))),
37999
+ mkString(decodeText(handle.stderr)),
38000
+ handle.exitCode
38001
+ ], { concurrency: 3 });
38002
+ return {
38003
+ status,
38004
+ stdout,
38005
+ stderr
38006
+ };
38007
+ })).pipe(catchTag$1("PlatformError", foldSpawnFailure), withSpan("git.exec", { attributes: {
38008
+ "git.command": input.command,
38009
+ "git.subcommand": input.args[0] ?? ""
38010
+ } }));
38011
+ };
37890
38012
  const runGit = (directory, args) => runCommand({
37891
38013
  command: "git",
37892
38014
  args,
@@ -37919,7 +38041,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37919
38041
  "rev-parse",
37920
38042
  "--verify",
37921
38043
  branch
37922
- ]).pipe(map$3((result) => result.status === 0));
38044
+ ]).pipe(map$3((result) => result.status === 0), catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(false) : fail$4(error)));
37923
38045
  const headSha = (directory) => runGit(directory, ["rev-parse", "HEAD"]).pipe(map$3((result) => result.status === 0 ? trimOrNull(result.stdout) : null));
37924
38046
  const mergeBase = (input) => isSafeGitRevision(input.ref) ? runGit(input.directory, [
37925
38047
  "merge-base",
@@ -38133,7 +38255,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38133
38255
  ]);
38134
38256
  if (result.status !== 0) return null;
38135
38257
  return parseChangedLineRanges(result.stdout);
38136
- }).pipe(withSpan("Git.changedLineRanges"))
38258
+ }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
38137
38259
  });
38138
38260
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
38139
38261
  /**
@@ -39208,11 +39330,12 @@ const REACT_COMPILER_TITLE = "React Compiler can't optimize this";
39208
39330
  const REACT_COMPILER_TODO_TITLE = "React Compiler doesn't support this syntax";
39209
39331
  const REACT_COMPILER_IMPACT = "This component misses React Compiler's automatic memoization & re-renders more than it should";
39210
39332
  const REACT_COMPILER_ACTION = "Rewrite the flagged code so the compiler can optimize it.";
39333
+ const REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION = "It's how the library works, not a bug in your code. Memoize values you pass from it into other memoized components, or suppress it with `// react-doctor-disable-next-line react-hooks-js/incompatible-library`.";
39211
39334
  const REACT_COMPILER_GENERIC_MESSAGE = `${REACT_COMPILER_IMPACT}. ${REACT_COMPILER_ACTION}`;
39212
- const buildReactCompilerMessage = (reasonSummary) => {
39335
+ const buildReactCompilerMessage = (reasonSummary, action = REACT_COMPILER_ACTION) => {
39213
39336
  const normalizedSummary = reasonSummary.replace(TRAILING_PERIOD_PATTERN, "");
39214
- if (!normalizedSummary) return REACT_COMPILER_GENERIC_MESSAGE;
39215
- return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${REACT_COMPILER_ACTION}`;
39337
+ if (!normalizedSummary) return `${REACT_COMPILER_IMPACT}. ${action}`;
39338
+ return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${action}`;
39216
39339
  };
39217
39340
  const PLUGIN_CATEGORY_MAP = {
39218
39341
  react: "Bugs",
@@ -39261,7 +39384,7 @@ const resolveCleanedDiagnostic = (message, help, plugin, rule, project) => {
39261
39384
  const [reasonSummary = "", ...reasonDetailLines] = bailoutReason.split("\n");
39262
39385
  const reasonDetail = reasonDetailLines.join("\n").trim();
39263
39386
  return {
39264
- message: buildReactCompilerMessage(reasonSummary.trim()),
39387
+ message: buildReactCompilerMessage(reasonSummary.trim(), rule === "incompatible-library" ? REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION : REACT_COMPILER_ACTION),
39265
39388
  help: appendReanimatedSharedValueHint(reasonDetail || help, rule, project)
39266
39389
  };
39267
39390
  }
@@ -40610,7 +40733,10 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40610
40733
  * diagnostics).
40611
40734
  * 2. beforeLint hook (e.g. CLI renders the project-detection block)
40612
40735
  * 3. environment checks (reduced-motion + pnpm hardening +
40613
- * expo/react-native + security scan), collected synchronously
40736
+ * expo/react-native), collected synchronously. The heavier
40737
+ * content-regex security scan is forked instead (like supply-chain
40738
+ * below) and joined before the concat, so its CPU overlaps lint
40739
+ * rather than blocking the event loop before it.
40614
40740
  * 4. The supply-chain check (Socket.dev) is forked onto a background
40615
40741
  * fiber so its ~100% network-bound time overlaps the ~100%
40616
40742
  * CPU/subprocess-bound lint pass below, collapsing two serial
@@ -40630,7 +40756,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40630
40756
  * order, so terminal output is identical either way; supply-chain
40631
40757
  * rides alongside without a spinner.
40632
40758
  * 6. Join the supply-chain fiber, then assemble the diagnostics in a
40633
- * FIXED order (env, supply-chain, lint, dead-code) so the output is
40759
+ * FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
40634
40760
  * byte-identical regardless of which fiber settled first. The
40635
40761
  * viewer-permission fiber is joined later, during score-metadata
40636
40762
  * assembly (it feeds score metadata, not diagnostics). The per-element
@@ -40691,12 +40817,12 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40691
40817
  ...checkPnpmHardening(scanDirectory),
40692
40818
  ...checkReactServerComponentsAdvisory(scanDirectory, project),
40693
40819
  ...checkExpoProject(scanDirectory, project),
40694
- ...checkReactNativeProject(scanDirectory, project),
40695
- ...checkSecurityScan(scanDirectory, {
40696
- project,
40697
- ignoredTags: input.ignoredTags
40698
- })
40820
+ ...checkReactNativeProject(scanDirectory, project)
40699
40821
  ])));
40822
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(promise(() => checkSecurityScanCooperative(scanDirectory, {
40823
+ project,
40824
+ ignoredTags: input.ignoredTags
40825
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)))))).pipe(withSpan("SecurityScan.run")));
40700
40826
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
40701
40827
  const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
40702
40828
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
@@ -40832,9 +40958,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40832
40958
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
40833
40959
  const supplyChainResult = yield* join(supplyChainFiber);
40834
40960
  const supplyChainCollected = supplyChainResult.diagnostics;
40961
+ const securityScanCollected = yield* join(securityScanFiber);
40835
40962
  yield* reporterService.finalize;
40836
40963
  const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
40837
40964
  ...envCollected,
40965
+ ...securityScanCollected,
40838
40966
  ...supplyChainCollected,
40839
40967
  ...lintCollected,
40840
40968
  ...deadCodeCollected
@@ -42906,6 +43034,7 @@ const SENTRY_FLUSH_TIMEOUT_MS = 2e3;
42906
43034
  const METRIC = {
42907
43035
  cliInvoked: "cli.invoked",
42908
43036
  cliError: "cli.error",
43037
+ cliEnvironmentError: "cli.env_error",
42909
43038
  projectDetected: "project.detected",
42910
43039
  projectPathSelected: "project.path_selected",
42911
43040
  projectConfigSelected: "project.config_selected",
@@ -42930,12 +43059,17 @@ const METRIC = {
42930
43059
  installWorkflow: "install.workflow",
42931
43060
  installAgentHooks: "install.agent_hooks",
42932
43061
  installDependency: "install.dependency",
43062
+ ciScaffolded: "ci.scaffolded",
43063
+ ciUpgraded: "ci.upgraded",
43064
+ ciConfigured: "ci.configured",
42933
43065
  rulesChanged: "rules.changed",
42934
43066
  rulesQueried: "rules.queried",
42935
43067
  lspSessionStarted: "lsp.session.started",
42936
43068
  lspScanCompleted: "lsp.scan.completed",
42937
43069
  lspScanDuration: "lsp.scan.duration",
42938
- lspScanDiagnostics: "lsp.scan.diagnostics"
43070
+ lspScanDiagnostics: "lsp.scan.diagnostics",
43071
+ aiTrainingWarningShown: "ai.training.warning_shown",
43072
+ jsonOutUsed: "json.out_used"
42939
43073
  };
42940
43074
  //#endregion
42941
43075
  //#region src/cli/utils/to-category-key.ts
@@ -43252,5 +43386,5 @@ const startLanguageServer = () => {
43252
43386
  };
43253
43387
  //#endregion
43254
43388
  export { startLanguageServer };
43255
- !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]="28a9132e-0eb3-5487-9d97-46eb9456a794")}catch(e){}}();
43256
- //# debugId=28a9132e-0eb3-5487-9d97-46eb9456a794
43389
+ !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]="5b21d2c0-25a8-52b7-ae5f-f4cc20771996")}catch(e){}}();
43390
+ //# debugId=5b21d2c0-25a8-52b7-ae5f-f4cc20771996
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.5.8-dev.c2ce298",
3
+ "version": "0.5.8-dev.e2393c4",
4
4
  "description": "Your agent writes bad React. This catches it",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -20,7 +20,7 @@
20
20
  "bugs": {
21
21
  "url": "https://github.com/millionco/react-doctor/issues"
22
22
  },
23
- "license": "MIT",
23
+ "license": "SEE LICENSE IN LICENSE",
24
24
  "author": "Million Software, Inc",
25
25
  "repository": {
26
26
  "type": "git",
@@ -34,7 +34,8 @@
34
34
  "bin/**",
35
35
  "dist/**/*.js",
36
36
  "dist/**/*.d.ts",
37
- "dist/skills/**"
37
+ "dist/skills/**",
38
+ "LICENSE"
38
39
  ],
39
40
  "type": "module",
40
41
  "sideEffects": false,
@@ -59,12 +60,13 @@
59
60
  "magicast": "^0.5.3",
60
61
  "oxlint": ">=1.66.0 <1.67.0",
61
62
  "prompts": "^2.4.2",
62
- "typescript": ">=5.0.4 <7",
63
+ "typescript": ">=5.0.4 <6",
63
64
  "vscode-languageserver": "^9.0.1",
64
65
  "vscode-languageserver-textdocument": "^1.0.12",
65
66
  "vscode-uri": "^3.1.0",
67
+ "yaml": "^2.9.0",
66
68
  "deslop-js": "0.5.8",
67
- "oxlint-plugin-react-doctor": "0.5.8-dev.c2ce298"
69
+ "oxlint-plugin-react-doctor": "0.5.8-dev.e2393c4"
68
70
  },
69
71
  "devDependencies": {
70
72
  "@types/babel__code-frame": "^7.27.0",
@@ -73,8 +75,8 @@
73
75
  "commander": "^14.0.3",
74
76
  "ora": "^9.4.0",
75
77
  "@react-doctor/api": "0.5.8",
76
- "@react-doctor/language-server": "0.5.8",
77
- "@react-doctor/core": "0.5.8"
78
+ "@react-doctor/core": "0.5.8",
79
+ "@react-doctor/language-server": "0.5.8"
78
80
  },
79
81
  "engines": {
80
82
  "node": "^20.19.0 || >=22.13.0"