react-doctor 0.9.3-dev.4e4740d → 0.9.3-dev.7ce5dfa

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.
@@ -1,6 +1,6 @@
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]="e34d02bb-f20a-5479-99b2-18946ed44afb")}catch(e){}}();
3
- import { $ as highlighter, A as hasLintHardFailure, B as isUnicodeSupported, C as shouldRecordOnboarding, D as buildCodeFrame, E as formatElapsedTime, F as resolveBlockingLevel, G as isCiEnvironment, H as METRIC, I as isReactDoctorWorkflowInstalled, J as Progress, K as DIAGNOSTIC_CATEGORY_BUCKETS, L as recordCount, M as buildSortedRuleGroups, N as formatFixRecipeLine, O as buildEmptyReportMessage, P as formatLearnMoreLine, Q as hasPublishedFixRecipe, R as toForwardSlashes, S as isOnboardingForced, T as computeProjectedScore, U as TUI_REPORT_COMPACT_MAX_ROWS, V as CI_TRUST_COMPANIES, W as TUI_REPORT_DETAIL_WIDTH_FRACTION, X as buildRuleDocsUrl, Y as Reporter, Z as getCategoryImpact, _ as printFooter, a as resolveCliInspectOptions, at as void_, b as markOnboardingComplete, c as setUpGitHubActions, d as CLI_AGENT_BINARIES, et as mapWithConcurrency, f as copyToClipboard, g as createInvocationInspect, h as isShareOptedOut, i as selectProjects, it as sync, j as buildRulePriorityMap, k as buildNoScoreMessage, l as openUrl, m as buildHandoffPayload, n as shouldFailScanGate, nt as resolveScanTarget, o as filterScansForSurface, ot as succeed, p as launchCliAgent, q as GITHUB_ACTIONS_SETUP_URL, r as discoverWorkspacePackages, rt as runPromise, s as countUniqueScannedFiles, t as exitGracefully, tt as mergeReactDoctorConfigs, u as detectLaunchableAgents, v as easeOutCubic, w as filterDiagnosticsByCategories, x as canAnimateOnboarding, y as doctorFace, z as findNearestPackageDirectory } from "./cli.js";
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]="536bbbaf-ba97-5df5-b678-926826008e2e")}catch(e){}}();
3
+ import { $ as TUI_REPORT_DETAIL_WIDTH_FRACTION, A as shouldRecordOnboarding, B as buildSortedRuleGroups, C as createInvocationInspect, D as markOnboardingComplete, E as doctorFace, F as buildEmptyReportMessage, G as recordCount, H as formatLearnMoreLine, I as hasIncompleteScoreAnalysis, J as findNearestPackageDirectory, K as recordDistribution, L as buildNoScoreMessage, M as computeProjectedScore, N as formatElapsedTime, O as canAnimateOnboarding, P as buildCodeFrame, Q as TUI_REPORT_COMPACT_MAX_ROWS, R as hasLintHardFailure, S as deduplicateProjectScans, T as easeOutCubic, U as resolveBlockingLevel, V as formatFixRecipeLine, W as isReactDoctorWorkflowInstalled, X as CI_TRUST_COMPANIES, Y as isUnicodeSupported, Z as METRIC, _ as launchCliAgent, _t as succeed, a as selectProjects, at as buildRuleDocsUrl, b as formatSkippedProjectsMessage, c as resolveWorkspaceDeadCodeOwner, ct as highlighter, d as countUniqueScannedFiles, dt as remainingDeadlineBudgetMs, et as isCiEnvironment, f as setUpGitHubActions, ft as resolveScanTarget, g as copyToClipboard, gt as void_, h as CLI_AGENT_BINARIES, ht as sync, i as discoverWorkspacePackages, it as Reporter, j as filterDiagnosticsByCategories, k as isOnboardingForced, l as resolveCliInspectOptions, lt as isPathInsideDirectory, m as detectLaunchableAgents, mt as runPromise, n as registerActiveTuiRenderer, nt as GITHUB_ACTIONS_SETUP_URL, o as resolveProjectScan, ot as getCategoryImpact, p as openUrl, pt as yieldToEventLoop, q as toForwardSlashes, r as shouldFailScanGate, rt as Progress, s as retryMissingProjectScores, st as hasPublishedFixRecipe, t as exitGracefully, tt as DIAGNOSTIC_CATEGORY_BUCKETS, u as filterScansForSurface, ut as mapWithConcurrency, v as buildHandoffPayload, w as printFooter, x as collectProjectSourceFileCounts, y as isShareOptedOut, z as buildRulePriorityMap } from "./cli.js";
4
4
  import path from "node:path";
5
5
  import { performance } from "node:perf_hooks";
6
6
  import { getSkillAgentConfig } from "agent-install";
@@ -40,11 +40,9 @@ const resolveVisibleStart = ({ itemCount, offset, selectedIndex, viewportHeight
40
40
  //#region src/cli/ink/hooks/use-exit-on-ctrl-c.ts
41
41
  const SHOW_CURSOR = "\x1B[?25h";
42
42
  const useExitOnCtrlC = () => {
43
- const { exit } = useApp();
44
43
  useInput((input, key) => {
45
44
  if (key.ctrl && input === "c") {
46
45
  recordCount(METRIC.tuiCancelled, 1, { resourceFooter: true });
47
- exit();
48
46
  process.stdin.setRawMode?.(false);
49
47
  process.stdout.write(SHOW_CURSOR);
50
48
  exitGracefully();
@@ -1178,9 +1176,10 @@ const HandoffCiRecommendation = ({ onAddToCi, onContinue, onQuit }) => {
1178
1176
  };
1179
1177
  //#endregion
1180
1178
  //#region src/cli/ink/components/report-landing.tsx
1181
- const ReportLanding = ({ header, phase, issueCount, emptyStateMessage, lintFailureReason, skippedChecks, actions, selectedIndex, onSelectionChange, onQuit }) => {
1179
+ const ReportLanding = ({ header, phase, issueCount, emptyStateMessage, lintFailureReason, skippedChecks, incompleteMessage, actions, selectedIndex, onSelectionChange, onQuit }) => {
1182
1180
  const showScore = phase === "actions" || phase === "score";
1183
- const skippedCheckLabel = skippedChecks?.join(" and ");
1181
+ const skippedCheckLabel = skippedChecks?.filter((skippedCheck) => skippedCheck !== "lint" || !lintFailureReason).join(" and ");
1182
+ const hasIncompleteResult = Boolean(incompleteMessage || lintFailureReason || skippedCheckLabel);
1184
1183
  useInput((input) => {
1185
1184
  if (input === "q") onQuit();
1186
1185
  }, { isActive: phase !== "actions" });
@@ -1188,22 +1187,32 @@ const ReportLanding = ({ header, phase, issueCount, emptyStateMessage, lintFailu
1188
1187
  flexDirection: "column",
1189
1188
  children: [
1190
1189
  showScore ? header : null,
1191
- issueCount === 0 ? /* @__PURE__ */ jsx(Box, {
1190
+ hasIncompleteResult || issueCount === 0 ? /* @__PURE__ */ jsxs(Box, {
1191
+ flexDirection: "column",
1192
1192
  marginTop: 1,
1193
- children: lintFailureReason ? /* @__PURE__ */ jsxs(Text, {
1194
- color: "yellow",
1195
- children: ["⚠ Lint did not run: ", lintFailureReason]
1196
- }) : skippedCheckLabel ? /* @__PURE__ */ jsxs(Text, {
1197
- color: "yellow",
1198
- children: [
1199
- "⚠ No issues detected, but ",
1200
- skippedCheckLabel,
1201
- " checks failed — results are incomplete."
1202
- ]
1203
- }) : /* @__PURE__ */ jsxs(Text, {
1204
- color: "green",
1205
- children: ["✔ ", emptyStateMessage ?? "No issues found. Nice work."]
1206
- })
1193
+ children: [
1194
+ incompleteMessage ? /* @__PURE__ */ jsxs(Text, {
1195
+ color: "yellow",
1196
+ children: ["⚠ ", incompleteMessage]
1197
+ }) : null,
1198
+ lintFailureReason ? /* @__PURE__ */ jsxs(Text, {
1199
+ color: "yellow",
1200
+ children: ["⚠ Lint did not run: ", lintFailureReason]
1201
+ }) : null,
1202
+ skippedCheckLabel ? /* @__PURE__ */ jsxs(Text, {
1203
+ color: "yellow",
1204
+ children: [
1205
+ "⚠ ",
1206
+ issueCount === 0 ? "No issues detected, but " : "",
1207
+ skippedCheckLabel,
1208
+ " checks failed — results are incomplete."
1209
+ ]
1210
+ }) : null,
1211
+ issueCount === 0 && !hasIncompleteResult ? /* @__PURE__ */ jsxs(Text, {
1212
+ color: "green",
1213
+ children: ["✔ ", emptyStateMessage ?? "No issues found. Nice work."]
1214
+ }) : null
1215
+ ]
1207
1216
  }) : null,
1208
1217
  phase === "actions" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ActionMenu, {
1209
1218
  actions,
@@ -1357,12 +1366,13 @@ const ScoreHeader = ({ variant, score, projectedScore, projectName, issueCount,
1357
1366
  if (!score) return /* @__PURE__ */ jsxs(Box, {
1358
1367
  flexDirection: "column",
1359
1368
  paddingLeft: 2,
1369
+ width: availableWidth,
1360
1370
  children: [/* @__PURE__ */ jsx(Text, {
1361
1371
  wrap: "truncate-end",
1362
1372
  children: /* @__PURE__ */ jsx(ReactDoctorLink, {})
1363
1373
  }), /* @__PURE__ */ jsx(Text, {
1364
1374
  dimColor: true,
1365
- wrap: "truncate-end",
1375
+ wrap: "wrap",
1366
1376
  children: noScoreMessage ?? `${pluralize(issueCount, "finding")} · ${projectName}`
1367
1377
  })]
1368
1378
  });
@@ -1648,6 +1658,7 @@ const Report = ({ report, onExit, onQuit = onExit, launchableAgents = EMPTY_LAUN
1648
1658
  emptyStateMessage: report.emptyStateMessage,
1649
1659
  lintFailureReason: report.lintFailureReason,
1650
1660
  skippedChecks: report.skippedChecks,
1661
+ incompleteMessage: report.incompleteMessage,
1651
1662
  actions: landingActions,
1652
1663
  selectedIndex: resolvedLandingSelectedIndex,
1653
1664
  onSelectionChange: setLandingSelectedIndex,
@@ -1683,26 +1694,15 @@ const Report = ({ report, onExit, onQuit = onExit, launchableAgents = EMPTY_LAUN
1683
1694
  };
1684
1695
  //#endregion
1685
1696
  //#region src/cli/ink/components/scanning.tsx
1686
- const Scanning = ({ progressText, liveCount, recent }) => {
1697
+ const Scanning = ({ progressText, recent }) => {
1687
1698
  return /* @__PURE__ */ jsxs(Box, {
1688
1699
  flexDirection: "column",
1689
1700
  children: [/* @__PURE__ */ jsxs(Text, {
1690
1701
  wrap: "truncate-end",
1691
- children: [
1692
- /* @__PURE__ */ jsx(Text, {
1693
- color: "cyan",
1694
- children: /* @__PURE__ */ jsx(Spinner, { type: "dots" })
1695
- }),
1696
- /* @__PURE__ */ jsxs(Text, { children: [" ", progressText ?? "Scanning…"] }),
1697
- liveCount > 0 ? /* @__PURE__ */ jsxs(Text, {
1698
- dimColor: true,
1699
- children: [
1700
- " · ",
1701
- liveCount,
1702
- " found"
1703
- ]
1704
- }) : null
1705
- ]
1702
+ children: [/* @__PURE__ */ jsx(Text, {
1703
+ color: "cyan",
1704
+ children: /* @__PURE__ */ jsx(Spinner, { type: "dots" })
1705
+ }), /* @__PURE__ */ jsxs(Text, { children: [" ", progressText ?? "Scanning…"] })]
1706
1706
  }), recent.map((diagnostic, diagnosticIndex) => {
1707
1707
  const variant = severityVariant(diagnostic.severity === "error" ? "error" : "warning");
1708
1708
  const location = formatDiagnosticSite(diagnostic);
@@ -1727,6 +1727,7 @@ const Scanning = ({ progressText, liveCount, recent }) => {
1727
1727
  //#endregion
1728
1728
  //#region src/cli/ink/components/summary.tsx
1729
1729
  const Summary = ({ summary, onExit, onQuit, launchableAgents, onHandoff, canAddToCi, onAddToCi }) => {
1730
+ const skippedProjects = summary.skippedProjects ?? [];
1730
1731
  return /* @__PURE__ */ jsx(Report, {
1731
1732
  report: {
1732
1733
  diagnostics: summary.combinedDiagnostics,
@@ -1739,6 +1740,7 @@ const Summary = ({ summary, onExit, onQuit, launchableAgents, onHandoff, canAddT
1739
1740
  isOffline: summary.isOffline,
1740
1741
  noScoreMessage: summary.noScoreMessage,
1741
1742
  skippedChecks: [...new Set(summary.projects.flatMap((project) => project.skippedChecks ?? []))],
1743
+ ...skippedProjects.length > 0 ? { incompleteMessage: formatSkippedProjectsMessage(skippedProjects.length) } : {},
1742
1744
  ...summary.emptyStateMessage ? { emptyStateMessage: summary.emptyStateMessage } : {},
1743
1745
  ...summary.lintFailureReason ? { lintFailureReason: summary.lintFailureReason } : {}
1744
1746
  },
@@ -1748,7 +1750,7 @@ const Summary = ({ summary, onExit, onQuit, launchableAgents, onHandoff, canAddT
1748
1750
  onHandoff,
1749
1751
  canAddToCi,
1750
1752
  onAddToCi,
1751
- projectCount: summary.projects.length,
1753
+ projectCount: summary.projects.length + skippedProjects.length,
1752
1754
  priorityScores: summary.projects.map((project) => project.score)
1753
1755
  });
1754
1756
  };
@@ -1757,7 +1759,7 @@ const Summary = ({ summary, onExit, onQuit, launchableAgents, onHandoff, canAddT
1757
1759
  const useScanStore = (store) => useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
1758
1760
  //#endregion
1759
1761
  //#region src/cli/ink/scan-app.tsx
1760
- const ScanApp = ({ store, launchableAgents, onHandoff, canAddToCi, onAddToCi, onQuit }) => {
1762
+ const ScanApp = ({ store, displayMode = "report", launchableAgents, onHandoff, canAddToCi, onAddToCi, onQuit }) => {
1761
1763
  const snapshot = useScanStore(store);
1762
1764
  const { exit } = useApp();
1763
1765
  useExitOnCtrlC();
@@ -1765,7 +1767,7 @@ const ScanApp = ({ store, launchableAgents, onHandoff, canAddToCi, onAddToCi, on
1765
1767
  onQuit?.();
1766
1768
  exit();
1767
1769
  };
1768
- if (snapshot.phase === "summary" && snapshot.summary) return /* @__PURE__ */ jsx(Summary, {
1770
+ if (displayMode === "report" && snapshot.phase === "summary" && snapshot.summary) return /* @__PURE__ */ jsx(Summary, {
1769
1771
  summary: snapshot.summary,
1770
1772
  launchableAgents,
1771
1773
  onHandoff,
@@ -1774,7 +1776,7 @@ const ScanApp = ({ store, launchableAgents, onHandoff, canAddToCi, onAddToCi, on
1774
1776
  onExit: exit,
1775
1777
  onQuit: handleQuit
1776
1778
  });
1777
- if (snapshot.phase === "report" && snapshot.report) return /* @__PURE__ */ jsx(Report, {
1779
+ if (displayMode === "report" && snapshot.phase === "report" && snapshot.report) return /* @__PURE__ */ jsx(Report, {
1778
1780
  report: snapshot.report,
1779
1781
  launchableAgents,
1780
1782
  onHandoff,
@@ -1785,7 +1787,6 @@ const ScanApp = ({ store, launchableAgents, onHandoff, canAddToCi, onAddToCi, on
1785
1787
  });
1786
1788
  return /* @__PURE__ */ jsx(Scanning, {
1787
1789
  progressText: snapshot.progress,
1788
- liveCount: snapshot.liveCount,
1789
1790
  recent: snapshot.liveDiagnostics.slice(-5)
1790
1791
  });
1791
1792
  };
@@ -1795,22 +1796,22 @@ const reporterLayerForStore = (store) => succeed(Reporter, Reporter.of({
1795
1796
  emit: (diagnostic) => sync(() => store.emitDiagnostic(diagnostic)),
1796
1797
  finalize: void_
1797
1798
  }));
1798
- const progressHandleForStore = (store) => (text) => {
1799
- store.setProgress(text);
1799
+ const progressHandleForStore = (store, options = {}) => (text) => {
1800
+ const transformText = options.transformText ?? ((displayText) => displayText);
1801
+ store.setProgress(transformText(text));
1800
1802
  return {
1801
- update: (displayText) => sync(() => store.setProgress(displayText)),
1802
- succeed: (displayText) => sync(() => store.setProgress(displayText)),
1803
- fail: (displayText) => sync(() => store.setProgress(displayText)),
1804
- stop: () => sync(() => store.setProgress(null))
1803
+ update: (displayText) => sync(() => store.scheduleProgress(transformText(displayText))),
1804
+ succeed: (displayText) => sync(() => store.setProgress(transformText(displayText))),
1805
+ fail: (displayText) => sync(() => store.setProgress(transformText(displayText))),
1806
+ stop: () => sync(() => store.setProgress(options.shouldClearOnStop === false ? transformText(text) : null))
1805
1807
  };
1806
1808
  };
1807
- const progressLayerForStore = (store) => Progress.layerOra(progressHandleForStore(store));
1809
+ const progressLayerForStore = (store, options = {}) => Progress.layerOra(progressHandleForStore(store, options));
1808
1810
  //#endregion
1809
1811
  //#region src/cli/ink/scan-store.ts
1810
1812
  const INITIAL_SNAPSHOT = {
1811
1813
  phase: "scanning",
1812
1814
  liveDiagnostics: [],
1813
- liveCount: 0,
1814
1815
  progress: null,
1815
1816
  report: null,
1816
1817
  summary: null
@@ -1818,10 +1819,25 @@ const INITIAL_SNAPSHOT = {
1818
1819
  const createScanStore = () => {
1819
1820
  let snapshot = INITIAL_SNAPSHOT;
1820
1821
  const listeners = /* @__PURE__ */ new Set();
1822
+ let pendingProgress = null;
1823
+ let progressTimer = null;
1821
1824
  const commit = (next) => {
1822
1825
  snapshot = next;
1823
1826
  for (const listener of listeners) listener();
1824
1827
  };
1828
+ const cancelPendingProgress = () => {
1829
+ pendingProgress = null;
1830
+ if (progressTimer === null) return;
1831
+ clearTimeout(progressTimer);
1832
+ progressTimer = null;
1833
+ };
1834
+ const setProgress = (progress) => {
1835
+ cancelPendingProgress();
1836
+ commit({
1837
+ ...snapshot,
1838
+ progress
1839
+ });
1840
+ };
1825
1841
  return {
1826
1842
  subscribe: (listener) => {
1827
1843
  listeners.add(listener);
@@ -1832,37 +1848,43 @@ const createScanStore = () => {
1832
1848
  getSnapshot: () => snapshot,
1833
1849
  emitDiagnostic: (diagnostic) => commit({
1834
1850
  ...snapshot,
1835
- liveDiagnostics: [...snapshot.liveDiagnostics, diagnostic].slice(-25),
1836
- liveCount: snapshot.liveCount + 1
1851
+ liveDiagnostics: [...snapshot.liveDiagnostics, diagnostic].slice(-25)
1837
1852
  }),
1838
- setProgress: (progress) => commit({
1839
- ...snapshot,
1840
- progress
1841
- }),
1842
- setReport: (report) => commit({
1843
- ...snapshot,
1844
- report,
1845
- phase: "report"
1846
- }),
1847
- setSummary: (summary) => commit({
1848
- ...snapshot,
1849
- summary,
1850
- phase: "summary"
1851
- })
1853
+ scheduleProgress: (progress) => {
1854
+ pendingProgress = progress;
1855
+ if (progressTimer !== null) return;
1856
+ progressTimer = setTimeout(() => {
1857
+ progressTimer = null;
1858
+ const progressToCommit = pendingProgress;
1859
+ pendingProgress = null;
1860
+ if (progressToCommit !== null) commit({
1861
+ ...snapshot,
1862
+ progress: progressToCommit
1863
+ });
1864
+ }, 250);
1865
+ progressTimer.unref?.();
1866
+ },
1867
+ setProgress,
1868
+ setReport: (report) => {
1869
+ cancelPendingProgress();
1870
+ commit({
1871
+ ...snapshot,
1872
+ report,
1873
+ phase: "report"
1874
+ });
1875
+ },
1876
+ setSummary: (summary) => {
1877
+ cancelPendingProgress();
1878
+ commit({
1879
+ ...snapshot,
1880
+ summary,
1881
+ phase: "summary"
1882
+ });
1883
+ }
1852
1884
  };
1853
1885
  };
1854
1886
  //#endregion
1855
1887
  //#region src/cli/ink/run-scan-app.tsx
1856
- const resolveProjectScan = async (rootScanTarget, projectDirectory) => {
1857
- const projectScanTarget = projectDirectory === rootScanTarget.resolvedDirectory ? rootScanTarget : await resolveScanTarget(projectDirectory, { allowAmbiguous: true });
1858
- const config = projectDirectory === rootScanTarget.resolvedDirectory ? rootScanTarget.userConfig : mergeReactDoctorConfigs(rootScanTarget.userConfig, projectScanTarget.userConfig ?? void 0);
1859
- const configSourceDirectory = projectScanTarget.userConfig?.plugins === void 0 ? rootScanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory;
1860
- return {
1861
- directory: projectScanTarget.resolvedDirectory,
1862
- config,
1863
- configSourceDirectory
1864
- };
1865
- };
1866
1888
  const qualifyDiagnosticPaths = (diagnostics, rootDirectory, projectDirectory) => {
1867
1889
  const prefix = path.relative(rootDirectory, projectDirectory);
1868
1890
  if (prefix === "" || prefix.startsWith("..")) return [...diagnostics];
@@ -1875,7 +1897,8 @@ const resolveScanPresentation = (input, projectScans) => {
1875
1897
  const isScoreDisabled = input.options?.noScore === true || projectScans.some((projectScan) => projectScan.config?.noScore === true);
1876
1898
  return {
1877
1899
  isOffline: isCiEnvironment() || input.share === false || isShareOptedOut(projectScans, input.options?.noScore),
1878
- noScoreMessage: buildNoScoreMessage(isScoreDisabled),
1900
+ initialProgress: projectScans.length > 1 ? "Indexing workspace files…" : "Scanning project…",
1901
+ noScoreMessage: buildNoScoreMessage({ isScoreDisabled }),
1879
1902
  shouldRecommendCi: projectScans.length > 1 || projectScans.some((projectScan) => isCiUnconfigured(projectScan.directory))
1880
1903
  };
1881
1904
  };
@@ -1895,15 +1918,31 @@ const resolveSelectedDirectories = async (rootDirectory, input) => {
1895
1918
  return promptProjectSelection(packages, rootDirectory);
1896
1919
  };
1897
1920
  const promptProjectSelection = (packages, rootDirectory) => new Promise((resolve) => {
1921
+ let disposeRenderer = () => {};
1922
+ recordCount(METRIC.tuiProjectSelectInlineShown);
1898
1923
  const instance = render(/* @__PURE__ */ jsx(ProjectSelect, {
1899
1924
  packages,
1900
1925
  rootDirectory,
1901
1926
  onSubmit: (directories) => {
1902
- instance.clear();
1903
- instance.unmount();
1927
+ disposeRenderer();
1904
1928
  resolve(directories);
1905
1929
  }
1906
- }), { exitOnCtrlC: false });
1930
+ }), {
1931
+ alternateScreen: false,
1932
+ exitOnCtrlC: false
1933
+ });
1934
+ let didClearRenderer = false;
1935
+ const clearRenderer = () => {
1936
+ if (didClearRenderer) return;
1937
+ didClearRenderer = true;
1938
+ instance.clear();
1939
+ instance.unmount();
1940
+ };
1941
+ const unregisterActiveTuiRenderer = registerActiveTuiRenderer({ clear: clearRenderer });
1942
+ disposeRenderer = () => {
1943
+ unregisterActiveTuiRenderer();
1944
+ clearRenderer();
1945
+ };
1907
1946
  });
1908
1947
  const resolveLintFailureReason = (results) => {
1909
1948
  for (const result of results) {
@@ -1974,28 +2013,50 @@ const performCiSetup = async (rootDirectory) => {
1974
2013
  created: didCreateWorkflow
1975
2014
  });
1976
2015
  };
1977
- const mountScanApp = async (rootDirectory, shouldRecommendCi) => {
2016
+ const mountScanApp = async (rootDirectory, shouldRecommendCi, initialProgress) => {
1978
2017
  const store = createScanStore();
2018
+ store.setProgress(initialProgress);
1979
2019
  const launchableAgents = await detectLaunchableAgents();
1980
2020
  const pendingActions = {
1981
2021
  handoffRequest: null,
1982
2022
  shouldSetUpCi: false,
1983
2023
  didQuit: false
1984
2024
  };
1985
- const instance = render(/* @__PURE__ */ jsx(ScanApp, {
1986
- store,
1987
- launchableAgents,
1988
- onHandoff: (request) => {
1989
- pendingActions.handoffRequest = request;
1990
- },
1991
- canAddToCi: shouldRecommendCi,
1992
- onAddToCi: () => {
1993
- pendingActions.shouldSetUpCi = true;
1994
- },
1995
- onQuit: () => {
1996
- pendingActions.didQuit = true;
1997
- }
1998
- }), { exitOnCtrlC: false });
2025
+ const mountRenderer = (displayMode) => {
2026
+ const instance = render(/* @__PURE__ */ jsx(ScanApp, {
2027
+ store,
2028
+ displayMode,
2029
+ launchableAgents,
2030
+ onHandoff: (request) => {
2031
+ pendingActions.handoffRequest = request;
2032
+ },
2033
+ canAddToCi: shouldRecommendCi,
2034
+ onAddToCi: () => {
2035
+ pendingActions.shouldSetUpCi = true;
2036
+ },
2037
+ onQuit: () => {
2038
+ pendingActions.didQuit = true;
2039
+ }
2040
+ }), {
2041
+ alternateScreen: false,
2042
+ exitOnCtrlC: false
2043
+ });
2044
+ let didClearRenderer = false;
2045
+ const clearRenderer = () => {
2046
+ if (didClearRenderer) return;
2047
+ didClearRenderer = true;
2048
+ instance.clear();
2049
+ instance.unmount();
2050
+ };
2051
+ const unregisterActiveTuiRenderer = registerActiveTuiRenderer({ clear: clearRenderer });
2052
+ return {
2053
+ instance,
2054
+ dispose: () => {
2055
+ unregisterActiveTuiRenderer();
2056
+ clearRenderer();
2057
+ }
2058
+ };
2059
+ };
1999
2060
  const executePendingActions = async () => {
2000
2061
  if (pendingActions.shouldSetUpCi) await performCiSetup(rootDirectory);
2001
2062
  if (pendingActions.didQuit) return;
@@ -2003,20 +2064,25 @@ const mountScanApp = async (rootDirectory, shouldRecommendCi) => {
2003
2064
  };
2004
2065
  return {
2005
2066
  store,
2006
- instance,
2007
2067
  pendingActions,
2068
+ mountRenderer,
2008
2069
  executePendingActions
2009
2070
  };
2010
2071
  };
2011
2072
  const runMountedScan = async (rootDirectory, presentation, blockingLevel, executeScan) => {
2012
- const { store, instance, pendingActions, executePendingActions } = await mountScanApp(rootDirectory, presentation.shouldRecommendCi);
2073
+ const { store, pendingActions, mountRenderer, executePendingActions } = await mountScanApp(rootDirectory, presentation.shouldRecommendCi, presentation.initialProgress);
2074
+ let mountedRenderer = mountRenderer("scan");
2075
+ recordCount(METRIC.tuiScanInlineShown);
2013
2076
  const context = {
2014
2077
  store,
2015
2078
  ...presentation
2016
2079
  };
2017
2080
  try {
2018
2081
  const completedScan = await executeScan(context);
2019
- await instance.waitUntilExit();
2082
+ mountedRenderer.dispose();
2083
+ mountedRenderer = mountRenderer("report");
2084
+ await mountedRenderer.instance.waitUntilExit();
2085
+ mountedRenderer.dispose();
2020
2086
  if (!pendingActions.didQuit) await printExitFooter({
2021
2087
  diagnostics: completedScan.diagnostics,
2022
2088
  scoreResult: completedScan.scoreResult,
@@ -2032,7 +2098,7 @@ const runMountedScan = async (rootDirectory, presentation, blockingLevel, execut
2032
2098
  blockingLevel
2033
2099
  }) };
2034
2100
  } catch (error) {
2035
- instance.unmount();
2101
+ mountedRenderer.dispose();
2036
2102
  throw error;
2037
2103
  }
2038
2104
  };
@@ -2069,7 +2135,11 @@ const runSingleProjectScan = async (rootScanTarget, projectDirectory, input, blo
2069
2135
  rootDirectory: projectScan.directory,
2070
2136
  projectedScore,
2071
2137
  isOffline: context.isOffline,
2072
- noScoreMessage: context.noScoreMessage,
2138
+ noScoreMessage: buildNoScoreMessage({
2139
+ isScoreDisabled: input.options?.noScore === true || projectScan.config?.noScore === true,
2140
+ isAnalysisIncomplete: hasIncompleteScoreAnalysis(result.skippedChecks),
2141
+ disabledMessage: input.options?.scoreDisabledMessage
2142
+ }),
2073
2143
  emptyStateMessage: resolveEmptyStateMessage(input, reportSelection.demotedDiagnosticCount)
2074
2144
  }));
2075
2145
  return {
@@ -2086,29 +2156,81 @@ const runSingleProjectScan = async (rootScanTarget, projectDirectory, input, blo
2086
2156
  });
2087
2157
  };
2088
2158
  const runMultiProjectScan = async (rootScanTarget, directories, input, blockingLevel, inspectProject) => {
2159
+ const feedbackStartTime = performance.now();
2089
2160
  const rootDirectory = rootScanTarget.resolvedDirectory;
2090
- const projectScans = await mapWithConcurrency([...directories], 4, (projectDirectory) => resolveProjectScan(rootScanTarget, projectDirectory));
2161
+ const projectScans = deduplicateProjectScans(await mapWithConcurrency([...directories], 4, (projectDirectory) => resolveProjectScan(rootScanTarget, projectDirectory)));
2162
+ const projectCount = projectScans.length;
2163
+ const rootProjectScan = projectScans.find((projectScan) => path.resolve(projectScan.directory) === path.resolve(rootDirectory));
2164
+ const workspaceDeadCodeOwner = resolveWorkspaceDeadCodeOwner({
2165
+ rootDirectory,
2166
+ projectDirectories: projectScans.map((projectScan) => projectScan.directory),
2167
+ isRootDeadCodeEnabled: input.options?.deadCode ?? rootProjectScan?.config?.deadCode ?? true
2168
+ });
2169
+ if (workspaceDeadCodeOwner !== null) recordCount(METRIC.scanWorkspaceDeadCodeShared, 1, { projectCount });
2091
2170
  return runMountedScan(rootDirectory, resolveScanPresentation(input, projectScans), blockingLevel, async (context) => {
2092
2171
  const startTime = performance.now();
2093
2172
  let finishedCount = 0;
2094
- context.store.setProgress(`Scanning ${directories.length} projects…`);
2095
- const results = await mapWithConcurrency(projectScans, 4, async (projectScan) => {
2173
+ recordDistribution(METRIC.scanFeedbackDelay, performance.now() - feedbackStartTime, {
2174
+ unit: "millisecond",
2175
+ attributes: {
2176
+ surface: "tui",
2177
+ projectCount
2178
+ }
2179
+ });
2180
+ context.store.setProgress(`Scanning ${projectCount} projects…`);
2181
+ await yieldToEventLoop();
2182
+ const precomputedSourceFileCounts = await collectProjectSourceFileCounts(rootDirectory, projectScans.map((projectScan) => projectScan.directory));
2183
+ const scanOutcomes = await mapWithConcurrency(projectScans, 4, async (projectScan) => {
2184
+ if (input.options?.deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(input.options.deadlineEpochMs) === 0) {
2185
+ finishedCount += 1;
2186
+ context.store.setProgress(`Scanning ${projectCount} projects… (${finishedCount}/${projectCount})`);
2187
+ return {
2188
+ status: "skipped",
2189
+ directory: projectScan.directory,
2190
+ reason: "max-duration"
2191
+ };
2192
+ }
2193
+ const projectLabel = path.relative(rootDirectory, projectScan.directory) || path.basename(rootDirectory);
2194
+ const formatProjectProgress = (displayText) => `Scanning ${projectCount} projects… (${finishedCount}/${projectCount}) · ${projectLabel}: ${displayText}`;
2195
+ const inspectOptions = resolveTuiInspectOptions(input, projectScan.config);
2196
+ const ownsWorkspaceDeadCode = projectScan.directory === workspaceDeadCodeOwner;
2096
2197
  const result = await inspectProject(projectScan.directory, {
2097
- ...resolveTuiInspectOptions(input, projectScan.config),
2198
+ ...inspectOptions,
2199
+ deadCode: workspaceDeadCodeOwner === null ? inspectOptions.deadCode : ownsWorkspaceDeadCode,
2098
2200
  isCi: isCiEnvironment(),
2099
2201
  configOverride: projectScan.config,
2100
2202
  configSourceDirectory: projectScan.configSourceDirectory ?? void 0,
2101
- uiLayers: { reporter: Reporter.layerNoop },
2102
- concurrentScan: true
2203
+ precomputedSourceFileCount: precomputedSourceFileCounts.get(projectScan.directory),
2204
+ uiLayers: {
2205
+ reporter: Reporter.layerNoop,
2206
+ progress: progressLayerForStore(context.store, {
2207
+ transformText: formatProjectProgress,
2208
+ shouldClearOnStop: false
2209
+ })
2210
+ },
2211
+ concurrentScan: true,
2212
+ excludedProjectDirectories: projectScans.filter((candidateProjectScan) => isPathInsideDirectory(candidateProjectScan.directory, projectScan.directory)).map((candidateProjectScan) => candidateProjectScan.directory),
2213
+ retainExcludedProjectDeadCodeDiagnostics: ownsWorkspaceDeadCode
2103
2214
  });
2104
2215
  finishedCount += 1;
2105
- context.store.setProgress(`Scanning ${directories.length} projects… (${finishedCount}/${directories.length})`);
2216
+ context.store.setProgress(`Scanning ${projectCount} projects… (${finishedCount}/${projectCount})`);
2217
+ await yieldToEventLoop();
2106
2218
  return {
2219
+ status: "completed",
2107
2220
  directory: projectScan.directory,
2108
2221
  result,
2109
2222
  config: projectScan.config
2110
2223
  };
2111
2224
  });
2225
+ const results = await retryMissingProjectScores(scanOutcomes.filter((scanOutcome) => scanOutcome.status === "completed").map((completedScan) => ({
2226
+ ...completedScan,
2227
+ isScoreDisabled: input.options?.noScore ?? completedScan.config?.noScore ?? false
2228
+ })));
2229
+ const skippedProjects = scanOutcomes.filter((scanOutcome) => scanOutcome.status === "skipped").map(({ directory, reason }) => ({
2230
+ directory,
2231
+ reason
2232
+ })).sort((left, right) => left.directory.localeCompare(right.directory));
2233
+ if (skippedProjects.length > 0) recordCount(METRIC.scanProjectSkipped, skippedProjects.length, { reason: "max-duration" });
2112
2234
  const projectEntries = results.map(({ directory, result, config }) => {
2113
2235
  const reportSelection = selectReportDiagnostics({
2114
2236
  scan: {
@@ -2125,7 +2247,11 @@ const runMultiProjectScan = async (rootScanTarget, directories, input, blockingL
2125
2247
  rootDirectory: directory,
2126
2248
  projectedScore: null,
2127
2249
  isOffline: context.isOffline,
2128
- noScoreMessage: context.noScoreMessage,
2250
+ noScoreMessage: buildNoScoreMessage({
2251
+ isScoreDisabled: input.options?.noScore === true || config?.noScore === true,
2252
+ isAnalysisIncomplete: hasIncompleteScoreAnalysis(result.skippedChecks),
2253
+ disabledMessage: input.options?.scoreDisabledMessage
2254
+ }),
2129
2255
  emptyStateMessage: resolveEmptyStateMessage(input, reportSelection.demotedDiagnosticCount)
2130
2256
  }),
2131
2257
  score: result.score,
@@ -2145,6 +2271,7 @@ const runMultiProjectScan = async (rootScanTarget, directories, input, blockingL
2145
2271
  const lintFailureReason = resolveLintFailureReason(results.map(({ result }) => result));
2146
2272
  const summary = {
2147
2273
  projects,
2274
+ skippedProjects,
2148
2275
  aggregateScore: lowestScoredReport?.score ?? null,
2149
2276
  projectedScore,
2150
2277
  combinedDiagnostics,
@@ -2153,7 +2280,11 @@ const runMultiProjectScan = async (rootScanTarget, directories, input, blockingL
2153
2280
  projectName: path.basename(rootDirectory),
2154
2281
  rootDirectory,
2155
2282
  isOffline: context.isOffline,
2156
- noScoreMessage: context.noScoreMessage,
2283
+ noScoreMessage: buildNoScoreMessage({
2284
+ isScoreDisabled: input.options?.noScore === true || results.some(({ config }) => config?.noScore === true),
2285
+ isAnalysisIncomplete: results.some(({ result }) => hasIncompleteScoreAnalysis(result.skippedChecks)),
2286
+ disabledMessage: input.options?.scoreDisabledMessage
2287
+ }),
2157
2288
  emptyStateMessage: resolveEmptyStateMessage(input, projectEntries.reduce((total, projectEntry) => total + projectEntry.demotedDiagnosticCount, 0)),
2158
2289
  ...lintFailureReason ? { lintFailureReason } : {}
2159
2290
  };
@@ -2191,5 +2322,5 @@ const runScanApp = async (input) => {
2191
2322
  //#endregion
2192
2323
  export { runScanApp };
2193
2324
 
2194
- //# sourceMappingURL=run-scan-app-DtYeufgr.js.map
2195
- //# debugId=e34d02bb-f20a-5479-99b2-18946ed44afb
2325
+ //# sourceMappingURL=run-scan-app-QbtG43NL.js.map
2326
+ //# debugId=536bbbaf-ba97-5df5-b678-926826008e2e
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.9.3-dev.4e4740d",
3
+ "version": "0.9.3-dev.7ce5dfa",
4
4
  "description": "Your agent writes bad React. This catches it",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -73,7 +73,7 @@
73
73
  "vscode-uri": "^3.1.0",
74
74
  "yaml": "^2.9.0",
75
75
  "deslop-js": "0.9.3",
76
- "oxlint-plugin-react-doctor": "0.9.3-dev.4e4740d"
76
+ "oxlint-plugin-react-doctor": "0.9.3-dev.7ce5dfa"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@types/babel__code-frame": "^7.27.0",
@@ -83,9 +83,9 @@
83
83
  "commander": "^14.0.3",
84
84
  "ink-testing-library": "^4.0.0",
85
85
  "ora": "^9.4.0",
86
- "@react-doctor/api": "0.9.3",
87
86
  "@react-doctor/core": "0.9.3",
88
- "@react-doctor/language-server": "0.9.3"
87
+ "@react-doctor/language-server": "0.9.3",
88
+ "@react-doctor/api": "0.9.3"
89
89
  },
90
90
  "engines": {
91
91
  "node": "^20.19.0 || >=22.13.0"