@flotrace/runtime-core 2.2.4 → 2.3.0

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.mjs CHANGED
@@ -1,3 +1,20 @@
1
+ import {
2
+ FLOTRACE_SOURCE,
3
+ JSX_RUNTIME_ACTIVE_KEY,
4
+ clearCallSiteRenders,
5
+ computeCallSiteId,
6
+ computeCallSiteMetricsPayload,
7
+ detectInlineLiterals,
8
+ getCallSiteRenderRate,
9
+ getCallSiteRenders,
10
+ isJsxRuntimeActive,
11
+ markJsxRuntimeActive,
12
+ normalizeJsxSourcePath,
13
+ readJsxSourceFromFiber,
14
+ recordCallSiteRender,
15
+ setDuplicateKeyEmitter
16
+ } from "./chunk-QLOJU5F2.mjs";
17
+
1
18
  // src/types.ts
2
19
  var DEFAULT_CONFIG = {
3
20
  port: 3457,
@@ -129,11 +146,7 @@ function serializeValue(value, depth = 0, seen = /* @__PURE__ */ new WeakSet())
129
146
  for (let i = 0; i < Math.min(keys.length, MAX_OBJECT_KEYS); i++) {
130
147
  const key = keys[i];
131
148
  try {
132
- result[key] = serializeValue(
133
- value[key],
134
- depth + 1,
135
- seen
136
- );
149
+ result[key] = serializeValue(value[key], depth + 1, seen);
137
150
  } catch {
138
151
  result[key] = { __type: "truncated", originalType: "error" };
139
152
  }
@@ -234,7 +247,12 @@ var _FloTraceWebSocketClient = class _FloTraceWebSocketClient {
234
247
  frameworkName: this.config.frameworkName,
235
248
  frameworkVersion: this.config.frameworkVersion,
236
249
  reactNativeVersion: this.config.reactNativeVersion,
237
- runtimeVersion: this.config.runtimeVersion
250
+ runtimeVersion: this.config.runtimeVersion,
251
+ // P5: JSX runtime adoption signal — read at WS-open time so
252
+ // multiple fibers have already rendered by the moment we report.
253
+ // `isJsxRuntimeActive` reads `globalThis[Symbol.for('flotrace.jsx-runtime-active')]`,
254
+ // which the dev jsx-runtime sets on first jsxDEV call.
255
+ jsxRuntimeActive: isJsxRuntimeActive()
238
256
  });
239
257
  this.flush();
240
258
  };
@@ -560,26 +578,31 @@ function classifyFromDebugLabel(state, index, effects, effectIdx, debugLabel) {
560
578
  const ms = state.memoizedState;
561
579
  const normalizedLabel = debugLabel.toLowerCase().replace(/\s/g, "");
562
580
  const labelMap = {
563
- "usestate": "useState",
564
- "usereducer": "useReducer",
565
- "useref": "useRef",
566
- "usememo": "useMemo",
567
- "usecallback": "useCallback",
568
- "useeffect": "useEffect",
569
- "uselayouteffect": "useLayoutEffect",
570
- "useinsertioneffect": "useInsertionEffect",
571
- "usecontext": "useContext",
572
- "useimperativehandle": "useImperativeHandle",
573
- "usedebugvalue": "useDebugValue",
574
- "usetransition": "useTransition",
575
- "usedeferredvalue": "useDeferredValue",
576
- "useid": "useId",
577
- "usesyncexternalstore": "useSyncExternalStore",
578
- "useoptimistic": "useOptimistic",
579
- "useformstatus": "useFormStatus"
581
+ usestate: "useState",
582
+ usereducer: "useReducer",
583
+ useref: "useRef",
584
+ usememo: "useMemo",
585
+ usecallback: "useCallback",
586
+ useeffect: "useEffect",
587
+ uselayouteffect: "useLayoutEffect",
588
+ useinsertioneffect: "useInsertionEffect",
589
+ usecontext: "useContext",
590
+ useimperativehandle: "useImperativeHandle",
591
+ usedebugvalue: "useDebugValue",
592
+ usetransition: "useTransition",
593
+ usedeferredvalue: "useDeferredValue",
594
+ useid: "useId",
595
+ usesyncexternalstore: "useSyncExternalStore",
596
+ useoptimistic: "useOptimistic",
597
+ useformstatus: "useFormStatus"
580
598
  };
581
599
  const hookType = labelMap[normalizedLabel] ?? "unknown";
582
- const base = { index, type: hookType, value: serializeValue(ms, 0, /* @__PURE__ */ new WeakSet()), debugLabel };
600
+ const base = {
601
+ index,
602
+ type: hookType,
603
+ value: serializeValue(ms, 0, /* @__PURE__ */ new WeakSet()),
604
+ debugLabel
605
+ };
583
606
  if (hookType === "useEffect" || hookType === "useLayoutEffect" || hookType === "useInsertionEffect") {
584
607
  if (effectIdx < effects.length) {
585
608
  const effect = effects[effectIdx];
@@ -1046,7 +1069,13 @@ var ClassComponent = 1;
1046
1069
  var ForwardRef = 11;
1047
1070
  var MemoComponent = 14;
1048
1071
  var SimpleMemoComponent = 15;
1049
- var USER_TAGS = /* @__PURE__ */ new Set([FunctionComponent, ClassComponent, ForwardRef, MemoComponent, SimpleMemoComponent]);
1072
+ var USER_TAGS = /* @__PURE__ */ new Set([
1073
+ FunctionComponent,
1074
+ ClassComponent,
1075
+ ForwardRef,
1076
+ MemoComponent,
1077
+ SimpleMemoComponent
1078
+ ]);
1050
1079
  function isMemoizedFiber(fiber) {
1051
1080
  return fiber.tag === MemoComponent || fiber.tag === SimpleMemoComponent;
1052
1081
  }
@@ -1130,13 +1159,15 @@ function buildCascadeTree(rootFiber, triggers) {
1130
1159
  triggerByName.set(t.componentName, t);
1131
1160
  }
1132
1161
  }
1133
- const stack = [{
1134
- fiber: rootFiber,
1135
- depth: 0,
1136
- parentRerendered: false,
1137
- parentNode: null,
1138
- isRoot: true
1139
- }];
1162
+ const stack = [
1163
+ {
1164
+ fiber: rootFiber,
1165
+ depth: 0,
1166
+ parentRerendered: false,
1167
+ parentNode: null,
1168
+ isRoot: true
1169
+ }
1170
+ ];
1140
1171
  while (stack.length > 0) {
1141
1172
  const entry = stack.pop();
1142
1173
  const { fiber, depth, parentRerendered, parentNode, isRoot } = entry;
@@ -1147,7 +1178,13 @@ function buildCascadeTree(rootFiber, triggers) {
1147
1178
  if (isNewMount && !didRender) {
1148
1179
  let child2 = fiber.child;
1149
1180
  while (child2) {
1150
- stack.push({ fiber: child2, depth: depth + 1, parentRerendered: false, parentNode, isRoot: false });
1181
+ stack.push({
1182
+ fiber: child2,
1183
+ depth: depth + 1,
1184
+ parentRerendered: false,
1185
+ parentNode,
1186
+ isRoot: false
1187
+ });
1151
1188
  child2 = child2.sibling;
1152
1189
  }
1153
1190
  continue;
@@ -1155,7 +1192,13 @@ function buildCascadeTree(rootFiber, triggers) {
1155
1192
  if (!USER_TAGS.has(fiber.tag)) {
1156
1193
  let child2 = fiber.child;
1157
1194
  while (child2) {
1158
- stack.push({ fiber: child2, depth: depth + 1, parentRerendered: didRender || parentRerendered, parentNode, isRoot: false });
1195
+ stack.push({
1196
+ fiber: child2,
1197
+ depth: depth + 1,
1198
+ parentRerendered: didRender || parentRerendered,
1199
+ parentNode,
1200
+ isRoot: false
1201
+ });
1159
1202
  child2 = child2.sibling;
1160
1203
  }
1161
1204
  continue;
@@ -1164,7 +1207,13 @@ function buildCascadeTree(rootFiber, triggers) {
1164
1207
  if (reason === null) {
1165
1208
  let child2 = fiber.child;
1166
1209
  while (child2) {
1167
- stack.push({ fiber: child2, depth: depth + 1, parentRerendered: false, parentNode, isRoot: false });
1210
+ stack.push({
1211
+ fiber: child2,
1212
+ depth: depth + 1,
1213
+ parentRerendered: false,
1214
+ parentNode,
1215
+ isRoot: false
1216
+ });
1168
1217
  child2 = child2.sibling;
1169
1218
  }
1170
1219
  continue;
@@ -1190,7 +1239,11 @@ function buildCascadeTree(rootFiber, triggers) {
1190
1239
  triggerId,
1191
1240
  children: [],
1192
1241
  depth,
1193
- isMemoized: isMemoizedFiber(fiber)
1242
+ isMemoized: isMemoizedFiber(fiber),
1243
+ // P6: JSX-runtime attribution — read from fiber.memoizedProps directly.
1244
+ // Same source the walker uses, so cascade nodes align with LiveTreeNode
1245
+ // attribution for the same user component.
1246
+ jsxSource: readJsxSourceFromFiber(fiber)
1194
1247
  };
1195
1248
  totalComponents++;
1196
1249
  if (reason === "parent-cascade") {
@@ -1223,7 +1276,10 @@ function analyzeCascade(root, triggers) {
1223
1276
  try {
1224
1277
  const finishedLanes = getFinishedLanes(root);
1225
1278
  const lane = classifyLanes(finishedLanes);
1226
- const { rootCauses, totalComponents, avoidableCount, avoidableDuration } = buildCascadeTree(root.current, triggers);
1279
+ const { rootCauses, totalComponents, avoidableCount, avoidableDuration } = buildCascadeTree(
1280
+ root.current,
1281
+ triggers
1282
+ );
1227
1283
  if (totalComponents === 0) return null;
1228
1284
  const totalDuration = rootCauses.reduce((sum, n) => sum + n.subtreeDuration, 0);
1229
1285
  const triggerIds = triggers.map((t) => t.triggerId);
@@ -1305,7 +1361,8 @@ function shouldFlagRename(value) {
1305
1361
  if (value === null || value === void 0) return false;
1306
1362
  if (typeof value !== "object") return false;
1307
1363
  if (Array.isArray(value) && value.length === 0) return false;
1308
- if (!Array.isArray(value) && Object.keys(value).length === 0) return false;
1364
+ if (!Array.isArray(value) && Object.keys(value).length === 0)
1365
+ return false;
1309
1366
  return true;
1310
1367
  }
1311
1368
  function computePropIntersectionRatio(nodeProps, childrenProps) {
@@ -1514,13 +1571,24 @@ function runAnalysis(tree, fiberRefMap2) {
1514
1571
  propKey: p.propKey,
1515
1572
  role,
1516
1573
  hookCount: hookCounts.get(p.nodeId) ?? 0,
1517
- hasContextHook: contextFlags.get(p.nodeId) ?? false
1574
+ hasContextHook: contextFlags.get(p.nodeId) ?? false,
1575
+ // P6: propagate JSX-runtime attribution from the tree node so the
1576
+ // drill-chain detail can render `(file:line)` per step + click-
1577
+ // to-IDE on each component along the chain. Undefined when the
1578
+ // user hasn't opted into the JSX runtime.
1579
+ jsxSource: n?.jsxSource
1518
1580
  };
1519
1581
  });
1520
1582
  const passthroughCount = chainNodes.filter((n) => n.role === "passthrough").length;
1521
1583
  const sourceNode = nodeMap.get(sourceId);
1522
1584
  const renames = path.flatMap(
1523
- (p, idx) => p.isRename ? [{ atNodeId: p.nodeId, fromKey: idx > 0 ? path[idx - 1].propKey : sourcePropName, toKey: p.propKey }] : []
1585
+ (p, idx) => p.isRename ? [
1586
+ {
1587
+ atNodeId: p.nodeId,
1588
+ fromKey: idx > 0 ? path[idx - 1].propKey : sourcePropName,
1589
+ toKey: p.propKey
1590
+ }
1591
+ ] : []
1524
1592
  );
1525
1593
  chains.push({
1526
1594
  chainId: makeChainId(sourceId, fp, consumerNodeId),
@@ -1615,10 +1683,7 @@ var SERVER_COMPONENT_PATTERNS = [
1615
1683
  /[\\/]app[\\/].+[\\/]error\.[jt]sx?$/
1616
1684
  // Next.js error UI
1617
1685
  ];
1618
- var SERVER_REFERENCE_PATTERNS = [
1619
- /_ServerReference$/,
1620
- /^RSC_/
1621
- ];
1686
+ var SERVER_REFERENCE_PATTERNS = [/_ServerReference$/, /^RSC_/];
1622
1687
  var detectionEmitted = false;
1623
1688
  function maybeEmitNextjsContext(client2) {
1624
1689
  if (detectionEmitted) return;
@@ -1712,7 +1777,9 @@ function scanActionStateChanges(fiberRefMap2, client2) {
1712
1777
  for (const [nodeId, fiber] of fiberRefMap2) {
1713
1778
  const entries = extractActionEntries(fiber);
1714
1779
  if (!entries) continue;
1715
- const snapshot = JSON.stringify(entries.map((e) => ({ i: e.hookIndex, p: e.isPending, s: e.state })));
1780
+ const snapshot = JSON.stringify(
1781
+ entries.map((e) => ({ i: e.hookIndex, p: e.isPending, s: e.state }))
1782
+ );
1716
1783
  if (prevActionStateMap.get(nodeId) === snapshot) continue;
1717
1784
  prevActionStateMap.set(nodeId, snapshot);
1718
1785
  const componentName = nodeId.split("/").pop()?.replace(/-\d+$/, "") ?? "Unknown";
@@ -1822,7 +1889,8 @@ function tagFetchData(obj, requestId, depth = 0) {
1822
1889
  const limit = Math.min(obj.length, FETCH_ORIGIN_TAG_ARRAY_LIMIT);
1823
1890
  for (let i = 0; i < limit; i++) tagFetchData(obj[i], requestId, depth + 1);
1824
1891
  } else {
1825
- for (const val of Object.values(obj)) tagFetchData(val, requestId, depth + 1);
1892
+ for (const val of Object.values(obj))
1893
+ tagFetchData(val, requestId, depth + 1);
1826
1894
  }
1827
1895
  }
1828
1896
  function hasActiveTags() {
@@ -1858,6 +1926,344 @@ function scanForOrigin(obj, depth, ignoreTTL) {
1858
1926
  return void 0;
1859
1927
  }
1860
1928
 
1929
+ // src/fiberDebugLogger.ts
1930
+ var MAX_FIBER_RECORDS = 500;
1931
+ var MAX_TREE_RECORDS = 50;
1932
+ var MAX_TOP_NAMES_PER_SNAPSHOT = 10;
1933
+ var MAX_CONTEXTS_PER_FIBER = 8;
1934
+ var totalFiberEvents = 0;
1935
+ var recordingStartedAt = null;
1936
+ var fiberRecords = /* @__PURE__ */ new Map();
1937
+ var treeRecords = [];
1938
+ function isRecording() {
1939
+ return Boolean(globalThis.__FT_DEBUG);
1940
+ }
1941
+ function setFiberDebug(enabled) {
1942
+ globalThis.__FT_DEBUG = enabled;
1943
+ if (enabled) {
1944
+ console.info(
1945
+ "%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",
1946
+ "background:#1e293b;color:#7dd3fc;padding:1px 6px;border-radius:3px;font-weight:600;",
1947
+ "color:#94a3b8;",
1948
+ "color:#a78bfa;font-weight:600;",
1949
+ "color:#94a3b8;",
1950
+ "color:#a78bfa;font-weight:600;",
1951
+ "color:#94a3b8;",
1952
+ "color:#a78bfa;font-weight:600;",
1953
+ "color:#94a3b8;"
1954
+ );
1955
+ }
1956
+ }
1957
+ function isFiberFunctionType(t) {
1958
+ return typeof t === "function";
1959
+ }
1960
+ function isFiberObjectType(t) {
1961
+ return typeof t === "object" && t !== null;
1962
+ }
1963
+ function isClassComponentType(t) {
1964
+ if (typeof t !== "function") return false;
1965
+ const proto = t.prototype;
1966
+ return typeof proto?.isReactComponent !== "undefined";
1967
+ }
1968
+ function describeFiberType(fiber) {
1969
+ const type = fiber.type;
1970
+ if (typeof type === "string") {
1971
+ return {
1972
+ kind: "host",
1973
+ name: type,
1974
+ displayName: void 0,
1975
+ resolved: type,
1976
+ looksMinified: false
1977
+ };
1978
+ }
1979
+ if (isFiberFunctionType(type)) {
1980
+ const isClass = isClassComponentType(type);
1981
+ const resolved = type.displayName ?? type.name ?? "Anonymous";
1982
+ return {
1983
+ kind: isClass ? "class" : "function",
1984
+ name: type.name,
1985
+ displayName: type.displayName,
1986
+ resolved,
1987
+ looksMinified: looksMinified(resolved)
1988
+ };
1989
+ }
1990
+ if (isFiberObjectType(type)) {
1991
+ if (type.render) {
1992
+ const resolved2 = type.render.displayName ?? type.render.name ?? "ForwardRef";
1993
+ return {
1994
+ kind: "forwardRef",
1995
+ name: type.render.name,
1996
+ displayName: type.render.displayName,
1997
+ resolved: resolved2,
1998
+ looksMinified: looksMinified(resolved2)
1999
+ };
2000
+ }
2001
+ if (type.type) {
2002
+ const resolved2 = type.type.displayName ?? type.type.name ?? "Memo";
2003
+ return {
2004
+ kind: "memo",
2005
+ name: type.type.name,
2006
+ displayName: type.type.displayName,
2007
+ resolved: resolved2,
2008
+ looksMinified: looksMinified(resolved2)
2009
+ };
2010
+ }
2011
+ const resolved = type.displayName ?? type.name ?? "Unknown";
2012
+ return {
2013
+ kind: "unknown",
2014
+ name: type.name,
2015
+ displayName: type.displayName,
2016
+ resolved,
2017
+ looksMinified: looksMinified(resolved)
2018
+ };
2019
+ }
2020
+ return {
2021
+ kind: "unknown",
2022
+ name: void 0,
2023
+ displayName: void 0,
2024
+ resolved: "Unknown",
2025
+ looksMinified: false
2026
+ };
2027
+ }
2028
+ function looksMinified(name) {
2029
+ return /^[a-z_$][a-z0-9_$]?$/i.test(name);
2030
+ }
2031
+ function logFiberType(fiber, context) {
2032
+ if (!isRecording()) return;
2033
+ const info = describeFiberType(fiber);
2034
+ const key = info.resolved;
2035
+ const now2 = Date.now();
2036
+ if (recordingStartedAt === null) recordingStartedAt = now2;
2037
+ totalFiberEvents += 1;
2038
+ const existing = fiberRecords.get(key);
2039
+ if (existing) {
2040
+ existing.count += 1;
2041
+ existing.lastSeen = now2;
2042
+ if (context && existing.contexts.size < MAX_CONTEXTS_PER_FIBER) {
2043
+ existing.contexts.add(context);
2044
+ }
2045
+ if (existing.exampleKey === void 0 && typeof fiber.key === "string") {
2046
+ existing.exampleKey = fiber.key;
2047
+ }
2048
+ return;
2049
+ }
2050
+ if (fiberRecords.size >= MAX_FIBER_RECORDS) {
2051
+ evictOldestFiber();
2052
+ }
2053
+ fiberRecords.set(key, {
2054
+ name: info.resolved,
2055
+ rawName: info.name,
2056
+ rawDisplayName: info.displayName,
2057
+ kind: info.kind,
2058
+ fiberTag: fiber.tag,
2059
+ looksMinified: info.looksMinified,
2060
+ count: 1,
2061
+ contexts: new Set(context ? [context] : []),
2062
+ firstSeen: now2,
2063
+ lastSeen: now2,
2064
+ source: fiber._debugSource ?? void 0,
2065
+ exampleKey: typeof fiber.key === "string" ? fiber.key : void 0
2066
+ });
2067
+ }
2068
+ function evictOldestFiber() {
2069
+ let oldestKey;
2070
+ let oldestTime = Infinity;
2071
+ for (const [key, rec] of fiberRecords) {
2072
+ if (rec.lastSeen < oldestTime) {
2073
+ oldestTime = rec.lastSeen;
2074
+ oldestKey = key;
2075
+ }
2076
+ }
2077
+ if (oldestKey) fiberRecords.delete(oldestKey);
2078
+ }
2079
+ function walkStats(node, depth, acc) {
2080
+ acc.total += 1;
2081
+ if (depth > acc.maxDepth) acc.maxDepth = depth;
2082
+ if (looksMinified(node.name)) acc.minifiedLike += 1;
2083
+ acc.byName.set(node.name, (acc.byName.get(node.name) ?? 0) + 1);
2084
+ for (const child of node.children) walkStats(child, depth + 1, acc);
2085
+ }
2086
+ function logTreeSnapshot(tree, context) {
2087
+ if (!isRecording() || !tree) return;
2088
+ const stats = { total: 0, minifiedLike: 0, byName: /* @__PURE__ */ new Map(), maxDepth: 0 };
2089
+ walkStats(tree, 0, stats);
2090
+ 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(",");
2091
+ treeRecords.push({
2092
+ ts: Date.now(),
2093
+ ctx: context ?? "",
2094
+ rootName: tree.name,
2095
+ totalNodes: stats.total,
2096
+ maxDepth: stats.maxDepth,
2097
+ minifiedLike: stats.minifiedLike,
2098
+ topNames
2099
+ });
2100
+ if (treeRecords.length > MAX_TREE_RECORDS) treeRecords.shift();
2101
+ }
2102
+ var logTreeSummary = logTreeSnapshot;
2103
+ function installConsoleApi() {
2104
+ if (globalThis.__ft) return;
2105
+ const api = {
2106
+ dump() {
2107
+ const fiberRows = serializeFiberRecords();
2108
+ const snapRows = serializeTreeRecords();
2109
+ const minifiedCount = fiberRows.filter((r) => r.looksMinified).length;
2110
+ const elapsedSec = recordingStartedAt === null ? 0 : (Date.now() - recordingStartedAt) / 1e3;
2111
+ const summary = [
2112
+ {
2113
+ metric: "uniqueComponents",
2114
+ value: fiberRecords.size
2115
+ },
2116
+ {
2117
+ metric: "totalFiberEvents",
2118
+ value: totalFiberEvents
2119
+ },
2120
+ {
2121
+ metric: "minifiedLike",
2122
+ value: `${minifiedCount} / ${fiberRecords.size}`
2123
+ },
2124
+ {
2125
+ metric: "snapshots",
2126
+ value: treeRecords.length
2127
+ },
2128
+ {
2129
+ metric: "recordingSec",
2130
+ value: elapsedSec.toFixed(1)
2131
+ }
2132
+ ];
2133
+ console.groupCollapsed(
2134
+ `%c[FT debug] dump%c \u2014 ${fiberRecords.size} components, ${totalFiberEvents} events, ${treeRecords.length} snapshots`,
2135
+ "background:#1e293b;color:#7dd3fc;padding:1px 6px;border-radius:3px;font-weight:600;",
2136
+ "color:#94a3b8;"
2137
+ );
2138
+ console.log("Summary:");
2139
+ console.table(summary, ["metric", "value"]);
2140
+ console.log("Fibers \u2014 every observed component (sorted by call count):");
2141
+ console.table(fiberRows, [
2142
+ "name",
2143
+ "rawName",
2144
+ "rawDisplayName",
2145
+ "kind",
2146
+ "fiberTag",
2147
+ "count",
2148
+ "looksMinified",
2149
+ "exampleKey",
2150
+ "contexts",
2151
+ "file",
2152
+ "line",
2153
+ "firstSeenAt",
2154
+ "lastSeenAt",
2155
+ "lastAgoSec"
2156
+ ]);
2157
+ console.log("Tree snapshots \u2014 newest last:");
2158
+ console.table(snapRows, [
2159
+ "ts",
2160
+ "ctx",
2161
+ "rootName",
2162
+ "totalNodes",
2163
+ "maxDepth",
2164
+ "minifiedLike",
2165
+ "topNames"
2166
+ ]);
2167
+ console.groupEnd();
2168
+ },
2169
+ fibers() {
2170
+ console.table(serializeFiberRecords(), [
2171
+ "name",
2172
+ "rawName",
2173
+ "rawDisplayName",
2174
+ "kind",
2175
+ "fiberTag",
2176
+ "count",
2177
+ "looksMinified",
2178
+ "exampleKey",
2179
+ "contexts",
2180
+ "file",
2181
+ "line",
2182
+ "firstSeenAt",
2183
+ "lastSeenAt",
2184
+ "lastAgoSec"
2185
+ ]);
2186
+ },
2187
+ snapshots() {
2188
+ console.table(serializeTreeRecords());
2189
+ },
2190
+ tail(n = 20) {
2191
+ console.table(treeRecords.slice(-n));
2192
+ },
2193
+ clear() {
2194
+ fiberRecords.clear();
2195
+ treeRecords.length = 0;
2196
+ console.info("[FT debug] cleared");
2197
+ },
2198
+ size() {
2199
+ return { fibers: fiberRecords.size, snapshots: treeRecords.length };
2200
+ },
2201
+ export() {
2202
+ return { fibers: serializeFiberRecords(), snapshots: treeRecords.slice() };
2203
+ },
2204
+ download(filename) {
2205
+ const data = api.export();
2206
+ const json = JSON.stringify(data, null, 2);
2207
+ const docRef = globalThis.document;
2208
+ const URLRef = globalThis.URL;
2209
+ if (!docRef || !URLRef || typeof URLRef.createObjectURL !== "function") {
2210
+ console.warn(
2211
+ "[FT debug] download() requires a browser environment \u2014 printing JSON instead"
2212
+ );
2213
+ console.log(json);
2214
+ return;
2215
+ }
2216
+ const blob = new Blob([json], { type: "application/json" });
2217
+ const url = URLRef.createObjectURL(blob);
2218
+ const a = docRef.createElement("a");
2219
+ a.href = url;
2220
+ a.download = filename ?? `flotrace-debug-${Date.now()}.json`;
2221
+ a.click();
2222
+ URLRef.revokeObjectURL(url);
2223
+ }
2224
+ };
2225
+ globalThis.__ft = api;
2226
+ }
2227
+ installConsoleApi();
2228
+ function formatTime(ms) {
2229
+ const d = new Date(ms);
2230
+ const hh = String(d.getHours()).padStart(2, "0");
2231
+ const mm = String(d.getMinutes()).padStart(2, "0");
2232
+ const ss = String(d.getSeconds()).padStart(2, "0");
2233
+ const mss = String(d.getMilliseconds()).padStart(3, "0");
2234
+ return `${hh}:${mm}:${ss}.${mss}`;
2235
+ }
2236
+ function serializeFiberRecords() {
2237
+ const now2 = Date.now();
2238
+ return [...fiberRecords.values()].sort((a, b) => b.count - a.count).map((r) => ({
2239
+ name: r.name,
2240
+ rawName: r.rawName ?? "",
2241
+ rawDisplayName: r.rawDisplayName ?? "",
2242
+ kind: r.kind,
2243
+ fiberTag: r.fiberTag ?? "",
2244
+ count: r.count,
2245
+ looksMinified: r.looksMinified,
2246
+ exampleKey: r.exampleKey ?? "",
2247
+ contexts: [...r.contexts].join(","),
2248
+ file: r.source?.fileName ?? "",
2249
+ line: r.source?.lineNumber ?? "",
2250
+ firstSeenAt: formatTime(r.firstSeen),
2251
+ lastSeenAt: formatTime(r.lastSeen),
2252
+ lastAgoSec: ((now2 - r.lastSeen) / 1e3).toFixed(1)
2253
+ }));
2254
+ }
2255
+ function serializeTreeRecords() {
2256
+ return treeRecords.map((t) => ({
2257
+ ts: formatTime(t.ts),
2258
+ ctx: t.ctx,
2259
+ rootName: t.rootName,
2260
+ totalNodes: t.totalNodes,
2261
+ maxDepth: t.maxDepth,
2262
+ minifiedLike: t.minifiedLike,
2263
+ topNames: t.topNames
2264
+ }));
2265
+ }
2266
+
1861
2267
  // src/fiberTreeWalker.ts
1862
2268
  var FIBER_TAGS = {
1863
2269
  FunctionComponent: 0,
@@ -2000,6 +2406,7 @@ function debugLog(...args) {
2000
2406
  }
2001
2407
  var fiberRefMap = /* @__PURE__ */ new Map();
2002
2408
  function getComponentName2(fiber) {
2409
+ logFiberType(fiber, "getName");
2003
2410
  const type = fiber.type;
2004
2411
  if (!type) return "Unknown";
2005
2412
  if (typeof type === "function") {
@@ -2125,6 +2532,8 @@ var FRAMEWORK_PATH_PATTERNS = [
2125
2532
  /formik/
2126
2533
  ];
2127
2534
  function resolveEffectiveSourcePath(fiber) {
2535
+ const jsxSrc = readJsxSourceFromFiber(fiber);
2536
+ if (jsxSrc) return jsxSrc.fileName;
2128
2537
  if (fiber._debugSource?.fileName) return fiber._debugSource.fileName;
2129
2538
  const ownerHit = walkAncestors(
2130
2539
  fiber._debugOwner ?? null,
@@ -2140,6 +2549,14 @@ function resolveEffectiveSourcePath(fiber) {
2140
2549
  }
2141
2550
  return null;
2142
2551
  }
2552
+ function resolveSourceConfidence(fiber, isFramework, isLibrary, precomputedJsxSource) {
2553
+ if (isFramework || isLibrary) return "package";
2554
+ const jsxSrc = precomputedJsxSource ?? readJsxSourceFromFiber(fiber);
2555
+ if (jsxSrc) return "exact";
2556
+ if (fiber._debugSource?.fileName) return "exact";
2557
+ if (resolveEffectiveSourcePath(fiber)) return "inferred";
2558
+ return "unknown";
2559
+ }
2143
2560
  var STOP_WALK = /* @__PURE__ */ Symbol("stop-walk");
2144
2561
  function walkAncestors(start, maxHops, next, visit) {
2145
2562
  let cur = start;
@@ -2306,13 +2723,7 @@ function walkFiber(fiber, parentId, sharedNameCountMap, depth = 0, inSuspenseFal
2306
2723
  { reason: renderReason },
2307
2724
  current.actualDuration
2308
2725
  );
2309
- const children = walkFiber(
2310
- current.child,
2311
- nodeId,
2312
- void 0,
2313
- depth + 1,
2314
- inSuspenseFallback
2315
- );
2726
+ const children = walkFiber(current.child, nodeId, void 0, depth + 1, inSuspenseFallback);
2316
2727
  const truncatedChildren = children.length > MAX_CHILDREN_PER_NODE ? children.slice(0, MAX_CHILDREN_PER_NODE) : children;
2317
2728
  const framework = isFrameworkComponent(current, name) || void 0;
2318
2729
  const queryHashes = detectQueryObserverHashes(current);
@@ -2320,6 +2731,13 @@ function walkFiber(fiber, parentId, sharedNameCountMap, depth = 0, inSuspenseFal
2320
2731
  const compilerStatus = detectCompilerStatus(current);
2321
2732
  const isServerComponent = detectServerComponent(current) || void 0;
2322
2733
  const libraryName = framework ? void 0 : detectLibraryName(current, name);
2734
+ const jsxSource = readJsxSourceFromFiber(current);
2735
+ const sourceConfidence = resolveSourceConfidence(
2736
+ current,
2737
+ framework === true,
2738
+ libraryName !== void 0,
2739
+ jsxSource
2740
+ );
2323
2741
  const node = {
2324
2742
  id: nodeId,
2325
2743
  name,
@@ -2328,8 +2746,8 @@ function walkFiber(fiber, parentId, sharedNameCountMap, depth = 0, inSuspenseFal
2328
2746
  renderPhase,
2329
2747
  renderReason,
2330
2748
  renderDuration: current.actualDuration,
2331
- filePath: current._debugSource?.fileName,
2332
- lineNumber: current._debugSource?.lineNumber,
2749
+ filePath: jsxSource?.fileName ?? current._debugSource?.fileName,
2750
+ lineNumber: jsxSource?.lineNumber ?? current._debugSource?.lineNumber,
2333
2751
  isFramework: framework,
2334
2752
  reactKey: resolveEffectiveReactKey(current),
2335
2753
  queryHashes,
@@ -2340,7 +2758,9 @@ function walkFiber(fiber, parentId, sharedNameCountMap, depth = 0, inSuspenseFal
2340
2758
  compilerStatus,
2341
2759
  isServerComponent,
2342
2760
  isLibrary: libraryName !== void 0 ? true : void 0,
2343
- libraryName
2761
+ libraryName,
2762
+ jsxSource,
2763
+ sourceConfidence
2344
2764
  };
2345
2765
  if (!walkerOptions.pruneSubtree?.(node)) {
2346
2766
  nodes.push(node);
@@ -2414,11 +2834,7 @@ function buildTreeFromFiberRoot(root) {
2414
2834
  }
2415
2835
  fiberRefMap.clear();
2416
2836
  const topLevelNodes = walkFiber(rootFiber.child, "");
2417
- debugLog(
2418
- "[FloTrace] walkFiber found",
2419
- topLevelNodes.length,
2420
- "top-level nodes"
2421
- );
2837
+ debugLog("[FloTrace] walkFiber found", topLevelNodes.length, "top-level nodes");
2422
2838
  if (topLevelNodes.length === 1) {
2423
2839
  return topLevelNodes[0];
2424
2840
  }
@@ -2485,9 +2901,7 @@ function findFiberRootFromDOM() {
2485
2901
  }
2486
2902
  }
2487
2903
  }
2488
- console.warn(
2489
- "[FloTrace] Could not find React fiber root from any DOM element"
2490
- );
2904
+ console.warn("[FloTrace] Could not find React fiber root from any DOM element");
2491
2905
  return null;
2492
2906
  } catch (error) {
2493
2907
  console.error("[FloTrace] Error finding fiber root from DOM:", error);
@@ -2549,9 +2963,7 @@ function executeSnapshot(root) {
2549
2963
  adaptSnapshotInterval(nodeCount);
2550
2964
  const client2 = getWebSocketClient();
2551
2965
  if (!client2.connected) {
2552
- console.warn(
2553
- "[FloTrace] WebSocket not connected, cannot send tree snapshot"
2554
- );
2966
+ console.warn("[FloTrace] WebSocket not connected, cannot send tree snapshot");
2555
2967
  return;
2556
2968
  }
2557
2969
  const currentFlatTree = flattenTree2(tree);
@@ -2567,6 +2979,7 @@ function executeSnapshot(root) {
2567
2979
  "nextInterval:",
2568
2980
  snapshotIntervalMs + "ms"
2569
2981
  );
2982
+ logTreeSnapshot(tree, `send seq=${snapshotCounter}`);
2570
2983
  client2.sendImmediate({
2571
2984
  type: "runtime:treeSnapshot",
2572
2985
  tree,
@@ -2587,6 +3000,7 @@ function executeSnapshot(root) {
2587
3000
  "updated:",
2588
3001
  diff.updated.length
2589
3002
  );
3003
+ logTreeSummary(tree, `diff seq=${diffSeq}`);
2590
3004
  client2.sendImmediate({
2591
3005
  type: "runtime:treeDiff",
2592
3006
  seq: diffSeq,
@@ -2727,9 +3141,7 @@ function installFiberTreeWalker(options = {}) {
2727
3141
  return () => uninstallFiberTreeWalker();
2728
3142
  }
2729
3143
  if (typeof window === "undefined") {
2730
- console.warn(
2731
- "[FloTrace] Not in browser environment, cannot install fiber tree walker"
2732
- );
3144
+ console.warn("[FloTrace] Not in browser environment, cannot install fiber tree walker");
2733
3145
  return () => {
2734
3146
  };
2735
3147
  }
@@ -2756,10 +3168,7 @@ function installFiberTreeWalker(options = {}) {
2756
3168
  try {
2757
3169
  originalOnCommitFiberRoot(rendererID, root, priority);
2758
3170
  } catch (error) {
2759
- console.error(
2760
- "[FloTrace] Error in original onCommitFiberRoot:",
2761
- error
2762
- );
3171
+ console.error("[FloTrace] Error in original onCommitFiberRoot:", error);
2763
3172
  }
2764
3173
  }
2765
3174
  if (hookedRendererID === null) {
@@ -2785,9 +3194,7 @@ function installFiberTreeWalker(options = {}) {
2785
3194
  scheduleSnapshot(root);
2786
3195
  };
2787
3196
  activeStrategy = "devtools";
2788
- console.log(
2789
- "[FloTrace] Fiber tree walker installed (DevTools hook strategy)"
2790
- );
3197
+ console.log("[FloTrace] Fiber tree walker installed (DevTools hook strategy)");
2791
3198
  setTimeout(() => {
2792
3199
  try {
2793
3200
  const root = findFiberRootFromDOM();
@@ -2800,9 +3207,7 @@ function installFiberTreeWalker(options = {}) {
2800
3207
  }, 100);
2801
3208
  } else {
2802
3209
  activeStrategy = "dom";
2803
- console.log(
2804
- "[FloTrace] Fiber tree walker installed (DOM fallback strategy)"
2805
- );
3210
+ console.log("[FloTrace] Fiber tree walker installed (DOM fallback strategy)");
2806
3211
  setTimeout(() => {
2807
3212
  try {
2808
3213
  const root = findFiberRootFromDOM();
@@ -3080,12 +3485,18 @@ function installZustandTracker(stores, client2) {
3080
3485
  try {
3081
3486
  scheduleStoreUpdate(storeName, prevState, newState, client2);
3082
3487
  } catch (error) {
3083
- console.error(`[FloTrace] Error in Zustand subscribe callback for "${storeName}":`, error);
3488
+ console.error(
3489
+ `[FloTrace] Error in Zustand subscribe callback for "${storeName}":`,
3490
+ error
3491
+ );
3084
3492
  }
3085
3493
  });
3086
3494
  activeUnsubscribers.push(unsubscribe);
3087
3495
  } catch (error) {
3088
- console.error(`[FloTrace] Failed to install tracker for Zustand store "${storeName}":`, error);
3496
+ console.error(
3497
+ `[FloTrace] Failed to install tracker for Zustand store "${storeName}":`,
3498
+ error
3499
+ );
3089
3500
  }
3090
3501
  }
3091
3502
  }
@@ -3128,10 +3539,13 @@ function scheduleStoreUpdate(storeName, prevState, newState, client2) {
3128
3539
  if (changedKeys.length === 0) return;
3129
3540
  const existing = debounceTimers.get(storeName);
3130
3541
  if (existing) clearTimeout(existing);
3131
- debounceTimers.set(storeName, setTimeout(() => {
3132
- debounceTimers.delete(storeName);
3133
- sendStoreUpdate(storeName, newState, changedKeys, client2);
3134
- }, DEBOUNCE_MS));
3542
+ debounceTimers.set(
3543
+ storeName,
3544
+ setTimeout(() => {
3545
+ debounceTimers.delete(storeName);
3546
+ sendStoreUpdate(storeName, newState, changedKeys, client2);
3547
+ }, DEBOUNCE_MS)
3548
+ );
3135
3549
  }
3136
3550
  function sendStoreUpdate(storeName, state, changedKeys, client2) {
3137
3551
  try {
@@ -3704,7 +4118,14 @@ function findMatchingPathInObject(target, targetFp, container, currentPath, dept
3704
4118
  const child = container[i];
3705
4119
  const directMatch = valuesMatch(target, targetFp, child, cache);
3706
4120
  if (directMatch) return { path: [...currentPath, String(i)], confidence: directMatch };
3707
- const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, String(i)], depth + 1, cache);
4121
+ const nested = findMatchingPathInObject(
4122
+ target,
4123
+ targetFp,
4124
+ child,
4125
+ [...currentPath, String(i)],
4126
+ depth + 1,
4127
+ cache
4128
+ );
3708
4129
  if (nested) return nested;
3709
4130
  }
3710
4131
  } else {
@@ -3712,7 +4133,14 @@ function findMatchingPathInObject(target, targetFp, container, currentPath, dept
3712
4133
  const child = container[key];
3713
4134
  const directMatch = valuesMatch(target, targetFp, child, cache);
3714
4135
  if (directMatch) return { path: [...currentPath, key], confidence: directMatch };
3715
- const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, key], depth + 1, cache);
4136
+ const nested = findMatchingPathInObject(
4137
+ target,
4138
+ targetFp,
4139
+ child,
4140
+ [...currentPath, key],
4141
+ depth + 1,
4142
+ cache
4143
+ );
3716
4144
  if (nested) return nested;
3717
4145
  }
3718
4146
  }
@@ -3817,7 +4245,12 @@ function resolveValueTrace(input) {
3817
4245
  nodeId: input.nodeId,
3818
4246
  componentName: rootComponentName,
3819
4247
  propPath: input.propPath,
3820
- confidence: "exact"
4248
+ confidence: "exact",
4249
+ // P6: `fiber.memoizedProps[FLOTRACE_SOURCE]` is the JSX call site where
4250
+ // this fiber was created — i.e., the `<Consumer .../>` JSX in the
4251
+ // PARENT's source file. That's exactly the "drilled from `Parent.tsx:42`"
4252
+ // attribution the user wants on the leaf prop step.
4253
+ callSiteOfParentJsx: readJsxSourceFromFiber(fiber)
3821
4254
  });
3822
4255
  } else if (input.hookPath) {
3823
4256
  steps.push({
@@ -3857,7 +4290,12 @@ function resolveValueTrace(input) {
3857
4290
  nodeId: ancestorNodeId,
3858
4291
  componentName: ancestorName,
3859
4292
  propPath: trailingSubPath.length > 0 ? [...matchPath, ...trailingSubPath] : matchPath,
3860
- confidence: matchConfidence
4293
+ confidence: matchConfidence,
4294
+ // P6: attribute the parent JSX call site that drilled this
4295
+ // prop. When the user opted into the JSX runtime AND this
4296
+ // ancestor's fiber went through jsxDEV, the consumer sees
4297
+ // "drilled from `Parent.tsx:42:8`" with a click-to-IDE link.
4298
+ callSiteOfParentJsx: readJsxSourceFromFiber(current)
3861
4299
  });
3862
4300
  }
3863
4301
  } else {
@@ -3906,7 +4344,11 @@ function resolveValueTrace(input) {
3906
4344
  const contextMatch = findContextMatch(fiber, rootValue, rootFp, fiberToNodeId, fpCache);
3907
4345
  if (contextMatch) {
3908
4346
  steps.push(contextMatch.step);
3909
- const providerStoreMatch = findStoreMatch(contextMatch.providerValue, cachedFp(contextMatch.providerValue, fpCache), fpCache);
4347
+ const providerStoreMatch = findStoreMatch(
4348
+ contextMatch.providerValue,
4349
+ cachedFp(contextMatch.providerValue, fpCache),
4350
+ fpCache
4351
+ );
3910
4352
  if (providerStoreMatch) {
3911
4353
  steps.push({
3912
4354
  kind: "store",
@@ -4099,13 +4541,22 @@ function detectWebFramework() {
4099
4541
  }
4100
4542
  export {
4101
4543
  DEFAULT_CONFIG,
4544
+ FLOTRACE_SOURCE,
4102
4545
  FloTraceWebSocketClient,
4546
+ JSX_RUNTIME_ACTIVE_KEY,
4103
4547
  buildAncestorChain,
4548
+ clearCallSiteRenders,
4104
4549
  clearFetchOriginTags,
4550
+ computeCallSiteId,
4551
+ computeCallSiteMetricsPayload,
4552
+ describeFiberType,
4553
+ detectInlineLiterals,
4105
4554
  detectServerComponent,
4106
4555
  detectWebFramework,
4107
4556
  disposeWebSocketClient,
4108
4557
  findFetchOrigin,
4558
+ getCallSiteRenderRate,
4559
+ getCallSiteRenders,
4109
4560
  getChangedKeys,
4110
4561
  getComponentNameFromFiber,
4111
4562
  getCurrentRenderingFiber,
@@ -4128,9 +4579,15 @@ export {
4128
4579
  installTanStackQueryTracker,
4129
4580
  installTimelineTracker,
4130
4581
  installZustandTracker,
4582
+ isJsxRuntimeActive,
4131
4583
  isReduxStore,
4132
4584
  isTanStackQueryClient,
4585
+ logTreeSnapshot,
4586
+ logTreeSummary,
4587
+ markJsxRuntimeActive,
4133
4588
  maybeEmitNextjsContext,
4589
+ normalizeJsxSourcePath,
4590
+ recordCallSiteRender,
4134
4591
  recordTimelineEvent,
4135
4592
  requestFullSnapshot,
4136
4593
  requestTreeSnapshot,
@@ -4138,6 +4595,8 @@ export {
4138
4595
  resolveValueTrace,
4139
4596
  serializeProps,
4140
4597
  serializeValue,
4598
+ setDuplicateKeyEmitter,
4599
+ setFiberDebug,
4141
4600
  tagFetchData,
4142
4601
  uninstallFiberTreeWalker,
4143
4602
  uninstallReduxTracker,