@flotrace/runtime-core 2.2.4 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -31,13 +31,22 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  DEFAULT_CONFIG: () => DEFAULT_CONFIG,
34
+ FLOTRACE_SOURCE: () => FLOTRACE_SOURCE,
34
35
  FloTraceWebSocketClient: () => FloTraceWebSocketClient,
36
+ JSX_RUNTIME_ACTIVE_KEY: () => JSX_RUNTIME_ACTIVE_KEY,
35
37
  buildAncestorChain: () => buildAncestorChain,
38
+ clearCallSiteRenders: () => clearCallSiteRenders,
36
39
  clearFetchOriginTags: () => clearFetchOriginTags,
40
+ computeCallSiteId: () => computeCallSiteId,
41
+ computeCallSiteMetricsPayload: () => computeCallSiteMetricsPayload,
42
+ describeFiberType: () => describeFiberType,
43
+ detectInlineLiterals: () => detectInlineLiterals,
37
44
  detectServerComponent: () => detectServerComponent,
38
45
  detectWebFramework: () => detectWebFramework,
39
46
  disposeWebSocketClient: () => disposeWebSocketClient,
40
47
  findFetchOrigin: () => findFetchOrigin,
48
+ getCallSiteRenderRate: () => getCallSiteRenderRate,
49
+ getCallSiteRenders: () => getCallSiteRenders,
41
50
  getChangedKeys: () => getChangedKeys,
42
51
  getComponentNameFromFiber: () => getComponentNameFromFiber,
43
52
  getCurrentRenderingFiber: () => getCurrentRenderingFiber,
@@ -60,9 +69,16 @@ __export(index_exports, {
60
69
  installTanStackQueryTracker: () => installTanStackQueryTracker,
61
70
  installTimelineTracker: () => installTimelineTracker,
62
71
  installZustandTracker: () => installZustandTracker,
72
+ isJsxRuntimeActive: () => isJsxRuntimeActive,
63
73
  isReduxStore: () => isReduxStore,
64
74
  isTanStackQueryClient: () => isTanStackQueryClient,
75
+ isUserComponent: () => isUserComponent,
76
+ logTreeSnapshot: () => logTreeSnapshot,
77
+ logTreeSummary: () => logTreeSummary,
78
+ markJsxRuntimeActive: () => markJsxRuntimeActive,
65
79
  maybeEmitNextjsContext: () => maybeEmitNextjsContext,
80
+ normalizeJsxSourcePath: () => normalizeJsxSourcePath,
81
+ recordCallSiteRender: () => recordCallSiteRender,
66
82
  recordTimelineEvent: () => recordTimelineEvent,
67
83
  requestFullSnapshot: () => requestFullSnapshot,
68
84
  requestTreeSnapshot: () => requestTreeSnapshot,
@@ -70,6 +86,8 @@ __export(index_exports, {
70
86
  resolveValueTrace: () => resolveValueTrace,
71
87
  serializeProps: () => serializeProps,
72
88
  serializeValue: () => serializeValue,
89
+ setDuplicateKeyEmitter: () => setDuplicateKeyEmitter,
90
+ setFiberDebug: () => setFiberDebug,
73
91
  tagFetchData: () => tagFetchData,
74
92
  uninstallFiberTreeWalker: () => uninstallFiberTreeWalker,
75
93
  uninstallReduxTracker: () => uninstallReduxTracker,
@@ -103,6 +121,223 @@ var DEFAULT_CONFIG = {
103
121
  getAppUrl: void 0
104
122
  };
105
123
 
124
+ // src/jsxRuntimeUtils.ts
125
+ var FLOTRACE_SOURCE = /* @__PURE__ */ Symbol.for("flotrace.source");
126
+ var JSX_RUNTIME_ACTIVE_KEY = /* @__PURE__ */ Symbol.for("flotrace.jsx-runtime-active");
127
+ function normalizeJsxSourcePath(fileName) {
128
+ let p = fileName;
129
+ if (p.startsWith("file://")) p = p.slice("file://".length);
130
+ if (p.startsWith("webpack-internal:///./")) p = p.slice("webpack-internal:///./".length);
131
+ if (p.startsWith("[project]/")) p = p.slice("[project]/".length);
132
+ if (p.startsWith("./")) p = p.slice(2);
133
+ if (/^\/[a-zA-Z]:[\\/]/.test(p)) p = p.slice(1);
134
+ if (/^[a-zA-Z]:[\\/]/.test(p)) p = p[0].toLowerCase() + p.slice(1);
135
+ return p;
136
+ }
137
+ function normalizeStackFramePath(rawPath) {
138
+ let p = rawPath;
139
+ const queryIdx = p.indexOf("?");
140
+ if (queryIdx !== -1) p = p.slice(0, queryIdx);
141
+ const httpMatch = p.match(/^https?:\/\/[^/]+(\/.+)$/);
142
+ if (httpMatch) {
143
+ p = httpMatch[1];
144
+ if (p.startsWith("/")) p = p.slice(1);
145
+ }
146
+ return normalizeJsxSourcePath(p);
147
+ }
148
+ function isJsBundlePath(rawPath) {
149
+ if (/\bindex\.bundle\b/.test(rawPath)) return true;
150
+ if (/\.bundle(\?|$|\.js(\?|$))/.test(rawPath)) return true;
151
+ if (/\?platform=(ios|android|web|native)\b/.test(rawPath)) return true;
152
+ return false;
153
+ }
154
+ function parseFirstNonReactFrame(stack) {
155
+ const lines = stack.split("\n");
156
+ for (const line of lines) {
157
+ const parened = line.match(/\(([^)]+):(\d+):(\d+)\)/);
158
+ const hermes = line.match(/@([^\s]+):(\d+):(\d+)$/);
159
+ const match = parened ?? hermes;
160
+ if (!match) continue;
161
+ const path = match[1];
162
+ if (path.includes("react-dom")) continue;
163
+ if (path.includes("react-native/Libraries")) continue;
164
+ if (path.includes("/react/cjs/")) continue;
165
+ if (path.includes("/scheduler/")) continue;
166
+ if (isJsBundlePath(path)) continue;
167
+ return {
168
+ fileName: normalizeStackFramePath(path),
169
+ lineNumber: Number(match[2]),
170
+ columnNumber: Number(match[3])
171
+ };
172
+ }
173
+ return null;
174
+ }
175
+ function computeCallSiteId(source) {
176
+ const normPath = normalizeJsxSourcePath(source.fileName);
177
+ const key = `${normPath}:${source.lineNumber}:${source.columnNumber}`;
178
+ let hash = 2166136261;
179
+ for (let i = 0; i < key.length; i++) {
180
+ hash ^= key.charCodeAt(i);
181
+ hash = Math.imul(hash, 16777619);
182
+ }
183
+ return (hash >>> 0).toString(16).padStart(8, "0");
184
+ }
185
+ var FLOTRACE_SRC_ATTR = "data-flotrace-src";
186
+ function parseDataFlotraceSrc(value) {
187
+ let obj;
188
+ try {
189
+ obj = JSON.parse(value);
190
+ } catch {
191
+ return void 0;
192
+ }
193
+ if (!obj || typeof obj !== "object") return void 0;
194
+ const o = obj;
195
+ if (typeof o.f !== "string" || typeof o.l !== "number" || typeof o.c !== "number") {
196
+ return void 0;
197
+ }
198
+ const fileName = o.f;
199
+ const lineNumber = o.l;
200
+ const columnNumber = o.c;
201
+ return {
202
+ fileName,
203
+ lineNumber,
204
+ columnNumber,
205
+ callSiteId: computeCallSiteId({ fileName, lineNumber, columnNumber })
206
+ };
207
+ }
208
+ function readJsxSourceFromFiber(fiber) {
209
+ const props = fiber.memoizedProps;
210
+ if (props) {
211
+ const symRaw = props[FLOTRACE_SOURCE];
212
+ if (symRaw && typeof symRaw === "object") {
213
+ const obj = symRaw;
214
+ if (typeof obj.fileName === "string" && typeof obj.lineNumber === "number" && typeof obj.columnNumber === "number" && typeof obj.callSiteId === "string") {
215
+ return symRaw;
216
+ }
217
+ }
218
+ const strRaw = props[FLOTRACE_SRC_ATTR];
219
+ if (typeof strRaw === "string") {
220
+ const parsed = parseDataFlotraceSrc(strRaw);
221
+ if (parsed) return parsed;
222
+ }
223
+ }
224
+ const type = fiber.type;
225
+ if (type !== null && (typeof type === "function" || typeof type === "object")) {
226
+ const typeAttr = type[FLOTRACE_SRC_ATTR];
227
+ if (typeof typeAttr === "string") {
228
+ const parsed = parseDataFlotraceSrc(typeAttr);
229
+ if (parsed) return parsed;
230
+ }
231
+ }
232
+ return void 0;
233
+ }
234
+ function isUserComponent(fiber) {
235
+ const jsxSrc = readJsxSourceFromFiber({
236
+ memoizedProps: fiber.memoizedProps ?? null,
237
+ type: fiber.type
238
+ });
239
+ if (jsxSrc && !jsxSrc.fileName.includes("node_modules")) return true;
240
+ const debugSourcePath = fiber._debugSource?.fileName;
241
+ if (typeof debugSourcePath === "string" && !debugSourcePath.includes("node_modules")) {
242
+ return true;
243
+ }
244
+ const stack = fiber._debugStack?.stack;
245
+ if (typeof stack === "string") {
246
+ const frame = parseFirstNonReactFrame(stack);
247
+ if (frame && !frame.fileName.includes("node_modules")) return true;
248
+ }
249
+ return false;
250
+ }
251
+ var callSiteRenders = /* @__PURE__ */ new Map();
252
+ var RING_BUFFER_MAX = 60;
253
+ function recordCallSiteRender(callSiteId, now2 = performance.now()) {
254
+ const arr = callSiteRenders.get(callSiteId);
255
+ if (arr === void 0) {
256
+ callSiteRenders.set(callSiteId, [now2]);
257
+ return;
258
+ }
259
+ arr.push(now2);
260
+ if (arr.length > RING_BUFFER_MAX) arr.shift();
261
+ }
262
+ function getCallSiteRenders(callSiteId) {
263
+ return callSiteRenders.get(callSiteId) ?? [];
264
+ }
265
+ function getCallSiteRenderRate(callSiteId, windowMs = 5e3, now2 = performance.now()) {
266
+ const arr = callSiteRenders.get(callSiteId);
267
+ if (!arr || arr.length === 0) return 0;
268
+ const cutoff = now2 - windowMs;
269
+ let count = 0;
270
+ for (let i = arr.length - 1; i >= 0; i--) {
271
+ if (arr[i] >= cutoff) count++;
272
+ else break;
273
+ }
274
+ return count / windowMs * 1e3;
275
+ }
276
+ function clearCallSiteRenders() {
277
+ callSiteRenders.clear();
278
+ }
279
+ function computeCallSiteMetricsPayload(windowMs = 5e3, now2 = performance.now()) {
280
+ let out = null;
281
+ const cutoff = now2 - windowMs;
282
+ for (const [callSiteId, arr] of callSiteRenders) {
283
+ if (arr.length === 0) continue;
284
+ let count = 0;
285
+ for (let i = arr.length - 1; i >= 0; i--) {
286
+ if (arr[i] >= cutoff) count++;
287
+ else break;
288
+ }
289
+ if (count > 0) {
290
+ (out ?? (out = {}))[callSiteId] = count / windowMs * 1e3;
291
+ }
292
+ }
293
+ return out;
294
+ }
295
+ var duplicateKeyEmitter = null;
296
+ function setDuplicateKeyEmitter(emitter) {
297
+ duplicateKeyEmitter = emitter;
298
+ }
299
+ function markJsxRuntimeActive() {
300
+ globalThis[JSX_RUNTIME_ACTIVE_KEY] = true;
301
+ }
302
+ function isJsxRuntimeActive() {
303
+ return globalThis[JSX_RUNTIME_ACTIVE_KEY] === true;
304
+ }
305
+ var KNOWN_REACT_PROPS = /* @__PURE__ */ new Set(["key", "ref", "children", "className"]);
306
+ var REACT_ELEMENT_TYPEOF_LEGACY = /* @__PURE__ */ Symbol.for("react.element");
307
+ var REACT_ELEMENT_TYPEOF_R19 = /* @__PURE__ */ Symbol.for("react.transitional.element");
308
+ function isReactElement(v) {
309
+ const typeOf = v.$$typeof;
310
+ return typeOf === REACT_ELEMENT_TYPEOF_LEGACY || typeOf === REACT_ELEMENT_TYPEOF_R19;
311
+ }
312
+ function detectInlineLiterals(props) {
313
+ let out;
314
+ for (const k in props) {
315
+ if (KNOWN_REACT_PROPS.has(k)) continue;
316
+ const v = props[k];
317
+ if (typeof v === "function") {
318
+ if (!v.name) {
319
+ (out ?? (out = {}))[k] = "fn";
320
+ }
321
+ } else if (Array.isArray(v)) {
322
+ if (v.length > 0) {
323
+ (out ?? (out = {}))[k] = "arr";
324
+ }
325
+ } else if (v !== null && typeof v === "object" && // Skip elements processed by our own runtime (marker present).
326
+ !(FLOTRACE_SOURCE in v) && // Skip React elements processed by ANY runtime — `$$typeof` is set on
327
+ // every element regardless of which jsx-runtime created it, so this
328
+ // catches mixed-runtime codebases. Without this guard, an inline
329
+ // `<Outer child={<Inner/>} />` would false-positive on `child` when
330
+ // Inner went through a different jsxImportSource.
331
+ !isReactElement(v)) {
332
+ const proto = Object.getPrototypeOf(v);
333
+ if (proto === Object.prototype || proto === null) {
334
+ (out ?? (out = {}))[k] = "obj";
335
+ }
336
+ }
337
+ }
338
+ return out;
339
+ }
340
+
106
341
  // src/serializer.ts
107
342
  var MAX_DEPTH = 5;
108
343
  var MAX_STRING_LENGTH = 500;
@@ -211,11 +446,7 @@ function serializeValue(value, depth = 0, seen = /* @__PURE__ */ new WeakSet())
211
446
  for (let i = 0; i < Math.min(keys.length, MAX_OBJECT_KEYS); i++) {
212
447
  const key = keys[i];
213
448
  try {
214
- result[key] = serializeValue(
215
- value[key],
216
- depth + 1,
217
- seen
218
- );
449
+ result[key] = serializeValue(value[key], depth + 1, seen);
219
450
  } catch {
220
451
  result[key] = { __type: "truncated", originalType: "error" };
221
452
  }
@@ -316,7 +547,12 @@ var _FloTraceWebSocketClient = class _FloTraceWebSocketClient {
316
547
  frameworkName: this.config.frameworkName,
317
548
  frameworkVersion: this.config.frameworkVersion,
318
549
  reactNativeVersion: this.config.reactNativeVersion,
319
- runtimeVersion: this.config.runtimeVersion
550
+ runtimeVersion: this.config.runtimeVersion,
551
+ // P5: JSX runtime adoption signal — read at WS-open time so
552
+ // multiple fibers have already rendered by the moment we report.
553
+ // `isJsxRuntimeActive` reads `globalThis[Symbol.for('flotrace.jsx-runtime-active')]`,
554
+ // which the dev jsx-runtime sets on first jsxDEV call.
555
+ jsxRuntimeActive: isJsxRuntimeActive()
320
556
  });
321
557
  this.flush();
322
558
  };
@@ -642,26 +878,31 @@ function classifyFromDebugLabel(state, index, effects, effectIdx, debugLabel) {
642
878
  const ms = state.memoizedState;
643
879
  const normalizedLabel = debugLabel.toLowerCase().replace(/\s/g, "");
644
880
  const labelMap = {
645
- "usestate": "useState",
646
- "usereducer": "useReducer",
647
- "useref": "useRef",
648
- "usememo": "useMemo",
649
- "usecallback": "useCallback",
650
- "useeffect": "useEffect",
651
- "uselayouteffect": "useLayoutEffect",
652
- "useinsertioneffect": "useInsertionEffect",
653
- "usecontext": "useContext",
654
- "useimperativehandle": "useImperativeHandle",
655
- "usedebugvalue": "useDebugValue",
656
- "usetransition": "useTransition",
657
- "usedeferredvalue": "useDeferredValue",
658
- "useid": "useId",
659
- "usesyncexternalstore": "useSyncExternalStore",
660
- "useoptimistic": "useOptimistic",
661
- "useformstatus": "useFormStatus"
881
+ usestate: "useState",
882
+ usereducer: "useReducer",
883
+ useref: "useRef",
884
+ usememo: "useMemo",
885
+ usecallback: "useCallback",
886
+ useeffect: "useEffect",
887
+ uselayouteffect: "useLayoutEffect",
888
+ useinsertioneffect: "useInsertionEffect",
889
+ usecontext: "useContext",
890
+ useimperativehandle: "useImperativeHandle",
891
+ usedebugvalue: "useDebugValue",
892
+ usetransition: "useTransition",
893
+ usedeferredvalue: "useDeferredValue",
894
+ useid: "useId",
895
+ usesyncexternalstore: "useSyncExternalStore",
896
+ useoptimistic: "useOptimistic",
897
+ useformstatus: "useFormStatus"
662
898
  };
663
899
  const hookType = labelMap[normalizedLabel] ?? "unknown";
664
- const base = { index, type: hookType, value: serializeValue(ms, 0, /* @__PURE__ */ new WeakSet()), debugLabel };
900
+ const base = {
901
+ index,
902
+ type: hookType,
903
+ value: serializeValue(ms, 0, /* @__PURE__ */ new WeakSet()),
904
+ debugLabel
905
+ };
665
906
  if (hookType === "useEffect" || hookType === "useLayoutEffect" || hookType === "useInsertionEffect") {
666
907
  if (effectIdx < effects.length) {
667
908
  const effect = effects[effectIdx];
@@ -1128,7 +1369,13 @@ var ClassComponent = 1;
1128
1369
  var ForwardRef = 11;
1129
1370
  var MemoComponent = 14;
1130
1371
  var SimpleMemoComponent = 15;
1131
- var USER_TAGS = /* @__PURE__ */ new Set([FunctionComponent, ClassComponent, ForwardRef, MemoComponent, SimpleMemoComponent]);
1372
+ var USER_TAGS = /* @__PURE__ */ new Set([
1373
+ FunctionComponent,
1374
+ ClassComponent,
1375
+ ForwardRef,
1376
+ MemoComponent,
1377
+ SimpleMemoComponent
1378
+ ]);
1132
1379
  function isMemoizedFiber(fiber) {
1133
1380
  return fiber.tag === MemoComponent || fiber.tag === SimpleMemoComponent;
1134
1381
  }
@@ -1212,13 +1459,15 @@ function buildCascadeTree(rootFiber, triggers) {
1212
1459
  triggerByName.set(t.componentName, t);
1213
1460
  }
1214
1461
  }
1215
- const stack = [{
1216
- fiber: rootFiber,
1217
- depth: 0,
1218
- parentRerendered: false,
1219
- parentNode: null,
1220
- isRoot: true
1221
- }];
1462
+ const stack = [
1463
+ {
1464
+ fiber: rootFiber,
1465
+ depth: 0,
1466
+ parentRerendered: false,
1467
+ parentNode: null,
1468
+ isRoot: true
1469
+ }
1470
+ ];
1222
1471
  while (stack.length > 0) {
1223
1472
  const entry = stack.pop();
1224
1473
  const { fiber, depth, parentRerendered, parentNode, isRoot } = entry;
@@ -1229,7 +1478,13 @@ function buildCascadeTree(rootFiber, triggers) {
1229
1478
  if (isNewMount && !didRender) {
1230
1479
  let child2 = fiber.child;
1231
1480
  while (child2) {
1232
- stack.push({ fiber: child2, depth: depth + 1, parentRerendered: false, parentNode, isRoot: false });
1481
+ stack.push({
1482
+ fiber: child2,
1483
+ depth: depth + 1,
1484
+ parentRerendered: false,
1485
+ parentNode,
1486
+ isRoot: false
1487
+ });
1233
1488
  child2 = child2.sibling;
1234
1489
  }
1235
1490
  continue;
@@ -1237,7 +1492,13 @@ function buildCascadeTree(rootFiber, triggers) {
1237
1492
  if (!USER_TAGS.has(fiber.tag)) {
1238
1493
  let child2 = fiber.child;
1239
1494
  while (child2) {
1240
- stack.push({ fiber: child2, depth: depth + 1, parentRerendered: didRender || parentRerendered, parentNode, isRoot: false });
1495
+ stack.push({
1496
+ fiber: child2,
1497
+ depth: depth + 1,
1498
+ parentRerendered: didRender || parentRerendered,
1499
+ parentNode,
1500
+ isRoot: false
1501
+ });
1241
1502
  child2 = child2.sibling;
1242
1503
  }
1243
1504
  continue;
@@ -1246,7 +1507,13 @@ function buildCascadeTree(rootFiber, triggers) {
1246
1507
  if (reason === null) {
1247
1508
  let child2 = fiber.child;
1248
1509
  while (child2) {
1249
- stack.push({ fiber: child2, depth: depth + 1, parentRerendered: false, parentNode, isRoot: false });
1510
+ stack.push({
1511
+ fiber: child2,
1512
+ depth: depth + 1,
1513
+ parentRerendered: false,
1514
+ parentNode,
1515
+ isRoot: false
1516
+ });
1250
1517
  child2 = child2.sibling;
1251
1518
  }
1252
1519
  continue;
@@ -1272,7 +1539,11 @@ function buildCascadeTree(rootFiber, triggers) {
1272
1539
  triggerId,
1273
1540
  children: [],
1274
1541
  depth,
1275
- isMemoized: isMemoizedFiber(fiber)
1542
+ isMemoized: isMemoizedFiber(fiber),
1543
+ // P6: JSX-runtime attribution — read from fiber.memoizedProps directly.
1544
+ // Same source the walker uses, so cascade nodes align with LiveTreeNode
1545
+ // attribution for the same user component.
1546
+ jsxSource: readJsxSourceFromFiber(fiber)
1276
1547
  };
1277
1548
  totalComponents++;
1278
1549
  if (reason === "parent-cascade") {
@@ -1305,7 +1576,10 @@ function analyzeCascade(root, triggers) {
1305
1576
  try {
1306
1577
  const finishedLanes = getFinishedLanes(root);
1307
1578
  const lane = classifyLanes(finishedLanes);
1308
- const { rootCauses, totalComponents, avoidableCount, avoidableDuration } = buildCascadeTree(root.current, triggers);
1579
+ const { rootCauses, totalComponents, avoidableCount, avoidableDuration } = buildCascadeTree(
1580
+ root.current,
1581
+ triggers
1582
+ );
1309
1583
  if (totalComponents === 0) return null;
1310
1584
  const totalDuration = rootCauses.reduce((sum, n) => sum + n.subtreeDuration, 0);
1311
1585
  const triggerIds = triggers.map((t) => t.triggerId);
@@ -1387,7 +1661,8 @@ function shouldFlagRename(value) {
1387
1661
  if (value === null || value === void 0) return false;
1388
1662
  if (typeof value !== "object") return false;
1389
1663
  if (Array.isArray(value) && value.length === 0) return false;
1390
- if (!Array.isArray(value) && Object.keys(value).length === 0) return false;
1664
+ if (!Array.isArray(value) && Object.keys(value).length === 0)
1665
+ return false;
1391
1666
  return true;
1392
1667
  }
1393
1668
  function computePropIntersectionRatio(nodeProps, childrenProps) {
@@ -1535,7 +1810,11 @@ function runAnalysis(tree, fiberRefMap2) {
1535
1810
  }
1536
1811
  }
1537
1812
  for (const sourceId of sourceNodeIds) {
1538
- let dfs2 = function(currentId, currentPropKey, currentPath, visited) {
1813
+ const firstEdge = outEdges.get(sourceId)?.[0];
1814
+ if (!firstEdge) continue;
1815
+ const sourcePropName = firstEdge.propKey;
1816
+ const allPaths = [];
1817
+ const dfs = (currentId, currentPropKey, currentPath, visited) => {
1539
1818
  if (visited.has(currentId)) return;
1540
1819
  visited.add(currentId);
1541
1820
  const outgoing = outEdges.get(currentId);
@@ -1548,7 +1827,7 @@ function runAnalysis(tree, fiberRefMap2) {
1548
1827
  }
1549
1828
  for (const edge of outgoing) {
1550
1829
  const isRename = edge.propKey !== edge.childPropKey;
1551
- dfs2(
1830
+ dfs(
1552
1831
  edge.childNodeId,
1553
1832
  edge.childPropKey,
1554
1833
  [...currentPath, { nodeId: edge.childNodeId, propKey: edge.childPropKey, isRename }],
@@ -1557,12 +1836,7 @@ function runAnalysis(tree, fiberRefMap2) {
1557
1836
  }
1558
1837
  visited.delete(currentId);
1559
1838
  };
1560
- var dfs = dfs2;
1561
- const firstEdge = outEdges.get(sourceId)?.[0];
1562
- if (!firstEdge) continue;
1563
- const sourcePropName = firstEdge.propKey;
1564
- const allPaths = [];
1565
- dfs2(
1839
+ dfs(
1566
1840
  sourceId,
1567
1841
  sourcePropName,
1568
1842
  [{ nodeId: sourceId, propKey: sourcePropName, isRename: false }],
@@ -1596,13 +1870,24 @@ function runAnalysis(tree, fiberRefMap2) {
1596
1870
  propKey: p.propKey,
1597
1871
  role,
1598
1872
  hookCount: hookCounts.get(p.nodeId) ?? 0,
1599
- hasContextHook: contextFlags.get(p.nodeId) ?? false
1873
+ hasContextHook: contextFlags.get(p.nodeId) ?? false,
1874
+ // P6: propagate JSX-runtime attribution from the tree node so the
1875
+ // drill-chain detail can render `(file:line)` per step + click-
1876
+ // to-IDE on each component along the chain. Undefined when the
1877
+ // user hasn't opted into the JSX runtime.
1878
+ jsxSource: n?.jsxSource
1600
1879
  };
1601
1880
  });
1602
1881
  const passthroughCount = chainNodes.filter((n) => n.role === "passthrough").length;
1603
1882
  const sourceNode = nodeMap.get(sourceId);
1604
1883
  const renames = path.flatMap(
1605
- (p, idx) => p.isRename ? [{ atNodeId: p.nodeId, fromKey: idx > 0 ? path[idx - 1].propKey : sourcePropName, toKey: p.propKey }] : []
1884
+ (p, idx) => p.isRename ? [
1885
+ {
1886
+ atNodeId: p.nodeId,
1887
+ fromKey: idx > 0 ? path[idx - 1].propKey : sourcePropName,
1888
+ toKey: p.propKey
1889
+ }
1890
+ ] : []
1606
1891
  );
1607
1892
  chains.push({
1608
1893
  chainId: makeChainId(sourceId, fp, consumerNodeId),
@@ -1697,10 +1982,7 @@ var SERVER_COMPONENT_PATTERNS = [
1697
1982
  /[\\/]app[\\/].+[\\/]error\.[jt]sx?$/
1698
1983
  // Next.js error UI
1699
1984
  ];
1700
- var SERVER_REFERENCE_PATTERNS = [
1701
- /_ServerReference$/,
1702
- /^RSC_/
1703
- ];
1985
+ var SERVER_REFERENCE_PATTERNS = [/_ServerReference$/, /^RSC_/];
1704
1986
  var detectionEmitted = false;
1705
1987
  function maybeEmitNextjsContext(client2) {
1706
1988
  if (detectionEmitted) return;
@@ -1794,7 +2076,9 @@ function scanActionStateChanges(fiberRefMap2, client2) {
1794
2076
  for (const [nodeId, fiber] of fiberRefMap2) {
1795
2077
  const entries = extractActionEntries(fiber);
1796
2078
  if (!entries) continue;
1797
- const snapshot = JSON.stringify(entries.map((e) => ({ i: e.hookIndex, p: e.isPending, s: e.state })));
2079
+ const snapshot = JSON.stringify(
2080
+ entries.map((e) => ({ i: e.hookIndex, p: e.isPending, s: e.state }))
2081
+ );
1798
2082
  if (prevActionStateMap.get(nodeId) === snapshot) continue;
1799
2083
  prevActionStateMap.set(nodeId, snapshot);
1800
2084
  const componentName = nodeId.split("/").pop()?.replace(/-\d+$/, "") ?? "Unknown";
@@ -1904,7 +2188,8 @@ function tagFetchData(obj, requestId, depth = 0) {
1904
2188
  const limit = Math.min(obj.length, FETCH_ORIGIN_TAG_ARRAY_LIMIT);
1905
2189
  for (let i = 0; i < limit; i++) tagFetchData(obj[i], requestId, depth + 1);
1906
2190
  } else {
1907
- for (const val of Object.values(obj)) tagFetchData(val, requestId, depth + 1);
2191
+ for (const val of Object.values(obj))
2192
+ tagFetchData(val, requestId, depth + 1);
1908
2193
  }
1909
2194
  }
1910
2195
  function hasActiveTags() {
@@ -1940,6 +2225,344 @@ function scanForOrigin(obj, depth, ignoreTTL) {
1940
2225
  return void 0;
1941
2226
  }
1942
2227
 
2228
+ // src/fiberDebugLogger.ts
2229
+ var MAX_FIBER_RECORDS = 500;
2230
+ var MAX_TREE_RECORDS = 50;
2231
+ var MAX_TOP_NAMES_PER_SNAPSHOT = 10;
2232
+ var MAX_CONTEXTS_PER_FIBER = 8;
2233
+ var totalFiberEvents = 0;
2234
+ var recordingStartedAt = null;
2235
+ var fiberRecords = /* @__PURE__ */ new Map();
2236
+ var treeRecords = [];
2237
+ function isRecording() {
2238
+ return Boolean(globalThis.__FT_DEBUG);
2239
+ }
2240
+ function setFiberDebug(enabled) {
2241
+ globalThis.__FT_DEBUG = enabled;
2242
+ if (enabled) {
2243
+ console.info(
2244
+ "%c[FT debug]%c recording started \u2014 call %c__ft.dump()%c to view, %c__ft.clear()%c to reset, %c__ft.download()%c to export",
2245
+ "background:#1e293b;color:#7dd3fc;padding:1px 6px;border-radius:3px;font-weight:600;",
2246
+ "color:#94a3b8;",
2247
+ "color:#a78bfa;font-weight:600;",
2248
+ "color:#94a3b8;",
2249
+ "color:#a78bfa;font-weight:600;",
2250
+ "color:#94a3b8;",
2251
+ "color:#a78bfa;font-weight:600;",
2252
+ "color:#94a3b8;"
2253
+ );
2254
+ }
2255
+ }
2256
+ function isFiberFunctionType(t) {
2257
+ return typeof t === "function";
2258
+ }
2259
+ function isFiberObjectType(t) {
2260
+ return typeof t === "object" && t !== null;
2261
+ }
2262
+ function isClassComponentType(t) {
2263
+ if (typeof t !== "function") return false;
2264
+ const proto = t.prototype;
2265
+ return typeof proto?.isReactComponent !== "undefined";
2266
+ }
2267
+ function describeFiberType(fiber) {
2268
+ const type = fiber.type;
2269
+ if (typeof type === "string") {
2270
+ return {
2271
+ kind: "host",
2272
+ name: type,
2273
+ displayName: void 0,
2274
+ resolved: type,
2275
+ looksMinified: false
2276
+ };
2277
+ }
2278
+ if (isFiberFunctionType(type)) {
2279
+ const isClass = isClassComponentType(type);
2280
+ const resolved = type.displayName ?? type.name ?? "Anonymous";
2281
+ return {
2282
+ kind: isClass ? "class" : "function",
2283
+ name: type.name,
2284
+ displayName: type.displayName,
2285
+ resolved,
2286
+ looksMinified: looksMinified(resolved)
2287
+ };
2288
+ }
2289
+ if (isFiberObjectType(type)) {
2290
+ if (type.render) {
2291
+ const resolved2 = type.render.displayName ?? type.render.name ?? "ForwardRef";
2292
+ return {
2293
+ kind: "forwardRef",
2294
+ name: type.render.name,
2295
+ displayName: type.render.displayName,
2296
+ resolved: resolved2,
2297
+ looksMinified: looksMinified(resolved2)
2298
+ };
2299
+ }
2300
+ if (type.type) {
2301
+ const resolved2 = type.type.displayName ?? type.type.name ?? "Memo";
2302
+ return {
2303
+ kind: "memo",
2304
+ name: type.type.name,
2305
+ displayName: type.type.displayName,
2306
+ resolved: resolved2,
2307
+ looksMinified: looksMinified(resolved2)
2308
+ };
2309
+ }
2310
+ const resolved = type.displayName ?? type.name ?? "Unknown";
2311
+ return {
2312
+ kind: "unknown",
2313
+ name: type.name,
2314
+ displayName: type.displayName,
2315
+ resolved,
2316
+ looksMinified: looksMinified(resolved)
2317
+ };
2318
+ }
2319
+ return {
2320
+ kind: "unknown",
2321
+ name: void 0,
2322
+ displayName: void 0,
2323
+ resolved: "Unknown",
2324
+ looksMinified: false
2325
+ };
2326
+ }
2327
+ function looksMinified(name) {
2328
+ return /^[a-z_$][a-z0-9_$]?$/i.test(name);
2329
+ }
2330
+ function logFiberType(fiber, context) {
2331
+ if (!isRecording()) return;
2332
+ const info = describeFiberType(fiber);
2333
+ const key = info.resolved;
2334
+ const now2 = Date.now();
2335
+ if (recordingStartedAt === null) recordingStartedAt = now2;
2336
+ totalFiberEvents += 1;
2337
+ const existing = fiberRecords.get(key);
2338
+ if (existing) {
2339
+ existing.count += 1;
2340
+ existing.lastSeen = now2;
2341
+ if (context && existing.contexts.size < MAX_CONTEXTS_PER_FIBER) {
2342
+ existing.contexts.add(context);
2343
+ }
2344
+ if (existing.exampleKey === void 0 && typeof fiber.key === "string") {
2345
+ existing.exampleKey = fiber.key;
2346
+ }
2347
+ return;
2348
+ }
2349
+ if (fiberRecords.size >= MAX_FIBER_RECORDS) {
2350
+ evictOldestFiber();
2351
+ }
2352
+ fiberRecords.set(key, {
2353
+ name: info.resolved,
2354
+ rawName: info.name,
2355
+ rawDisplayName: info.displayName,
2356
+ kind: info.kind,
2357
+ fiberTag: fiber.tag,
2358
+ looksMinified: info.looksMinified,
2359
+ count: 1,
2360
+ contexts: new Set(context ? [context] : []),
2361
+ firstSeen: now2,
2362
+ lastSeen: now2,
2363
+ source: fiber._debugSource ?? void 0,
2364
+ exampleKey: typeof fiber.key === "string" ? fiber.key : void 0
2365
+ });
2366
+ }
2367
+ function evictOldestFiber() {
2368
+ let oldestKey;
2369
+ let oldestTime = Infinity;
2370
+ for (const [key, rec] of fiberRecords) {
2371
+ if (rec.lastSeen < oldestTime) {
2372
+ oldestTime = rec.lastSeen;
2373
+ oldestKey = key;
2374
+ }
2375
+ }
2376
+ if (oldestKey) fiberRecords.delete(oldestKey);
2377
+ }
2378
+ function walkStats(node, depth, acc) {
2379
+ acc.total += 1;
2380
+ if (depth > acc.maxDepth) acc.maxDepth = depth;
2381
+ if (looksMinified(node.name)) acc.minifiedLike += 1;
2382
+ acc.byName.set(node.name, (acc.byName.get(node.name) ?? 0) + 1);
2383
+ for (const child of node.children) walkStats(child, depth + 1, acc);
2384
+ }
2385
+ function logTreeSnapshot(tree, context) {
2386
+ if (!isRecording() || !tree) return;
2387
+ const stats = { total: 0, minifiedLike: 0, byName: /* @__PURE__ */ new Map(), maxDepth: 0 };
2388
+ walkStats(tree, 0, stats);
2389
+ const topNames = [...stats.byName.entries()].sort((a, b) => b[1] - a[1]).slice(0, MAX_TOP_NAMES_PER_SNAPSHOT).map(([n, c]) => `${n}:${c}`).join(",");
2390
+ treeRecords.push({
2391
+ ts: Date.now(),
2392
+ ctx: context ?? "",
2393
+ rootName: tree.name,
2394
+ totalNodes: stats.total,
2395
+ maxDepth: stats.maxDepth,
2396
+ minifiedLike: stats.minifiedLike,
2397
+ topNames
2398
+ });
2399
+ if (treeRecords.length > MAX_TREE_RECORDS) treeRecords.shift();
2400
+ }
2401
+ var logTreeSummary = logTreeSnapshot;
2402
+ function installConsoleApi() {
2403
+ if (globalThis.__ft) return;
2404
+ const api = {
2405
+ dump() {
2406
+ const fiberRows = serializeFiberRecords();
2407
+ const snapRows = serializeTreeRecords();
2408
+ const minifiedCount = fiberRows.filter((r) => r.looksMinified).length;
2409
+ const elapsedSec = recordingStartedAt === null ? 0 : (Date.now() - recordingStartedAt) / 1e3;
2410
+ const summary = [
2411
+ {
2412
+ metric: "uniqueComponents",
2413
+ value: fiberRecords.size
2414
+ },
2415
+ {
2416
+ metric: "totalFiberEvents",
2417
+ value: totalFiberEvents
2418
+ },
2419
+ {
2420
+ metric: "minifiedLike",
2421
+ value: `${minifiedCount} / ${fiberRecords.size}`
2422
+ },
2423
+ {
2424
+ metric: "snapshots",
2425
+ value: treeRecords.length
2426
+ },
2427
+ {
2428
+ metric: "recordingSec",
2429
+ value: elapsedSec.toFixed(1)
2430
+ }
2431
+ ];
2432
+ console.groupCollapsed(
2433
+ `%c[FT debug] dump%c \u2014 ${fiberRecords.size} components, ${totalFiberEvents} events, ${treeRecords.length} snapshots`,
2434
+ "background:#1e293b;color:#7dd3fc;padding:1px 6px;border-radius:3px;font-weight:600;",
2435
+ "color:#94a3b8;"
2436
+ );
2437
+ console.log("Summary:");
2438
+ console.table(summary, ["metric", "value"]);
2439
+ console.log("Fibers \u2014 every observed component (sorted by call count):");
2440
+ console.table(fiberRows, [
2441
+ "name",
2442
+ "rawName",
2443
+ "rawDisplayName",
2444
+ "kind",
2445
+ "fiberTag",
2446
+ "count",
2447
+ "looksMinified",
2448
+ "exampleKey",
2449
+ "contexts",
2450
+ "file",
2451
+ "line",
2452
+ "firstSeenAt",
2453
+ "lastSeenAt",
2454
+ "lastAgoSec"
2455
+ ]);
2456
+ console.log("Tree snapshots \u2014 newest last:");
2457
+ console.table(snapRows, [
2458
+ "ts",
2459
+ "ctx",
2460
+ "rootName",
2461
+ "totalNodes",
2462
+ "maxDepth",
2463
+ "minifiedLike",
2464
+ "topNames"
2465
+ ]);
2466
+ console.groupEnd();
2467
+ },
2468
+ fibers() {
2469
+ console.table(serializeFiberRecords(), [
2470
+ "name",
2471
+ "rawName",
2472
+ "rawDisplayName",
2473
+ "kind",
2474
+ "fiberTag",
2475
+ "count",
2476
+ "looksMinified",
2477
+ "exampleKey",
2478
+ "contexts",
2479
+ "file",
2480
+ "line",
2481
+ "firstSeenAt",
2482
+ "lastSeenAt",
2483
+ "lastAgoSec"
2484
+ ]);
2485
+ },
2486
+ snapshots() {
2487
+ console.table(serializeTreeRecords());
2488
+ },
2489
+ tail(n = 20) {
2490
+ console.table(treeRecords.slice(-n));
2491
+ },
2492
+ clear() {
2493
+ fiberRecords.clear();
2494
+ treeRecords.length = 0;
2495
+ console.info("[FT debug] cleared");
2496
+ },
2497
+ size() {
2498
+ return { fibers: fiberRecords.size, snapshots: treeRecords.length };
2499
+ },
2500
+ export() {
2501
+ return { fibers: serializeFiberRecords(), snapshots: treeRecords.slice() };
2502
+ },
2503
+ download(filename) {
2504
+ const data = api.export();
2505
+ const json = JSON.stringify(data, null, 2);
2506
+ const docRef = globalThis.document;
2507
+ const URLRef = globalThis.URL;
2508
+ if (!docRef || !URLRef || typeof URLRef.createObjectURL !== "function") {
2509
+ console.warn(
2510
+ "[FT debug] download() requires a browser environment \u2014 printing JSON instead"
2511
+ );
2512
+ console.log(json);
2513
+ return;
2514
+ }
2515
+ const blob = new Blob([json], { type: "application/json" });
2516
+ const url = URLRef.createObjectURL(blob);
2517
+ const a = docRef.createElement("a");
2518
+ a.href = url;
2519
+ a.download = filename ?? `flotrace-debug-${Date.now()}.json`;
2520
+ a.click();
2521
+ URLRef.revokeObjectURL(url);
2522
+ }
2523
+ };
2524
+ globalThis.__ft = api;
2525
+ }
2526
+ installConsoleApi();
2527
+ function formatTime(ms) {
2528
+ const d = new Date(ms);
2529
+ const hh = String(d.getHours()).padStart(2, "0");
2530
+ const mm = String(d.getMinutes()).padStart(2, "0");
2531
+ const ss = String(d.getSeconds()).padStart(2, "0");
2532
+ const mss = String(d.getMilliseconds()).padStart(3, "0");
2533
+ return `${hh}:${mm}:${ss}.${mss}`;
2534
+ }
2535
+ function serializeFiberRecords() {
2536
+ const now2 = Date.now();
2537
+ return [...fiberRecords.values()].sort((a, b) => b.count - a.count).map((r) => ({
2538
+ name: r.name,
2539
+ rawName: r.rawName ?? "",
2540
+ rawDisplayName: r.rawDisplayName ?? "",
2541
+ kind: r.kind,
2542
+ fiberTag: r.fiberTag ?? "",
2543
+ count: r.count,
2544
+ looksMinified: r.looksMinified,
2545
+ exampleKey: r.exampleKey ?? "",
2546
+ contexts: [...r.contexts].join(","),
2547
+ file: r.source?.fileName ?? "",
2548
+ line: r.source?.lineNumber ?? "",
2549
+ firstSeenAt: formatTime(r.firstSeen),
2550
+ lastSeenAt: formatTime(r.lastSeen),
2551
+ lastAgoSec: ((now2 - r.lastSeen) / 1e3).toFixed(1)
2552
+ }));
2553
+ }
2554
+ function serializeTreeRecords() {
2555
+ return treeRecords.map((t) => ({
2556
+ ts: formatTime(t.ts),
2557
+ ctx: t.ctx,
2558
+ rootName: t.rootName,
2559
+ totalNodes: t.totalNodes,
2560
+ maxDepth: t.maxDepth,
2561
+ minifiedLike: t.minifiedLike,
2562
+ topNames: t.topNames
2563
+ }));
2564
+ }
2565
+
1943
2566
  // src/fiberTreeWalker.ts
1944
2567
  var FIBER_TAGS = {
1945
2568
  FunctionComponent: 0,
@@ -2082,6 +2705,7 @@ function debugLog(...args) {
2082
2705
  }
2083
2706
  var fiberRefMap = /* @__PURE__ */ new Map();
2084
2707
  function getComponentName2(fiber) {
2708
+ logFiberType(fiber, "getName");
2085
2709
  const type = fiber.type;
2086
2710
  if (!type) return "Unknown";
2087
2711
  if (typeof type === "function") {
@@ -2102,7 +2726,7 @@ function getComponentName2(fiber) {
2102
2726
  }
2103
2727
  return "Unknown";
2104
2728
  }
2105
- function isUserComponent(fiber) {
2729
+ function isLikelyUserComponent(fiber) {
2106
2730
  if (!USER_COMPONENT_TAGS.has(fiber.tag)) return false;
2107
2731
  const name = getComponentName2(fiber);
2108
2732
  if (name === "Anonymous" || name === "Unknown" || name === "ForwardRef" || name === "Memo")
@@ -2206,15 +2830,28 @@ var FRAMEWORK_PATH_PATTERNS = [
2206
2830
  /react-hook-form/,
2207
2831
  /formik/
2208
2832
  ];
2209
- function resolveEffectiveSourcePath(fiber) {
2210
- if (fiber._debugSource?.fileName) return fiber._debugSource.fileName;
2833
+ function resolveEffectiveSourceLocation(fiber) {
2834
+ const jsxSrc = readJsxSourceFromFiber(fiber);
2835
+ if (jsxSrc) {
2836
+ return {
2837
+ fileName: jsxSrc.fileName,
2838
+ lineNumber: jsxSrc.lineNumber,
2839
+ columnNumber: jsxSrc.columnNumber
2840
+ };
2841
+ }
2842
+ if (fiber._debugSource?.fileName) {
2843
+ return {
2844
+ fileName: fiber._debugSource.fileName,
2845
+ lineNumber: fiber._debugSource.lineNumber
2846
+ };
2847
+ }
2211
2848
  const ownerHit = walkAncestors(
2212
2849
  fiber._debugOwner ?? null,
2213
2850
  3,
2214
2851
  (f) => f._debugOwner ?? null,
2215
2852
  (cur) => cur._debugSource?.fileName ?? void 0
2216
2853
  );
2217
- if (ownerHit) return ownerHit;
2854
+ if (ownerHit) return { fileName: ownerHit };
2218
2855
  const stack = fiber._debugStack?.stack;
2219
2856
  if (typeof stack === "string") {
2220
2857
  const parsed = parseFirstNonReactFrame(stack);
@@ -2222,6 +2859,18 @@ function resolveEffectiveSourcePath(fiber) {
2222
2859
  }
2223
2860
  return null;
2224
2861
  }
2862
+ function resolveEffectiveSourcePath(fiber) {
2863
+ return resolveEffectiveSourceLocation(fiber)?.fileName ?? null;
2864
+ }
2865
+ function resolveSourceConfidence(fiber, isFramework, isLibrary, precomputedJsxSource) {
2866
+ if (isUserComponent(fiber)) return "exact";
2867
+ if (isFramework || isLibrary) return "package";
2868
+ const jsxSrc = precomputedJsxSource ?? readJsxSourceFromFiber(fiber);
2869
+ if (jsxSrc) return "exact";
2870
+ if (fiber._debugSource?.fileName) return "exact";
2871
+ if (resolveEffectiveSourcePath(fiber)) return "inferred";
2872
+ return "unknown";
2873
+ }
2225
2874
  var STOP_WALK = /* @__PURE__ */ Symbol("stop-walk");
2226
2875
  function walkAncestors(start, maxHops, next, visit) {
2227
2876
  let cur = start;
@@ -2232,22 +2881,8 @@ function walkAncestors(start, maxHops, next, visit) {
2232
2881
  }
2233
2882
  return void 0;
2234
2883
  }
2235
- function parseFirstNonReactFrame(stack) {
2236
- const lines = stack.split("\n");
2237
- for (const line of lines) {
2238
- const parened = line.match(/\(([^)]+):\d+:\d+\)/);
2239
- const hermes = line.match(/@([^\s]+):\d+:\d+$/);
2240
- const path = parened?.[1] ?? hermes?.[1];
2241
- if (!path) continue;
2242
- if (path.includes("react-dom")) continue;
2243
- if (path.includes("react-native/Libraries")) continue;
2244
- if (path.includes("/react/cjs/")) continue;
2245
- if (path.includes("/scheduler/")) continue;
2246
- return path;
2247
- }
2248
- return null;
2249
- }
2250
2884
  function isFrameworkComponent(fiber, name) {
2885
+ if (isUserComponent(fiber)) return false;
2251
2886
  if (walkerFilterConfig.frameworkNames.has(name)) return true;
2252
2887
  for (const pattern of walkerFilterConfig.frameworkNamePatterns) {
2253
2888
  if (pattern.test(name)) return true;
@@ -2287,6 +2922,7 @@ var KNOWN_LIBRARY_NAMES = /* @__PURE__ */ new Map([
2287
2922
  ["HeroIcon", "heroicons"]
2288
2923
  ]);
2289
2924
  function detectLibraryName(fiber, name) {
2925
+ if (isUserComponent(fiber)) return void 0;
2290
2926
  if (name.includes(".")) {
2291
2927
  return name.split(".")[0].toLowerCase();
2292
2928
  }
@@ -2357,7 +2993,7 @@ function resolveEffectiveReactKey(fiber) {
2357
2993
  6,
2358
2994
  (f) => f.return ?? null,
2359
2995
  (cur) => {
2360
- if (isUserComponent(cur) && !isFrameworkComponent(cur, getComponentName2(cur))) {
2996
+ if (isLikelyUserComponent(cur) && !isFrameworkComponent(cur, getComponentName2(cur))) {
2361
2997
  return STOP_WALK;
2362
2998
  }
2363
2999
  return typeof cur.key === "string" ? cur.key : void 0;
@@ -2373,7 +3009,7 @@ function walkFiber(fiber, parentId, sharedNameCountMap, depth = 0, inSuspenseFal
2373
3009
  while (current) {
2374
3010
  try {
2375
3011
  const tag = current.tag;
2376
- if (isUserComponent(current)) {
3012
+ if (isLikelyUserComponent(current)) {
2377
3013
  const name = getComponentName2(current);
2378
3014
  const nameCount = nameCountMap.get(name) || 0;
2379
3015
  nameCountMap.set(name, nameCount + 1);
@@ -2388,13 +3024,7 @@ function walkFiber(fiber, parentId, sharedNameCountMap, depth = 0, inSuspenseFal
2388
3024
  { reason: renderReason },
2389
3025
  current.actualDuration
2390
3026
  );
2391
- const children = walkFiber(
2392
- current.child,
2393
- nodeId,
2394
- void 0,
2395
- depth + 1,
2396
- inSuspenseFallback
2397
- );
3027
+ const children = walkFiber(current.child, nodeId, void 0, depth + 1, inSuspenseFallback);
2398
3028
  const truncatedChildren = children.length > MAX_CHILDREN_PER_NODE ? children.slice(0, MAX_CHILDREN_PER_NODE) : children;
2399
3029
  const framework = isFrameworkComponent(current, name) || void 0;
2400
3030
  const queryHashes = detectQueryObserverHashes(current);
@@ -2402,6 +3032,15 @@ function walkFiber(fiber, parentId, sharedNameCountMap, depth = 0, inSuspenseFal
2402
3032
  const compilerStatus = detectCompilerStatus(current);
2403
3033
  const isServerComponent = detectServerComponent(current) || void 0;
2404
3034
  const libraryName = framework ? void 0 : detectLibraryName(current, name);
3035
+ const jsxSource = readJsxSourceFromFiber(current);
3036
+ const sourceConfidence = resolveSourceConfidence(
3037
+ current,
3038
+ framework === true,
3039
+ libraryName !== void 0,
3040
+ jsxSource
3041
+ );
3042
+ const needsStackFallback = (jsxSource?.fileName ?? current._debugSource?.fileName) === void 0 || (jsxSource?.lineNumber ?? current._debugSource?.lineNumber) === void 0;
3043
+ const stackLocation = needsStackFallback ? resolveEffectiveSourceLocation(current) : null;
2405
3044
  const node = {
2406
3045
  id: nodeId,
2407
3046
  name,
@@ -2410,8 +3049,8 @@ function walkFiber(fiber, parentId, sharedNameCountMap, depth = 0, inSuspenseFal
2410
3049
  renderPhase,
2411
3050
  renderReason,
2412
3051
  renderDuration: current.actualDuration,
2413
- filePath: current._debugSource?.fileName,
2414
- lineNumber: current._debugSource?.lineNumber,
3052
+ filePath: jsxSource?.fileName ?? current._debugSource?.fileName ?? stackLocation?.fileName,
3053
+ lineNumber: jsxSource?.lineNumber ?? current._debugSource?.lineNumber ?? stackLocation?.lineNumber,
2415
3054
  isFramework: framework,
2416
3055
  reactKey: resolveEffectiveReactKey(current),
2417
3056
  queryHashes,
@@ -2422,7 +3061,9 @@ function walkFiber(fiber, parentId, sharedNameCountMap, depth = 0, inSuspenseFal
2422
3061
  compilerStatus,
2423
3062
  isServerComponent,
2424
3063
  isLibrary: libraryName !== void 0 ? true : void 0,
2425
- libraryName
3064
+ libraryName,
3065
+ jsxSource,
3066
+ sourceConfidence
2426
3067
  };
2427
3068
  if (!walkerOptions.pruneSubtree?.(node)) {
2428
3069
  nodes.push(node);
@@ -2496,11 +3137,7 @@ function buildTreeFromFiberRoot(root) {
2496
3137
  }
2497
3138
  fiberRefMap.clear();
2498
3139
  const topLevelNodes = walkFiber(rootFiber.child, "");
2499
- debugLog(
2500
- "[FloTrace] walkFiber found",
2501
- topLevelNodes.length,
2502
- "top-level nodes"
2503
- );
3140
+ debugLog("[FloTrace] walkFiber found", topLevelNodes.length, "top-level nodes");
2504
3141
  if (topLevelNodes.length === 1) {
2505
3142
  return topLevelNodes[0];
2506
3143
  }
@@ -2567,9 +3204,7 @@ function findFiberRootFromDOM() {
2567
3204
  }
2568
3205
  }
2569
3206
  }
2570
- console.warn(
2571
- "[FloTrace] Could not find React fiber root from any DOM element"
2572
- );
3207
+ console.warn("[FloTrace] Could not find React fiber root from any DOM element");
2573
3208
  return null;
2574
3209
  } catch (error) {
2575
3210
  console.error("[FloTrace] Error finding fiber root from DOM:", error);
@@ -2631,9 +3266,7 @@ function executeSnapshot(root) {
2631
3266
  adaptSnapshotInterval(nodeCount);
2632
3267
  const client2 = getWebSocketClient();
2633
3268
  if (!client2.connected) {
2634
- console.warn(
2635
- "[FloTrace] WebSocket not connected, cannot send tree snapshot"
2636
- );
3269
+ console.warn("[FloTrace] WebSocket not connected, cannot send tree snapshot");
2637
3270
  return;
2638
3271
  }
2639
3272
  const currentFlatTree = flattenTree2(tree);
@@ -2649,6 +3282,7 @@ function executeSnapshot(root) {
2649
3282
  "nextInterval:",
2650
3283
  snapshotIntervalMs + "ms"
2651
3284
  );
3285
+ logTreeSnapshot(tree, `send seq=${snapshotCounter}`);
2652
3286
  client2.sendImmediate({
2653
3287
  type: "runtime:treeSnapshot",
2654
3288
  tree,
@@ -2669,6 +3303,7 @@ function executeSnapshot(root) {
2669
3303
  "updated:",
2670
3304
  diff.updated.length
2671
3305
  );
3306
+ logTreeSummary(tree, `diff seq=${diffSeq}`);
2672
3307
  client2.sendImmediate({
2673
3308
  type: "runtime:treeDiff",
2674
3309
  seq: diffSeq,
@@ -2809,9 +3444,7 @@ function installFiberTreeWalker(options = {}) {
2809
3444
  return () => uninstallFiberTreeWalker();
2810
3445
  }
2811
3446
  if (typeof window === "undefined") {
2812
- console.warn(
2813
- "[FloTrace] Not in browser environment, cannot install fiber tree walker"
2814
- );
3447
+ console.warn("[FloTrace] Not in browser environment, cannot install fiber tree walker");
2815
3448
  return () => {
2816
3449
  };
2817
3450
  }
@@ -2838,10 +3471,7 @@ function installFiberTreeWalker(options = {}) {
2838
3471
  try {
2839
3472
  originalOnCommitFiberRoot(rendererID, root, priority);
2840
3473
  } catch (error) {
2841
- console.error(
2842
- "[FloTrace] Error in original onCommitFiberRoot:",
2843
- error
2844
- );
3474
+ console.error("[FloTrace] Error in original onCommitFiberRoot:", error);
2845
3475
  }
2846
3476
  }
2847
3477
  if (hookedRendererID === null) {
@@ -2867,9 +3497,7 @@ function installFiberTreeWalker(options = {}) {
2867
3497
  scheduleSnapshot(root);
2868
3498
  };
2869
3499
  activeStrategy = "devtools";
2870
- console.log(
2871
- "[FloTrace] Fiber tree walker installed (DevTools hook strategy)"
2872
- );
3500
+ console.log("[FloTrace] Fiber tree walker installed (DevTools hook strategy)");
2873
3501
  setTimeout(() => {
2874
3502
  try {
2875
3503
  const root = findFiberRootFromDOM();
@@ -2882,9 +3510,7 @@ function installFiberTreeWalker(options = {}) {
2882
3510
  }, 100);
2883
3511
  } else {
2884
3512
  activeStrategy = "dom";
2885
- console.log(
2886
- "[FloTrace] Fiber tree walker installed (DOM fallback strategy)"
2887
- );
3513
+ console.log("[FloTrace] Fiber tree walker installed (DOM fallback strategy)");
2888
3514
  setTimeout(() => {
2889
3515
  try {
2890
3516
  const root = findFiberRootFromDOM();
@@ -3162,12 +3788,18 @@ function installZustandTracker(stores, client2) {
3162
3788
  try {
3163
3789
  scheduleStoreUpdate(storeName, prevState, newState, client2);
3164
3790
  } catch (error) {
3165
- console.error(`[FloTrace] Error in Zustand subscribe callback for "${storeName}":`, error);
3791
+ console.error(
3792
+ `[FloTrace] Error in Zustand subscribe callback for "${storeName}":`,
3793
+ error
3794
+ );
3166
3795
  }
3167
3796
  });
3168
3797
  activeUnsubscribers.push(unsubscribe);
3169
3798
  } catch (error) {
3170
- console.error(`[FloTrace] Failed to install tracker for Zustand store "${storeName}":`, error);
3799
+ console.error(
3800
+ `[FloTrace] Failed to install tracker for Zustand store "${storeName}":`,
3801
+ error
3802
+ );
3171
3803
  }
3172
3804
  }
3173
3805
  }
@@ -3210,10 +3842,13 @@ function scheduleStoreUpdate(storeName, prevState, newState, client2) {
3210
3842
  if (changedKeys.length === 0) return;
3211
3843
  const existing = debounceTimers.get(storeName);
3212
3844
  if (existing) clearTimeout(existing);
3213
- debounceTimers.set(storeName, setTimeout(() => {
3214
- debounceTimers.delete(storeName);
3215
- sendStoreUpdate(storeName, newState, changedKeys, client2);
3216
- }, DEBOUNCE_MS));
3845
+ debounceTimers.set(
3846
+ storeName,
3847
+ setTimeout(() => {
3848
+ debounceTimers.delete(storeName);
3849
+ sendStoreUpdate(storeName, newState, changedKeys, client2);
3850
+ }, DEBOUNCE_MS)
3851
+ );
3217
3852
  }
3218
3853
  function sendStoreUpdate(storeName, state, changedKeys, client2) {
3219
3854
  try {
@@ -3786,7 +4421,14 @@ function findMatchingPathInObject(target, targetFp, container, currentPath, dept
3786
4421
  const child = container[i];
3787
4422
  const directMatch = valuesMatch(target, targetFp, child, cache);
3788
4423
  if (directMatch) return { path: [...currentPath, String(i)], confidence: directMatch };
3789
- const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, String(i)], depth + 1, cache);
4424
+ const nested = findMatchingPathInObject(
4425
+ target,
4426
+ targetFp,
4427
+ child,
4428
+ [...currentPath, String(i)],
4429
+ depth + 1,
4430
+ cache
4431
+ );
3790
4432
  if (nested) return nested;
3791
4433
  }
3792
4434
  } else {
@@ -3794,7 +4436,14 @@ function findMatchingPathInObject(target, targetFp, container, currentPath, dept
3794
4436
  const child = container[key];
3795
4437
  const directMatch = valuesMatch(target, targetFp, child, cache);
3796
4438
  if (directMatch) return { path: [...currentPath, key], confidence: directMatch };
3797
- const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, key], depth + 1, cache);
4439
+ const nested = findMatchingPathInObject(
4440
+ target,
4441
+ targetFp,
4442
+ child,
4443
+ [...currentPath, key],
4444
+ depth + 1,
4445
+ cache
4446
+ );
3798
4447
  if (nested) return nested;
3799
4448
  }
3800
4449
  }
@@ -3899,7 +4548,12 @@ function resolveValueTrace(input) {
3899
4548
  nodeId: input.nodeId,
3900
4549
  componentName: rootComponentName,
3901
4550
  propPath: input.propPath,
3902
- confidence: "exact"
4551
+ confidence: "exact",
4552
+ // P6: `fiber.memoizedProps[FLOTRACE_SOURCE]` is the JSX call site where
4553
+ // this fiber was created — i.e., the `<Consumer .../>` JSX in the
4554
+ // PARENT's source file. That's exactly the "drilled from `Parent.tsx:42`"
4555
+ // attribution the user wants on the leaf prop step.
4556
+ callSiteOfParentJsx: readJsxSourceFromFiber(fiber)
3903
4557
  });
3904
4558
  } else if (input.hookPath) {
3905
4559
  steps.push({
@@ -3939,7 +4593,12 @@ function resolveValueTrace(input) {
3939
4593
  nodeId: ancestorNodeId,
3940
4594
  componentName: ancestorName,
3941
4595
  propPath: trailingSubPath.length > 0 ? [...matchPath, ...trailingSubPath] : matchPath,
3942
- confidence: matchConfidence
4596
+ confidence: matchConfidence,
4597
+ // P6: attribute the parent JSX call site that drilled this
4598
+ // prop. When the user opted into the JSX runtime AND this
4599
+ // ancestor's fiber went through jsxDEV, the consumer sees
4600
+ // "drilled from `Parent.tsx:42:8`" with a click-to-IDE link.
4601
+ callSiteOfParentJsx: readJsxSourceFromFiber(current)
3943
4602
  });
3944
4603
  }
3945
4604
  } else {
@@ -3988,7 +4647,11 @@ function resolveValueTrace(input) {
3988
4647
  const contextMatch = findContextMatch(fiber, rootValue, rootFp, fiberToNodeId, fpCache);
3989
4648
  if (contextMatch) {
3990
4649
  steps.push(contextMatch.step);
3991
- const providerStoreMatch = findStoreMatch(contextMatch.providerValue, cachedFp(contextMatch.providerValue, fpCache), fpCache);
4650
+ const providerStoreMatch = findStoreMatch(
4651
+ contextMatch.providerValue,
4652
+ cachedFp(contextMatch.providerValue, fpCache),
4653
+ fpCache
4654
+ );
3992
4655
  if (providerStoreMatch) {
3993
4656
  steps.push({
3994
4657
  kind: "store",
@@ -4182,13 +4845,22 @@ function detectWebFramework() {
4182
4845
  // Annotate the CommonJS export names for ESM import in node:
4183
4846
  0 && (module.exports = {
4184
4847
  DEFAULT_CONFIG,
4848
+ FLOTRACE_SOURCE,
4185
4849
  FloTraceWebSocketClient,
4850
+ JSX_RUNTIME_ACTIVE_KEY,
4186
4851
  buildAncestorChain,
4852
+ clearCallSiteRenders,
4187
4853
  clearFetchOriginTags,
4854
+ computeCallSiteId,
4855
+ computeCallSiteMetricsPayload,
4856
+ describeFiberType,
4857
+ detectInlineLiterals,
4188
4858
  detectServerComponent,
4189
4859
  detectWebFramework,
4190
4860
  disposeWebSocketClient,
4191
4861
  findFetchOrigin,
4862
+ getCallSiteRenderRate,
4863
+ getCallSiteRenders,
4192
4864
  getChangedKeys,
4193
4865
  getComponentNameFromFiber,
4194
4866
  getCurrentRenderingFiber,
@@ -4211,9 +4883,16 @@ function detectWebFramework() {
4211
4883
  installTanStackQueryTracker,
4212
4884
  installTimelineTracker,
4213
4885
  installZustandTracker,
4886
+ isJsxRuntimeActive,
4214
4887
  isReduxStore,
4215
4888
  isTanStackQueryClient,
4889
+ isUserComponent,
4890
+ logTreeSnapshot,
4891
+ logTreeSummary,
4892
+ markJsxRuntimeActive,
4216
4893
  maybeEmitNextjsContext,
4894
+ normalizeJsxSourcePath,
4895
+ recordCallSiteRender,
4217
4896
  recordTimelineEvent,
4218
4897
  requestFullSnapshot,
4219
4898
  requestTreeSnapshot,
@@ -4221,6 +4900,8 @@ function detectWebFramework() {
4221
4900
  resolveValueTrace,
4222
4901
  serializeProps,
4223
4902
  serializeValue,
4903
+ setDuplicateKeyEmitter,
4904
+ setFiberDebug,
4224
4905
  tagFetchData,
4225
4906
  uninstallFiberTreeWalker,
4226
4907
  uninstallReduxTracker,