react-doctor 0.5.8-dev.bd0f465 → 0.5.8-dev.c2af308

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lsp.js CHANGED
@@ -6,7 +6,7 @@ import os from "node:os";
6
6
  import * as Path from "node:path";
7
7
  import path from "node:path";
8
8
  import * as NodeChildProcess from "node:child_process";
9
- import { spawn, spawnSync } from "node:child_process";
9
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
10
10
  import * as ts from "typescript";
11
11
  import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
12
12
  import { parseJSON5 } from "confbox";
@@ -33731,6 +33731,9 @@ 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
33733
  const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
33734
+ const SPAWN_ARGS_MAX_LENGTH_CHARS_POSIX = 15e5;
33735
+ const SPAWN_ARGS_MAX_LENGTH_CHARS_DARWIN = 8e5;
33736
+ const NODE_VERSION_PROBE_TIMEOUT_MS = 5e3;
33734
33737
  const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
33735
33738
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
33736
33739
  const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
@@ -34578,7 +34581,7 @@ const tokenizeRuleList = (ruleList) => {
34578
34581
  const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34579
34582
  const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34580
34583
  const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34581
- const buildHint = (tool, token, ruleId) => `oxlint matches plugin rules only by their full name, so \`${token}\` in your ${tool}-disable comment does not silence \`${ruleId}\` — change it to \`${ruleId}\`.`;
34584
+ const buildHint = (tool, token, ruleId) => `oxlint matches plugin rules only by their full name, so \`${token}\` in your ${tool}-disable comment does not silence \`${ruleId}\` — add \`${ruleId}\` to the comment (e.g. \`${token}, ${ruleId}\`).`;
34582
34585
  const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
34583
34586
  const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34584
34587
  const candidates = [{
@@ -35054,6 +35057,7 @@ const collectStringSet = (values) => {
35054
35057
  if (!Array.isArray(values)) return /* @__PURE__ */ new Set();
35055
35058
  return new Set(values.filter((value) => typeof value === "string"));
35056
35059
  };
35060
+ const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
35057
35061
  /**
35058
35062
  * Pre-compiles every stateful filter and returns a single
35059
35063
  * `apply(diagnostic)` closure that runs (in order):
@@ -35087,11 +35091,28 @@ const buildDiagnosticPipeline = (input) => {
35087
35091
  const compiledOverrides = compileIgnoreOverrides(userConfig);
35088
35092
  const textComponentNames = collectStringSet(userConfig?.textComponents);
35089
35093
  const rawTextWrapperComponentNames = collectStringSet(userConfig?.rawTextWrapperComponents);
35094
+ const runtimeGlobalNames = collectStringSet(userConfig?.runtimeGlobals);
35090
35095
  const hasTextComponents = textComponentNames.size > 0;
35091
35096
  const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
35097
+ const hasRuntimeGlobals = runtimeGlobalNames.size > 0;
35092
35098
  const fileLinesCache = /* @__PURE__ */ new Map();
35093
35099
  const fileContextCache = /* @__PURE__ */ new Map();
35094
35100
  const libraryFileCache = /* @__PURE__ */ new Map();
35101
+ const suppressions = /* @__PURE__ */ new Map();
35102
+ const suppress = (diagnostic, source) => {
35103
+ const { ruleKey } = getDiagnosticRuleIdentity(diagnostic);
35104
+ const suppressionKey = `${ruleKey}\u0000${source}`;
35105
+ const existing = suppressions.get(suppressionKey);
35106
+ suppressions.set(suppressionKey, existing ? {
35107
+ ...existing,
35108
+ count: existing.count + 1
35109
+ } : {
35110
+ rule: ruleKey,
35111
+ source,
35112
+ count: 1
35113
+ });
35114
+ return null;
35115
+ };
35095
35116
  const isLibraryFile = (filePath) => {
35096
35117
  let cached = libraryFileCache.get(filePath);
35097
35118
  if (cached === void 0) {
@@ -35140,50 +35161,60 @@ const buildDiagnosticPipeline = (input) => {
35140
35161
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
35141
35162
  return false;
35142
35163
  };
35143
- return { apply: (diagnostic) => {
35144
- if (shouldAutoSuppress(diagnostic)) return null;
35145
- let current = diagnostic;
35146
- let explicitSeverityOverride;
35147
- let explicitRuleOverride;
35148
- if (severityControls) {
35149
- const { ruleKey, category } = getDiagnosticRuleIdentity(current);
35150
- explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey }, severityControls);
35151
- explicitSeverityOverride = resolveRuleSeverityOverride({
35152
- ruleKey,
35153
- category
35154
- }, severityControls);
35155
- if (explicitSeverityOverride === "off") return null;
35156
- if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
35157
- }
35158
- if (explicitRuleOverride === void 0) {
35159
- if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
35160
- }
35161
- if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
35162
- if (userConfig) {
35163
- if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return null;
35164
- if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
35165
- if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return null;
35166
- if (isRnRawTextSuppressedByConfig(current)) return null;
35167
- }
35168
- if (respectInlineDisables && current.line > 0) {
35169
- const lines = getFileLines(current.filePath);
35170
- if (lines) {
35171
- const ruleIdentifier = `${current.plugin}/${current.rule}`;
35172
- const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
35173
- if (evaluation.isSuppressed) return null;
35174
- if (evaluation.nearMissHint) current = {
35175
- ...current,
35176
- suppressionHint: evaluation.nearMissHint
35177
- };
35164
+ const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
35165
+ if (!hasRuntimeGlobals) return false;
35166
+ if (diagnostic.rule !== "jsx-no-undef") return false;
35167
+ const identifierMatch = JSX_NO_UNDEF_IDENTIFIER_PATTERN.exec(diagnostic.message);
35168
+ return identifierMatch !== null && runtimeGlobalNames.has(identifierMatch[1]);
35169
+ };
35170
+ return {
35171
+ apply: (diagnostic) => {
35172
+ if (shouldAutoSuppress(diagnostic)) return null;
35173
+ let current = diagnostic;
35174
+ let explicitSeverityOverride;
35175
+ let explicitRuleOverride;
35176
+ if (severityControls) {
35177
+ const { ruleKey, category } = getDiagnosticRuleIdentity(current);
35178
+ explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey }, severityControls);
35179
+ explicitSeverityOverride = resolveRuleSeverityOverride({
35180
+ ruleKey,
35181
+ category
35182
+ }, severityControls);
35183
+ if (explicitSeverityOverride === "off") return suppress(current, "config");
35184
+ if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
35178
35185
  }
35179
- }
35180
- const fileContext = getFileContext(current.filePath);
35181
- if (fileContext !== "production") current = {
35182
- ...current,
35183
- fileContext
35184
- };
35185
- return current;
35186
- } };
35186
+ if (explicitRuleOverride === void 0) {
35187
+ if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
35188
+ }
35189
+ if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
35190
+ if (userConfig) {
35191
+ if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return suppress(current, "config");
35192
+ if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
35193
+ if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return suppress(current, "override");
35194
+ if (isRnRawTextSuppressedByConfig(current)) return null;
35195
+ if (isJsxNoUndefSuppressedByConfig(current)) return null;
35196
+ }
35197
+ if (respectInlineDisables && current.line > 0) {
35198
+ const lines = getFileLines(current.filePath);
35199
+ if (lines) {
35200
+ const ruleIdentifier = `${current.plugin}/${current.rule}`;
35201
+ const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
35202
+ if (evaluation.isSuppressed) return suppress(current, "inline");
35203
+ if (evaluation.nearMissHint) current = {
35204
+ ...current,
35205
+ suppressionHint: evaluation.nearMissHint
35206
+ };
35207
+ }
35208
+ }
35209
+ const fileContext = getFileContext(current.filePath);
35210
+ if (fileContext !== "production") current = {
35211
+ ...current,
35212
+ fileContext
35213
+ };
35214
+ return current;
35215
+ },
35216
+ summarizeSuppressions: () => [...suppressions.values()]
35217
+ };
35187
35218
  };
35188
35219
  const OxlintUnavailableKind = Literals(["binary-not-found", "native-binding-missing"]);
35189
35220
  var OxlintUnavailable = class extends TaggedErrorClass()("OxlintUnavailable", {
@@ -35465,13 +35496,7 @@ var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { de
35465
35496
  * without recompiling react-doctor. Tests override via
35466
35497
  * `Layer.succeed(SupplyChainOverlapTimeoutMs, ...)`.
35467
35498
  */
35468
- var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => {
35469
- const raw = process.env["REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS"];
35470
- if (raw === void 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35471
- const parsed = Number(raw);
35472
- if (!Number.isFinite(parsed) || parsed <= 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
35473
- return parsed;
35474
- } }) {};
35499
+ var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS", SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS) }) {};
35475
35500
  /**
35476
35501
  * Hard cap on combined stdout+stderr bytes per oxlint batch. The
35477
35502
  * subprocess gets SIGKILL'd if it produces more; the recovery path
@@ -35619,6 +35644,7 @@ const STRING_ARRAY_FIELD_NAMES = [
35619
35644
  "projects",
35620
35645
  "textComponents",
35621
35646
  "rawTextWrapperComponents",
35647
+ "runtimeGlobals",
35622
35648
  "serverAuthFunctionNames"
35623
35649
  ];
35624
35650
  const SURFACE_CONTROL_FIELD_NAMES = [
@@ -37275,6 +37301,11 @@ const toCanonicalPath = (filePath) => {
37275
37301
  };
37276
37302
  const DEAD_CODE_PLUGIN = "deslop";
37277
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
+ ]);
37278
37309
  const DEAD_CODE_WORKER_SCRIPT = `
37279
37310
  const inputChunks = [];
37280
37311
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -37580,6 +37611,7 @@ const checkDeadCode = async (options) => {
37580
37611
  });
37581
37612
  }
37582
37613
  for (const unusedDependency of result.unusedDependencies) {
37614
+ if (REACT_DOCTOR_TOOLCHAIN_PACKAGES.has(unusedDependency.name)) continue;
37583
37615
  const label = unusedDependency.isDevDependency ? "devDependency" : "dependency";
37584
37616
  diagnostics.push({
37585
37617
  filePath: "package.json",
@@ -37758,7 +37790,6 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37758
37790
  return yield* tryPromise({
37759
37791
  try: (signal) => checkDeadCode({
37760
37792
  rootDirectory: input.rootDirectory,
37761
- userConfig: input.userConfig,
37762
37793
  parseConcurrency: input.parseConcurrency,
37763
37794
  workerTimeoutMs: input.workerTimeoutMs,
37764
37795
  abortSignal: signal
@@ -37881,6 +37912,11 @@ const isSafeGitRevision = (candidate) => {
37881
37912
  if (candidate.includes("..") || candidate.includes("@{")) return false;
37882
37913
  return SAFE_GIT_REVISION_PATTERN.test(candidate);
37883
37914
  };
37915
+ const resolveSpawnArgsLengthCap = () => {
37916
+ if (process.platform === "win32") return SPAWN_ARGS_MAX_LENGTH_CHARS;
37917
+ if (process.platform === "darwin") return SPAWN_ARGS_MAX_LENGTH_CHARS_DARWIN;
37918
+ return SPAWN_ARGS_MAX_LENGTH_CHARS_POSIX;
37919
+ };
37884
37920
  /**
37885
37921
  * Splits a git revision range into its endpoints: three-dot `A...B`
37886
37922
  * (symmetric, merge-base) or two-dot `A..B` (direct). Returns `null`
@@ -37965,7 +38001,8 @@ var Git = class Git extends Service()("react-doctor/Git") {
37965
38001
  return scoped(gen(function* () {
37966
38002
  if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
37967
38003
  const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
37968
- if (argvLengthChars > 24e3) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${SPAWN_ARGS_MAX_LENGTH_CHARS})`);
38004
+ const spawnArgsLengthCap = resolveSpawnArgsLengthCap();
38005
+ if (argvLengthChars > spawnArgsLengthCap) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${spawnArgsLengthCap})`);
37969
38006
  const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37970
38007
  cwd: input.directory,
37971
38008
  env: input.env,
@@ -38134,6 +38171,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38134
38171
  if (resolvedCurrentBranch === null && explicitBaseBranch === void 0) return null;
38135
38172
  const baseBranch = explicitBaseBranch ?? (yield* defaultBranch(directory));
38136
38173
  if (baseBranch === null) return null;
38174
+ if (!isSafeGitRevision(baseBranch)) return null;
38137
38175
  if (explicitBaseBranch !== void 0) {
38138
38176
  if (!(yield* branchExists(directory, explicitBaseBranch))) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchMissing({ branch: explicitBaseBranch }) }));
38139
38177
  }
@@ -38484,7 +38522,7 @@ const normalizeConfigForHash = (config) => {
38484
38522
  if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
38485
38523
  return clone;
38486
38524
  };
38487
- const computeRulesetHash = (input) => crypto.createHash("sha1").update(JSON.stringify(normalizeConfigForHash(input.config))).update("\0").update([...input.toolchainVersions].join("\0")).update("\0").update([...input.ignorePatterns].join("\n")).update(" ").update(input.tsconfigContent ?? "").digest("hex");
38525
+ const computeRulesetHash = (input) => crypto.createHash("sha1").update(JSON.stringify(normalizeConfigForHash(input.config))).update("\0").update([...input.toolchainVersions].join("\0")).update("\0").update([...input.ignorePatterns].join("\n")).update("\0").update(input.tsconfigContent ?? "").digest("hex");
38488
38526
  /**
38489
38527
  * Loads a plugin module via the local require resolver and extracts
38490
38528
  * `(name, ruleNames)` from either `module.exports.meta + rules` or
@@ -38762,24 +38800,38 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38762
38800
  updatedAtMs: Date.now(),
38763
38801
  files: Object.fromEntries(cappedEntries)
38764
38802
  };
38765
- const keptHashes = Object.entries(rulesets).sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8).map(([hash]) => hash);
38766
- const prunedRulesets = {};
38767
- for (const hash of keptHashes) prunedRulesets[hash] = rulesets[hash];
38768
38803
  atomicWriteJson(cacheFilePath, {
38769
38804
  version: 1,
38770
- rulesets: prunedRulesets
38805
+ rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
38771
38806
  });
38772
38807
  }
38773
38808
  };
38774
38809
  };
38775
38810
  const bundledRequire = createRequire(import.meta.url);
38811
+ const childNodeVersionByBinaryPath = /* @__PURE__ */ new Map();
38812
+ const resolveChildNodeVersion = (nodeBinaryPath) => {
38813
+ if (nodeBinaryPath === process.execPath) return process.version;
38814
+ const cachedVersion = childNodeVersionByBinaryPath.get(nodeBinaryPath);
38815
+ if (cachedVersion !== void 0) return cachedVersion;
38816
+ let version;
38817
+ try {
38818
+ version = execFileSync(nodeBinaryPath, ["--version"], {
38819
+ encoding: "utf8",
38820
+ timeout: NODE_VERSION_PROBE_TIMEOUT_MS
38821
+ }).trim();
38822
+ } catch {
38823
+ version = `unknown:${nodeBinaryPath}`;
38824
+ }
38825
+ childNodeVersionByBinaryPath.set(nodeBinaryPath, version);
38826
+ return version;
38827
+ };
38776
38828
  const TOOLCHAIN_PACKAGE_SPECIFIERS = [
38777
38829
  "oxlint/package.json",
38778
38830
  "oxlint-plugin-react-doctor/package.json",
38779
38831
  "eslint-plugin-react-hooks/package.json"
38780
38832
  ];
38781
- const resolveOxlintToolchainVersions = () => {
38782
- const versions = [`node=${process.version}`];
38833
+ const resolveOxlintToolchainVersions = (nodeBinaryPath = process.execPath) => {
38834
+ const versions = [`node=${resolveChildNodeVersion(nodeBinaryPath)}`];
38783
38835
  for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
38784
38836
  const packageJson = bundledRequire(specifier);
38785
38837
  const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
@@ -39313,11 +39365,12 @@ const REACT_COMPILER_TITLE = "React Compiler can't optimize this";
39313
39365
  const REACT_COMPILER_TODO_TITLE = "React Compiler doesn't support this syntax";
39314
39366
  const REACT_COMPILER_IMPACT = "This component misses React Compiler's automatic memoization & re-renders more than it should";
39315
39367
  const REACT_COMPILER_ACTION = "Rewrite the flagged code so the compiler can optimize it.";
39368
+ const REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION = "It's how the library works, not a bug in your code. Memoize values you pass from it into other memoized components, or suppress it with `// react-doctor-disable-next-line react-hooks-js/incompatible-library`.";
39316
39369
  const REACT_COMPILER_GENERIC_MESSAGE = `${REACT_COMPILER_IMPACT}. ${REACT_COMPILER_ACTION}`;
39317
- const buildReactCompilerMessage = (reasonSummary) => {
39370
+ const buildReactCompilerMessage = (reasonSummary, action = REACT_COMPILER_ACTION) => {
39318
39371
  const normalizedSummary = reasonSummary.replace(TRAILING_PERIOD_PATTERN, "");
39319
- if (!normalizedSummary) return REACT_COMPILER_GENERIC_MESSAGE;
39320
- return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${REACT_COMPILER_ACTION}`;
39372
+ if (!normalizedSummary) return `${REACT_COMPILER_IMPACT}. ${action}`;
39373
+ return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${action}`;
39321
39374
  };
39322
39375
  const PLUGIN_CATEGORY_MAP = {
39323
39376
  react: "Bugs",
@@ -39366,7 +39419,7 @@ const resolveCleanedDiagnostic = (message, help, plugin, rule, project) => {
39366
39419
  const [reasonSummary = "", ...reasonDetailLines] = bailoutReason.split("\n");
39367
39420
  const reasonDetail = reasonDetailLines.join("\n").trim();
39368
39421
  return {
39369
- message: buildReactCompilerMessage(reasonSummary.trim()),
39422
+ message: buildReactCompilerMessage(reasonSummary.trim(), rule === "incompatible-library" ? REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION : REACT_COMPILER_ACTION),
39370
39423
  help: appendReanimatedSharedValueHint(reasonDetail || help, rule, project)
39371
39424
  };
39372
39425
  }
@@ -39634,21 +39687,27 @@ const spawnLintBatches = async (input) => {
39634
39687
  const allDiagnostics = [];
39635
39688
  const droppedFiles = [];
39636
39689
  let firstDropReason = null;
39637
- const splitDeadlineMs = Date.now() + splitTotalBudgetMs;
39638
- const spawnLintBatch = async (batch, depth) => {
39690
+ const spawnLintBatch = async (batch, depth, splitBudget) => {
39639
39691
  const batchArgs = [...baseArgs, ...batch];
39640
39692
  try {
39641
39693
  return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
39642
39694
  } catch (error) {
39643
39695
  if (!isSplittableReactDoctorError(error)) throw error;
39644
- const splitBudgetExhausted = Date.now() >= splitDeadlineMs || depth >= splitMaxDepth;
39645
- if (batch.length <= 1 || splitBudgetExhausted) {
39696
+ splitBudget.deadlineMs ??= Date.now() + splitTotalBudgetMs;
39697
+ const isBudgetElapsed = Date.now() >= splitBudget.deadlineMs;
39698
+ const isDepthCapReached = depth >= splitMaxDepth;
39699
+ if (batch.length <= 1 || isBudgetElapsed || isDepthCapReached) {
39646
39700
  droppedFiles.push(...batch);
39647
- if (firstDropReason === null) firstDropReason = splitBudgetExhausted && batch.length > 1 ? `${error.message} (split budget exhausted after ${splitMaxDepth} levels / ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s)` : error.message;
39701
+ if (firstDropReason === null) {
39702
+ let limitHint = "";
39703
+ if (isDepthCapReached) limitHint = ` (split depth cap of ${splitMaxDepth} levels reached)`;
39704
+ else if (isBudgetElapsed) limitHint = ` (split budget of ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s exhausted at depth ${depth})`;
39705
+ firstDropReason = batch.length > 1 ? `${error.message}${limitHint}` : error.message;
39706
+ }
39648
39707
  return [];
39649
39708
  }
39650
39709
  const splitIndex = Math.ceil(batch.length / 2);
39651
- return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1)];
39710
+ return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1, splitBudget), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1, splitBudget)];
39652
39711
  }
39653
39712
  };
39654
39713
  let startedFileCount = 0;
@@ -39665,7 +39724,7 @@ const spawnLintBatches = async (input) => {
39665
39724
  try {
39666
39725
  const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
39667
39726
  startedFileCount += batch.length;
39668
- const batchDiagnostics = await spawnLintBatch(batch, 0);
39727
+ const batchDiagnostics = await spawnLintBatch(batch, 0, { deadlineMs: null });
39669
39728
  scannedFileCount += batch.length;
39670
39729
  if (onFileProgress) {
39671
39730
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
@@ -39739,7 +39798,14 @@ const resolveReactDoctorCacheDir = (projectDirectory) => {
39739
39798
  if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
39740
39799
  const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
39741
39800
  if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
39742
- return Path.join(os.tmpdir(), "react-doctor-cache", projectCacheSubdir(projectDirectory));
39801
+ return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
39802
+ };
39803
+ const resolveUserCacheScope = () => {
39804
+ try {
39805
+ return String(process.getuid?.() ?? os.userInfo().username);
39806
+ } catch {
39807
+ return "shared";
39808
+ }
39743
39809
  };
39744
39810
  const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
39745
39811
  /**
@@ -39907,7 +39973,7 @@ const runOxlint = async (options) => {
39907
39973
  extendsPaths: [],
39908
39974
  ruleSelection: "cacheable"
39909
39975
  }),
39910
- toolchainVersions: resolveOxlintToolchainVersions(),
39976
+ toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
39911
39977
  ignorePatterns: combinedPatterns,
39912
39978
  tsconfigContent
39913
39979
  });
@@ -39921,7 +39987,7 @@ const runOxlint = async (options) => {
39921
39987
  missFiles.push(candidateFile);
39922
39988
  continue;
39923
39989
  }
39924
- const cacheKey = `${candidateFile.replaceAll("\\", "/")}${contentHash}`;
39990
+ const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
39925
39991
  cacheKeyByFile.set(candidateFile, cacheKey);
39926
39992
  const cachedDiagnostics = cache.lookup(cacheKey);
39927
39993
  if (cachedDiagnostics === null) missFiles.push(candidateFile);
@@ -40237,7 +40303,7 @@ const ScoreApiResponseSchema = Struct({
40237
40303
  })))
40238
40304
  });
40239
40305
  const parseScoreResult = (value) => getOrNull(decodeUnknownOption(ScoreApiResponseSchema)(value));
40240
- const stripLocalFileFields = (diagnostics) => diagnostics.map(({ filePath: _filePath, fileContext: _fileContext, ...rest }) => rest);
40306
+ const stripLocalFileFields = (diagnostics) => diagnostics.map(({ filePath: _filePath, fileContext: _fileContext, fixGroupId: _fixGroupId, ...rest }) => rest);
40241
40307
  const isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
40242
40308
  const describeFailure = (error) => {
40243
40309
  if (isAbortError(error)) return `timed out after ${FETCH_TIMEOUT_MS / 1e3}s`;
@@ -40541,7 +40607,10 @@ const ALERT_SEVERITY_RANK = {
40541
40607
  medium: 2,
40542
40608
  low: 1
40543
40609
  };
40544
- const severityRank = (severity) => ALERT_SEVERITY_RANK[severity.toLowerCase()] ?? 0;
40610
+ const severityRank = (severity) => {
40611
+ const normalized = severity.toLowerCase();
40612
+ return Object.hasOwn(ALERT_SEVERITY_RANK, normalized) ? ALERT_SEVERITY_RANK[normalized] : 0;
40613
+ };
40545
40614
  const displaySeverity = (severity) => {
40546
40615
  const normalized = sanitizeTerminalText(severity.toLowerCase());
40547
40616
  return normalized === "middle" ? "medium" : normalized;
@@ -40558,7 +40627,7 @@ const ALERT_TYPE_LABELS = {
40558
40627
  typosquat: "possible typosquat"
40559
40628
  };
40560
40629
  const humanizeAlertType = (type) => type.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase().trim();
40561
- const friendlyAlertType = (type) => ALERT_TYPE_LABELS[type] ?? sanitizeTerminalText(humanizeAlertType(type));
40630
+ const friendlyAlertType = (type) => Object.hasOwn(ALERT_TYPE_LABELS, type) ? ALERT_TYPE_LABELS[type] : sanitizeTerminalText(humanizeAlertType(type));
40562
40631
  const summarizeAlertNote = (note) => {
40563
40632
  const collapsed = sanitizeTerminalText(note.replace(/\s+/g, " ").trim());
40564
40633
  const firstSentence = collapsed.split(/(?<=\.)\s/)[0] || collapsed;
@@ -40653,7 +40722,10 @@ const checkSupplyChain = (input) => gen(function* () {
40653
40722
  * outage.
40654
40723
  */
40655
40724
  var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
40656
- static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(checkSupplyChain(input).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
40725
+ static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(flatMap$2(SupplyChainOverlapTimeoutMs, (totalTimeoutMs) => checkSupplyChain({
40726
+ ...input,
40727
+ totalTimeoutMs
40728
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
40657
40729
  static layerOf = (diagnostics) => succeed$3(SupplyChain, SupplyChain.of({ run: () => fromIterable$1(diagnostics) }));
40658
40730
  };
40659
40731
  const getObjectProperty = (value, propertyName) => {
@@ -40801,10 +40873,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40801
40873
  ...checkExpoProject(scanDirectory, project),
40802
40874
  ...checkReactNativeProject(scanDirectory, project)
40803
40875
  ])));
40804
- const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(promise(() => checkSecurityScanCooperative(scanDirectory, {
40876
+ const securityScanFailedRef = yield* make$13(false);
40877
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise(() => checkSecurityScanCooperative(scanDirectory, {
40805
40878
  project,
40806
40879
  ignoredTags: input.ignoredTags
40807
- })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)))))).pipe(withSpan("SecurityScan.run")));
40880
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), catch_$1(() => set(securityScanFailedRef, true).pipe(as(empty$4))))))).pipe(withSpan("SecurityScan.run")));
40808
40881
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
40809
40882
  const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
40810
40883
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
@@ -40842,7 +40915,6 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40842
40915
  const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
40843
40916
  const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
40844
40917
  rootDirectory: scanDirectory,
40845
- userConfig: resolvedConfig.config,
40846
40918
  parseConcurrency: deadCodeParseConcurrency,
40847
40919
  workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
40848
40920
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
@@ -40970,6 +41042,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40970
41042
  metadata: scoreMetadata
40971
41043
  });
40972
41044
  const lintPartialFailures = yield* get$2(partialFailuresRef);
41045
+ const securityScanFailed = yield* get$2(securityScanFailedRef);
40973
41046
  return {
40974
41047
  project,
40975
41048
  userConfig: resolvedConfig.config,
@@ -40990,8 +41063,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40990
41063
  scanElapsedMilliseconds,
40991
41064
  scanConcurrency,
40992
41065
  supplyChainOverlapTimedOut: supplyChainResult.timedOut,
41066
+ securityScanFailed,
40993
41067
  lintCacheHitFileCount,
40994
- lintCacheTotalFileCount
41068
+ lintCacheTotalFileCount,
41069
+ suppressedRuleCounts: transform.summarizeSuppressions()
40995
41070
  };
40996
41071
  }).pipe(withSpan("runInspect", { attributes: {
40997
41072
  "inspect.directory": input.directory,
@@ -43029,6 +43104,8 @@ const METRIC = {
43029
43104
  scanCheckSkipped: "scan.check_skipped",
43030
43105
  baselineDegraded: "baseline.degraded",
43031
43106
  ruleFired: "rule.fired",
43107
+ ruleDisabled: "rule.disabled",
43108
+ ruleSuppressed: "rule.suppressed",
43032
43109
  lintFailed: "lint.failed",
43033
43110
  deadCodeFailed: "deadcode.failed",
43034
43111
  scoreUnavailable: "score.unavailable",
@@ -43041,6 +43118,9 @@ const METRIC = {
43041
43118
  installWorkflow: "install.workflow",
43042
43119
  installAgentHooks: "install.agent_hooks",
43043
43120
  installDependency: "install.dependency",
43121
+ ciScaffolded: "ci.scaffolded",
43122
+ ciUpgraded: "ci.upgraded",
43123
+ ciConfigured: "ci.configured",
43044
43124
  rulesChanged: "rules.changed",
43045
43125
  rulesQueried: "rules.queried",
43046
43126
  lspSessionStarted: "lsp.session.started",
@@ -43365,5 +43445,5 @@ const startLanguageServer = () => {
43365
43445
  };
43366
43446
  //#endregion
43367
43447
  export { startLanguageServer };
43368
- !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]="c5b6c740-461d-5f65-a993-9e6d35ad9113")}catch(e){}}();
43369
- //# debugId=c5b6c740-461d-5f65-a993-9e6d35ad9113
43448
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="349b2e37-65e9-5f86-8168-70334a95aeec")}catch(e){}}();
43449
+ //# debugId=349b2e37-65e9-5f86-8168-70334a95aeec
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.5.8-dev.bd0f465",
3
+ "version": "0.5.8-dev.c2af308",
4
4
  "description": "Your agent writes bad React. This catches it",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -64,7 +64,8 @@
64
64
  "vscode-languageserver": "^9.0.1",
65
65
  "vscode-languageserver-textdocument": "^1.0.12",
66
66
  "vscode-uri": "^3.1.0",
67
- "oxlint-plugin-react-doctor": "0.5.8-dev.bd0f465",
67
+ "yaml": "^2.9.0",
68
+ "oxlint-plugin-react-doctor": "0.5.8-dev.c2af308",
68
69
  "deslop-js": "0.5.8"
69
70
  },
70
71
  "devDependencies": {
@@ -73,9 +74,9 @@
73
74
  "@xterm/headless": "^6.0.0",
74
75
  "commander": "^14.0.3",
75
76
  "ora": "^9.4.0",
76
- "@react-doctor/api": "0.5.8",
77
77
  "@react-doctor/core": "0.5.8",
78
- "@react-doctor/language-server": "0.5.8"
78
+ "@react-doctor/language-server": "0.5.8",
79
+ "@react-doctor/api": "0.5.8"
79
80
  },
80
81
  "engines": {
81
82
  "node": "^20.19.0 || >=22.13.0"