react-doctor 0.9.3-dev.44922d6 → 0.9.3-dev.48ec9a8

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]="de10285a-30f0-5d70-ad61-327a97f8b1ba")}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,30 @@ 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 = () => {};
1898
1922
  const instance = render(/* @__PURE__ */ jsx(ProjectSelect, {
1899
1923
  packages,
1900
1924
  rootDirectory,
1901
1925
  onSubmit: (directories) => {
1902
- instance.clear();
1903
- instance.unmount();
1926
+ disposeRenderer();
1904
1927
  resolve(directories);
1905
1928
  }
1906
- }), { exitOnCtrlC: false });
1929
+ }), {
1930
+ alternateScreen: true,
1931
+ exitOnCtrlC: false
1932
+ });
1933
+ let didClearRenderer = false;
1934
+ const clearRenderer = () => {
1935
+ if (didClearRenderer) return;
1936
+ didClearRenderer = true;
1937
+ instance.clear();
1938
+ instance.unmount();
1939
+ };
1940
+ const unregisterActiveTuiRenderer = registerActiveTuiRenderer({ clear: clearRenderer });
1941
+ disposeRenderer = () => {
1942
+ unregisterActiveTuiRenderer();
1943
+ clearRenderer();
1944
+ };
1907
1945
  });
1908
1946
  const resolveLintFailureReason = (results) => {
1909
1947
  for (const result of results) {
@@ -1974,28 +2012,50 @@ const performCiSetup = async (rootDirectory) => {
1974
2012
  created: didCreateWorkflow
1975
2013
  });
1976
2014
  };
1977
- const mountScanApp = async (rootDirectory, shouldRecommendCi) => {
2015
+ const mountScanApp = async (rootDirectory, shouldRecommendCi, initialProgress) => {
1978
2016
  const store = createScanStore();
2017
+ store.setProgress(initialProgress);
1979
2018
  const launchableAgents = await detectLaunchableAgents();
1980
2019
  const pendingActions = {
1981
2020
  handoffRequest: null,
1982
2021
  shouldSetUpCi: false,
1983
2022
  didQuit: false
1984
2023
  };
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 });
2024
+ const mountRenderer = (displayMode) => {
2025
+ const instance = render(/* @__PURE__ */ jsx(ScanApp, {
2026
+ store,
2027
+ displayMode,
2028
+ launchableAgents,
2029
+ onHandoff: (request) => {
2030
+ pendingActions.handoffRequest = request;
2031
+ },
2032
+ canAddToCi: shouldRecommendCi,
2033
+ onAddToCi: () => {
2034
+ pendingActions.shouldSetUpCi = true;
2035
+ },
2036
+ onQuit: () => {
2037
+ pendingActions.didQuit = true;
2038
+ }
2039
+ }), {
2040
+ alternateScreen: false,
2041
+ exitOnCtrlC: false
2042
+ });
2043
+ let didClearRenderer = false;
2044
+ const clearRenderer = () => {
2045
+ if (didClearRenderer) return;
2046
+ didClearRenderer = true;
2047
+ instance.clear();
2048
+ instance.unmount();
2049
+ };
2050
+ const unregisterActiveTuiRenderer = registerActiveTuiRenderer({ clear: clearRenderer });
2051
+ return {
2052
+ instance,
2053
+ dispose: () => {
2054
+ unregisterActiveTuiRenderer();
2055
+ clearRenderer();
2056
+ }
2057
+ };
2058
+ };
1999
2059
  const executePendingActions = async () => {
2000
2060
  if (pendingActions.shouldSetUpCi) await performCiSetup(rootDirectory);
2001
2061
  if (pendingActions.didQuit) return;
@@ -2003,20 +2063,25 @@ const mountScanApp = async (rootDirectory, shouldRecommendCi) => {
2003
2063
  };
2004
2064
  return {
2005
2065
  store,
2006
- instance,
2007
2066
  pendingActions,
2067
+ mountRenderer,
2008
2068
  executePendingActions
2009
2069
  };
2010
2070
  };
2011
2071
  const runMountedScan = async (rootDirectory, presentation, blockingLevel, executeScan) => {
2012
- const { store, instance, pendingActions, executePendingActions } = await mountScanApp(rootDirectory, presentation.shouldRecommendCi);
2072
+ const { store, pendingActions, mountRenderer, executePendingActions } = await mountScanApp(rootDirectory, presentation.shouldRecommendCi, presentation.initialProgress);
2073
+ let mountedRenderer = mountRenderer("scan");
2074
+ recordCount(METRIC.tuiScanInlineShown);
2013
2075
  const context = {
2014
2076
  store,
2015
2077
  ...presentation
2016
2078
  };
2017
2079
  try {
2018
2080
  const completedScan = await executeScan(context);
2019
- await instance.waitUntilExit();
2081
+ mountedRenderer.dispose();
2082
+ mountedRenderer = mountRenderer("report");
2083
+ await mountedRenderer.instance.waitUntilExit();
2084
+ mountedRenderer.dispose();
2020
2085
  if (!pendingActions.didQuit) await printExitFooter({
2021
2086
  diagnostics: completedScan.diagnostics,
2022
2087
  scoreResult: completedScan.scoreResult,
@@ -2032,7 +2097,7 @@ const runMountedScan = async (rootDirectory, presentation, blockingLevel, execut
2032
2097
  blockingLevel
2033
2098
  }) };
2034
2099
  } catch (error) {
2035
- instance.unmount();
2100
+ mountedRenderer.dispose();
2036
2101
  throw error;
2037
2102
  }
2038
2103
  };
@@ -2069,7 +2134,11 @@ const runSingleProjectScan = async (rootScanTarget, projectDirectory, input, blo
2069
2134
  rootDirectory: projectScan.directory,
2070
2135
  projectedScore,
2071
2136
  isOffline: context.isOffline,
2072
- noScoreMessage: context.noScoreMessage,
2137
+ noScoreMessage: buildNoScoreMessage({
2138
+ isScoreDisabled: input.options?.noScore === true || projectScan.config?.noScore === true,
2139
+ isAnalysisIncomplete: hasIncompleteScoreAnalysis(result.skippedChecks),
2140
+ disabledMessage: input.options?.scoreDisabledMessage
2141
+ }),
2073
2142
  emptyStateMessage: resolveEmptyStateMessage(input, reportSelection.demotedDiagnosticCount)
2074
2143
  }));
2075
2144
  return {
@@ -2086,29 +2155,81 @@ const runSingleProjectScan = async (rootScanTarget, projectDirectory, input, blo
2086
2155
  });
2087
2156
  };
2088
2157
  const runMultiProjectScan = async (rootScanTarget, directories, input, blockingLevel, inspectProject) => {
2158
+ const feedbackStartTime = performance.now();
2089
2159
  const rootDirectory = rootScanTarget.resolvedDirectory;
2090
- const projectScans = await mapWithConcurrency([...directories], 4, (projectDirectory) => resolveProjectScan(rootScanTarget, projectDirectory));
2160
+ const projectScans = deduplicateProjectScans(await mapWithConcurrency([...directories], 4, (projectDirectory) => resolveProjectScan(rootScanTarget, projectDirectory)));
2161
+ const projectCount = projectScans.length;
2162
+ const rootProjectScan = projectScans.find((projectScan) => path.resolve(projectScan.directory) === path.resolve(rootDirectory));
2163
+ const workspaceDeadCodeOwner = resolveWorkspaceDeadCodeOwner({
2164
+ rootDirectory,
2165
+ projectDirectories: projectScans.map((projectScan) => projectScan.directory),
2166
+ isRootDeadCodeEnabled: input.options?.deadCode ?? rootProjectScan?.config?.deadCode ?? true
2167
+ });
2168
+ if (workspaceDeadCodeOwner !== null) recordCount(METRIC.scanWorkspaceDeadCodeShared, 1, { projectCount });
2091
2169
  return runMountedScan(rootDirectory, resolveScanPresentation(input, projectScans), blockingLevel, async (context) => {
2092
2170
  const startTime = performance.now();
2093
2171
  let finishedCount = 0;
2094
- context.store.setProgress(`Scanning ${directories.length} projects…`);
2095
- const results = await mapWithConcurrency(projectScans, 4, async (projectScan) => {
2172
+ recordDistribution(METRIC.scanFeedbackDelay, performance.now() - feedbackStartTime, {
2173
+ unit: "millisecond",
2174
+ attributes: {
2175
+ surface: "tui",
2176
+ projectCount
2177
+ }
2178
+ });
2179
+ context.store.setProgress(`Scanning ${projectCount} projects…`);
2180
+ await yieldToEventLoop();
2181
+ const precomputedSourceFileCounts = await collectProjectSourceFileCounts(rootDirectory, projectScans.map((projectScan) => projectScan.directory));
2182
+ const scanOutcomes = await mapWithConcurrency(projectScans, 4, async (projectScan) => {
2183
+ if (input.options?.deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(input.options.deadlineEpochMs) === 0) {
2184
+ finishedCount += 1;
2185
+ context.store.setProgress(`Scanning ${projectCount} projects… (${finishedCount}/${projectCount})`);
2186
+ return {
2187
+ status: "skipped",
2188
+ directory: projectScan.directory,
2189
+ reason: "max-duration"
2190
+ };
2191
+ }
2192
+ const projectLabel = path.relative(rootDirectory, projectScan.directory) || path.basename(rootDirectory);
2193
+ const formatProjectProgress = (displayText) => `Scanning ${projectCount} projects… (${finishedCount}/${projectCount}) · ${projectLabel}: ${displayText}`;
2194
+ const inspectOptions = resolveTuiInspectOptions(input, projectScan.config);
2195
+ const ownsWorkspaceDeadCode = projectScan.directory === workspaceDeadCodeOwner;
2096
2196
  const result = await inspectProject(projectScan.directory, {
2097
- ...resolveTuiInspectOptions(input, projectScan.config),
2197
+ ...inspectOptions,
2198
+ deadCode: workspaceDeadCodeOwner === null ? inspectOptions.deadCode : ownsWorkspaceDeadCode,
2098
2199
  isCi: isCiEnvironment(),
2099
2200
  configOverride: projectScan.config,
2100
2201
  configSourceDirectory: projectScan.configSourceDirectory ?? void 0,
2101
- uiLayers: { reporter: Reporter.layerNoop },
2102
- concurrentScan: true
2202
+ precomputedSourceFileCount: precomputedSourceFileCounts.get(projectScan.directory),
2203
+ uiLayers: {
2204
+ reporter: Reporter.layerNoop,
2205
+ progress: progressLayerForStore(context.store, {
2206
+ transformText: formatProjectProgress,
2207
+ shouldClearOnStop: false
2208
+ })
2209
+ },
2210
+ concurrentScan: true,
2211
+ excludedProjectDirectories: projectScans.filter((candidateProjectScan) => isPathInsideDirectory(candidateProjectScan.directory, projectScan.directory)).map((candidateProjectScan) => candidateProjectScan.directory),
2212
+ retainExcludedProjectDeadCodeDiagnostics: ownsWorkspaceDeadCode
2103
2213
  });
2104
2214
  finishedCount += 1;
2105
- context.store.setProgress(`Scanning ${directories.length} projects… (${finishedCount}/${directories.length})`);
2215
+ context.store.setProgress(`Scanning ${projectCount} projects… (${finishedCount}/${projectCount})`);
2216
+ await yieldToEventLoop();
2106
2217
  return {
2218
+ status: "completed",
2107
2219
  directory: projectScan.directory,
2108
2220
  result,
2109
2221
  config: projectScan.config
2110
2222
  };
2111
2223
  });
2224
+ const results = await retryMissingProjectScores(scanOutcomes.filter((scanOutcome) => scanOutcome.status === "completed").map((completedScan) => ({
2225
+ ...completedScan,
2226
+ isScoreDisabled: input.options?.noScore ?? completedScan.config?.noScore ?? false
2227
+ })));
2228
+ const skippedProjects = scanOutcomes.filter((scanOutcome) => scanOutcome.status === "skipped").map(({ directory, reason }) => ({
2229
+ directory,
2230
+ reason
2231
+ })).sort((left, right) => left.directory.localeCompare(right.directory));
2232
+ if (skippedProjects.length > 0) recordCount(METRIC.scanProjectSkipped, skippedProjects.length, { reason: "max-duration" });
2112
2233
  const projectEntries = results.map(({ directory, result, config }) => {
2113
2234
  const reportSelection = selectReportDiagnostics({
2114
2235
  scan: {
@@ -2125,7 +2246,11 @@ const runMultiProjectScan = async (rootScanTarget, directories, input, blockingL
2125
2246
  rootDirectory: directory,
2126
2247
  projectedScore: null,
2127
2248
  isOffline: context.isOffline,
2128
- noScoreMessage: context.noScoreMessage,
2249
+ noScoreMessage: buildNoScoreMessage({
2250
+ isScoreDisabled: input.options?.noScore === true || config?.noScore === true,
2251
+ isAnalysisIncomplete: hasIncompleteScoreAnalysis(result.skippedChecks),
2252
+ disabledMessage: input.options?.scoreDisabledMessage
2253
+ }),
2129
2254
  emptyStateMessage: resolveEmptyStateMessage(input, reportSelection.demotedDiagnosticCount)
2130
2255
  }),
2131
2256
  score: result.score,
@@ -2145,6 +2270,7 @@ const runMultiProjectScan = async (rootScanTarget, directories, input, blockingL
2145
2270
  const lintFailureReason = resolveLintFailureReason(results.map(({ result }) => result));
2146
2271
  const summary = {
2147
2272
  projects,
2273
+ skippedProjects,
2148
2274
  aggregateScore: lowestScoredReport?.score ?? null,
2149
2275
  projectedScore,
2150
2276
  combinedDiagnostics,
@@ -2153,7 +2279,11 @@ const runMultiProjectScan = async (rootScanTarget, directories, input, blockingL
2153
2279
  projectName: path.basename(rootDirectory),
2154
2280
  rootDirectory,
2155
2281
  isOffline: context.isOffline,
2156
- noScoreMessage: context.noScoreMessage,
2282
+ noScoreMessage: buildNoScoreMessage({
2283
+ isScoreDisabled: input.options?.noScore === true || results.some(({ config }) => config?.noScore === true),
2284
+ isAnalysisIncomplete: results.some(({ result }) => hasIncompleteScoreAnalysis(result.skippedChecks)),
2285
+ disabledMessage: input.options?.scoreDisabledMessage
2286
+ }),
2157
2287
  emptyStateMessage: resolveEmptyStateMessage(input, projectEntries.reduce((total, projectEntry) => total + projectEntry.demotedDiagnosticCount, 0)),
2158
2288
  ...lintFailureReason ? { lintFailureReason } : {}
2159
2289
  };
@@ -2191,5 +2321,5 @@ const runScanApp = async (input) => {
2191
2321
  //#endregion
2192
2322
  export { runScanApp };
2193
2323
 
2194
- //# sourceMappingURL=run-scan-app-DtYeufgr.js.map
2195
- //# debugId=e34d02bb-f20a-5479-99b2-18946ed44afb
2324
+ //# sourceMappingURL=run-scan-app-BphjJB7x.js.map
2325
+ //# debugId=de10285a-30f0-5d70-ad61-327a97f8b1ba
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.9.3-dev.44922d6",
3
+ "version": "0.9.3-dev.48ec9a8",
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.44922d6"
76
+ "oxlint-plugin-react-doctor": "0.9.3-dev.48ec9a8"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@types/babel__code-frame": "^7.27.0",