react-doctor 0.5.8-dev.849580c → 0.5.8-dev.88a5c3c

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";
@@ -19400,6 +19400,15 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
19400
19400
  ok: Boolean$1,
19401
19401
  directory: String$1,
19402
19402
  mode: JsonReportMode,
19403
+ /**
19404
+ * True when a `changed` (compare) run was intended but the base couldn't be
19405
+ * resolved — a shallow CI checkout with no merge base, or a failed base/head
19406
+ * lint. The report then lists every finding in the changed files (mode
19407
+ * downgrades to `diff`, the `baseline` block is dropped, the CI gate is
19408
+ * skipped) rather than only the ones the change introduced. Absent on a
19409
+ * successful `baseline` run (`schemaVersion: 2`) and on non-compare scopes.
19410
+ */
19411
+ baselineDegraded: optional(Boolean$1),
19403
19412
  diff: NullOr(JsonReportDiffInfo),
19404
19413
  projects: ArraySchema(JsonReportProjectEntry),
19405
19414
  diagnostics: ArraySchema(Diagnostic),
@@ -33731,6 +33740,9 @@ const SCORE_API_URL = "https://www.react.doctor/api/score";
33731
33740
  const FETCH_TIMEOUT_MS = 1e4;
33732
33741
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
33733
33742
  const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
33743
+ const SPAWN_ARGS_MAX_LENGTH_CHARS_POSIX = 15e5;
33744
+ const SPAWN_ARGS_MAX_LENGTH_CHARS_DARWIN = 8e5;
33745
+ const NODE_VERSION_PROBE_TIMEOUT_MS = 5e3;
33734
33746
  const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
33735
33747
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
33736
33748
  const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
@@ -34578,7 +34590,7 @@ const tokenizeRuleList = (ruleList) => {
34578
34590
  const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34579
34591
  const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34580
34592
  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}\`.`;
34593
+ 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
34594
  const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
34583
34595
  const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34584
34596
  const candidates = [{
@@ -35054,6 +35066,7 @@ const collectStringSet = (values) => {
35054
35066
  if (!Array.isArray(values)) return /* @__PURE__ */ new Set();
35055
35067
  return new Set(values.filter((value) => typeof value === "string"));
35056
35068
  };
35069
+ const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
35057
35070
  /**
35058
35071
  * Pre-compiles every stateful filter and returns a single
35059
35072
  * `apply(diagnostic)` closure that runs (in order):
@@ -35087,11 +35100,28 @@ const buildDiagnosticPipeline = (input) => {
35087
35100
  const compiledOverrides = compileIgnoreOverrides(userConfig);
35088
35101
  const textComponentNames = collectStringSet(userConfig?.textComponents);
35089
35102
  const rawTextWrapperComponentNames = collectStringSet(userConfig?.rawTextWrapperComponents);
35103
+ const runtimeGlobalNames = collectStringSet(userConfig?.runtimeGlobals);
35090
35104
  const hasTextComponents = textComponentNames.size > 0;
35091
35105
  const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
35106
+ const hasRuntimeGlobals = runtimeGlobalNames.size > 0;
35092
35107
  const fileLinesCache = /* @__PURE__ */ new Map();
35093
35108
  const fileContextCache = /* @__PURE__ */ new Map();
35094
35109
  const libraryFileCache = /* @__PURE__ */ new Map();
35110
+ const suppressions = /* @__PURE__ */ new Map();
35111
+ const suppress = (diagnostic, source) => {
35112
+ const { ruleKey } = getDiagnosticRuleIdentity(diagnostic);
35113
+ const suppressionKey = `${ruleKey}\u0000${source}`;
35114
+ const existing = suppressions.get(suppressionKey);
35115
+ suppressions.set(suppressionKey, existing ? {
35116
+ ...existing,
35117
+ count: existing.count + 1
35118
+ } : {
35119
+ rule: ruleKey,
35120
+ source,
35121
+ count: 1
35122
+ });
35123
+ return null;
35124
+ };
35095
35125
  const isLibraryFile = (filePath) => {
35096
35126
  let cached = libraryFileCache.get(filePath);
35097
35127
  if (cached === void 0) {
@@ -35140,50 +35170,60 @@ const buildDiagnosticPipeline = (input) => {
35140
35170
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
35141
35171
  return false;
35142
35172
  };
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
- };
35173
+ const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
35174
+ if (!hasRuntimeGlobals) return false;
35175
+ if (diagnostic.rule !== "jsx-no-undef") return false;
35176
+ const identifierMatch = JSX_NO_UNDEF_IDENTIFIER_PATTERN.exec(diagnostic.message);
35177
+ return identifierMatch !== null && runtimeGlobalNames.has(identifierMatch[1]);
35178
+ };
35179
+ return {
35180
+ apply: (diagnostic) => {
35181
+ if (shouldAutoSuppress(diagnostic)) return null;
35182
+ let current = diagnostic;
35183
+ let explicitSeverityOverride;
35184
+ let explicitRuleOverride;
35185
+ if (severityControls) {
35186
+ const { ruleKey, category } = getDiagnosticRuleIdentity(current);
35187
+ explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey }, severityControls);
35188
+ explicitSeverityOverride = resolveRuleSeverityOverride({
35189
+ ruleKey,
35190
+ category
35191
+ }, severityControls);
35192
+ if (explicitSeverityOverride === "off") return suppress(current, "config");
35193
+ if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
35178
35194
  }
35179
- }
35180
- const fileContext = getFileContext(current.filePath);
35181
- if (fileContext !== "production") current = {
35182
- ...current,
35183
- fileContext
35184
- };
35185
- return current;
35186
- } };
35195
+ if (explicitRuleOverride === void 0) {
35196
+ if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
35197
+ }
35198
+ if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
35199
+ if (userConfig) {
35200
+ if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return suppress(current, "config");
35201
+ if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
35202
+ if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return suppress(current, "override");
35203
+ if (isRnRawTextSuppressedByConfig(current)) return null;
35204
+ if (isJsxNoUndefSuppressedByConfig(current)) return null;
35205
+ }
35206
+ if (respectInlineDisables && current.line > 0) {
35207
+ const lines = getFileLines(current.filePath);
35208
+ if (lines) {
35209
+ const ruleIdentifier = `${current.plugin}/${current.rule}`;
35210
+ const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
35211
+ if (evaluation.isSuppressed) return suppress(current, "inline");
35212
+ if (evaluation.nearMissHint) current = {
35213
+ ...current,
35214
+ suppressionHint: evaluation.nearMissHint
35215
+ };
35216
+ }
35217
+ }
35218
+ const fileContext = getFileContext(current.filePath);
35219
+ if (fileContext !== "production") current = {
35220
+ ...current,
35221
+ fileContext
35222
+ };
35223
+ return current;
35224
+ },
35225
+ summarizeSuppressions: () => [...suppressions.values()]
35226
+ };
35187
35227
  };
35188
35228
  const OxlintUnavailableKind = Literals(["binary-not-found", "native-binding-missing"]);
35189
35229
  var OxlintUnavailable = class extends TaggedErrorClass()("OxlintUnavailable", {
@@ -35465,13 +35505,7 @@ var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { de
35465
35505
  * without recompiling react-doctor. Tests override via
35466
35506
  * `Layer.succeed(SupplyChainOverlapTimeoutMs, ...)`.
35467
35507
  */
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
- } }) {};
35508
+ var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS", SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS) }) {};
35475
35509
  /**
35476
35510
  * Hard cap on combined stdout+stderr bytes per oxlint batch. The
35477
35511
  * subprocess gets SIGKILL'd if it produces more; the recovery path
@@ -35619,6 +35653,7 @@ const STRING_ARRAY_FIELD_NAMES = [
35619
35653
  "projects",
35620
35654
  "textComponents",
35621
35655
  "rawTextWrapperComponents",
35656
+ "runtimeGlobals",
35622
35657
  "serverAuthFunctionNames"
35623
35658
  ];
35624
35659
  const SURFACE_CONTROL_FIELD_NAMES = [
@@ -37275,6 +37310,11 @@ const toCanonicalPath = (filePath) => {
37275
37310
  };
37276
37311
  const DEAD_CODE_PLUGIN = "deslop";
37277
37312
  const DEAD_CODE_CATEGORY = "Maintainability";
37313
+ const REACT_DOCTOR_TOOLCHAIN_PACKAGES = new Set([
37314
+ "react-doctor",
37315
+ "eslint-plugin-react-doctor",
37316
+ "oxlint-plugin-react-doctor"
37317
+ ]);
37278
37318
  const DEAD_CODE_WORKER_SCRIPT = `
37279
37319
  const inputChunks = [];
37280
37320
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -37580,6 +37620,7 @@ const checkDeadCode = async (options) => {
37580
37620
  });
37581
37621
  }
37582
37622
  for (const unusedDependency of result.unusedDependencies) {
37623
+ if (REACT_DOCTOR_TOOLCHAIN_PACKAGES.has(unusedDependency.name)) continue;
37583
37624
  const label = unusedDependency.isDevDependency ? "devDependency" : "dependency";
37584
37625
  diagnostics.push({
37585
37626
  filePath: "package.json",
@@ -37758,7 +37799,6 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
37758
37799
  return yield* tryPromise({
37759
37800
  try: (signal) => checkDeadCode({
37760
37801
  rootDirectory: input.rootDirectory,
37761
- userConfig: input.userConfig,
37762
37802
  parseConcurrency: input.parseConcurrency,
37763
37803
  workerTimeoutMs: input.workerTimeoutMs,
37764
37804
  abortSignal: signal
@@ -37881,6 +37921,11 @@ const isSafeGitRevision = (candidate) => {
37881
37921
  if (candidate.includes("..") || candidate.includes("@{")) return false;
37882
37922
  return SAFE_GIT_REVISION_PATTERN.test(candidate);
37883
37923
  };
37924
+ const resolveSpawnArgsLengthCap = () => {
37925
+ if (process.platform === "win32") return SPAWN_ARGS_MAX_LENGTH_CHARS;
37926
+ if (process.platform === "darwin") return SPAWN_ARGS_MAX_LENGTH_CHARS_DARWIN;
37927
+ return SPAWN_ARGS_MAX_LENGTH_CHARS_POSIX;
37928
+ };
37884
37929
  /**
37885
37930
  * Splits a git revision range into its endpoints: three-dot `A...B`
37886
37931
  * (symmetric, merge-base) or two-dot `A..B` (direct). Returns `null`
@@ -37965,7 +38010,8 @@ var Git = class Git extends Service()("react-doctor/Git") {
37965
38010
  return scoped(gen(function* () {
37966
38011
  if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
37967
38012
  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})`);
38013
+ const spawnArgsLengthCap = resolveSpawnArgsLengthCap();
38014
+ if (argvLengthChars > spawnArgsLengthCap) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${spawnArgsLengthCap})`);
37969
38015
  const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37970
38016
  cwd: input.directory,
37971
38017
  env: input.env,
@@ -38134,6 +38180,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38134
38180
  if (resolvedCurrentBranch === null && explicitBaseBranch === void 0) return null;
38135
38181
  const baseBranch = explicitBaseBranch ?? (yield* defaultBranch(directory));
38136
38182
  if (baseBranch === null) return null;
38183
+ if (!isSafeGitRevision(baseBranch)) return null;
38137
38184
  if (explicitBaseBranch !== void 0) {
38138
38185
  if (!(yield* branchExists(directory, explicitBaseBranch))) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchMissing({ branch: explicitBaseBranch }) }));
38139
38186
  }
@@ -38484,7 +38531,7 @@ const normalizeConfigForHash = (config) => {
38484
38531
  if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
38485
38532
  return clone;
38486
38533
  };
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");
38534
+ 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
38535
  /**
38489
38536
  * Loads a plugin module via the local require resolver and extracts
38490
38537
  * `(name, ruleNames)` from either `module.exports.meta + rules` or
@@ -38655,12 +38702,12 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
38655
38702
  if (customRulesOnly && registryEntry.originallyExternal) continue;
38656
38703
  if (rule.framework !== "global" && !rule.requires) continue;
38657
38704
  if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) continue;
38658
- const explicitSeverity = resolveRuleSeverityOverride({
38705
+ const explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey: registryEntry.key }, severityControls);
38706
+ if (rule.defaultEnabled === false && explicitRuleOverride === void 0) continue;
38707
+ const severity = resolveRuleSeverityOverride({
38659
38708
  ruleKey: registryEntry.key,
38660
38709
  category: rule.category
38661
- }, severityControls);
38662
- if (rule.defaultEnabled === false && explicitSeverity === void 0) continue;
38663
- const severity = explicitSeverity ?? resolveCompilerCleanupBucketSeverity(registryEntry.key, severityControls) ?? rule.severity;
38710
+ }, severityControls) ?? resolveCompilerCleanupBucketSeverity(registryEntry.key, severityControls) ?? rule.severity;
38664
38711
  if (severity === "off") continue;
38665
38712
  enabledReactDoctorRules[registryEntry.key] = severity;
38666
38713
  }
@@ -38762,24 +38809,38 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
38762
38809
  updatedAtMs: Date.now(),
38763
38810
  files: Object.fromEntries(cappedEntries)
38764
38811
  };
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
38812
  atomicWriteJson(cacheFilePath, {
38769
38813
  version: 1,
38770
- rulesets: prunedRulesets
38814
+ 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
38815
  });
38772
38816
  }
38773
38817
  };
38774
38818
  };
38775
38819
  const bundledRequire = createRequire(import.meta.url);
38820
+ const childNodeVersionByBinaryPath = /* @__PURE__ */ new Map();
38821
+ const resolveChildNodeVersion = (nodeBinaryPath) => {
38822
+ if (nodeBinaryPath === process.execPath) return process.version;
38823
+ const cachedVersion = childNodeVersionByBinaryPath.get(nodeBinaryPath);
38824
+ if (cachedVersion !== void 0) return cachedVersion;
38825
+ let version;
38826
+ try {
38827
+ version = execFileSync(nodeBinaryPath, ["--version"], {
38828
+ encoding: "utf8",
38829
+ timeout: NODE_VERSION_PROBE_TIMEOUT_MS
38830
+ }).trim();
38831
+ } catch {
38832
+ version = `unknown:${nodeBinaryPath}`;
38833
+ }
38834
+ childNodeVersionByBinaryPath.set(nodeBinaryPath, version);
38835
+ return version;
38836
+ };
38776
38837
  const TOOLCHAIN_PACKAGE_SPECIFIERS = [
38777
38838
  "oxlint/package.json",
38778
38839
  "oxlint-plugin-react-doctor/package.json",
38779
38840
  "eslint-plugin-react-hooks/package.json"
38780
38841
  ];
38781
- const resolveOxlintToolchainVersions = () => {
38782
- const versions = [`node=${process.version}`];
38842
+ const resolveOxlintToolchainVersions = (nodeBinaryPath = process.execPath) => {
38843
+ const versions = [`node=${resolveChildNodeVersion(nodeBinaryPath)}`];
38783
38844
  for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
38784
38845
  const packageJson = bundledRequire(specifier);
38785
38846
  const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
@@ -39313,11 +39374,12 @@ const REACT_COMPILER_TITLE = "React Compiler can't optimize this";
39313
39374
  const REACT_COMPILER_TODO_TITLE = "React Compiler doesn't support this syntax";
39314
39375
  const REACT_COMPILER_IMPACT = "This component misses React Compiler's automatic memoization & re-renders more than it should";
39315
39376
  const REACT_COMPILER_ACTION = "Rewrite the flagged code so the compiler can optimize it.";
39377
+ 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
39378
  const REACT_COMPILER_GENERIC_MESSAGE = `${REACT_COMPILER_IMPACT}. ${REACT_COMPILER_ACTION}`;
39317
- const buildReactCompilerMessage = (reasonSummary) => {
39379
+ const buildReactCompilerMessage = (reasonSummary, action = REACT_COMPILER_ACTION) => {
39318
39380
  const normalizedSummary = reasonSummary.replace(TRAILING_PERIOD_PATTERN, "");
39319
- if (!normalizedSummary) return REACT_COMPILER_GENERIC_MESSAGE;
39320
- return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${REACT_COMPILER_ACTION}`;
39381
+ if (!normalizedSummary) return `${REACT_COMPILER_IMPACT}. ${action}`;
39382
+ return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${action}`;
39321
39383
  };
39322
39384
  const PLUGIN_CATEGORY_MAP = {
39323
39385
  react: "Bugs",
@@ -39366,7 +39428,7 @@ const resolveCleanedDiagnostic = (message, help, plugin, rule, project) => {
39366
39428
  const [reasonSummary = "", ...reasonDetailLines] = bailoutReason.split("\n");
39367
39429
  const reasonDetail = reasonDetailLines.join("\n").trim();
39368
39430
  return {
39369
- message: buildReactCompilerMessage(reasonSummary.trim()),
39431
+ message: buildReactCompilerMessage(reasonSummary.trim(), rule === "incompatible-library" ? REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION : REACT_COMPILER_ACTION),
39370
39432
  help: appendReanimatedSharedValueHint(reasonDetail || help, rule, project)
39371
39433
  };
39372
39434
  }
@@ -39634,21 +39696,27 @@ const spawnLintBatches = async (input) => {
39634
39696
  const allDiagnostics = [];
39635
39697
  const droppedFiles = [];
39636
39698
  let firstDropReason = null;
39637
- const splitDeadlineMs = Date.now() + splitTotalBudgetMs;
39638
- const spawnLintBatch = async (batch, depth) => {
39699
+ const spawnLintBatch = async (batch, depth, splitBudget) => {
39639
39700
  const batchArgs = [...baseArgs, ...batch];
39640
39701
  try {
39641
39702
  return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
39642
39703
  } catch (error) {
39643
39704
  if (!isSplittableReactDoctorError(error)) throw error;
39644
- const splitBudgetExhausted = Date.now() >= splitDeadlineMs || depth >= splitMaxDepth;
39645
- if (batch.length <= 1 || splitBudgetExhausted) {
39705
+ splitBudget.deadlineMs ??= Date.now() + splitTotalBudgetMs;
39706
+ const isBudgetElapsed = Date.now() >= splitBudget.deadlineMs;
39707
+ const isDepthCapReached = depth >= splitMaxDepth;
39708
+ if (batch.length <= 1 || isBudgetElapsed || isDepthCapReached) {
39646
39709
  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;
39710
+ if (firstDropReason === null) {
39711
+ let limitHint = "";
39712
+ if (isDepthCapReached) limitHint = ` (split depth cap of ${splitMaxDepth} levels reached)`;
39713
+ else if (isBudgetElapsed) limitHint = ` (split budget of ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s exhausted at depth ${depth})`;
39714
+ firstDropReason = batch.length > 1 ? `${error.message}${limitHint}` : error.message;
39715
+ }
39648
39716
  return [];
39649
39717
  }
39650
39718
  const splitIndex = Math.ceil(batch.length / 2);
39651
- return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1)];
39719
+ return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1, splitBudget), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1, splitBudget)];
39652
39720
  }
39653
39721
  };
39654
39722
  let startedFileCount = 0;
@@ -39665,7 +39733,7 @@ const spawnLintBatches = async (input) => {
39665
39733
  try {
39666
39734
  const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
39667
39735
  startedFileCount += batch.length;
39668
- const batchDiagnostics = await spawnLintBatch(batch, 0);
39736
+ const batchDiagnostics = await spawnLintBatch(batch, 0, { deadlineMs: null });
39669
39737
  scannedFileCount += batch.length;
39670
39738
  if (onFileProgress) {
39671
39739
  displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
@@ -39739,7 +39807,14 @@ const resolveReactDoctorCacheDir = (projectDirectory) => {
39739
39807
  if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
39740
39808
  const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
39741
39809
  if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
39742
- return Path.join(os.tmpdir(), "react-doctor-cache", projectCacheSubdir(projectDirectory));
39810
+ return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
39811
+ };
39812
+ const resolveUserCacheScope = () => {
39813
+ try {
39814
+ return String(process.getuid?.() ?? os.userInfo().username);
39815
+ } catch {
39816
+ return "shared";
39817
+ }
39743
39818
  };
39744
39819
  const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
39745
39820
  /**
@@ -39907,7 +39982,7 @@ const runOxlint = async (options) => {
39907
39982
  extendsPaths: [],
39908
39983
  ruleSelection: "cacheable"
39909
39984
  }),
39910
- toolchainVersions: resolveOxlintToolchainVersions(),
39985
+ toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
39911
39986
  ignorePatterns: combinedPatterns,
39912
39987
  tsconfigContent
39913
39988
  });
@@ -39921,7 +39996,7 @@ const runOxlint = async (options) => {
39921
39996
  missFiles.push(candidateFile);
39922
39997
  continue;
39923
39998
  }
39924
- const cacheKey = `${candidateFile.replaceAll("\\", "/")}${contentHash}`;
39999
+ const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
39925
40000
  cacheKeyByFile.set(candidateFile, cacheKey);
39926
40001
  const cachedDiagnostics = cache.lookup(cacheKey);
39927
40002
  if (cachedDiagnostics === null) missFiles.push(candidateFile);
@@ -40237,7 +40312,7 @@ const ScoreApiResponseSchema = Struct({
40237
40312
  })))
40238
40313
  });
40239
40314
  const parseScoreResult = (value) => getOrNull(decodeUnknownOption(ScoreApiResponseSchema)(value));
40240
- const stripLocalFileFields = (diagnostics) => diagnostics.map(({ filePath: _filePath, fileContext: _fileContext, ...rest }) => rest);
40315
+ const stripLocalFileFields = (diagnostics) => diagnostics.map(({ filePath: _filePath, fileContext: _fileContext, fixGroupId: _fixGroupId, ...rest }) => rest);
40241
40316
  const isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
40242
40317
  const describeFailure = (error) => {
40243
40318
  if (isAbortError(error)) return `timed out after ${FETCH_TIMEOUT_MS / 1e3}s`;
@@ -40541,7 +40616,10 @@ const ALERT_SEVERITY_RANK = {
40541
40616
  medium: 2,
40542
40617
  low: 1
40543
40618
  };
40544
- const severityRank = (severity) => ALERT_SEVERITY_RANK[severity.toLowerCase()] ?? 0;
40619
+ const severityRank = (severity) => {
40620
+ const normalized = severity.toLowerCase();
40621
+ return Object.hasOwn(ALERT_SEVERITY_RANK, normalized) ? ALERT_SEVERITY_RANK[normalized] : 0;
40622
+ };
40545
40623
  const displaySeverity = (severity) => {
40546
40624
  const normalized = sanitizeTerminalText(severity.toLowerCase());
40547
40625
  return normalized === "middle" ? "medium" : normalized;
@@ -40558,7 +40636,7 @@ const ALERT_TYPE_LABELS = {
40558
40636
  typosquat: "possible typosquat"
40559
40637
  };
40560
40638
  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));
40639
+ const friendlyAlertType = (type) => Object.hasOwn(ALERT_TYPE_LABELS, type) ? ALERT_TYPE_LABELS[type] : sanitizeTerminalText(humanizeAlertType(type));
40562
40640
  const summarizeAlertNote = (note) => {
40563
40641
  const collapsed = sanitizeTerminalText(note.replace(/\s+/g, " ").trim());
40564
40642
  const firstSentence = collapsed.split(/(?<=\.)\s/)[0] || collapsed;
@@ -40653,7 +40731,10 @@ const checkSupplyChain = (input) => gen(function* () {
40653
40731
  * outage.
40654
40732
  */
40655
40733
  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"))) }));
40734
+ static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(flatMap$2(SupplyChainOverlapTimeoutMs, (totalTimeoutMs) => checkSupplyChain({
40735
+ ...input,
40736
+ totalTimeoutMs
40737
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
40657
40738
  static layerOf = (diagnostics) => succeed$3(SupplyChain, SupplyChain.of({ run: () => fromIterable$1(diagnostics) }));
40658
40739
  };
40659
40740
  const getObjectProperty = (value, propertyName) => {
@@ -40801,10 +40882,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40801
40882
  ...checkExpoProject(scanDirectory, project),
40802
40883
  ...checkReactNativeProject(scanDirectory, project)
40803
40884
  ])));
40804
- const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(promise(() => checkSecurityScanCooperative(scanDirectory, {
40885
+ const securityScanFailedRef = yield* make$13(false);
40886
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise(() => checkSecurityScanCooperative(scanDirectory, {
40805
40887
  project,
40806
40888
  ignoredTags: input.ignoredTags
40807
- })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)))))).pipe(withSpan("SecurityScan.run")));
40889
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), catch_$1(() => set(securityScanFailedRef, true).pipe(as(empty$4))))))).pipe(withSpan("SecurityScan.run")));
40808
40890
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
40809
40891
  const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
40810
40892
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
@@ -40842,7 +40924,6 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40842
40924
  const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
40843
40925
  const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
40844
40926
  rootDirectory: scanDirectory,
40845
- userConfig: resolvedConfig.config,
40846
40927
  parseConcurrency: deadCodeParseConcurrency,
40847
40928
  workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
40848
40929
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
@@ -40970,6 +41051,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40970
41051
  metadata: scoreMetadata
40971
41052
  });
40972
41053
  const lintPartialFailures = yield* get$2(partialFailuresRef);
41054
+ const securityScanFailed = yield* get$2(securityScanFailedRef);
40973
41055
  return {
40974
41056
  project,
40975
41057
  userConfig: resolvedConfig.config,
@@ -40990,8 +41072,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40990
41072
  scanElapsedMilliseconds,
40991
41073
  scanConcurrency,
40992
41074
  supplyChainOverlapTimedOut: supplyChainResult.timedOut,
41075
+ securityScanFailed,
40993
41076
  lintCacheHitFileCount,
40994
- lintCacheTotalFileCount
41077
+ lintCacheTotalFileCount,
41078
+ suppressedRuleCounts: transform.summarizeSuppressions()
40995
41079
  };
40996
41080
  }).pipe(withSpan("runInspect", { attributes: {
40997
41081
  "inspect.directory": input.directory,
@@ -43029,6 +43113,8 @@ const METRIC = {
43029
43113
  scanCheckSkipped: "scan.check_skipped",
43030
43114
  baselineDegraded: "baseline.degraded",
43031
43115
  ruleFired: "rule.fired",
43116
+ ruleDisabled: "rule.disabled",
43117
+ ruleSuppressed: "rule.suppressed",
43032
43118
  lintFailed: "lint.failed",
43033
43119
  deadCodeFailed: "deadcode.failed",
43034
43120
  scoreUnavailable: "score.unavailable",
@@ -43041,13 +43127,17 @@ const METRIC = {
43041
43127
  installWorkflow: "install.workflow",
43042
43128
  installAgentHooks: "install.agent_hooks",
43043
43129
  installDependency: "install.dependency",
43130
+ ciScaffolded: "ci.scaffolded",
43131
+ ciUpgraded: "ci.upgraded",
43132
+ ciConfigured: "ci.configured",
43044
43133
  rulesChanged: "rules.changed",
43045
43134
  rulesQueried: "rules.queried",
43046
43135
  lspSessionStarted: "lsp.session.started",
43047
43136
  lspScanCompleted: "lsp.scan.completed",
43048
43137
  lspScanDuration: "lsp.scan.duration",
43049
43138
  lspScanDiagnostics: "lsp.scan.diagnostics",
43050
- aiTrainingWarningShown: "ai.training.warning_shown"
43139
+ aiTrainingWarningShown: "ai.training.warning_shown",
43140
+ jsonOutUsed: "json.out_used"
43051
43141
  };
43052
43142
  //#endregion
43053
43143
  //#region src/cli/utils/to-category-key.ts
@@ -43364,5 +43454,5 @@ const startLanguageServer = () => {
43364
43454
  };
43365
43455
  //#endregion
43366
43456
  export { startLanguageServer };
43367
- !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]="b3c6b2e5-253e-5be2-aacb-bd57bbb3e5e6")}catch(e){}}();
43368
- //# debugId=b3c6b2e5-253e-5be2-aacb-bd57bbb3e5e6
43457
+ !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]="f7d16df6-f4da-5a95-ab64-acd6d3a0f5ca")}catch(e){}}();
43458
+ //# debugId=f7d16df6-f4da-5a95-ab64-acd6d3a0f5ca
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.5.8-dev.849580c",
3
+ "version": "0.5.8-dev.88a5c3c",
4
4
  "description": "Your agent writes bad React. This catches it",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -60,12 +60,13 @@
60
60
  "magicast": "^0.5.3",
61
61
  "oxlint": ">=1.66.0 <1.67.0",
62
62
  "prompts": "^2.4.2",
63
- "typescript": ">=5.0.4 <7",
63
+ "typescript": ">=5.0.4 <6",
64
64
  "vscode-languageserver": "^9.0.1",
65
65
  "vscode-languageserver-textdocument": "^1.0.12",
66
66
  "vscode-uri": "^3.1.0",
67
+ "yaml": "^2.9.0",
67
68
  "deslop-js": "0.5.8",
68
- "oxlint-plugin-react-doctor": "0.5.8-dev.849580c"
69
+ "oxlint-plugin-react-doctor": "0.5.8-dev.88a5c3c"
69
70
  },
70
71
  "devDependencies": {
71
72
  "@types/babel__code-frame": "^7.27.0",
@@ -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"