react-doctor 0.9.12-dev.a2b460d → 0.9.12-dev.ac87f7d

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.
Files changed (50) hide show
  1. package/README.md +28 -1
  2. package/bin/react-doctor.js +1 -9
  3. package/dist/active-run-trace-dlIZeEvb.js +18 -0
  4. package/dist/active-scan-abort-registry-DwG6tSPy.js +28 -0
  5. package/dist/{build-section-divider-Vz879zXD.js → build-section-divider-BpENvzwG.js} +10 -25
  6. package/dist/{ci-BVYrYekU.js → ci-BhOBG3T0.js} +13 -12
  7. package/dist/cli.js +84 -32
  8. package/dist/{design-D-TYN_tr.js → design-Bq6di1tM.js} +4 -4
  9. package/dist/{devtools-DPRVRu3S.js → devtools-BiC-AP3B.js} +4 -4
  10. package/dist/{exit-gracefully-Cjv5kC_l.js → exit-gracefully-CYfF0GCJ.js} +15 -9
  11. package/dist/get-east-asian-width-Cglw13G7.js +444 -0
  12. package/dist/{report-error-1l7v6_dE.js → handle-error-CU-FMRyI.js} +18 -68
  13. package/dist/index.d.ts +53 -42
  14. package/dist/index.js +55121 -54716
  15. package/dist/{inspect-kkvDHKDE.js → inspect-B9H_IT4O.js} +22 -20
  16. package/dist/{install-ZeYvD9Rw.js → install-DoihOMNC.js} +9 -8
  17. package/dist/{install-doctor-script-D58Hr1dM.js → install-doctor-script-GGnj8tcj.js} +7 -449
  18. package/dist/{install-github-workflow-DkMicv8E.js → install-github-workflow-DWmRP55H.js} +10 -6
  19. package/dist/{install-react-doctor-CjgE52al.js → install-react-doctor-Gzbw8Ue9.js} +47 -45
  20. package/dist/{run-scan-app-Ciq22jzp.js → jsx-runtime-CMY_Q08d.js} +281 -2759
  21. package/dist/{print-branded-header-D4QYSu-h.js → print-branded-header-CqvClso3.js} +4 -4
  22. package/dist/project-analysis-worker.d.ts +1 -0
  23. package/dist/project-analysis-worker.js +51901 -0
  24. package/dist/prompt-runtime-scan-url-3SOiiFcU.js +240 -0
  25. package/dist/{read-object-file-OFRlvCAL.js → read-object-file-NSp2cOs9.js} +4 -4
  26. package/dist/{record-metric-CMe2d1Xv.js → record-metric-DVm5sVu2.js} +3259 -6046
  27. package/dist/report-error-JXHy8xn6.js +64 -0
  28. package/dist/{report-workflow-result-EagkH_v9.js → report-workflow-result-CPMR84K_.js} +5 -5
  29. package/dist/{resolve-scope-CMNrOdDf.js → resolve-scope-CWo3o4bf.js} +6 -6
  30. package/dist/{rules-CzR1qUru.js → rules-B4Dq5c0s.js} +6 -6
  31. package/dist/run-scan-app-DiVEX1yA.js +2510 -0
  32. package/dist/runtime-scan/browser-probe.iife.js +855 -0
  33. package/dist/runtime-scan-WErSEFfG.js +770 -0
  34. package/dist/sanitize-runtime-url-BB5x484j.js +76 -0
  35. package/dist/{scan-D4m5r4Rs.js → scan-58Yq0rYJ.js} +10 -22
  36. package/dist/semver-DFtnEmN-.js +3219 -0
  37. package/dist/{should-fail-scan-gate-lMTlhEhE.js → should-fail-scan-gate-BCCblBL_.js} +153 -184
  38. package/dist/{should-skip-prompts-mXKZBNCz.js → should-skip-prompts-DzzBAamD.js} +4 -4
  39. package/dist/should-use-tui-DVrQvWL6.js +25 -0
  40. package/dist/skills/improve-threejs/SKILL.md +102 -0
  41. package/dist/skills/performance/SKILL.md +113 -0
  42. package/dist/skills/react-doctor/SKILL.md +6 -0
  43. package/dist/{unref-stdin-Cxhw67r6.js → telemetry-runtime-B45Ns9aS.js} +57 -33
  44. package/dist/unref-stdin-BKT_s38a.js +12 -0
  45. package/dist/{version-CnvVY2VD.js → version-Cad39N0q.js} +4 -4
  46. package/dist/{why-BktDEhhH.js → why-DIEM7rdZ.js} +4 -4
  47. package/dist/with-run-span-B9ScJJx3.js +110 -0
  48. package/package.json +8 -9
  49. package/dist/chunk-q7NCDQ7-.js +0 -28
  50. package/dist/lsp.js +0 -81145
@@ -0,0 +1,770 @@
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]="06ff1d8f-03c6-578b-8857-68b7f148385f")}catch(e){}}();
3
+ import { Kt as highlighter, T as METRIC, t as recordCount, u as enableJsonMode, z as CliInputError } from "./record-metric-DVm5sVu2.js";
4
+ import { t as activeScanAbortRegistry } from "./active-scan-abort-registry-DwG6tSPy.js";
5
+ import { t as reportErrorToSentry } from "./report-error-JXHy8xn6.js";
6
+ import { n as resetSentryRunState, r as withRunSpan } from "./with-run-span-B9ScJJx3.js";
7
+ import { _ as RUNTIME_SCAN_SNAPSHOT_TIMEOUT_MS, b as RUNTIME_SCAN_TRACING_COMPLETE_TIMEOUT_MS, f as RUNTIME_SCAN_PROBE_RELATIVE_PATH, g as RUNTIME_SCAN_SAFE_CDP_PAGE_URLS, h as RUNTIME_SCAN_PROBE_SNAPSHOT_TOKEN_PLACEHOLDER, l as RUNTIME_SCAN_MAX_SNAPSHOT_PAYLOAD_BYTES, m as RUNTIME_SCAN_PROBE_SNAPSHOT_BINDING_NAME_PLACEHOLDER, n as RUNTIME_SCAN_BROWSER_WIDTH_PX, o as RUNTIME_SCAN_MAX_COMPONENT_EVENTS, p as RUNTIME_SCAN_PROBE_SNAPSHOT_ATTRIBUTE_NAME_PLACEHOLDER, r as RUNTIME_SCAN_CONTROL_CHARACTERS_PATTERN, s as RUNTIME_SCAN_MAX_RECORDING_DURATION_MS, t as sanitizeRuntimeUrl, v as RUNTIME_SCAN_TRACE_CATEGORIES, y as RUNTIME_SCAN_TRACE_FILE_EXTENSION } from "./sanitize-runtime-url-BB5x484j.js";
8
+ import { r as resolveTuiEnvironment, t as isTuiEnvironmentSupported } from "./should-use-tui-DVrQvWL6.js";
9
+ import * as path$1 from "node:path";
10
+ import * as fs$1 from "node:fs";
11
+ import { randomUUID } from "node:crypto";
12
+ import * as os$1 from "node:os";
13
+ import * as fsp from "node:fs/promises";
14
+ import { createGzip } from "node:zlib";
15
+ import { performance } from "node:perf_hooks";
16
+ import { pipeline } from "node:stream/promises";
17
+ import { chromium } from "playwright-core";
18
+ import { Buffer as Buffer$1 } from "node:buffer";
19
+ import * as readline from "node:readline/promises";
20
+ //#region src/cli/runtime-scan/build-runtime-scan-report.ts
21
+ const buildInteractions = (snapshot) => {
22
+ const interactionsById = /* @__PURE__ */ new Map();
23
+ for (const interaction of snapshot.interactions) {
24
+ const key = `${interaction.documentIndex ?? 0}:${interaction.interactionId}`;
25
+ const existing = interactionsById.get(key);
26
+ if (existing === void 0) {
27
+ interactionsById.set(key, { ...interaction });
28
+ continue;
29
+ }
30
+ existing.name = interaction.name;
31
+ existing.startTime = Math.min(existing.startTime, interaction.startTime);
32
+ existing.durationMs = Math.max(existing.durationMs, interaction.durationMs);
33
+ existing.processingStart = Math.min(existing.processingStart, interaction.processingStart);
34
+ existing.processingEnd = Math.max(existing.processingEnd, interaction.processingEnd);
35
+ existing.targetTag ??= interaction.targetTag;
36
+ }
37
+ return [...interactionsById.values()].sort((left, right) => left.startTime - right.startTime);
38
+ };
39
+ const buildScriptHotspots = (snapshot) => {
40
+ const hotspots = /* @__PURE__ */ new Map();
41
+ for (const longAnimationFrame of snapshot.longAnimationFrames) {
42
+ const hotspotKeysInFrame = /* @__PURE__ */ new Set();
43
+ for (const script of longAnimationFrame.scripts) {
44
+ const key = `${script.sourceUrl}\u0000${script.sourceFunctionName}\u0000${script.sourceCharPosition}\u0000${script.invoker}`;
45
+ const existing = hotspots.get(key);
46
+ if (existing === void 0) hotspots.set(key, {
47
+ sourceUrl: script.sourceUrl,
48
+ functionName: script.sourceFunctionName || "(anonymous)",
49
+ sourceCharPosition: script.sourceCharPosition,
50
+ invoker: script.invoker,
51
+ totalDurationMs: script.durationMs,
52
+ maxDurationMs: script.durationMs,
53
+ forcedStyleAndLayoutDurationMs: script.forcedStyleAndLayoutDurationMs,
54
+ frameCount: 1
55
+ });
56
+ else {
57
+ existing.totalDurationMs += script.durationMs;
58
+ existing.maxDurationMs = Math.max(existing.maxDurationMs, script.durationMs);
59
+ existing.forcedStyleAndLayoutDurationMs += script.forcedStyleAndLayoutDurationMs;
60
+ if (!hotspotKeysInFrame.has(key)) existing.frameCount += 1;
61
+ }
62
+ hotspotKeysInFrame.add(key);
63
+ }
64
+ }
65
+ return [...hotspots.values()].sort((left, right) => right.totalDurationMs - left.totalDurationMs).slice(0, 10);
66
+ };
67
+ const buildComponentHotspots = (snapshot) => {
68
+ const hotspots = /* @__PURE__ */ new Map();
69
+ for (const componentEvent of snapshot.componentEvents) {
70
+ const key = `${componentEvent.source}\u0000${componentEvent.name}`;
71
+ const existing = hotspots.get(key);
72
+ if (existing === void 0) {
73
+ hotspots.set(key, {
74
+ name: componentEvent.name,
75
+ source: componentEvent.source,
76
+ renderCount: 1,
77
+ totalDurationMs: componentEvent.durationMs,
78
+ maxDurationMs: componentEvent.durationMs
79
+ });
80
+ continue;
81
+ }
82
+ existing.renderCount += 1;
83
+ existing.totalDurationMs += componentEvent.durationMs;
84
+ existing.maxDurationMs = Math.max(existing.maxDurationMs, componentEvent.durationMs);
85
+ }
86
+ return [...hotspots.values()].sort((left, right) => right.totalDurationMs - left.totalDurationMs).slice(0, 10);
87
+ };
88
+ const buildWarnings = (snapshot) => {
89
+ const warnings = [];
90
+ if (!snapshot.support.reactDetected) warnings.push("No React renderer was detected during the recording.");
91
+ else if (!snapshot.support.nativeReactTracks && !snapshot.support.bippyComponentTracks) warnings.push("React component timings were unavailable. Production builds require react-dom/profiling.");
92
+ if (!snapshot.support.loaf) warnings.push("Long Animation Frame entries are unavailable in this browser.");
93
+ if (snapshot.droppedLongAnimationFrames > 0 || snapshot.droppedScriptTimings > 0 || snapshot.droppedComponentEvents > 0 || snapshot.droppedInteractions > 0) warnings.push(`The bounded probe dropped ${snapshot.droppedLongAnimationFrames} frames, ${snapshot.droppedScriptTimings} script timings, ${snapshot.droppedComponentEvents} component events, and ${snapshot.droppedInteractions} interactions.`);
94
+ if (snapshot.longAnimationFrames.flatMap((longAnimationFrame) => longAnimationFrame.scripts).find((script) => script.durationMs > 0 && script.forcedStyleAndLayoutDurationMs / script.durationMs >= .25) !== void 0) warnings.push("At least one long frame was substantially dominated by forced style and layout.");
95
+ return warnings;
96
+ };
97
+ const buildRuntimeScanReport = (input) => {
98
+ const longAnimationFrames = [...input.snapshot.longAnimationFrames].sort((left, right) => right.durationMs - left.durationMs);
99
+ const interactions = buildInteractions(input.snapshot);
100
+ const interactionDurations = interactions.map((interaction) => interaction.durationMs);
101
+ return {
102
+ schemaVersion: 1,
103
+ kind: "react-doctor-runtime-scan",
104
+ requestedUrl: sanitizeRuntimeUrl(input.requestedUrl),
105
+ finalUrl: sanitizeRuntimeUrl(input.snapshot.finalUrl),
106
+ tracePath: input.tracePath,
107
+ capturedAt: input.capturedAt,
108
+ timeOrigin: input.snapshot.timeOrigin,
109
+ connection: input.connection,
110
+ support: input.snapshot.support,
111
+ summary: {
112
+ durationMs: input.durationMs,
113
+ longAnimationFrameCount: longAnimationFrames.length,
114
+ worstFrameDurationMs: longAnimationFrames[0]?.durationMs ?? 0,
115
+ totalBlockingDurationMs: longAnimationFrames.reduce((total, longAnimationFrame) => total + longAnimationFrame.blockingDurationMs, 0),
116
+ interactionCount: interactions.length,
117
+ worstInteractionDurationMs: interactionDurations.length === 0 ? 0 : Math.max(...interactionDurations),
118
+ cumulativeLayoutShift: input.snapshot.cumulativeLayoutShift,
119
+ largestContentfulPaintMs: input.snapshot.largestContentfulPaintMs
120
+ },
121
+ scriptHotspots: buildScriptHotspots(input.snapshot),
122
+ componentHotspots: buildComponentHotspots(input.snapshot),
123
+ longAnimationFrames,
124
+ interactions,
125
+ warnings: buildWarnings(input.snapshot)
126
+ };
127
+ };
128
+ //#endregion
129
+ //#region src/cli/runtime-scan/sanitize-runtime-text.ts
130
+ const sanitizeRuntimeText = (value) => value.replace(RUNTIME_SCAN_CONTROL_CHARACTERS_PATTERN, "");
131
+ //#endregion
132
+ //#region src/cli/runtime-scan/format-runtime-scan-report.ts
133
+ const formatMilliseconds = (durationMs) => `${durationMs.toFixed(1)}ms`;
134
+ const formatTextReport = (report) => {
135
+ const lines = [
136
+ `${highlighter.success("✔")} Runtime trace captured`,
137
+ highlighter.dim(` ${sanitizeRuntimeText(report.finalUrl)}`),
138
+ "",
139
+ `${highlighter.info("Trace")} ${sanitizeRuntimeText(report.tracePath)}`,
140
+ `${highlighter.info("Browser")} ${report.connection === "cdp" ? "attached over CDP" : "isolated profile"} · ${formatMilliseconds(report.summary.durationMs)} captured`,
141
+ `${highlighter.info("React")} ${report.support.reactDetected ? `${sanitizeRuntimeText(report.support.reactVersion ?? "unknown")} (${sanitizeRuntimeText(report.support.reactBuildType ?? "unknown")})` : "not detected"}`,
142
+ "",
143
+ highlighter.info("Performance"),
144
+ ` ├─ ${report.summary.longAnimationFrameCount} long animation frame${report.summary.longAnimationFrameCount === 1 ? "" : "s"}; worst ${formatMilliseconds(report.summary.worstFrameDurationMs)}`,
145
+ ` ├─ ${formatMilliseconds(report.summary.totalBlockingDurationMs)} total blocking time`,
146
+ ` ├─ ${report.summary.interactionCount} interaction${report.summary.interactionCount === 1 ? "" : "s"}; worst ${formatMilliseconds(report.summary.worstInteractionDurationMs)}`,
147
+ ` ├─ CLS ${report.summary.cumulativeLayoutShift.toFixed(1)}`,
148
+ ` └─ LCP ${report.summary.largestContentfulPaintMs === null ? "unavailable" : formatMilliseconds(report.summary.largestContentfulPaintMs)}`
149
+ ];
150
+ if (report.scriptHotspots.length > 0) {
151
+ lines.push("", highlighter.info("Script hotspots"));
152
+ for (const [index, hotspot] of report.scriptHotspots.entries()) {
153
+ const prefix = index === report.scriptHotspots.length - 1 ? "└─" : "├─";
154
+ const sourceLocation = hotspot.sourceCharPosition > 0 ? `${sanitizeRuntimeText(hotspot.sourceUrl)} @ char ${hotspot.sourceCharPosition}` : sanitizeRuntimeText(hotspot.sourceUrl);
155
+ const location = sourceLocation ? `${sourceLocation}${hotspot.functionName ? ` · ${sanitizeRuntimeText(hotspot.functionName)}` : ""}` : sanitizeRuntimeText(hotspot.functionName);
156
+ lines.push(` ${prefix} ${formatMilliseconds(hotspot.totalDurationMs)} ${location}`, ` ${hotspot.frameCount} frame${hotspot.frameCount === 1 ? "" : "s"}, ${formatMilliseconds(hotspot.forcedStyleAndLayoutDurationMs)} forced layout`);
157
+ }
158
+ }
159
+ if (report.componentHotspots.length > 0) {
160
+ lines.push("", highlighter.info("React component hotspots"));
161
+ for (const [index, hotspot] of report.componentHotspots.entries()) {
162
+ const prefix = index === report.componentHotspots.length - 1 ? "└─" : "├─";
163
+ lines.push(` ${prefix} ${sanitizeRuntimeText(hotspot.name)} · ${formatMilliseconds(hotspot.totalDurationMs)} across ${hotspot.renderCount} render${hotspot.renderCount === 1 ? "" : "s"}`);
164
+ }
165
+ }
166
+ if (report.warnings.length > 0) {
167
+ lines.push("", highlighter.warn("Limitations"));
168
+ for (const warning of report.warnings) lines.push(` - ${sanitizeRuntimeText(warning)}`);
169
+ }
170
+ return `${lines.join("\n")}\n`;
171
+ };
172
+ const formatJsonlReport = (report) => {
173
+ return `${[
174
+ {
175
+ schemaVersion: report.schemaVersion,
176
+ kind: "metadata",
177
+ data: {
178
+ requestedUrl: report.requestedUrl,
179
+ finalUrl: report.finalUrl,
180
+ tracePath: report.tracePath,
181
+ capturedAt: report.capturedAt,
182
+ timeOrigin: report.timeOrigin,
183
+ connection: report.connection,
184
+ support: report.support,
185
+ warnings: report.warnings
186
+ }
187
+ },
188
+ {
189
+ schemaVersion: report.schemaVersion,
190
+ kind: "summary",
191
+ data: report.summary
192
+ },
193
+ ...report.scriptHotspots.map((scriptHotspot) => ({
194
+ schemaVersion: report.schemaVersion,
195
+ kind: "script-hotspot",
196
+ data: scriptHotspot
197
+ })),
198
+ ...report.componentHotspots.map((componentHotspot) => ({
199
+ schemaVersion: report.schemaVersion,
200
+ kind: "component-hotspot",
201
+ data: componentHotspot
202
+ })),
203
+ ...report.longAnimationFrames.map((longAnimationFrame) => ({
204
+ schemaVersion: report.schemaVersion,
205
+ kind: "long-animation-frame",
206
+ data: longAnimationFrame
207
+ })),
208
+ ...report.interactions.map((interaction) => ({
209
+ schemaVersion: report.schemaVersion,
210
+ kind: "interaction",
211
+ data: interaction
212
+ }))
213
+ ].map((record) => JSON.stringify(record)).join("\n")}\n`;
214
+ };
215
+ const formatRuntimeScanReport = (report, format) => {
216
+ if (format === "json") return `${JSON.stringify(report, null, 2)}\n`;
217
+ if (format === "jsonl") return formatJsonlReport(report);
218
+ return formatTextReport(report);
219
+ };
220
+ //#endregion
221
+ //#region src/cli/runtime-scan/assert-runtime-scan-cdp-profile.ts
222
+ const safeCdpPageUrls = new Set(RUNTIME_SCAN_SAFE_CDP_PAGE_URLS);
223
+ const assertRuntimeScanCdpProfile = (pageUrls) => {
224
+ if (pageUrls.every((pageUrl) => safeCdpPageUrls.has(pageUrl))) return;
225
+ throw new CliInputError("Chrome performance tracing includes every open tab. Close all tabs in the dedicated debug profile and retry; a blank new tab can stay open.");
226
+ };
227
+ //#endregion
228
+ //#region src/cli/runtime-scan/assert-runtime-scan-interactive.ts
229
+ const assertRuntimeScanInteractive = () => {
230
+ if (process.stdin.isTTY === true) return;
231
+ throw new CliInputError("`react-doctor scan <url>` requires an interactive terminal. Run it directly, interact with Chrome, then press Enter to stop recording.");
232
+ };
233
+ //#endregion
234
+ //#region src/cli/runtime-scan/merge-runtime-scan-probe-snapshots.ts
235
+ const shiftOptionalTimestamp = (timestamp, offset) => timestamp === 0 ? 0 : timestamp + offset;
236
+ const takeLatestEntries = (entries, limit) => [...entries].sort((left, right) => left.startTime - right.startTime).slice(-limit);
237
+ const shiftLongAnimationFrame = (frame, offset) => ({
238
+ ...frame,
239
+ startTime: frame.startTime + offset,
240
+ renderStart: shiftOptionalTimestamp(frame.renderStart, offset),
241
+ styleAndLayoutStart: shiftOptionalTimestamp(frame.styleAndLayoutStart, offset),
242
+ firstUiEventTimestamp: shiftOptionalTimestamp(frame.firstUiEventTimestamp, offset),
243
+ scripts: frame.scripts.map((script) => ({
244
+ ...script,
245
+ executionStart: shiftOptionalTimestamp(script.executionStart, offset)
246
+ }))
247
+ });
248
+ const shiftComponentEvent = (componentEvent, offset) => ({
249
+ ...componentEvent,
250
+ startTime: componentEvent.startTime + offset
251
+ });
252
+ const shiftInteraction = (interaction, offset, documentIndex) => ({
253
+ ...interaction,
254
+ startTime: interaction.startTime + offset,
255
+ processingStart: interaction.processingStart + offset,
256
+ processingEnd: interaction.processingEnd + offset,
257
+ documentIndex
258
+ });
259
+ const mergeRuntimeScanProbeSnapshots = (capturedSnapshots) => {
260
+ const snapshotsByTimeOrigin = /* @__PURE__ */ new Map();
261
+ for (const snapshot of capturedSnapshots) {
262
+ snapshotsByTimeOrigin.delete(snapshot.timeOrigin);
263
+ snapshotsByTimeOrigin.set(snapshot.timeOrigin, snapshot);
264
+ }
265
+ const snapshots = [...snapshotsByTimeOrigin.values()];
266
+ const finalSnapshot = snapshots.at(-1);
267
+ if (finalSnapshot === void 0) throw new Error("At least one runtime scan probe snapshot is required.");
268
+ const timeOrigin = Math.min(...snapshots.map((snapshot) => snapshot.timeOrigin));
269
+ const documentIndexByTimeOrigin = /* @__PURE__ */ new Map();
270
+ const snapshotsInCreationOrder = [...snapshots].sort((left, right) => left.timeOrigin - right.timeOrigin);
271
+ for (const [documentIndex, snapshot] of snapshotsInCreationOrder.entries()) documentIndexByTimeOrigin.set(snapshot.timeOrigin, documentIndex);
272
+ const longAnimationFrames = snapshots.flatMap((snapshot) => {
273
+ const offset = snapshot.timeOrigin - timeOrigin;
274
+ return snapshot.longAnimationFrames.map((frame) => shiftLongAnimationFrame(frame, offset));
275
+ });
276
+ const componentEvents = snapshots.flatMap((snapshot) => {
277
+ const offset = snapshot.timeOrigin - timeOrigin;
278
+ return snapshot.componentEvents.map((componentEvent) => shiftComponentEvent(componentEvent, offset));
279
+ });
280
+ const interactions = snapshots.flatMap((snapshot) => {
281
+ const offset = snapshot.timeOrigin - timeOrigin;
282
+ const documentIndex = documentIndexByTimeOrigin.get(snapshot.timeOrigin) ?? 0;
283
+ return snapshot.interactions.map((interaction) => shiftInteraction(interaction, offset, documentIndex));
284
+ });
285
+ const latestReactSnapshot = [...snapshots].reverse().find((snapshot) => snapshot.support.reactDetected);
286
+ return {
287
+ timeOrigin,
288
+ finalUrl: finalSnapshot.finalUrl,
289
+ support: {
290
+ reactDetected: snapshots.some((snapshot) => snapshot.support.reactDetected),
291
+ reactVersion: latestReactSnapshot?.support.reactVersion ?? null,
292
+ reactBuildType: latestReactSnapshot?.support.reactBuildType ?? null,
293
+ nativeReactTracks: snapshots.some((snapshot) => snapshot.support.nativeReactTracks),
294
+ bippyComponentTracks: snapshots.some((snapshot) => snapshot.support.bippyComponentTracks),
295
+ loaf: snapshots.some((snapshot) => snapshot.support.loaf)
296
+ },
297
+ longAnimationFrames: takeLatestEntries(longAnimationFrames, 100),
298
+ componentEvents: takeLatestEntries(componentEvents, RUNTIME_SCAN_MAX_COMPONENT_EVENTS),
299
+ interactions: takeLatestEntries(interactions, 500),
300
+ cumulativeLayoutShift: Math.max(...snapshots.map((snapshot) => snapshot.cumulativeLayoutShift)),
301
+ largestContentfulPaintMs: finalSnapshot.largestContentfulPaintMs,
302
+ droppedLongAnimationFrames: snapshots.reduce((total, snapshot) => total + snapshot.droppedLongAnimationFrames, 0) + Math.max(0, longAnimationFrames.length - 100),
303
+ droppedScriptTimings: snapshots.reduce((total, snapshot) => total + snapshot.droppedScriptTimings, 0),
304
+ droppedComponentEvents: snapshots.reduce((total, snapshot) => total + snapshot.droppedComponentEvents, 0) + Math.max(0, componentEvents.length - RUNTIME_SCAN_MAX_COMPONENT_EVENTS),
305
+ droppedInteractions: snapshots.reduce((total, snapshot) => total + snapshot.droppedInteractions, 0) + Math.max(0, interactions.length - 500)
306
+ };
307
+ };
308
+ //#endregion
309
+ //#region src/cli/runtime-scan/parse-runtime-scan-snapshot-payload.ts
310
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
311
+ const isBoundedString = (value) => typeof value === "string" && value.length <= 4096;
312
+ const isNullableBoundedString = (value) => value === null || isBoundedString(value);
313
+ const isSanitizedRuntimeUrl = (value) => {
314
+ if (!isBoundedString(value) || !URL.canParse(value)) return false;
315
+ const parsedUrl = new URL(value);
316
+ return (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") && parsedUrl.username === "" && parsedUrl.password === "" && parsedUrl.search === "" && parsedUrl.hash === "";
317
+ };
318
+ const isNonNegativeFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
319
+ const isNonNegativeSafeInteger = (value) => Number.isSafeInteger(value) && isNonNegativeFiniteNumber(value);
320
+ const isSourceCharPosition = (value) => value === -1 || isNonNegativeSafeInteger(value);
321
+ const isSnapshotWithinByteLimit = (value) => {
322
+ try {
323
+ const serializedSnapshot = JSON.stringify(value);
324
+ return serializedSnapshot !== void 0 && Buffer$1.byteLength(serializedSnapshot) <= 5177344;
325
+ } catch {
326
+ return false;
327
+ }
328
+ };
329
+ const isScriptTiming = (value) => isRecord(value) && isBoundedString(value.invoker) && isBoundedString(value.invokerType) && isBoundedString(value.sourceUrl) && isBoundedString(value.sourceFunctionName) && isSourceCharPosition(value.sourceCharPosition) && isNonNegativeFiniteNumber(value.executionStart) && isNonNegativeFiniteNumber(value.durationMs) && isNonNegativeFiniteNumber(value.forcedStyleAndLayoutDurationMs) && isNonNegativeFiniteNumber(value.pauseDurationMs);
330
+ const isLongAnimationFrame = (value) => isRecord(value) && isNonNegativeFiniteNumber(value.startTime) && isNonNegativeFiniteNumber(value.durationMs) && isNonNegativeFiniteNumber(value.blockingDurationMs) && isNonNegativeFiniteNumber(value.renderStart) && isNonNegativeFiniteNumber(value.styleAndLayoutStart) && isNonNegativeFiniteNumber(value.firstUiEventTimestamp) && Array.isArray(value.scripts) && value.scripts.length <= 100 && value.scripts.every(isScriptTiming);
331
+ const isComponentEvent = (value) => isRecord(value) && isBoundedString(value.name) && isNonNegativeFiniteNumber(value.startTime) && isNonNegativeFiniteNumber(value.durationMs) && isNonNegativeSafeInteger(value.depth) && (value.source === "native" || value.source === "bippy");
332
+ const isInteraction = (value) => isRecord(value) && isBoundedString(value.name) && isNonNegativeFiniteNumber(value.startTime) && isNonNegativeFiniteNumber(value.durationMs) && isNonNegativeFiniteNumber(value.processingStart) && isNonNegativeFiniteNumber(value.processingEnd) && isNonNegativeSafeInteger(value.interactionId) && (value.documentIndex === void 0 || isNonNegativeSafeInteger(value.documentIndex)) && isNullableBoundedString(value.targetTag);
333
+ const isRuntimeScanProbeSnapshot = (value) => isRecord(value) && isNonNegativeFiniteNumber(value.timeOrigin) && isSanitizedRuntimeUrl(value.finalUrl) && isRecord(value.support) && typeof value.support.reactDetected === "boolean" && isNullableBoundedString(value.support.reactVersion) && (value.support.reactBuildType === null || value.support.reactBuildType === "development" || value.support.reactBuildType === "production") && typeof value.support.nativeReactTracks === "boolean" && typeof value.support.bippyComponentTracks === "boolean" && typeof value.support.loaf === "boolean" && Array.isArray(value.longAnimationFrames) && value.longAnimationFrames.length <= 100 && value.longAnimationFrames.every(isLongAnimationFrame) && Array.isArray(value.componentEvents) && value.componentEvents.length <= 2e3 && value.componentEvents.every(isComponentEvent) && Array.isArray(value.interactions) && value.interactions.length <= 500 && value.interactions.every(isInteraction) && isNonNegativeFiniteNumber(value.cumulativeLayoutShift) && (value.largestContentfulPaintMs === null || isNonNegativeFiniteNumber(value.largestContentfulPaintMs)) && isNonNegativeSafeInteger(value.droppedLongAnimationFrames) && isNonNegativeSafeInteger(value.droppedScriptTimings) && isNonNegativeSafeInteger(value.droppedComponentEvents) && isNonNegativeSafeInteger(value.droppedInteractions) && isSnapshotWithinByteLimit(value);
334
+ const parseRuntimeScanSnapshotPayload = (payload, expectedToken) => {
335
+ if (Buffer$1.byteLength(payload) > 5242880) return null;
336
+ try {
337
+ const envelope = JSON.parse(payload);
338
+ if (!isRecord(envelope) || envelope.token !== expectedToken || !isRuntimeScanProbeSnapshot(envelope.snapshot)) return null;
339
+ return envelope.snapshot;
340
+ } catch {
341
+ return null;
342
+ }
343
+ };
344
+ //#endregion
345
+ //#region src/cli/runtime-scan/resolve-runtime-trace-path.ts
346
+ const resolveRuntimeTracePath = (traceOut, capturedAt) => {
347
+ if (traceOut !== void 0) return path$1.resolve(traceOut);
348
+ const timestamp = capturedAt.toISOString().replaceAll(/[:.]/g, "-");
349
+ return path$1.join(os$1.tmpdir(), `react-doctor-runtime-${timestamp}-${randomUUID()}${RUNTIME_SCAN_TRACE_FILE_EXTENSION}`);
350
+ };
351
+ //#endregion
352
+ //#region src/cli/runtime-scan/wait-for-runtime-scan-stop.ts
353
+ const waitForRuntimeScanStop = async () => {
354
+ assertRuntimeScanInteractive();
355
+ const interfaceHandle = readline.createInterface({
356
+ input: process.stdin,
357
+ output: process.stderr,
358
+ terminal: true
359
+ });
360
+ const timeoutController = new AbortController();
361
+ const timeoutHandle = setTimeout(() => timeoutController.abort(), RUNTIME_SCAN_MAX_RECORDING_DURATION_MS);
362
+ try {
363
+ await interfaceHandle.question("Recording runtime performance. Interact with the page, then press Enter to stop.\n", { signal: timeoutController.signal }).catch((cause) => {
364
+ if (!timeoutController.signal.aborted) throw cause;
365
+ });
366
+ } finally {
367
+ clearTimeout(timeoutHandle);
368
+ interfaceHandle.close();
369
+ }
370
+ };
371
+ //#endregion
372
+ //#region src/cli/runtime-scan/record-runtime-trace.ts
373
+ const resolveProbePath = () => {
374
+ const probePath = [path$1.join(import.meta.dirname, RUNTIME_SCAN_PROBE_RELATIVE_PATH), path$1.resolve(import.meta.dirname, "../../../dist", RUNTIME_SCAN_PROBE_RELATIVE_PATH)].find((candidate) => fs$1.existsSync(candidate));
375
+ if (probePath === void 0) throw new Error("The runtime scan browser probe is missing. Rebuild react-doctor and retry.");
376
+ return probePath;
377
+ };
378
+ const buildSnapshotRelaySource = (attributeName, bindingName) => `
379
+ (() => {
380
+ let didRelaySnapshot = false;
381
+ const getSnapshotAttribute = Element.prototype.getAttribute;
382
+ const removeSnapshotAttribute = Element.prototype.removeAttribute;
383
+ const relaySnapshot = () => {
384
+ const documentElement = document.documentElement;
385
+ if (documentElement === null) return;
386
+ const payload = getSnapshotAttribute.call(documentElement, ${JSON.stringify(attributeName)});
387
+ removeSnapshotAttribute.call(documentElement, ${JSON.stringify(attributeName)});
388
+ if (
389
+ didRelaySnapshot ||
390
+ typeof payload !== "string" ||
391
+ payload.length > ${RUNTIME_SCAN_MAX_SNAPSHOT_PAYLOAD_BYTES}
392
+ ) return;
393
+ if (new TextEncoder().encode(payload).byteLength > ${RUNTIME_SCAN_MAX_SNAPSHOT_PAYLOAD_BYTES}) return;
394
+ const captureBinding = Reflect.get(globalThis, ${JSON.stringify(bindingName)});
395
+ if (typeof captureBinding !== "function") return;
396
+ didRelaySnapshot = true;
397
+ captureBinding(payload);
398
+ };
399
+ new MutationObserver(relaySnapshot).observe(document, {
400
+ attributes: true,
401
+ subtree: true,
402
+ attributeFilter: [${JSON.stringify(attributeName)}],
403
+ });
404
+ })();
405
+ `;
406
+ const readProbeSnapshot = async (page) => {
407
+ let timeoutHandle;
408
+ const snapshotTimeout = new Promise((resolve) => {
409
+ timeoutHandle = setTimeout(() => resolve(null), RUNTIME_SCAN_SNAPSHOT_TIMEOUT_MS);
410
+ });
411
+ try {
412
+ const snapshot = await Promise.race([page.evaluate(() => window.__REACT_DOCTOR_RUNTIME_SCAN__?.snapshot()).catch(() => null), snapshotTimeout]);
413
+ return isRuntimeScanProbeSnapshot(snapshot) ? snapshot : null;
414
+ } finally {
415
+ if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
416
+ }
417
+ };
418
+ const createFallbackSnapshot = (timeOrigin, finalUrl) => ({
419
+ timeOrigin,
420
+ finalUrl,
421
+ support: {
422
+ reactDetected: false,
423
+ reactVersion: null,
424
+ reactBuildType: null,
425
+ nativeReactTracks: false,
426
+ bippyComponentTracks: false,
427
+ loaf: false
428
+ },
429
+ longAnimationFrames: [],
430
+ componentEvents: [],
431
+ interactions: [],
432
+ cumulativeLayoutShift: 0,
433
+ largestContentfulPaintMs: null,
434
+ droppedLongAnimationFrames: 0,
435
+ droppedScriptTimings: 0,
436
+ droppedComponentEvents: 0,
437
+ droppedInteractions: 0
438
+ });
439
+ const startDevtoolsTrace = async (cdpSession) => {
440
+ try {
441
+ await cdpSession.send("Tracing.start", {
442
+ categories: RUNTIME_SCAN_TRACE_CATEGORIES.join(","),
443
+ transferMode: "ReturnAsStream"
444
+ });
445
+ } catch (cause) {
446
+ const message = cause instanceof Error ? cause.message : String(cause);
447
+ if (/Tracing (?:has )?already (?:been )?started/i.test(message)) throw new CliInputError("Chrome is already recording a performance trace. Stop the existing DevTools recording and retry.");
448
+ throw cause;
449
+ }
450
+ };
451
+ const endDevtoolsTrace = async (cdpSession) => {
452
+ const traceComplete = new Promise((resolve) => {
453
+ cdpSession.once("Tracing.tracingComplete", resolve);
454
+ });
455
+ let timeoutHandle;
456
+ const traceTimeout = new Promise((_resolve, reject) => {
457
+ timeoutHandle = setTimeout(() => {
458
+ reject(/* @__PURE__ */ new Error("Chrome did not finish the performance trace in time."));
459
+ }, RUNTIME_SCAN_TRACING_COMPLETE_TIMEOUT_MS);
460
+ });
461
+ let stream;
462
+ try {
463
+ const finishTrace = async () => {
464
+ await cdpSession.send("Tracing.end");
465
+ return traceComplete;
466
+ };
467
+ ({stream} = await Promise.race([finishTrace(), traceTimeout]));
468
+ } finally {
469
+ if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
470
+ }
471
+ if (stream === void 0) throw new Error("Chrome returned a trace without a data stream.");
472
+ return stream;
473
+ };
474
+ const readDevtoolsTrace = async function* (cdpSession, stream) {
475
+ let isComplete = false;
476
+ let traceBytes = 0;
477
+ try {
478
+ while (!isComplete) {
479
+ const result = await cdpSession.send("IO.read", { handle: stream });
480
+ const chunk = Buffer.from(result.data, result.base64Encoded === true ? "base64" : "utf8");
481
+ traceBytes += chunk.byteLength;
482
+ if (traceBytes > 536870912) throw new CliInputError("The DevTools trace exceeded the 512 MB safety limit. Record a shorter, focused interaction and retry.");
483
+ yield chunk;
484
+ isComplete = result.eof === true;
485
+ }
486
+ } finally {
487
+ await cdpSession.send("IO.close", { handle: stream }).catch(() => {});
488
+ }
489
+ };
490
+ const writeDevtoolsTrace = async (cdpSession, stream, tracePath) => {
491
+ await fsp.mkdir(path$1.dirname(tracePath), { recursive: true });
492
+ const temporaryTracePath = `${tracePath}.${randomUUID()}.tmp`;
493
+ let didMoveTrace = false;
494
+ try {
495
+ await pipeline(readDevtoolsTrace(cdpSession, stream), createGzip(), fs$1.createWriteStream(temporaryTracePath, {
496
+ flags: "wx",
497
+ mode: 384,
498
+ flush: true
499
+ }));
500
+ await fsp.rename(temporaryTracePath, tracePath);
501
+ didMoveTrace = true;
502
+ } finally {
503
+ if (!didMoveTrace) await fsp.rm(temporaryTracePath, { force: true }).catch(() => {});
504
+ }
505
+ };
506
+ const connectToBrowser = async (cdpUrl) => {
507
+ try {
508
+ if (cdpUrl !== void 0) return {
509
+ browser: await chromium.connectOverCDP(cdpUrl),
510
+ connection: "cdp"
511
+ };
512
+ return {
513
+ browser: await chromium.launch({
514
+ channel: "chrome",
515
+ headless: false,
516
+ args: ["--no-first-run", "--no-default-browser-check"]
517
+ }),
518
+ connection: "isolated"
519
+ };
520
+ } catch {
521
+ throw new CliInputError(`Could not connect to Chrome. ${cdpUrl === void 0 ? "Install Google Chrome, or pass --cdp <url> for a browser started with remote debugging." : "Confirm the browser is running with remote debugging and that the CDP URL is reachable."}`);
522
+ }
523
+ };
524
+ const navigateToRuntimeUrl = async (page, url) => {
525
+ try {
526
+ await page.goto(url, { waitUntil: "load" });
527
+ } catch {
528
+ throw new CliInputError("Could not load the target URL. Confirm the app is running and reachable from Chrome.");
529
+ }
530
+ };
531
+ const recordRuntimeTrace = async (input) => {
532
+ const sanitizedRequestedUrl = sanitizeRuntimeUrl(input.url);
533
+ assertRuntimeScanInteractive();
534
+ const tracePath = resolveRuntimeTracePath(input.traceOut, /* @__PURE__ */ new Date());
535
+ const snapshotIdentifier = randomUUID().replaceAll("-", "");
536
+ const snapshotBindingName = `${RUNTIME_SCAN_PROBE_SNAPSHOT_BINDING_NAME_PLACEHOLDER}_${snapshotIdentifier}`;
537
+ const snapshotAttributeName = `${RUNTIME_SCAN_PROBE_SNAPSHOT_ATTRIBUTE_NAME_PLACEHOLDER}-${snapshotIdentifier}`;
538
+ const snapshotRelayWorldName = `react-doctor-runtime-scan-${snapshotIdentifier}`;
539
+ const snapshotToken = randomUUID();
540
+ const probeSource = (await fsp.readFile(resolveProbePath(), "utf8")).replaceAll(RUNTIME_SCAN_PROBE_SNAPSHOT_ATTRIBUTE_NAME_PLACEHOLDER, snapshotAttributeName).replaceAll(RUNTIME_SCAN_PROBE_SNAPSHOT_TOKEN_PLACEHOLDER, snapshotToken);
541
+ const snapshotRelaySource = buildSnapshotRelaySource(snapshotAttributeName, snapshotBindingName);
542
+ const { browser, connection } = await connectToBrowser(input.cdpUrl);
543
+ const navigationSnapshotsByTimeOrigin = /* @__PURE__ */ new Map();
544
+ const discardedSnapshotEvidence = {
545
+ droppedLongAnimationFrames: 0,
546
+ droppedScriptTimings: 0,
547
+ droppedComponentEvents: 0,
548
+ droppedInteractions: 0
549
+ };
550
+ const scanPages = /* @__PURE__ */ new Set();
551
+ const pageSetupPromises = /* @__PURE__ */ new Map();
552
+ let context;
553
+ let page;
554
+ let latestPage;
555
+ let traceCdpSession;
556
+ let didCreateContext = false;
557
+ let isTracing = false;
558
+ let traceStreamPromise;
559
+ let cleanupPromise;
560
+ let preexistingCdpPages = [];
561
+ const storeNavigationSnapshot = (snapshot) => {
562
+ if (!navigationSnapshotsByTimeOrigin.delete(snapshot.timeOrigin) && navigationSnapshotsByTimeOrigin.size >= 10) {
563
+ const oldestTimeOrigin = navigationSnapshotsByTimeOrigin.keys().next().value;
564
+ if (oldestTimeOrigin !== void 0) {
565
+ const discardedSnapshot = navigationSnapshotsByTimeOrigin.get(oldestTimeOrigin);
566
+ if (discardedSnapshot !== void 0) {
567
+ discardedSnapshotEvidence.droppedLongAnimationFrames += discardedSnapshot.longAnimationFrames.length + discardedSnapshot.droppedLongAnimationFrames;
568
+ discardedSnapshotEvidence.droppedScriptTimings += discardedSnapshot.longAnimationFrames.reduce((scriptCount, longAnimationFrame) => scriptCount + longAnimationFrame.scripts.length, 0) + discardedSnapshot.droppedScriptTimings;
569
+ discardedSnapshotEvidence.droppedComponentEvents += discardedSnapshot.componentEvents.length + discardedSnapshot.droppedComponentEvents;
570
+ discardedSnapshotEvidence.droppedInteractions += discardedSnapshot.interactions.length + discardedSnapshot.droppedInteractions;
571
+ }
572
+ navigationSnapshotsByTimeOrigin.delete(oldestTimeOrigin);
573
+ }
574
+ }
575
+ navigationSnapshotsByTimeOrigin.set(snapshot.timeOrigin, snapshot);
576
+ };
577
+ const initializeRuntimeScanPage = async (runtimePage) => {
578
+ if (context === void 0) throw new Error("The runtime scan browser context is unavailable.");
579
+ const cdpSession = await context.newCDPSession(runtimePage);
580
+ await cdpSession.send("Page.enable");
581
+ cdpSession.on("Runtime.bindingCalled", (event) => {
582
+ if (event.name !== snapshotBindingName) return;
583
+ const snapshot = parseRuntimeScanSnapshotPayload(event.payload, snapshotToken);
584
+ if (snapshot !== null) storeNavigationSnapshot(snapshot);
585
+ });
586
+ await cdpSession.send("Runtime.enable");
587
+ await cdpSession.send("Runtime.addBinding", {
588
+ name: snapshotBindingName,
589
+ executionContextName: snapshotRelayWorldName
590
+ });
591
+ await cdpSession.send("Page.addScriptToEvaluateOnNewDocument", {
592
+ source: snapshotRelaySource,
593
+ worldName: snapshotRelayWorldName
594
+ });
595
+ const frameTree = await cdpSession.send("Page.getFrameTree");
596
+ const isolatedWorld = await cdpSession.send("Page.createIsolatedWorld", {
597
+ frameId: frameTree.frameTree.frame.id,
598
+ worldName: snapshotRelayWorldName
599
+ });
600
+ await cdpSession.send("Runtime.evaluate", {
601
+ expression: snapshotRelaySource,
602
+ contextId: isolatedWorld.executionContextId
603
+ });
604
+ };
605
+ const registerRuntimeScanPage = (runtimePage) => {
606
+ latestPage = runtimePage;
607
+ scanPages.add(runtimePage);
608
+ const existingSetup = pageSetupPromises.get(runtimePage);
609
+ if (existingSetup !== void 0) return existingSetup;
610
+ const setupPromise = initializeRuntimeScanPage(runtimePage);
611
+ pageSetupPromises.set(runtimePage, setupPromise);
612
+ setupPromise.catch(() => {});
613
+ return setupPromise;
614
+ };
615
+ const stopTracing = async () => {
616
+ if (!isTracing || traceCdpSession === void 0) return;
617
+ try {
618
+ traceStreamPromise ??= endDevtoolsTrace(traceCdpSession);
619
+ const traceStream = await traceStreamPromise;
620
+ isTracing = false;
621
+ await traceCdpSession.send("IO.close", { handle: traceStream });
622
+ } catch {}
623
+ };
624
+ const closeBrowserResources = async () => {
625
+ await Promise.all([...scanPages].map((runtimePage) => runtimePage.close().catch(() => {})));
626
+ if (connection === "cdp" && didCreateContext) await context?.close().catch(() => {});
627
+ await browser.close().catch(() => {});
628
+ };
629
+ const cleanup = () => {
630
+ cleanupPromise ??= stopTracing().then(closeBrowserResources);
631
+ return cleanupPromise;
632
+ };
633
+ const unregisterCleanup = activeScanAbortRegistry.registerCleanup(cleanup);
634
+ try {
635
+ if (connection === "cdp") {
636
+ preexistingCdpPages = browser.contexts().flatMap((browserContext) => browserContext.pages());
637
+ assertRuntimeScanCdpProfile(preexistingCdpPages.map((browserPage) => browserPage.url()));
638
+ context = browser.contexts()[0];
639
+ }
640
+ if (context === void 0) {
641
+ context = await browser.newContext({ viewport: {
642
+ width: RUNTIME_SCAN_BROWSER_WIDTH_PX,
643
+ height: 900
644
+ } });
645
+ didCreateContext = true;
646
+ }
647
+ context.on("page", (runtimePage) => {
648
+ registerRuntimeScanPage(runtimePage);
649
+ });
650
+ await context.addInitScript({ content: probeSource });
651
+ page = await context.newPage();
652
+ if (connection === "cdp") await Promise.all(preexistingCdpPages.map(async (browserPage) => {
653
+ if (!browserPage.isClosed()) await browserPage.close();
654
+ }));
655
+ await registerRuntimeScanPage(page);
656
+ traceCdpSession = await browser.newBrowserCDPSession();
657
+ await startDevtoolsTrace(traceCdpSession);
658
+ isTracing = true;
659
+ const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
660
+ const recordingStartedAt = performance.now();
661
+ await navigateToRuntimeUrl(page, input.url);
662
+ await waitForRuntimeScanStop();
663
+ const durationMs = performance.now() - recordingStartedAt;
664
+ await Promise.allSettled([...pageSetupPromises.values()]);
665
+ const finalSnapshotsPromise = Promise.all([...scanPages].map(readProbeSnapshot));
666
+ traceStreamPromise = endDevtoolsTrace(traceCdpSession);
667
+ const [finalSnapshots, traceStream] = await Promise.all([finalSnapshotsPromise, traceStreamPromise]);
668
+ for (const finalSnapshot of finalSnapshots) if (finalSnapshot !== null) storeNavigationSnapshot(finalSnapshot);
669
+ isTracing = false;
670
+ await writeDevtoolsTrace(traceCdpSession, traceStream, tracePath);
671
+ const capturedSnapshots = [...navigationSnapshotsByTimeOrigin.values()];
672
+ let fallbackFinalUrl = sanitizedRequestedUrl;
673
+ try {
674
+ if (latestPage !== void 0 && !latestPage.isClosed()) fallbackFinalUrl = sanitizeRuntimeUrl(latestPage.url());
675
+ } catch {}
676
+ const mergedSnapshot = capturedSnapshots.length > 0 ? mergeRuntimeScanProbeSnapshots(capturedSnapshots) : createFallbackSnapshot(Date.parse(capturedAt), fallbackFinalUrl);
677
+ return {
678
+ tracePath,
679
+ capturedAt,
680
+ durationMs,
681
+ snapshot: {
682
+ ...mergedSnapshot,
683
+ droppedLongAnimationFrames: mergedSnapshot.droppedLongAnimationFrames + discardedSnapshotEvidence.droppedLongAnimationFrames,
684
+ droppedScriptTimings: mergedSnapshot.droppedScriptTimings + discardedSnapshotEvidence.droppedScriptTimings,
685
+ droppedComponentEvents: mergedSnapshot.droppedComponentEvents + discardedSnapshotEvidence.droppedComponentEvents,
686
+ droppedInteractions: mergedSnapshot.droppedInteractions + discardedSnapshotEvidence.droppedInteractions
687
+ },
688
+ connection
689
+ };
690
+ } finally {
691
+ await cleanup();
692
+ unregisterCleanup();
693
+ }
694
+ };
695
+ //#endregion
696
+ //#region src/cli/runtime-scan/resolve-runtime-scan-format.ts
697
+ const resolveRuntimeScanFormat = (format) => {
698
+ if (format === void 0 || format === "text") return "text";
699
+ if (format === "json" || format === "jsonl") return format;
700
+ throw new CliInputError(`--format must be one of: text, json, jsonl. Received "${format}".`);
701
+ };
702
+ //#endregion
703
+ //#region src/cli/commands/runtime-scan.ts
704
+ const writeRuntimeScanErrorReport = (format, message, sentryEventId) => {
705
+ const errorReport = {
706
+ schemaVersion: 1,
707
+ kind: "react-doctor-runtime-scan-error",
708
+ error: {
709
+ message,
710
+ sentryEventId
711
+ }
712
+ };
713
+ process.stdout.write(format === "json" ? `${JSON.stringify(errorReport, null, 2)}\n` : `${JSON.stringify({
714
+ ...errorReport,
715
+ kind: "error"
716
+ })}\n`);
717
+ };
718
+ const resolveRuntimeScanUrl = async (url) => {
719
+ if (url !== void 0) return url;
720
+ if (!isTuiEnvironmentSupported(resolveTuiEnvironment(process.stderr.isTTY === true))) throw new CliInputError("A URL is required outside an interactive terminal. Run `react-doctor scan <url>`.");
721
+ recordCount(METRIC.runtimeScanUrlPromptShown);
722
+ const { promptRuntimeScanUrl } = await import("./prompt-runtime-scan-url-3SOiiFcU.js");
723
+ return promptRuntimeScanUrl();
724
+ };
725
+ const runtimeScanAction = async (url, flags) => {
726
+ const format = resolveRuntimeScanFormat(flags.format);
727
+ if (format !== "text") enableJsonMode({
728
+ compact: format === "jsonl",
729
+ directory: process.cwd(),
730
+ writeCancellationError: () => {
731
+ writeRuntimeScanErrorReport(format, "Runtime scan cancelled by user.");
732
+ }
733
+ });
734
+ recordCount(METRIC.cliInvoked, 1, { command: "scan" });
735
+ resetSentryRunState();
736
+ try {
737
+ const resolvedUrl = await resolveRuntimeScanUrl(url);
738
+ if (resolvedUrl === null) {
739
+ resetSentryRunState();
740
+ return;
741
+ }
742
+ await withRunSpan(async () => {
743
+ const capture = await recordRuntimeTrace({
744
+ url: resolvedUrl,
745
+ traceOut: flags.traceOut,
746
+ cdpUrl: flags.cdp
747
+ });
748
+ const report = buildRuntimeScanReport({
749
+ requestedUrl: resolvedUrl,
750
+ tracePath: capture.tracePath,
751
+ capturedAt: capture.capturedAt,
752
+ durationMs: capture.durationMs,
753
+ snapshot: capture.snapshot,
754
+ connection: capture.connection
755
+ });
756
+ process.stdout.write(formatRuntimeScanReport(report, format));
757
+ }, { mapErrorForSpan: (error) => /* @__PURE__ */ new Error(error instanceof CliInputError ? "Runtime scan input failed." : "Runtime scan failed.") });
758
+ resetSentryRunState();
759
+ } catch (error) {
760
+ if (format === "text") throw error;
761
+ const sentryEventId = error instanceof CliInputError ? void 0 : await reportErrorToSentry(error);
762
+ writeRuntimeScanErrorReport(format, error instanceof CliInputError ? error.message : "Runtime scan failed. Use the error reference when reporting this issue.", sentryEventId);
763
+ process.exitCode = 1;
764
+ }
765
+ };
766
+ //#endregion
767
+ export { runtimeScanAction };
768
+
769
+ //# sourceMappingURL=runtime-scan-WErSEFfG.js.map
770
+ //# debugId=06ff1d8f-03c6-578b-8857-68b7f148385f