react-doctor 0.7.6-dev.1f14fa1 → 0.7.6-dev.3f22fe3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="9a5c1a3c-7706-5a23-bca4-ddddd2f085af")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="4c1e681e-4dee-5ffd-a4ba-69b876f47147")}catch(e){}}();
3
3
  import { createRequire } from "node:module";
4
4
  import * as NodeChildProcess from "node:child_process";
5
5
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
@@ -39594,6 +39594,49 @@ const assignFixGroups = (diagnostics) => {
39594
39594
  };
39595
39595
  });
39596
39596
  };
39597
+ const DERIVED_STATE_RULE_PRIORITY = new Map([
39598
+ ["no-initialize-state", 0],
39599
+ ["no-adjust-state-on-prop-change", 1],
39600
+ ["no-derived-state", 2],
39601
+ ["no-derived-state-effect", 3]
39602
+ ]);
39603
+ const buildDiagnosticSiteKey = (diagnostic) => `${diagnostic.filePath}\u0000${diagnostic.line}\u0000${diagnostic.column}`;
39604
+ const isReactDoctorRulesOfHooksDiagnostic = (diagnostic) => diagnostic.plugin === "react-doctor" && diagnostic.rule === "rules-of-hooks";
39605
+ const isReactCompilerHooksDiagnostic = (diagnostic) => diagnostic.plugin === "react-hooks-js" && diagnostic.rule === "hooks";
39606
+ const doDiagnosticSpansOverlap = (first, second) => {
39607
+ if (first.filePath !== second.filePath || first.plugin !== second.plugin) return false;
39608
+ if (first.offset !== void 0 && second.offset !== void 0) {
39609
+ const firstEnd = first.offset + Math.max(first.length ?? 1, 1);
39610
+ const secondEnd = second.offset + Math.max(second.length ?? 1, 1);
39611
+ return first.offset < secondEnd && second.offset < firstEnd;
39612
+ }
39613
+ const firstEndLine = first.endLine ?? first.line;
39614
+ const secondEndLine = second.endLine ?? second.line;
39615
+ return first.line <= secondEndLine && second.line <= firstEndLine;
39616
+ };
39617
+ const keepHighestSeverity = (survivingDiagnostic, collapsedDiagnostic) => collapsedDiagnostic.severity === "error" && survivingDiagnostic.severity !== "error" ? {
39618
+ ...survivingDiagnostic,
39619
+ severity: "error"
39620
+ } : survivingDiagnostic;
39621
+ const dedupeRelatedDiagnostics = (diagnostics) => {
39622
+ const reactDoctorHookSites = new Set(diagnostics.filter(isReactDoctorRulesOfHooksDiagnostic).map(buildDiagnosticSiteKey));
39623
+ const uniqueDiagnostics = [];
39624
+ for (const diagnostic of diagnostics) {
39625
+ if (isReactCompilerHooksDiagnostic(diagnostic) && reactDoctorHookSites.has(buildDiagnosticSiteKey(diagnostic))) continue;
39626
+ const priority = DERIVED_STATE_RULE_PRIORITY.get(diagnostic.rule);
39627
+ if (priority !== void 0) {
39628
+ const overlappingDiagnosticIndex = uniqueDiagnostics.findIndex((candidate) => candidate.rule !== diagnostic.rule && DERIVED_STATE_RULE_PRIORITY.has(candidate.rule) && doDiagnosticSpansOverlap(candidate, diagnostic));
39629
+ if (overlappingDiagnosticIndex >= 0) {
39630
+ const existingDiagnostic = uniqueDiagnostics[overlappingDiagnosticIndex];
39631
+ const existingPriority = existingDiagnostic ? DERIVED_STATE_RULE_PRIORITY.get(existingDiagnostic.rule) : void 0;
39632
+ if (existingDiagnostic && existingPriority !== void 0) uniqueDiagnostics[overlappingDiagnosticIndex] = priority < existingPriority ? keepHighestSeverity(diagnostic, existingDiagnostic) : keepHighestSeverity(existingDiagnostic, diagnostic);
39633
+ continue;
39634
+ }
39635
+ }
39636
+ uniqueDiagnostics.push(diagnostic);
39637
+ }
39638
+ return uniqueDiagnostics;
39639
+ };
39597
39640
  const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
39598
39641
  const sortDiagnosticsStable = (diagnostics) => [...diagnostics].sort((left, right) => compareStrings(left.filePath, right.filePath) || left.line - right.line || left.column - right.column || compareStrings(left.plugin, right.plugin) || compareStrings(left.rule, right.rule) || compareStrings(left.severity, right.severity) || compareStrings(left.message, right.message));
39599
39642
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
@@ -41823,6 +41866,7 @@ const parseGithubViewerPermission = (stdout) => {
41823
41866
  return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
41824
41867
  };
41825
41868
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
41869
+ const UNTRACKED_FILE_LAST_LINE = Number.MAX_SAFE_INTEGER;
41826
41870
  /**
41827
41871
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
41828
41872
  * behind a `Context.Service`. The production layer (`layerNode`)
@@ -41893,6 +41937,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
41893
41937
  args,
41894
41938
  directory
41895
41939
  });
41940
+ const listUntrackedFilePaths = (directory, includePaths = []) => runGit(directory, [
41941
+ "ls-files",
41942
+ "-z",
41943
+ "--others",
41944
+ "--exclude-standard",
41945
+ ...includePaths.length > 0 ? ["--", ...includePaths] : []
41946
+ ]).pipe(map$3((result) => result.status === 0 ? splitNullSeparated(result.stdout) : null));
41947
+ const mergeUntracked = (directory, trackedFilePaths, includeUntracked) => includeUntracked ? listUntrackedFilePaths(directory).pipe(map$3((untracked) => untracked === null ? trackedFilePaths : [...new Set([...trackedFilePaths, ...untracked])])) : succeed$2(trackedFilePaths);
41896
41948
  const currentBranch = (directory) => runGit(directory, [
41897
41949
  "rev-parse",
41898
41950
  "--abbrev-ref",
@@ -42015,7 +42067,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42015
42067
  githubViewerPermission,
42016
42068
  branchExists,
42017
42069
  mergeBase,
42018
- diffSelection: ({ directory, explicitBaseBranch }) => gen(function* () {
42070
+ diffSelection: ({ directory, explicitBaseBranch, includeUntracked = false }) => gen(function* () {
42019
42071
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
42020
42072
  if (explicitBaseBranch !== void 0) {
42021
42073
  const range = parseGitDiffRange(explicitBaseBranch);
@@ -42044,7 +42096,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42044
42096
  "HEAD"
42045
42097
  ]);
42046
42098
  if (uncommitted.status !== 0) return null;
42047
- const files = splitNullSeparated(uncommitted.stdout);
42099
+ const files = yield* mergeUntracked(directory, splitNullSeparated(uncommitted.stdout), includeUntracked);
42048
42100
  if (files.length === 0) return null;
42049
42101
  return {
42050
42102
  currentBranch: resolvedCurrentBranch,
@@ -42074,7 +42126,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42074
42126
  currentBranch: resolvedCurrentBranch,
42075
42127
  baseBranch,
42076
42128
  diffBaseRef: mergeBaseRef,
42077
- changedFiles: splitNullSeparated(diff.stdout),
42129
+ changedFiles: yield* mergeUntracked(directory, splitNullSeparated(diff.stdout), includeUntracked),
42078
42130
  isCurrentChanges: false
42079
42131
  };
42080
42132
  }).pipe(withSpan("Git.diffSelection")),
@@ -42120,7 +42172,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42120
42172
  stdout: result.stdout
42121
42173
  };
42122
42174
  }).pipe(withSpan("Git.grep")),
42123
- changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
42175
+ changedLineRanges: ({ directory, baseRef, cached, files, includeUntracked = false }) => gen(function* () {
42124
42176
  if (files.length === 0) return [];
42125
42177
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
42126
42178
  const result = yield* runGit(directory, [
@@ -42134,7 +42186,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
42134
42186
  ...files
42135
42187
  ]);
42136
42188
  if (result.status !== 0) return null;
42137
- return parseChangedLineRanges(result.stdout);
42189
+ const changedLineRanges = parseChangedLineRanges(result.stdout);
42190
+ if (cached || !includeUntracked) return changedLineRanges;
42191
+ const untrackedFilePaths = yield* listUntrackedFilePaths(directory, files);
42192
+ if (untrackedFilePaths === null) return changedLineRanges;
42193
+ return [...changedLineRanges, ...untrackedFilePaths.map((file) => ({
42194
+ file,
42195
+ ranges: [[1, UNTRACKED_FILE_LAST_LINE]]
42196
+ }))];
42138
42197
  }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
42139
42198
  });
42140
42199
  })).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
@@ -42719,13 +42778,13 @@ const encodeSidecarFileEntry = (entry) => ({
42719
42778
  diagnostics: entry.diagnostics
42720
42779
  });
42721
42780
  const emptyCache = () => ({
42722
- version: 1,
42781
+ version: 2,
42723
42782
  rulesets: {}
42724
42783
  });
42725
42784
  const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
42726
42785
  const entries = /* @__PURE__ */ new Map();
42727
42786
  const persisted = failOpenReadJson(cacheFilePath, emptyCache());
42728
- if (persisted.version !== 1 || !isRecord$1(persisted.rulesets)) return entries;
42787
+ if (persisted.version !== 2 || !isRecord$1(persisted.rulesets)) return entries;
42729
42788
  const bucket = persisted.rulesets[rulesetHash];
42730
42789
  if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
42731
42790
  for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
@@ -42745,7 +42804,7 @@ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
42745
42804
  },
42746
42805
  persist: () => {
42747
42806
  const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
42748
- const rulesets = onDisk.version === 1 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
42807
+ const rulesets = onDisk.version === 2 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
42749
42808
  const existingBucket = rulesets[rulesetHash];
42750
42809
  const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
42751
42810
  const ourFiles = {};
@@ -42759,7 +42818,7 @@ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
42759
42818
  files: Object.fromEntries(cappedEntries)
42760
42819
  };
42761
42820
  atomicWriteJson(cacheFilePath, {
42762
- version: 1,
42821
+ version: 2,
42763
42822
  rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord$1(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
42764
42823
  });
42765
42824
  }
@@ -42810,23 +42869,6 @@ const resolveTsConfigRelativePath = (rootDirectory) => {
42810
42869
  for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
42811
42870
  return null;
42812
42871
  };
42813
- const DERIVED_STATE_RULE_PRIORITY = new Map([
42814
- ["no-initialize-state", 0],
42815
- ["no-adjust-state-on-prop-change", 1],
42816
- ["no-derived-state", 2],
42817
- ["no-derived-state-effect", 3]
42818
- ]);
42819
- const doDiagnosticSpansOverlap = (first, second) => {
42820
- if (first.filePath !== second.filePath || first.plugin !== second.plugin) return false;
42821
- if (first.offset !== void 0 && second.offset !== void 0) {
42822
- const firstEnd = first.offset + Math.max(first.length ?? 1, 1);
42823
- const secondEnd = second.offset + Math.max(second.length ?? 1, 1);
42824
- return first.offset < secondEnd && second.offset < firstEnd;
42825
- }
42826
- const firstEndLine = first.endLine ?? first.line;
42827
- const secondEndLine = second.endLine ?? second.line;
42828
- return first.line <= secondEndLine && second.line <= firstEndLine;
42829
- };
42830
42872
  const dedupeDiagnostics = (diagnostics) => {
42831
42873
  const seenKeys = /* @__PURE__ */ new Set();
42832
42874
  const uniqueDiagnostics = [];
@@ -42834,16 +42876,6 @@ const dedupeDiagnostics = (diagnostics) => {
42834
42876
  const key = `${diagnostic.filePath}\u0000${diagnostic.line}\u0000${diagnostic.column}\u0000${diagnostic.plugin}\u0000${diagnostic.rule}\u0000${diagnostic.severity}\u0000${diagnostic.message}`;
42835
42877
  if (seenKeys.has(key)) continue;
42836
42878
  seenKeys.add(key);
42837
- const priority = DERIVED_STATE_RULE_PRIORITY.get(diagnostic.rule);
42838
- if (priority !== void 0) {
42839
- const overlappingDiagnosticIndex = uniqueDiagnostics.findIndex((candidate) => candidate.rule !== diagnostic.rule && candidate.severity === diagnostic.severity && DERIVED_STATE_RULE_PRIORITY.has(candidate.rule) && doDiagnosticSpansOverlap(candidate, diagnostic));
42840
- if (overlappingDiagnosticIndex >= 0) {
42841
- const existingDiagnostic = uniqueDiagnostics[overlappingDiagnosticIndex];
42842
- const existingPriority = existingDiagnostic ? DERIVED_STATE_RULE_PRIORITY.get(existingDiagnostic.rule) : void 0;
42843
- if (existingPriority !== void 0 && priority < existingPriority) uniqueDiagnostics[overlappingDiagnosticIndex] = diagnostic;
42844
- continue;
42845
- }
42846
- }
42847
42879
  uniqueDiagnostics.push(diagnostic);
42848
42880
  }
42849
42881
  return uniqueDiagnostics;
@@ -43643,11 +43675,11 @@ const buildRelatedLocations = (labels, filePath) => {
43643
43675
  }
43644
43676
  return related;
43645
43677
  };
43646
- const isOxlintOutput = (value) => {
43647
- if (typeof value !== "object" || value === null) return false;
43648
- const candidate = value;
43649
- return Array.isArray(candidate.diagnostics);
43650
- };
43678
+ const isOxlintSpan = (value) => isRecord$1(value) && typeof value.offset === "number" && typeof value.length === "number" && typeof value.line === "number" && typeof value.column === "number";
43679
+ const isOxlintLabel = (value) => isRecord$1(value) && isOxlintSpan(value.span);
43680
+ const isMappableOxlintDiagnostic = (value) => isRecord$1(value) && typeof value.code === "string" && value.code.length > 0 && typeof value.filename === "string" && value.filename.length > 0 && (value.severity === "warning" || value.severity === "error") && Array.isArray(value.labels) && value.labels.every(isOxlintLabel);
43681
+ const isEngineFailureDiagnostic = (value) => !isRecord$1(value) || typeof value.filename !== "string" || value.filename.length === 0;
43682
+ const isOxlintOutput = (value) => isRecord$1(value) && Array.isArray(value.diagnostics);
43651
43683
  /**
43652
43684
  * Parses one oxlint subprocess's stdout into a flat `Diagnostic[]`.
43653
43685
  * Tolerates the leading-notice-line shape oxlint sometimes prints
@@ -43666,6 +43698,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43666
43698
  throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
43667
43699
  }
43668
43700
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
43701
+ const engineFailureDiagnostic = parsed.diagnostics.find(isEngineFailureDiagnostic);
43702
+ if (engineFailureDiagnostic !== void 0) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: JSON.stringify(engineFailureDiagnostic).slice(0, 600) }) });
43669
43703
  const minifiedFileCache = /* @__PURE__ */ new Map();
43670
43704
  const sourceBufferCache = /* @__PURE__ */ new Map();
43671
43705
  const sourceLinesCache = /* @__PURE__ */ new Map();
@@ -43700,7 +43734,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43700
43734
  minifiedFileCache.set(absolutePath, minified);
43701
43735
  return minified;
43702
43736
  };
43703
- return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
43737
+ return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => isMappableOxlintDiagnostic(diagnostic) && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
43704
43738
  const { plugin, rule } = parseRuleCode(diagnostic.code);
43705
43739
  const primaryLabel = diagnostic.labels[0];
43706
43740
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -45535,7 +45569,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
45535
45569
  respectInlineDisables: input.respectInlineDisables,
45536
45570
  showWarnings
45537
45571
  });
45538
- const applyPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(filterMapNullable(transform.apply)), tap((diagnostic) => reporterService.emit(diagnostic)));
45572
+ const filterPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(filterMapNullable(transform.apply)));
45573
+ const applyPerElementPipeline = (rawStream) => filterPerElementPipeline(rawStream).pipe(tap((diagnostic) => reporterService.emit(diagnostic)));
45539
45574
  const envCollected = yield* runCollect(applyPerElementPipeline(fromIterable$1(isDiffMode ? [] : [
45540
45575
  ...checkReducedMotion(scanDirectory),
45541
45576
  ...checkPnpmHardening(scanDirectory),
@@ -45664,7 +45699,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
45664
45699
  });
45665
45700
  return empty$4;
45666
45701
  }))));
45667
- const lintCollected = yield* runCollect(applyPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
45702
+ const lintCollected = dedupeRelatedDiagnostics(yield* runCollect(filterPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
45668
45703
  onNone: () => set(lintFailure, {
45669
45704
  didFail: true,
45670
45705
  reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
@@ -45672,7 +45707,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
45672
45707
  reasonKind: null
45673
45708
  }).pipe(as([])),
45674
45709
  onSome: succeed$2
45675
- })));
45710
+ }))));
45711
+ yield* forEach$1(lintCollected, reporterService.emit, { discard: true });
45676
45712
  const lintFailureState = yield* get$2(lintFailure);
45677
45713
  yield* afterLint(lintFailureState.didFail);
45678
45714
  if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
@@ -46266,10 +46302,11 @@ const computeDiagnosticDelta = (input) => {
46266
46302
  * render diff-base mistakes (`GitBaseBranchInvalid` /
46267
46303
  * `GitBaseBranchMissing`) as clean user errors instead of crashes.
46268
46304
  */
46269
- const getDiffInfo = (directory, explicitBaseBranch) => runPromise(gen(function* () {
46305
+ const getDiffInfo = (directory, explicitBaseBranch, includeUntracked) => runPromise(gen(function* () {
46270
46306
  const selection = yield* (yield* Git).diffSelection({
46271
46307
  directory,
46272
- explicitBaseBranch
46308
+ explicitBaseBranch,
46309
+ includeUntracked
46273
46310
  });
46274
46311
  if (selection === null) return null;
46275
46312
  return {
@@ -46605,6 +46642,43 @@ const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.
46605
46642
  //#region src/cli/utils/constants.ts
46606
46643
  const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
46607
46644
  const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
46645
+ const STAGED_SNAPSHOT_ADDITIONAL_CONFIG_FILENAMES = [
46646
+ ".babelrc",
46647
+ ".babelrc.json",
46648
+ ".eslintignore",
46649
+ ".eslintrc.json",
46650
+ ".oxlintignore",
46651
+ "app.config.cjs",
46652
+ "app.config.js",
46653
+ "app.config.json",
46654
+ "app.config.mjs",
46655
+ "app.config.ts",
46656
+ "app.json",
46657
+ "babel.config.cjs",
46658
+ "babel.config.js",
46659
+ "babel.config.json",
46660
+ "babel.config.mjs",
46661
+ "eslint.config.cjs",
46662
+ "eslint.config.cts",
46663
+ "eslint.config.js",
46664
+ "eslint.config.mjs",
46665
+ "eslint.config.mts",
46666
+ "eslint.config.ts",
46667
+ "next.config.cjs",
46668
+ "next.config.js",
46669
+ "next.config.mjs",
46670
+ "next.config.ts",
46671
+ "oxlint.config.json",
46672
+ "react-doctor.config.json",
46673
+ "vite.config.cjs",
46674
+ "vite.config.cts",
46675
+ "vite.config.js",
46676
+ "vite.config.mjs",
46677
+ "vite.config.mts",
46678
+ "vite.config.ts",
46679
+ "vitest.config.js",
46680
+ "vitest.config.ts"
46681
+ ];
46608
46682
  const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
46609
46683
  const SCAN_RESULT_CACHE_FILENAME = "scan-cache.json";
46610
46684
  const RUN_GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
@@ -46619,6 +46693,7 @@ const METRIC = {
46619
46693
  cliInvoked: "cli.invoked",
46620
46694
  cliError: "cli.error",
46621
46695
  cliEnvironmentError: "cli.env_error",
46696
+ stagedSnapshotDivergence: "staged.snapshot_divergence",
46622
46697
  projectDetected: "project.detected",
46623
46698
  projectPathSelected: "project.path_selected",
46624
46699
  projectConfigSelected: "project.config_selected",
@@ -46699,7 +46774,7 @@ const makeNoopConsole = () => ({
46699
46774
  });
46700
46775
  //#endregion
46701
46776
  //#region src/cli/utils/version.ts
46702
- const VERSION = "0.7.6-dev.1f14fa1";
46777
+ const VERSION = "0.7.6-dev.3f22fe3";
46703
46778
  //#endregion
46704
46779
  //#region src/cli/utils/json-mode.ts
46705
46780
  let context = null;
@@ -47068,13 +47143,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
47068
47143
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
47069
47144
  * standard `SENTRY_RELEASE` override.
47070
47145
  */
47071
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.6-dev.1f14fa1`;
47146
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.6-dev.3f22fe3`;
47072
47147
  /**
47073
47148
  * Deployment environment shown in Sentry's environment filter. Defaults to
47074
47149
  * `production` for tagged releases and `development` for dev/unbuilt versions,
47075
47150
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
47076
47151
  */
47077
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.6-dev.1f14fa1") ? "development" : "production");
47152
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.6-dev.3f22fe3") ? "development" : "production");
47078
47153
  /**
47079
47154
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
47080
47155
  * (set to `0` to disable tracing) and falls back to
@@ -47266,7 +47341,7 @@ const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
47266
47341
  "fi"
47267
47342
  ].join(" ");
47268
47343
  const PACKAGE_JSON_FILE_NAME = "package.json";
47269
- const runGit = (projectRoot, args) => {
47344
+ const runGitRaw = (projectRoot, args) => {
47270
47345
  try {
47271
47346
  return execFileSync("git", [...args], {
47272
47347
  cwd: projectRoot,
@@ -47277,11 +47352,12 @@ const runGit = (projectRoot, args) => {
47277
47352
  "ignore"
47278
47353
  ],
47279
47354
  maxBuffer: RUN_GIT_MAX_BUFFER_BYTES
47280
- }).trim();
47355
+ });
47281
47356
  } catch {
47282
47357
  return null;
47283
47358
  }
47284
47359
  };
47360
+ const runGit = (projectRoot, args) => runGitRaw(projectRoot, args)?.trim() ?? null;
47285
47361
  const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
47286
47362
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
47287
47363
  const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
@@ -59152,6 +59228,17 @@ const buildRuleBlastRadii = (diagnostics) => buildSortedRuleGroups(diagnostics).
59152
59228
  })).toSorted((left, right) => right.fileCount - left.fileCount);
59153
59229
  const findMigrationScaleBuckets = (diagnostics) => buildRuleBlastRadii(diagnostics).filter((bucket) => bucket.fileCount >= 40);
59154
59230
  //#endregion
59231
+ //#region src/cli/utils/has-lint-hard-failure.ts
59232
+ const hasLintHardFailure = (result) => result.skippedChecks.includes("lint");
59233
+ //#endregion
59234
+ //#region src/cli/utils/is-inspect-result-complete.ts
59235
+ const isInspectResultComplete = (result) => isScanComplete({
59236
+ analyzedFileCount: result.analyzedFiles?.length,
59237
+ scannedFileCount: result.scannedFileCount,
59238
+ skippedCheckCount: result.skippedChecks.length,
59239
+ skippedCheckReasonCount: Object.keys(result.skippedCheckReasons ?? {}).length
59240
+ });
59241
+ //#endregion
59155
59242
  //#region src/cli/utils/scan-result-cache.ts
59156
59243
  const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
59157
59244
  const TOOLCHAIN_PACKAGE_SPECIFIERS = [
@@ -59557,15 +59644,12 @@ const buildOutcomeAttributes = (input) => {
59557
59644
  const summary = summarizeDiagnostics(result.diagnostics);
59558
59645
  const blockingLevel = resolveTelemetryBlocking(input.userConfig);
59559
59646
  const gateDiagnostics = filterDiagnosticsForSurface(result.diagnostics, "ciFailure", input.userConfig);
59647
+ const complete = isInspectResultComplete(result);
59648
+ const didLintHardFail = hasLintHardFailure(result);
59649
+ const failsOnHardFailure = didLintHardFail && blockingLevel !== "none";
59560
59650
  const wouldBlock = !input.scoreOnly && !input.gateExempt && shouldBlockCi(gateDiagnostics, blockingLevel);
59561
- const complete = isScanComplete({
59562
- analyzedFileCount: result.analyzedFiles?.length,
59563
- scannedFileCount: result.scannedFileCount,
59564
- skippedCheckCount: result.skippedChecks.length,
59565
- skippedCheckReasonCount: Object.keys(result.skippedCheckReasons ?? {}).length
59566
- });
59567
59651
  const isClean = result.diagnostics.length === 0 && complete;
59568
- const outcome = wouldBlock ? "blocked" : isClean ? "clean" : "ok";
59652
+ const outcome = didLintHardFail ? "error" : wouldBlock ? "blocked" : isClean ? "clean" : "ok";
59569
59653
  const firings = summarizeRuleFirings(result.diagnostics);
59570
59654
  const countByRule = /* @__PURE__ */ new Map();
59571
59655
  const countByCategory = /* @__PURE__ */ new Map();
@@ -59610,7 +59694,7 @@ const buildOutcomeAttributes = (input) => {
59610
59694
  const attributes = {
59611
59695
  ...withNamespace("outcome", {
59612
59696
  status: outcome,
59613
- exitCode: wouldBlock ? 1 : 0,
59697
+ exitCode: failsOnHardFailure || wouldBlock ? 1 : 0,
59614
59698
  wouldBlock,
59615
59699
  blocking: blockingLevel,
59616
59700
  clean: isClean,
@@ -63792,10 +63876,15 @@ const warnIfAiTrainingEnvironment = () => {
63792
63876
  //#region src/cli/utils/validate-mode-flags.ts
63793
63877
  const usedDiffAlias = (flags) => flags.diff !== void 0 && flags.diff !== false && flags.diff !== "false" && flags.diff !== "";
63794
63878
  const usedScope = (flags) => typeof flags.scope === "string" && flags.scope.length > 0;
63879
+ const validateIncludeUntrackedScope = (includeUntracked, scope) => {
63880
+ if (!includeUntracked || scope !== void 0 && scope !== "full") return;
63881
+ throw new CliInputError("--include-untracked requires the files, changed, or lines scope (via --scope, --diff, or config).");
63882
+ };
63795
63883
  const validateModeFlags = (flags) => {
63796
63884
  if (usedScope(flags) && usedDiffAlias(flags)) throw new CliInputError("Cannot combine --scope and --diff; --diff is the deprecated alias.");
63797
63885
  if (flags.staged && usedDiffAlias(flags)) throw new CliInputError("Cannot combine --staged and --diff; pick one mode.");
63798
63886
  if (flags.staged && (flags.scope === "full" || flags.scope === "changed")) throw new CliInputError(`Cannot combine --staged with --scope ${flags.scope}; use --scope files or --scope lines, or drop --scope.`);
63887
+ if (flags.includeUntracked && flags.staged) throw new CliInputError("Cannot combine --include-untracked with --staged; the git index never holds untracked files.");
63799
63888
  if (flags.score && flags.json) throw new CliInputError("Cannot combine --score and --json; pick one output mode.");
63800
63889
  if (flags.score && flags.telemetry === false) throw new CliInputError("Cannot combine --score with --no-telemetry; --score prints the score that --no-telemetry disables.");
63801
63890
  if (flags.debug && (flags.score === false || flags.telemetry === false)) {
@@ -63804,6 +63893,39 @@ const validateModeFlags = (flags) => {
63804
63893
  }
63805
63894
  };
63806
63895
  //#endregion
63896
+ //#region src/cli/utils/parse-staged-snapshot-divergences.ts
63897
+ const SNAPSHOT_CONFIG_FILENAMES = new Set([...STAGED_FILES_PROJECT_CONFIG_FILENAMES, ...STAGED_SNAPSHOT_ADDITIONAL_CONFIG_FILENAMES]);
63898
+ const parseStagedSnapshotDivergences = (statusOutput) => {
63899
+ const divergentConfigFiles = /* @__PURE__ */ new Set();
63900
+ const statusEntries = statusOutput.split("\0").filter((entry) => entry.length > 0);
63901
+ for (let entryIndex = 0; entryIndex < statusEntries.length; entryIndex += 1) {
63902
+ const entry = statusEntries[entryIndex];
63903
+ const [indexStatus, worktreeStatus] = entry;
63904
+ const filePath = entry.slice(3);
63905
+ const hasRenameOrCopySource = indexStatus === "R" || indexStatus === "C" || worktreeStatus === "R" || worktreeStatus === "C";
63906
+ const sourcePath = hasRenameOrCopySource ? statusEntries[entryIndex + 1] : void 0;
63907
+ const worktreePaths = worktreeStatus === "R" || worktreeStatus === "C" ? [filePath, sourcePath] : [filePath];
63908
+ if (worktreeStatus !== " ") {
63909
+ for (const worktreePath of worktreePaths) if (worktreePath && SNAPSHOT_CONFIG_FILENAMES.has(Path.basename(worktreePath))) divergentConfigFiles.add(worktreePath);
63910
+ }
63911
+ if (hasRenameOrCopySource) entryIndex += 1;
63912
+ }
63913
+ return [...divergentConfigFiles].sort();
63914
+ };
63915
+ //#endregion
63916
+ //#region src/cli/utils/find-staged-snapshot-divergences.ts
63917
+ const findStagedSnapshotDivergences = (directory) => {
63918
+ const statusOutput = runGitRaw(directory, [
63919
+ "status",
63920
+ "--porcelain=v1",
63921
+ "-z",
63922
+ "--untracked-files=all",
63923
+ "--ignored=matching"
63924
+ ]);
63925
+ if (statusOutput === null) return null;
63926
+ return parseStagedSnapshotDivergences(statusOutput);
63927
+ };
63928
+ //#endregion
63807
63929
  //#region src/cli/commands/inspect.ts
63808
63930
  const filterCompletedScansByCategories = (completedScans, categoryFilters) => {
63809
63931
  if (categoryFilters.size === 0) return [...completedScans];
@@ -63818,9 +63940,14 @@ const filterCompletedScansByCategories = (completedScans, categoryFilters) => {
63818
63940
  /**
63819
63941
  * Post-scan finalization shared by the staged-arm and project-loop
63820
63942
  * paths of `inspectAction`: emit the JSON report (when in JSON mode)
63821
- * and set `process.exitCode = 1` when a diagnostic at or above the
63822
- * `--blocking` threshold (default `"error"`) reaches the `ciFailure`
63823
- * surface. `--blocking none` keeps the scan advisory (always exits 0).
63943
+ * and set `process.exitCode = 1` when any scan's lint pass hard-failed
63944
+ * (an engine/plugin/binding failure destroys the findings, so success
63945
+ * would be a false clean) or a diagnostic at or above the `--blocking`
63946
+ * threshold (default `"error"`) reaches the `ciFailure` surface.
63947
+ * `--blocking none` keeps the scan advisory (always exits 0), and
63948
+ * fail-open degradations — `--no-lint`, `--max-duration` truncation,
63949
+ * supply-chain/security skips — stay advisory too, surfaced through
63950
+ * `complete: false` in the JSON report.
63824
63951
  */
63825
63952
  const finalizeScans = (input) => {
63826
63953
  const baselineDeltas = input.completedScans.flatMap((scan) => scan.result.baselineDelta ? [scan.result.baselineDelta] : []);
@@ -63850,8 +63977,13 @@ const finalizeScans = (input) => {
63850
63977
  baselineDegraded
63851
63978
  }));
63852
63979
  }
63980
+ const blockingLevel = resolveBlockingLevel(input.flags, input.userConfig);
63981
+ if (input.completedScans.some(({ result }) => hasLintHardFailure(result)) && blockingLevel !== "none") {
63982
+ process.exitCode = 1;
63983
+ return;
63984
+ }
63853
63985
  if (input.isScoreOnly || baselineDegraded) return;
63854
- if (shouldBlockCi(filterScansForSurface(input.completedScans, "ciFailure"), resolveBlockingLevel(input.flags, input.userConfig))) process.exitCode = 1;
63986
+ if (shouldBlockCi(filterScansForSurface(input.completedScans, "ciFailure"), blockingLevel)) process.exitCode = 1;
63855
63987
  };
63856
63988
  const buildChangedFilesDiffInfo = (changedFiles) => ({
63857
63989
  currentBranch: process.env.GITHUB_HEAD_REF?.trim() || null,
@@ -63889,6 +64021,7 @@ const inspectAction = async (directory, flags) => {
63889
64021
  recordCount(METRIC.cliInvoked, 1, { command: "inspect" });
63890
64022
  try {
63891
64023
  validateModeFlags(flags);
64024
+ if (flags.staged) setJsonReportMode("staged");
63892
64025
  await maybeMigrateLegacyConfig(requestedDirectory, {
63893
64026
  isQuiet,
63894
64027
  isStaged: Boolean(flags.staged)
@@ -63904,6 +64037,14 @@ const inspectAction = async (directory, flags) => {
63904
64037
  cliLogger.dim(`Redirected to ${highlighter.info(toRelativePath(resolvedDirectory, requestedDirectory))} via react-doctor config "rootDir".`);
63905
64038
  cliLogger.break();
63906
64039
  }
64040
+ if (flags.staged) {
64041
+ const divergentConfigFiles = findStagedSnapshotDivergences(resolvedDirectory);
64042
+ if (divergentConfigFiles === null) throw new CliInputError("Could not verify that staged configuration matches the worktree. Run the command from a Git worktree with Git available.");
64043
+ if (divergentConfigFiles.length > 0) {
64044
+ recordCount(METRIC.stagedSnapshotDivergence, 1, { divergentInputCount: divergentConfigFiles.length });
64045
+ throw new CliInputError(`Cannot scan staged files while configuration differs between the index and worktree: ${divergentConfigFiles.join(", ")}. Stage or restore those files, then rerun react-doctor --staged.`);
64046
+ }
64047
+ }
63907
64048
  const explainArgument = flags.explain;
63908
64049
  if (explainArgument !== void 0) {
63909
64050
  await runExplain(explainArgument, {
@@ -63924,7 +64065,6 @@ const inspectAction = async (directory, flags) => {
63924
64065
  json: flags.json
63925
64066
  });
63926
64067
  if (flags.staged) {
63927
- setJsonReportMode("staged");
63928
64068
  const stagedFiles = await getStagedSourceFiles(resolvedDirectory);
63929
64069
  if (stagedFiles.length === 0) {
63930
64070
  if (isJsonMode) writeJsonReport(buildJsonReport({
@@ -64005,13 +64145,15 @@ const inspectAction = async (directory, flags) => {
64005
64145
  const projectDirectories = await selectProjects(resolvedDirectory, flags.project, skipPrompts, userConfig?.projects);
64006
64146
  const changedFilesDiffInfo = flags.changedFilesFrom ? buildChangedFilesDiffInfo(readChangedFilesFrom(Path.resolve(flags.changedFilesFrom))) : null;
64007
64147
  const requestedScope = resolveScope(flags, userConfig);
64148
+ const includeUntracked = flags.includeUntracked ?? false;
64008
64149
  const scopeRequest = requestedScope.scope === void 0 && changedFilesDiffInfo !== null ? {
64009
64150
  ...requestedScope,
64010
64151
  scope: "changed"
64011
64152
  } : requestedScope;
64153
+ validateIncludeUntrackedScope(includeUntracked, scopeRequest.scope);
64012
64154
  const wantsDiffMode = scopeRequest.scope !== void 0 && scopeRequest.scope !== "full";
64013
64155
  const shouldDetectDiff = changedFilesDiffInfo === null && (wantsDiffMode || scopeRequest.scope === void 0 && !skipPrompts && !isQuiet);
64014
- const diffInfo = changedFilesDiffInfo ?? (shouldDetectDiff ? await getDiffInfo(resolvedDirectory, scopeRequest.base) : null);
64156
+ const diffInfo = changedFilesDiffInfo ?? (shouldDetectDiff ? await getDiffInfo(resolvedDirectory, scopeRequest.base, includeUntracked) : null);
64015
64157
  const scope = await finalizeScope({
64016
64158
  requested: scopeRequest,
64017
64159
  diffInfo,
@@ -64025,7 +64167,8 @@ const inspectAction = async (directory, flags) => {
64025
64167
  const changedLineRanges = scope === "lines" && diffInfo !== null && (diffInfo.isCurrentChanges || linesBaseRef !== null) && diffInfo !== null ? await getChangedLineRanges({
64026
64168
  directory: resolvedDirectory,
64027
64169
  baseRef: linesBaseRef ?? void 0,
64028
- files: [...diffInfo.changedFiles]
64170
+ files: [...diffInfo.changedFiles],
64171
+ includeUntracked
64029
64172
  }) : null;
64030
64173
  if (scope === "lines" && changedLineRanges === null && !isQuiet) {
64031
64174
  cliLogger.warn("Could not determine changed lines (no base ref or git diff failed); reporting all issues in changed files.");
@@ -64958,6 +65101,7 @@ const ROOT_FLAG_SPEC = {
64958
65101
  "--dead-code",
64959
65102
  "--debug",
64960
65103
  "--help",
65104
+ "--include-untracked",
64961
65105
  "--json",
64962
65106
  "--json-compact",
64963
65107
  "--lint",
@@ -65221,7 +65365,7 @@ ${highlighter.dim("Learn more:")}
65221
65365
  ${highlighter.info(CI_URL)}
65222
65366
  `;
65223
65367
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
65224
- const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much supported JS/TS source to scan/report: full (default), files, changed (only new issues vs base), or lines (issues whose source spans touch changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
65368
+ const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much supported JS/TS source to scan/report: full (default), files, changed (only new issues vs base), or lines (issues whose source spans touch changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").option("--include-untracked", "with --scope files/changed/lines, also scan ordinary untracked files (respects .gitignore)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
65225
65369
  program.action(inspectAction);
65226
65370
  program.command("why <location>").description("Explain why a rule fired (or why a suppression didn't apply) at a file:line").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple)").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").action((location, options) => whyAction(location, options));
65227
65371
  program.command("install").alias("setup").description("Install the react-doctor skill into your coding agents and optional git hook").option("-y, --yes", "skip prompts, install for all detected agents").option("--dry-run", "show what would be installed without writing files").option("--agent-hooks", "install native non-blocking agent hooks for Claude Code and Cursor").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderInstallHelpEpilog).action(installAction);
@@ -65271,4 +65415,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
65271
65415
  export {};
65272
65416
 
65273
65417
  //# sourceMappingURL=cli.js.map
65274
- //# debugId=9a5c1a3c-7706-5a23-bca4-ddddd2f085af
65418
+ //# debugId=4c1e681e-4dee-5ffd-a4ba-69b876f47147
package/dist/index.d.ts CHANGED
@@ -10256,7 +10256,7 @@ declare const buildJsonReport: (input: BuildJsonReportInput) => JsonReportV3$1;
10256
10256
  * render diff-base mistakes (`GitBaseBranchInvalid` /
10257
10257
  * `GitBaseBranchMissing`) as clean user errors instead of crashes.
10258
10258
  */
10259
- declare const getDiffInfo: (directory: string, explicitBaseBranch?: string) => Promise<DiffInfo | null>;
10259
+ declare const getDiffInfo: (directory: string, explicitBaseBranch?: string, includeUntracked?: boolean) => Promise<DiffInfo | null>;
10260
10260
  declare const filterSourceFiles: (filePaths: string[]) => string[];
10261
10261
  /**
10262
10262
  * Programmatic façade over `Git.changedLineRanges` (the `lines` scope). Diffs
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="11364a34-6c8b-5bd3-b9e8-aaa7c0ea264c")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="4da79386-f10f-5bae-95ac-7333b2d6c4e8")}catch(e){}}();
3
3
  import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
4
4
  import { createRequire } from "node:module";
5
5
  import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
@@ -36366,6 +36366,49 @@ const assignFixGroups = (diagnostics) => {
36366
36366
  };
36367
36367
  });
36368
36368
  };
36369
+ const DERIVED_STATE_RULE_PRIORITY = new Map([
36370
+ ["no-initialize-state", 0],
36371
+ ["no-adjust-state-on-prop-change", 1],
36372
+ ["no-derived-state", 2],
36373
+ ["no-derived-state-effect", 3]
36374
+ ]);
36375
+ const buildDiagnosticSiteKey = (diagnostic) => `${diagnostic.filePath}\u0000${diagnostic.line}\u0000${diagnostic.column}`;
36376
+ const isReactDoctorRulesOfHooksDiagnostic = (diagnostic) => diagnostic.plugin === "react-doctor" && diagnostic.rule === "rules-of-hooks";
36377
+ const isReactCompilerHooksDiagnostic = (diagnostic) => diagnostic.plugin === "react-hooks-js" && diagnostic.rule === "hooks";
36378
+ const doDiagnosticSpansOverlap = (first, second) => {
36379
+ if (first.filePath !== second.filePath || first.plugin !== second.plugin) return false;
36380
+ if (first.offset !== void 0 && second.offset !== void 0) {
36381
+ const firstEnd = first.offset + Math.max(first.length ?? 1, 1);
36382
+ const secondEnd = second.offset + Math.max(second.length ?? 1, 1);
36383
+ return first.offset < secondEnd && second.offset < firstEnd;
36384
+ }
36385
+ const firstEndLine = first.endLine ?? first.line;
36386
+ const secondEndLine = second.endLine ?? second.line;
36387
+ return first.line <= secondEndLine && second.line <= firstEndLine;
36388
+ };
36389
+ const keepHighestSeverity = (survivingDiagnostic, collapsedDiagnostic) => collapsedDiagnostic.severity === "error" && survivingDiagnostic.severity !== "error" ? {
36390
+ ...survivingDiagnostic,
36391
+ severity: "error"
36392
+ } : survivingDiagnostic;
36393
+ const dedupeRelatedDiagnostics = (diagnostics) => {
36394
+ const reactDoctorHookSites = new Set(diagnostics.filter(isReactDoctorRulesOfHooksDiagnostic).map(buildDiagnosticSiteKey));
36395
+ const uniqueDiagnostics = [];
36396
+ for (const diagnostic of diagnostics) {
36397
+ if (isReactCompilerHooksDiagnostic(diagnostic) && reactDoctorHookSites.has(buildDiagnosticSiteKey(diagnostic))) continue;
36398
+ const priority = DERIVED_STATE_RULE_PRIORITY.get(diagnostic.rule);
36399
+ if (priority !== void 0) {
36400
+ const overlappingDiagnosticIndex = uniqueDiagnostics.findIndex((candidate) => candidate.rule !== diagnostic.rule && DERIVED_STATE_RULE_PRIORITY.has(candidate.rule) && doDiagnosticSpansOverlap(candidate, diagnostic));
36401
+ if (overlappingDiagnosticIndex >= 0) {
36402
+ const existingDiagnostic = uniqueDiagnostics[overlappingDiagnosticIndex];
36403
+ const existingPriority = existingDiagnostic ? DERIVED_STATE_RULE_PRIORITY.get(existingDiagnostic.rule) : void 0;
36404
+ if (existingDiagnostic && existingPriority !== void 0) uniqueDiagnostics[overlappingDiagnosticIndex] = priority < existingPriority ? keepHighestSeverity(diagnostic, existingDiagnostic) : keepHighestSeverity(existingDiagnostic, diagnostic);
36405
+ continue;
36406
+ }
36407
+ }
36408
+ uniqueDiagnostics.push(diagnostic);
36409
+ }
36410
+ return uniqueDiagnostics;
36411
+ };
36369
36412
  const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
36370
36413
  const sortDiagnosticsStable = (diagnostics) => [...diagnostics].sort((left, right) => compareStrings(left.filePath, right.filePath) || left.line - right.line || left.column - right.column || compareStrings(left.plugin, right.plugin) || compareStrings(left.rule, right.rule) || compareStrings(left.severity, right.severity) || compareStrings(left.message, right.message));
36371
36414
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
@@ -38579,6 +38622,7 @@ const parseGithubViewerPermission = (stdout) => {
38579
38622
  return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
38580
38623
  };
38581
38624
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
38625
+ const UNTRACKED_FILE_LAST_LINE = Number.MAX_SAFE_INTEGER;
38582
38626
  /**
38583
38627
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
38584
38628
  * behind a `Context.Service`. The production layer (`layerNode`)
@@ -38649,6 +38693,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
38649
38693
  args,
38650
38694
  directory
38651
38695
  });
38696
+ const listUntrackedFilePaths = (directory, includePaths = []) => runGit(directory, [
38697
+ "ls-files",
38698
+ "-z",
38699
+ "--others",
38700
+ "--exclude-standard",
38701
+ ...includePaths.length > 0 ? ["--", ...includePaths] : []
38702
+ ]).pipe(map$3((result) => result.status === 0 ? splitNullSeparated(result.stdout) : null));
38703
+ const mergeUntracked = (directory, trackedFilePaths, includeUntracked) => includeUntracked ? listUntrackedFilePaths(directory).pipe(map$3((untracked) => untracked === null ? trackedFilePaths : [...new Set([...trackedFilePaths, ...untracked])])) : succeed$2(trackedFilePaths);
38652
38704
  const currentBranch = (directory) => runGit(directory, [
38653
38705
  "rev-parse",
38654
38706
  "--abbrev-ref",
@@ -38771,7 +38823,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38771
38823
  githubViewerPermission,
38772
38824
  branchExists,
38773
38825
  mergeBase,
38774
- diffSelection: ({ directory, explicitBaseBranch }) => gen(function* () {
38826
+ diffSelection: ({ directory, explicitBaseBranch, includeUntracked = false }) => gen(function* () {
38775
38827
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
38776
38828
  if (explicitBaseBranch !== void 0) {
38777
38829
  const range = parseGitDiffRange(explicitBaseBranch);
@@ -38800,7 +38852,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38800
38852
  "HEAD"
38801
38853
  ]);
38802
38854
  if (uncommitted.status !== 0) return null;
38803
- const files = splitNullSeparated(uncommitted.stdout);
38855
+ const files = yield* mergeUntracked(directory, splitNullSeparated(uncommitted.stdout), includeUntracked);
38804
38856
  if (files.length === 0) return null;
38805
38857
  return {
38806
38858
  currentBranch: resolvedCurrentBranch,
@@ -38830,7 +38882,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38830
38882
  currentBranch: resolvedCurrentBranch,
38831
38883
  baseBranch,
38832
38884
  diffBaseRef: mergeBaseRef,
38833
- changedFiles: splitNullSeparated(diff.stdout),
38885
+ changedFiles: yield* mergeUntracked(directory, splitNullSeparated(diff.stdout), includeUntracked),
38834
38886
  isCurrentChanges: false
38835
38887
  };
38836
38888
  }).pipe(withSpan("Git.diffSelection")),
@@ -38876,7 +38928,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38876
38928
  stdout: result.stdout
38877
38929
  };
38878
38930
  }).pipe(withSpan("Git.grep")),
38879
- changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
38931
+ changedLineRanges: ({ directory, baseRef, cached, files, includeUntracked = false }) => gen(function* () {
38880
38932
  if (files.length === 0) return [];
38881
38933
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
38882
38934
  const result = yield* runGit(directory, [
@@ -38890,7 +38942,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
38890
38942
  ...files
38891
38943
  ]);
38892
38944
  if (result.status !== 0) return null;
38893
- return parseChangedLineRanges(result.stdout);
38945
+ const changedLineRanges = parseChangedLineRanges(result.stdout);
38946
+ if (cached || !includeUntracked) return changedLineRanges;
38947
+ const untrackedFilePaths = yield* listUntrackedFilePaths(directory, files);
38948
+ if (untrackedFilePaths === null) return changedLineRanges;
38949
+ return [...changedLineRanges, ...untrackedFilePaths.map((file) => ({
38950
+ file,
38951
+ ranges: [[1, UNTRACKED_FILE_LAST_LINE]]
38952
+ }))];
38894
38953
  }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
38895
38954
  });
38896
38955
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
@@ -39475,13 +39534,13 @@ const encodeSidecarFileEntry = (entry) => ({
39475
39534
  diagnostics: entry.diagnostics
39476
39535
  });
39477
39536
  const emptyCache = () => ({
39478
- version: 1,
39537
+ version: 2,
39479
39538
  rulesets: {}
39480
39539
  });
39481
39540
  const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
39482
39541
  const entries = /* @__PURE__ */ new Map();
39483
39542
  const persisted = failOpenReadJson(cacheFilePath, emptyCache());
39484
- if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
39543
+ if (persisted.version !== 2 || !isRecord(persisted.rulesets)) return entries;
39485
39544
  const bucket = persisted.rulesets[rulesetHash];
39486
39545
  if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
39487
39546
  for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
@@ -39501,7 +39560,7 @@ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
39501
39560
  },
39502
39561
  persist: () => {
39503
39562
  const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
39504
- const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
39563
+ const rulesets = onDisk.version === 2 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
39505
39564
  const existingBucket = rulesets[rulesetHash];
39506
39565
  const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
39507
39566
  const ourFiles = {};
@@ -39515,7 +39574,7 @@ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
39515
39574
  files: Object.fromEntries(cappedEntries)
39516
39575
  };
39517
39576
  atomicWriteJson(cacheFilePath, {
39518
- version: 1,
39577
+ version: 2,
39519
39578
  rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
39520
39579
  });
39521
39580
  }
@@ -39566,23 +39625,6 @@ const resolveTsConfigRelativePath = (rootDirectory) => {
39566
39625
  for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
39567
39626
  return null;
39568
39627
  };
39569
- const DERIVED_STATE_RULE_PRIORITY = new Map([
39570
- ["no-initialize-state", 0],
39571
- ["no-adjust-state-on-prop-change", 1],
39572
- ["no-derived-state", 2],
39573
- ["no-derived-state-effect", 3]
39574
- ]);
39575
- const doDiagnosticSpansOverlap = (first, second) => {
39576
- if (first.filePath !== second.filePath || first.plugin !== second.plugin) return false;
39577
- if (first.offset !== void 0 && second.offset !== void 0) {
39578
- const firstEnd = first.offset + Math.max(first.length ?? 1, 1);
39579
- const secondEnd = second.offset + Math.max(second.length ?? 1, 1);
39580
- return first.offset < secondEnd && second.offset < firstEnd;
39581
- }
39582
- const firstEndLine = first.endLine ?? first.line;
39583
- const secondEndLine = second.endLine ?? second.line;
39584
- return first.line <= secondEndLine && second.line <= firstEndLine;
39585
- };
39586
39628
  const dedupeDiagnostics = (diagnostics) => {
39587
39629
  const seenKeys = /* @__PURE__ */ new Set();
39588
39630
  const uniqueDiagnostics = [];
@@ -39590,16 +39632,6 @@ const dedupeDiagnostics = (diagnostics) => {
39590
39632
  const key = `${diagnostic.filePath}\u0000${diagnostic.line}\u0000${diagnostic.column}\u0000${diagnostic.plugin}\u0000${diagnostic.rule}\u0000${diagnostic.severity}\u0000${diagnostic.message}`;
39591
39633
  if (seenKeys.has(key)) continue;
39592
39634
  seenKeys.add(key);
39593
- const priority = DERIVED_STATE_RULE_PRIORITY.get(diagnostic.rule);
39594
- if (priority !== void 0) {
39595
- const overlappingDiagnosticIndex = uniqueDiagnostics.findIndex((candidate) => candidate.rule !== diagnostic.rule && candidate.severity === diagnostic.severity && DERIVED_STATE_RULE_PRIORITY.has(candidate.rule) && doDiagnosticSpansOverlap(candidate, diagnostic));
39596
- if (overlappingDiagnosticIndex >= 0) {
39597
- const existingDiagnostic = uniqueDiagnostics[overlappingDiagnosticIndex];
39598
- const existingPriority = existingDiagnostic ? DERIVED_STATE_RULE_PRIORITY.get(existingDiagnostic.rule) : void 0;
39599
- if (existingPriority !== void 0 && priority < existingPriority) uniqueDiagnostics[overlappingDiagnosticIndex] = diagnostic;
39600
- continue;
39601
- }
39602
- }
39603
39635
  uniqueDiagnostics.push(diagnostic);
39604
39636
  }
39605
39637
  return uniqueDiagnostics;
@@ -40399,11 +40431,11 @@ const buildRelatedLocations = (labels, filePath) => {
40399
40431
  }
40400
40432
  return related;
40401
40433
  };
40402
- const isOxlintOutput = (value) => {
40403
- if (typeof value !== "object" || value === null) return false;
40404
- const candidate = value;
40405
- return Array.isArray(candidate.diagnostics);
40406
- };
40434
+ const isOxlintSpan = (value) => isRecord(value) && typeof value.offset === "number" && typeof value.length === "number" && typeof value.line === "number" && typeof value.column === "number";
40435
+ const isOxlintLabel = (value) => isRecord(value) && isOxlintSpan(value.span);
40436
+ const isMappableOxlintDiagnostic = (value) => isRecord(value) && typeof value.code === "string" && value.code.length > 0 && typeof value.filename === "string" && value.filename.length > 0 && (value.severity === "warning" || value.severity === "error") && Array.isArray(value.labels) && value.labels.every(isOxlintLabel);
40437
+ const isEngineFailureDiagnostic = (value) => !isRecord(value) || typeof value.filename !== "string" || value.filename.length === 0;
40438
+ const isOxlintOutput = (value) => isRecord(value) && Array.isArray(value.diagnostics);
40407
40439
  /**
40408
40440
  * Parses one oxlint subprocess's stdout into a flat `Diagnostic[]`.
40409
40441
  * Tolerates the leading-notice-line shape oxlint sometimes prints
@@ -40422,6 +40454,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40422
40454
  throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40423
40455
  }
40424
40456
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40457
+ const engineFailureDiagnostic = parsed.diagnostics.find(isEngineFailureDiagnostic);
40458
+ if (engineFailureDiagnostic !== void 0) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: JSON.stringify(engineFailureDiagnostic).slice(0, 600) }) });
40425
40459
  const minifiedFileCache = /* @__PURE__ */ new Map();
40426
40460
  const sourceBufferCache = /* @__PURE__ */ new Map();
40427
40461
  const sourceLinesCache = /* @__PURE__ */ new Map();
@@ -40456,7 +40490,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40456
40490
  minifiedFileCache.set(absolutePath, minified);
40457
40491
  return minified;
40458
40492
  };
40459
- return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
40493
+ return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => isMappableOxlintDiagnostic(diagnostic) && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
40460
40494
  const { plugin, rule } = parseRuleCode(diagnostic.code);
40461
40495
  const primaryLabel = diagnostic.labels[0];
40462
40496
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -42291,7 +42325,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42291
42325
  respectInlineDisables: input.respectInlineDisables,
42292
42326
  showWarnings
42293
42327
  });
42294
- const applyPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(filterMapNullable(transform.apply)), tap((diagnostic) => reporterService.emit(diagnostic)));
42328
+ const filterPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(filterMapNullable(transform.apply)));
42329
+ const applyPerElementPipeline = (rawStream) => filterPerElementPipeline(rawStream).pipe(tap((diagnostic) => reporterService.emit(diagnostic)));
42295
42330
  const envCollected = yield* runCollect(applyPerElementPipeline(fromIterable$1(isDiffMode ? [] : [
42296
42331
  ...checkReducedMotion(scanDirectory),
42297
42332
  ...checkPnpmHardening(scanDirectory),
@@ -42420,7 +42455,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42420
42455
  });
42421
42456
  return empty$4;
42422
42457
  }))));
42423
- const lintCollected = yield* runCollect(applyPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
42458
+ const lintCollected = dedupeRelatedDiagnostics(yield* runCollect(filterPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
42424
42459
  onNone: () => set(lintFailure, {
42425
42460
  didFail: true,
42426
42461
  reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
@@ -42428,7 +42463,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42428
42463
  reasonKind: null
42429
42464
  }).pipe(as([])),
42430
42465
  onSome: succeed$2
42431
- })));
42466
+ }))));
42467
+ yield* forEach$1(lintCollected, reporterService.emit, { discard: true });
42432
42468
  const lintFailureState = yield* get$2(lintFailure);
42433
42469
  yield* afterLint(lintFailureState.didFail);
42434
42470
  if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
@@ -42948,10 +42984,11 @@ const buildSkippedChecks = (input) => {
42948
42984
  * render diff-base mistakes (`GitBaseBranchInvalid` /
42949
42985
  * `GitBaseBranchMissing`) as clean user errors instead of crashes.
42950
42986
  */
42951
- const getDiffInfo = (directory, explicitBaseBranch) => runPromise(gen(function* () {
42987
+ const getDiffInfo = (directory, explicitBaseBranch, includeUntracked) => runPromise(gen(function* () {
42952
42988
  const selection = yield* (yield* Git).diffSelection({
42953
42989
  directory,
42954
- explicitBaseBranch
42990
+ explicitBaseBranch,
42991
+ includeUntracked
42955
42992
  });
42956
42993
  if (selection === null) return null;
42957
42994
  return {
@@ -43241,4 +43278,4 @@ const toJsonReport = (result, options) => buildJsonReport({
43241
43278
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
43242
43279
 
43243
43280
  //# sourceMappingURL=index.js.map
43244
- //# debugId=11364a34-6c8b-5bd3-b9e8-aaa7c0ea264c
43281
+ //# debugId=4da79386-f10f-5bae-95ac-7333b2d6c4e8
package/dist/lsp.js CHANGED
@@ -36344,6 +36344,49 @@ const assignFixGroups = (diagnostics) => {
36344
36344
  };
36345
36345
  });
36346
36346
  };
36347
+ const DERIVED_STATE_RULE_PRIORITY = new Map([
36348
+ ["no-initialize-state", 0],
36349
+ ["no-adjust-state-on-prop-change", 1],
36350
+ ["no-derived-state", 2],
36351
+ ["no-derived-state-effect", 3]
36352
+ ]);
36353
+ const buildDiagnosticSiteKey = (diagnostic) => `${diagnostic.filePath}\u0000${diagnostic.line}\u0000${diagnostic.column}`;
36354
+ const isReactDoctorRulesOfHooksDiagnostic = (diagnostic) => diagnostic.plugin === "react-doctor" && diagnostic.rule === "rules-of-hooks";
36355
+ const isReactCompilerHooksDiagnostic = (diagnostic) => diagnostic.plugin === "react-hooks-js" && diagnostic.rule === "hooks";
36356
+ const doDiagnosticSpansOverlap = (first, second) => {
36357
+ if (first.filePath !== second.filePath || first.plugin !== second.plugin) return false;
36358
+ if (first.offset !== void 0 && second.offset !== void 0) {
36359
+ const firstEnd = first.offset + Math.max(first.length ?? 1, 1);
36360
+ const secondEnd = second.offset + Math.max(second.length ?? 1, 1);
36361
+ return first.offset < secondEnd && second.offset < firstEnd;
36362
+ }
36363
+ const firstEndLine = first.endLine ?? first.line;
36364
+ const secondEndLine = second.endLine ?? second.line;
36365
+ return first.line <= secondEndLine && second.line <= firstEndLine;
36366
+ };
36367
+ const keepHighestSeverity = (survivingDiagnostic, collapsedDiagnostic) => collapsedDiagnostic.severity === "error" && survivingDiagnostic.severity !== "error" ? {
36368
+ ...survivingDiagnostic,
36369
+ severity: "error"
36370
+ } : survivingDiagnostic;
36371
+ const dedupeRelatedDiagnostics = (diagnostics) => {
36372
+ const reactDoctorHookSites = new Set(diagnostics.filter(isReactDoctorRulesOfHooksDiagnostic).map(buildDiagnosticSiteKey));
36373
+ const uniqueDiagnostics = [];
36374
+ for (const diagnostic of diagnostics) {
36375
+ if (isReactCompilerHooksDiagnostic(diagnostic) && reactDoctorHookSites.has(buildDiagnosticSiteKey(diagnostic))) continue;
36376
+ const priority = DERIVED_STATE_RULE_PRIORITY.get(diagnostic.rule);
36377
+ if (priority !== void 0) {
36378
+ const overlappingDiagnosticIndex = uniqueDiagnostics.findIndex((candidate) => candidate.rule !== diagnostic.rule && DERIVED_STATE_RULE_PRIORITY.has(candidate.rule) && doDiagnosticSpansOverlap(candidate, diagnostic));
36379
+ if (overlappingDiagnosticIndex >= 0) {
36380
+ const existingDiagnostic = uniqueDiagnostics[overlappingDiagnosticIndex];
36381
+ const existingPriority = existingDiagnostic ? DERIVED_STATE_RULE_PRIORITY.get(existingDiagnostic.rule) : void 0;
36382
+ if (existingDiagnostic && existingPriority !== void 0) uniqueDiagnostics[overlappingDiagnosticIndex] = priority < existingPriority ? keepHighestSeverity(diagnostic, existingDiagnostic) : keepHighestSeverity(existingDiagnostic, diagnostic);
36383
+ continue;
36384
+ }
36385
+ }
36386
+ uniqueDiagnostics.push(diagnostic);
36387
+ }
36388
+ return uniqueDiagnostics;
36389
+ };
36347
36390
  const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
36348
36391
  const sortDiagnosticsStable = (diagnostics) => [...diagnostics].sort((left, right) => compareStrings(left.filePath, right.filePath) || left.line - right.line || left.column - right.column || compareStrings(left.plugin, right.plugin) || compareStrings(left.rule, right.rule) || compareStrings(left.severity, right.severity) || compareStrings(left.message, right.message));
36349
36392
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
@@ -38557,6 +38600,7 @@ const parseGithubViewerPermission = (stdout) => {
38557
38600
  return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
38558
38601
  };
38559
38602
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
38603
+ const UNTRACKED_FILE_LAST_LINE = Number.MAX_SAFE_INTEGER;
38560
38604
  /**
38561
38605
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
38562
38606
  * behind a `Context.Service`. The production layer (`layerNode`)
@@ -38627,6 +38671,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
38627
38671
  args,
38628
38672
  directory
38629
38673
  });
38674
+ const listUntrackedFilePaths = (directory, includePaths = []) => runGit(directory, [
38675
+ "ls-files",
38676
+ "-z",
38677
+ "--others",
38678
+ "--exclude-standard",
38679
+ ...includePaths.length > 0 ? ["--", ...includePaths] : []
38680
+ ]).pipe(map$3((result) => result.status === 0 ? splitNullSeparated(result.stdout) : null));
38681
+ const mergeUntracked = (directory, trackedFilePaths, includeUntracked) => includeUntracked ? listUntrackedFilePaths(directory).pipe(map$3((untracked) => untracked === null ? trackedFilePaths : [...new Set([...trackedFilePaths, ...untracked])])) : succeed$2(trackedFilePaths);
38630
38682
  const currentBranch = (directory) => runGit(directory, [
38631
38683
  "rev-parse",
38632
38684
  "--abbrev-ref",
@@ -38749,7 +38801,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38749
38801
  githubViewerPermission,
38750
38802
  branchExists,
38751
38803
  mergeBase,
38752
- diffSelection: ({ directory, explicitBaseBranch }) => gen(function* () {
38804
+ diffSelection: ({ directory, explicitBaseBranch, includeUntracked = false }) => gen(function* () {
38753
38805
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
38754
38806
  if (explicitBaseBranch !== void 0) {
38755
38807
  const range = parseGitDiffRange(explicitBaseBranch);
@@ -38778,7 +38830,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38778
38830
  "HEAD"
38779
38831
  ]);
38780
38832
  if (uncommitted.status !== 0) return null;
38781
- const files = splitNullSeparated(uncommitted.stdout);
38833
+ const files = yield* mergeUntracked(directory, splitNullSeparated(uncommitted.stdout), includeUntracked);
38782
38834
  if (files.length === 0) return null;
38783
38835
  return {
38784
38836
  currentBranch: resolvedCurrentBranch,
@@ -38808,7 +38860,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38808
38860
  currentBranch: resolvedCurrentBranch,
38809
38861
  baseBranch,
38810
38862
  diffBaseRef: mergeBaseRef,
38811
- changedFiles: splitNullSeparated(diff.stdout),
38863
+ changedFiles: yield* mergeUntracked(directory, splitNullSeparated(diff.stdout), includeUntracked),
38812
38864
  isCurrentChanges: false
38813
38865
  };
38814
38866
  }).pipe(withSpan("Git.diffSelection")),
@@ -38854,7 +38906,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38854
38906
  stdout: result.stdout
38855
38907
  };
38856
38908
  }).pipe(withSpan("Git.grep")),
38857
- changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
38909
+ changedLineRanges: ({ directory, baseRef, cached, files, includeUntracked = false }) => gen(function* () {
38858
38910
  if (files.length === 0) return [];
38859
38911
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
38860
38912
  const result = yield* runGit(directory, [
@@ -38868,7 +38920,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
38868
38920
  ...files
38869
38921
  ]);
38870
38922
  if (result.status !== 0) return null;
38871
- return parseChangedLineRanges(result.stdout);
38923
+ const changedLineRanges = parseChangedLineRanges(result.stdout);
38924
+ if (cached || !includeUntracked) return changedLineRanges;
38925
+ const untrackedFilePaths = yield* listUntrackedFilePaths(directory, files);
38926
+ if (untrackedFilePaths === null) return changedLineRanges;
38927
+ return [...changedLineRanges, ...untrackedFilePaths.map((file) => ({
38928
+ file,
38929
+ ranges: [[1, UNTRACKED_FILE_LAST_LINE]]
38930
+ }))];
38872
38931
  }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
38873
38932
  });
38874
38933
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
@@ -39453,13 +39512,13 @@ const encodeSidecarFileEntry = (entry) => ({
39453
39512
  diagnostics: entry.diagnostics
39454
39513
  });
39455
39514
  const emptyCache = () => ({
39456
- version: 1,
39515
+ version: 2,
39457
39516
  rulesets: {}
39458
39517
  });
39459
39518
  const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
39460
39519
  const entries = /* @__PURE__ */ new Map();
39461
39520
  const persisted = failOpenReadJson(cacheFilePath, emptyCache());
39462
- if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
39521
+ if (persisted.version !== 2 || !isRecord(persisted.rulesets)) return entries;
39463
39522
  const bucket = persisted.rulesets[rulesetHash];
39464
39523
  if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
39465
39524
  for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
@@ -39479,7 +39538,7 @@ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
39479
39538
  },
39480
39539
  persist: () => {
39481
39540
  const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
39482
- const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
39541
+ const rulesets = onDisk.version === 2 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
39483
39542
  const existingBucket = rulesets[rulesetHash];
39484
39543
  const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
39485
39544
  const ourFiles = {};
@@ -39493,7 +39552,7 @@ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
39493
39552
  files: Object.fromEntries(cappedEntries)
39494
39553
  };
39495
39554
  atomicWriteJson(cacheFilePath, {
39496
- version: 1,
39555
+ version: 2,
39497
39556
  rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
39498
39557
  });
39499
39558
  }
@@ -39544,23 +39603,6 @@ const resolveTsConfigRelativePath = (rootDirectory) => {
39544
39603
  for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
39545
39604
  return null;
39546
39605
  };
39547
- const DERIVED_STATE_RULE_PRIORITY = new Map([
39548
- ["no-initialize-state", 0],
39549
- ["no-adjust-state-on-prop-change", 1],
39550
- ["no-derived-state", 2],
39551
- ["no-derived-state-effect", 3]
39552
- ]);
39553
- const doDiagnosticSpansOverlap = (first, second) => {
39554
- if (first.filePath !== second.filePath || first.plugin !== second.plugin) return false;
39555
- if (first.offset !== void 0 && second.offset !== void 0) {
39556
- const firstEnd = first.offset + Math.max(first.length ?? 1, 1);
39557
- const secondEnd = second.offset + Math.max(second.length ?? 1, 1);
39558
- return first.offset < secondEnd && second.offset < firstEnd;
39559
- }
39560
- const firstEndLine = first.endLine ?? first.line;
39561
- const secondEndLine = second.endLine ?? second.line;
39562
- return first.line <= secondEndLine && second.line <= firstEndLine;
39563
- };
39564
39606
  const dedupeDiagnostics = (diagnostics) => {
39565
39607
  const seenKeys = /* @__PURE__ */ new Set();
39566
39608
  const uniqueDiagnostics = [];
@@ -39568,16 +39610,6 @@ const dedupeDiagnostics = (diagnostics) => {
39568
39610
  const key = `${diagnostic.filePath}\u0000${diagnostic.line}\u0000${diagnostic.column}\u0000${diagnostic.plugin}\u0000${diagnostic.rule}\u0000${diagnostic.severity}\u0000${diagnostic.message}`;
39569
39611
  if (seenKeys.has(key)) continue;
39570
39612
  seenKeys.add(key);
39571
- const priority = DERIVED_STATE_RULE_PRIORITY.get(diagnostic.rule);
39572
- if (priority !== void 0) {
39573
- const overlappingDiagnosticIndex = uniqueDiagnostics.findIndex((candidate) => candidate.rule !== diagnostic.rule && candidate.severity === diagnostic.severity && DERIVED_STATE_RULE_PRIORITY.has(candidate.rule) && doDiagnosticSpansOverlap(candidate, diagnostic));
39574
- if (overlappingDiagnosticIndex >= 0) {
39575
- const existingDiagnostic = uniqueDiagnostics[overlappingDiagnosticIndex];
39576
- const existingPriority = existingDiagnostic ? DERIVED_STATE_RULE_PRIORITY.get(existingDiagnostic.rule) : void 0;
39577
- if (existingPriority !== void 0 && priority < existingPriority) uniqueDiagnostics[overlappingDiagnosticIndex] = diagnostic;
39578
- continue;
39579
- }
39580
- }
39581
39613
  uniqueDiagnostics.push(diagnostic);
39582
39614
  }
39583
39615
  return uniqueDiagnostics;
@@ -40377,11 +40409,11 @@ const buildRelatedLocations = (labels, filePath) => {
40377
40409
  }
40378
40410
  return related;
40379
40411
  };
40380
- const isOxlintOutput = (value) => {
40381
- if (typeof value !== "object" || value === null) return false;
40382
- const candidate = value;
40383
- return Array.isArray(candidate.diagnostics);
40384
- };
40412
+ const isOxlintSpan = (value) => isRecord(value) && typeof value.offset === "number" && typeof value.length === "number" && typeof value.line === "number" && typeof value.column === "number";
40413
+ const isOxlintLabel = (value) => isRecord(value) && isOxlintSpan(value.span);
40414
+ const isMappableOxlintDiagnostic = (value) => isRecord(value) && typeof value.code === "string" && value.code.length > 0 && typeof value.filename === "string" && value.filename.length > 0 && (value.severity === "warning" || value.severity === "error") && Array.isArray(value.labels) && value.labels.every(isOxlintLabel);
40415
+ const isEngineFailureDiagnostic = (value) => !isRecord(value) || typeof value.filename !== "string" || value.filename.length === 0;
40416
+ const isOxlintOutput = (value) => isRecord(value) && Array.isArray(value.diagnostics);
40385
40417
  /**
40386
40418
  * Parses one oxlint subprocess's stdout into a flat `Diagnostic[]`.
40387
40419
  * Tolerates the leading-notice-line shape oxlint sometimes prints
@@ -40400,6 +40432,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40400
40432
  throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40401
40433
  }
40402
40434
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40435
+ const engineFailureDiagnostic = parsed.diagnostics.find(isEngineFailureDiagnostic);
40436
+ if (engineFailureDiagnostic !== void 0) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: JSON.stringify(engineFailureDiagnostic).slice(0, 600) }) });
40403
40437
  const minifiedFileCache = /* @__PURE__ */ new Map();
40404
40438
  const sourceBufferCache = /* @__PURE__ */ new Map();
40405
40439
  const sourceLinesCache = /* @__PURE__ */ new Map();
@@ -40434,7 +40468,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40434
40468
  minifiedFileCache.set(absolutePath, minified);
40435
40469
  return minified;
40436
40470
  };
40437
- return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
40471
+ return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => isMappableOxlintDiagnostic(diagnostic) && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
40438
40472
  const { plugin, rule } = parseRuleCode(diagnostic.code);
40439
40473
  const primaryLabel = diagnostic.labels[0];
40440
40474
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -42269,7 +42303,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42269
42303
  respectInlineDisables: input.respectInlineDisables,
42270
42304
  showWarnings
42271
42305
  });
42272
- const applyPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(filterMapNullable(transform.apply)), tap((diagnostic) => reporterService.emit(diagnostic)));
42306
+ const filterPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(filterMapNullable(transform.apply)));
42307
+ const applyPerElementPipeline = (rawStream) => filterPerElementPipeline(rawStream).pipe(tap((diagnostic) => reporterService.emit(diagnostic)));
42273
42308
  const envCollected = yield* runCollect(applyPerElementPipeline(fromIterable$1(isDiffMode ? [] : [
42274
42309
  ...checkReducedMotion(scanDirectory),
42275
42310
  ...checkPnpmHardening(scanDirectory),
@@ -42398,7 +42433,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42398
42433
  });
42399
42434
  return empty$4;
42400
42435
  }))));
42401
- const lintCollected = yield* runCollect(applyPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
42436
+ const lintCollected = dedupeRelatedDiagnostics(yield* runCollect(filterPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
42402
42437
  onNone: () => set(lintFailure, {
42403
42438
  didFail: true,
42404
42439
  reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
@@ -42406,7 +42441,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42406
42441
  reasonKind: null
42407
42442
  }).pipe(as([])),
42408
42443
  onSome: succeed$2
42409
- })));
42444
+ }))));
42445
+ yield* forEach$1(lintCollected, reporterService.emit, { discard: true });
42410
42446
  const lintFailureState = yield* get$2(lintFailure);
42411
42447
  yield* afterLint(lintFailureState.didFail);
42412
42448
  if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
@@ -44524,6 +44560,7 @@ const METRIC = {
44524
44560
  cliInvoked: "cli.invoked",
44525
44561
  cliError: "cli.error",
44526
44562
  cliEnvironmentError: "cli.env_error",
44563
+ stagedSnapshotDivergence: "staged.snapshot_divergence",
44527
44564
  projectDetected: "project.detected",
44528
44565
  projectPathSelected: "project.path_selected",
44529
44566
  projectConfigSelected: "project.config_selected",
@@ -44878,5 +44915,5 @@ const startLanguageServer = () => {
44878
44915
  };
44879
44916
  //#endregion
44880
44917
  export { startLanguageServer };
44881
- !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]="f9337714-b38f-5f28-ac00-c9d7d79ebaea")}catch(e){}}();
44882
- //# debugId=f9337714-b38f-5f28-ac00-c9d7d79ebaea
44918
+ !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]="b75c36e9-f19d-5127-be1a-89ac93910ff0")}catch(e){}}();
44919
+ //# debugId=b75c36e9-f19d-5127-be1a-89ac93910ff0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.7.6-dev.1f14fa1",
3
+ "version": "0.7.6-dev.3f22fe3",
4
4
  "description": "Your agent writes bad React. This catches it",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -65,8 +65,8 @@
65
65
  "vscode-languageserver-textdocument": "^1.0.12",
66
66
  "vscode-uri": "^3.1.0",
67
67
  "yaml": "^2.9.0",
68
- "oxlint-plugin-react-doctor": "0.7.6-dev.1f14fa1",
69
- "deslop-js": "0.7.6"
68
+ "deslop-js": "0.7.6",
69
+ "oxlint-plugin-react-doctor": "0.7.6-dev.3f22fe3"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/babel__code-frame": "^7.27.0",