@flotrace/runtime-core 2.3.0 → 2.3.3

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.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Type declarations for `@flotrace/runtime-core/babel-plugin`.
3
+ *
4
+ * The plugin itself is a plain CommonJS module (`babel-plugin.js`) so Babel
5
+ * — which loads plugins via `require()` — can consume it without a build
6
+ * step. These declarations give the test suite and any TypeScript consumer
7
+ * a typed handle on the export.
8
+ */
9
+
10
+ import type * as BabelTypes from '@babel/types';
11
+ import type { NodePath, PluginObj } from '@babel/traverse';
12
+
13
+ export interface FlotraceBabelPluginOptions {
14
+ /** Set to `false` to no-op (e.g. for production builds). Default `true`. */
15
+ development?: boolean;
16
+ }
17
+
18
+ export interface FlotraceBabelPluginState {
19
+ opts: FlotraceBabelPluginOptions;
20
+ file: { opts: { filename?: string | null } };
21
+ }
22
+
23
+ declare const flotraceBabelPlugin: {
24
+ (api: { types: typeof BabelTypes }): PluginObj<FlotraceBabelPluginState> & {
25
+ visitor: {
26
+ JSXOpeningElement(
27
+ path: NodePath<BabelTypes.JSXOpeningElement>,
28
+ state: FlotraceBabelPluginState,
29
+ ): void;
30
+ };
31
+ };
32
+ /** Constant attribute name — `data-flotrace-src`. Avoid string drift. */
33
+ FLOTRACE_ATTR_NAME: string;
34
+ };
35
+
36
+ export default flotraceBabelPlugin;
@@ -0,0 +1,302 @@
1
+ /**
2
+ * @flotrace/runtime-core/babel-plugin
3
+ *
4
+ * Injects source attribution into user code in two complementary places so
5
+ * every fiber in the FloTrace tree gets click-to-IDE coverage:
6
+ *
7
+ * (A) `data-flotrace-src` JSX attribute on every JSX element.
8
+ * Read at runtime from `fiber.memoizedProps['data-flotrace-src']`.
9
+ * This covers every fiber that was created via user-written JSX
10
+ * (`<MyComponent />`) — the "call site" attribution.
11
+ *
12
+ * (B) `Component['data-flotrace-src'] = '{...}'` assignment after every
13
+ * PascalCase function / arrow / class declaration. Read at runtime
14
+ * from `fiber.type['data-flotrace-src']` as a fallback for fibers
15
+ * whose memoizedProps lacks the attribute — i.e. when the component
16
+ * was instantiated via `React.createElement(Component, ...)` from
17
+ * inside a library (react-navigation `<Stack.Screen component={X}>`,
18
+ * HOC-wrapped components, top-level App registered via
19
+ * AppRegistry.registerComponent). This is the "definition site"
20
+ * attribution.
21
+ *
22
+ * Together (A) + (B) ensure that every user component in the tree has a
23
+ * source — never a broken-state "no nav button" card.
24
+ *
25
+ * Why a string-keyed JSXAttribute (route A) instead of the JSX-runtime opt-in:
26
+ *
27
+ * 1. Coexists with any other `jsxImportSource` consumer (nativewind /
28
+ * react-native-css-interop, Solid-style runtimes, etc.) — those claim
29
+ * the single `importSource` slot per file; this plugin is independent.
30
+ *
31
+ * 2. Survives React 19's keyed-element prop clone. Symbol-keyed props get
32
+ * silently dropped by React 19's `for-in` config clone whenever an
33
+ * element has a `key` prop (list rows, `.map()` children, navigation
34
+ * screens — the high-value source-tracking sites). String-keyed props
35
+ * enumerate via for-in and survive.
36
+ *
37
+ * 3. `data-*` attributes are valid on every React DOM host element (no
38
+ * unknown-prop warning) and silently ignored by React Native's native
39
+ * renderer (no warning either).
40
+ *
41
+ * 4. JSON-encoded string value parses safely on Windows paths (`C:\...`)
42
+ * where a `file:line:col` delimiter would be ambiguous.
43
+ *
44
+ * Usage in user babel.config.js (single line, no other config changes):
45
+ *
46
+ * plugins: ['@flotrace/runtime-core/babel-plugin']
47
+ *
48
+ * Options:
49
+ * - development (boolean, default true): when false, the plugin no-ops so
50
+ * production bundles aren't bloated with source attributions.
51
+ *
52
+ * No conflict with `@babel/plugin-transform-react-jsx` (any runtime/source/
53
+ * importSource config) — this plugin only ADDS attributes and assignments,
54
+ * it doesn't touch the JSX transform itself.
55
+ */
56
+ 'use strict';
57
+
58
+ const FLOTRACE_ATTR_NAME = 'data-flotrace-src';
59
+
60
+ // React-component naming convention. Anything starting with an uppercase
61
+ // letter is treated as a component candidate. Underscores tolerated for
62
+ // generated names (`_AppComponent` etc.).
63
+ const PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9_$]*$/;
64
+
65
+ /** Build the JSON payload string for the `data-flotrace-src` value. */
66
+ function buildPayload(filename, loc) {
67
+ return JSON.stringify({
68
+ f: filename,
69
+ l: loc.start.line,
70
+ c: loc.start.column + 1, // 1-indexed to match React's convention
71
+ });
72
+ }
73
+
74
+ /** Filename + loc validation gates shared by all visitors. */
75
+ function shouldVisit(state, loc) {
76
+ if (state.opts && state.opts.development === false) return null;
77
+ const filename = state.file && state.file.opts && state.file.opts.filename;
78
+ if (!filename) return null;
79
+ if (filename.indexOf('/node_modules/') !== -1) return null;
80
+ if (filename.indexOf('\\node_modules\\') !== -1) return null;
81
+ if (!loc || !loc.start) return null;
82
+ return filename;
83
+ }
84
+
85
+ /**
86
+ * Reject obviously-non-component initializer types. PascalCase + a function
87
+ * / class / HOC-call init IS a component (or close enough that tagging is
88
+ * harmless). PascalCase + a primitive / array / object literal is NOT.
89
+ */
90
+ function isComponentLikeInit(node) {
91
+ if (!node) return false;
92
+ switch (node.type) {
93
+ case 'NullLiteral':
94
+ case 'NumericLiteral':
95
+ case 'StringLiteral':
96
+ case 'BooleanLiteral':
97
+ case 'BigIntLiteral':
98
+ case 'ArrayExpression':
99
+ case 'ObjectExpression':
100
+ case 'TemplateLiteral':
101
+ case 'RegExpLiteral':
102
+ return false;
103
+ default:
104
+ // Component-shaped: ArrowFunctionExpression, FunctionExpression,
105
+ // ClassExpression, CallExpression (HOC wrap), Identifier (re-export),
106
+ // MemberExpression (`X.Y`), ConditionalExpression, etc.
107
+ return true;
108
+ }
109
+ }
110
+
111
+ module.exports = function flotraceSourceAttributionPlugin({ types: t }) {
112
+ /**
113
+ * Build the `Identifier['data-flotrace-src'] = '<payload>';` statement.
114
+ * Guarded with `if (Identifier)` so reassignments to `undefined`/`null`
115
+ * before the assignment runs don't throw at module load.
116
+ */
117
+ function buildDeclTaggingStatement(name, payload) {
118
+ const idRef = t.identifier(name);
119
+ const memberAccess = t.memberExpression(
120
+ t.identifier(name),
121
+ t.stringLiteral(FLOTRACE_ATTR_NAME),
122
+ /* computed */ true,
123
+ );
124
+ return t.ifStatement(
125
+ idRef,
126
+ t.expressionStatement(
127
+ t.assignmentExpression('=', memberAccess, t.stringLiteral(payload)),
128
+ ),
129
+ );
130
+ }
131
+
132
+ /**
133
+ * Insert a sibling assignment immediately after a top-level declaration
134
+ * path. `path` here is the *outermost* statement we want to insert after
135
+ * — for an `export default function X() {}`, the outer path is the
136
+ * ExportDefaultDeclaration, not the FunctionDeclaration inside it.
137
+ * Returns true when injection happened (used by callers to mark the node
138
+ * as visited and prevent re-injection).
139
+ */
140
+ function insertDeclTagAfter(path, name, payload) {
141
+ // Idempotency: scan siblings for an existing assignment to the same
142
+ // identifier + same attribute. Babel can re-visit the path after other
143
+ // plugins mutate the program.
144
+ const siblings =
145
+ path.parentPath && path.parentPath.get('body')
146
+ ? [].concat(path.parentPath.get('body'))
147
+ : [];
148
+ for (let i = 0; i < siblings.length; i++) {
149
+ const sib = siblings[i].node;
150
+ if (
151
+ sib &&
152
+ sib.type === 'IfStatement' &&
153
+ sib.test &&
154
+ sib.test.type === 'Identifier' &&
155
+ sib.test.name === name &&
156
+ sib.consequent &&
157
+ sib.consequent.type === 'ExpressionStatement' &&
158
+ sib.consequent.expression &&
159
+ sib.consequent.expression.type === 'AssignmentExpression'
160
+ ) {
161
+ const left = sib.consequent.expression.left;
162
+ if (
163
+ left.type === 'MemberExpression' &&
164
+ left.computed &&
165
+ left.property.type === 'StringLiteral' &&
166
+ left.property.value === FLOTRACE_ATTR_NAME
167
+ ) {
168
+ return false;
169
+ }
170
+ }
171
+ }
172
+ path.insertAfter(buildDeclTaggingStatement(name, payload));
173
+ return true;
174
+ }
175
+
176
+ return {
177
+ name: 'flotrace-source-attribution',
178
+ visitor: {
179
+ // ────────────────────────────────────────────────────────────
180
+ // (A) Tag every JSX element with its call-site source.
181
+ // ────────────────────────────────────────────────────────────
182
+ JSXOpeningElement(path, state) {
183
+ const filename = shouldVisit(state, path.node.loc);
184
+ if (!filename) return;
185
+
186
+ // Idempotency: skip if a previous run already injected the attribute
187
+ // (Babel can re-traverse after another plugin mutates the node).
188
+ const attrs = path.node.attributes;
189
+ for (let i = 0; i < attrs.length; i++) {
190
+ const attr = attrs[i];
191
+ if (
192
+ attr.type === 'JSXAttribute' &&
193
+ attr.name &&
194
+ attr.name.type === 'JSXIdentifier' &&
195
+ attr.name.name === FLOTRACE_ATTR_NAME
196
+ ) {
197
+ return;
198
+ }
199
+ }
200
+
201
+ // Skip `<Fragment>` openings — Fragment doesn't accept arbitrary
202
+ // props, and React would warn on the unknown attribute. Note that
203
+ // `<></>` short-syntax is a `JSXFragment` AST node (not a
204
+ // `JSXOpeningElement`), so the visitor never fires for it — only
205
+ // the named forms reach this point.
206
+ const openingName = path.node.name;
207
+ if (openingName.type === 'JSXIdentifier' && openingName.name === 'Fragment') {
208
+ return;
209
+ }
210
+ // `<React.Fragment>` / `<MyNs.Fragment>` — JSXMemberExpression with
211
+ // a trailing `Fragment` identifier. (`React.Fragment` cannot appear
212
+ // as a plain JSXIdentifier — identifiers don't permit dots.)
213
+ if (
214
+ openingName.type === 'JSXMemberExpression' &&
215
+ openingName.property &&
216
+ openingName.property.name === 'Fragment'
217
+ ) {
218
+ return;
219
+ }
220
+
221
+ const payload = buildPayload(filename, path.node.loc);
222
+ // Wrap the JSON payload in a JSX expression container so the value
223
+ // uses JS-string-literal escaping rules. A bare `t.stringLiteral`
224
+ // here would render as `data-flotrace-src="..."` — but JSX string
225
+ // attributes don't support C-style escapes, so embedded `"` chars
226
+ // produce invalid JSX. The expression-container form (`name={...}`)
227
+ // sidesteps the whole quoting problem.
228
+ attrs.push(
229
+ t.jsxAttribute(
230
+ t.jsxIdentifier(FLOTRACE_ATTR_NAME),
231
+ t.jsxExpressionContainer(t.stringLiteral(payload)),
232
+ ),
233
+ );
234
+ },
235
+
236
+ // ────────────────────────────────────────────────────────────
237
+ // (B) Tag every PascalCase component definition with its declaration
238
+ // source. Covers fibers instantiated via `React.createElement`
239
+ // from inside a library (react-navigation screens, HOC-wrapped
240
+ // components, AppRegistry-registered roots).
241
+ // ────────────────────────────────────────────────────────────
242
+ FunctionDeclaration(path, state) {
243
+ const id = path.node.id;
244
+ if (!id || !PASCAL_CASE_RE.test(id.name)) return;
245
+ const filename = shouldVisit(state, path.node.loc);
246
+ if (!filename) return;
247
+ // If wrapped in `export default function X() {}` /
248
+ // `export function X() {}`, attach the assignment AFTER the export
249
+ // statement so it's evaluated at module-eval time (function decls
250
+ // are hoisted, so the identifier is defined by then).
251
+ const target =
252
+ path.parentPath.isExportDefaultDeclaration() ||
253
+ path.parentPath.isExportNamedDeclaration()
254
+ ? path.parentPath
255
+ : path;
256
+ const payload = buildPayload(filename, path.node.loc);
257
+ insertDeclTagAfter(target, id.name, payload);
258
+ },
259
+
260
+ ClassDeclaration(path, state) {
261
+ const id = path.node.id;
262
+ if (!id || !PASCAL_CASE_RE.test(id.name)) return;
263
+ const filename = shouldVisit(state, path.node.loc);
264
+ if (!filename) return;
265
+ const target =
266
+ path.parentPath.isExportDefaultDeclaration() ||
267
+ path.parentPath.isExportNamedDeclaration()
268
+ ? path.parentPath
269
+ : path;
270
+ const payload = buildPayload(filename, path.node.loc);
271
+ insertDeclTagAfter(target, id.name, payload);
272
+ },
273
+
274
+ VariableDeclarator(path, state) {
275
+ const id = path.node.id;
276
+ if (!id || id.type !== 'Identifier') return;
277
+ if (!PASCAL_CASE_RE.test(id.name)) return;
278
+ if (!isComponentLikeInit(path.node.init)) return;
279
+ const filename = shouldVisit(state, path.node.loc);
280
+ if (!filename) return;
281
+ // VariableDeclarator's parent is VariableDeclaration; that may be
282
+ // wrapped in ExportNamedDeclaration (`export const Foo = ...`).
283
+ // Walk up to the outermost statement so the assignment lands at
284
+ // the right scope.
285
+ let target = path.parentPath; // VariableDeclaration
286
+ if (
287
+ target.parentPath &&
288
+ (target.parentPath.isExportNamedDeclaration() ||
289
+ target.parentPath.isExportDefaultDeclaration())
290
+ ) {
291
+ target = target.parentPath;
292
+ }
293
+ const payload = buildPayload(filename, path.node.loc);
294
+ insertDeclTagAfter(target, id.name, payload);
295
+ },
296
+ },
297
+ };
298
+ };
299
+
300
+ // Re-export the attribute name so the runtime reader and tests have a
301
+ // single source of truth (no string drift between plugin and walker).
302
+ module.exports.FLOTRACE_ATTR_NAME = FLOTRACE_ATTR_NAME;
@@ -11,6 +11,44 @@ function normalizeJsxSourcePath(fileName) {
11
11
  if (/^[a-zA-Z]:[\\/]/.test(p)) p = p[0].toLowerCase() + p.slice(1);
12
12
  return p;
13
13
  }
14
+ function normalizeStackFramePath(rawPath) {
15
+ let p = rawPath;
16
+ const queryIdx = p.indexOf("?");
17
+ if (queryIdx !== -1) p = p.slice(0, queryIdx);
18
+ const httpMatch = p.match(/^https?:\/\/[^/]+(\/.+)$/);
19
+ if (httpMatch) {
20
+ p = httpMatch[1];
21
+ if (p.startsWith("/")) p = p.slice(1);
22
+ }
23
+ return normalizeJsxSourcePath(p);
24
+ }
25
+ function isJsBundlePath(rawPath) {
26
+ if (/\bindex\.bundle\b/.test(rawPath)) return true;
27
+ if (/\.bundle(\?|$|\.js(\?|$))/.test(rawPath)) return true;
28
+ if (/\?platform=(ios|android|web|native)\b/.test(rawPath)) return true;
29
+ return false;
30
+ }
31
+ function parseFirstNonReactFrame(stack) {
32
+ const lines = stack.split("\n");
33
+ for (const line of lines) {
34
+ const parened = line.match(/\(([^)]+):(\d+):(\d+)\)/);
35
+ const hermes = line.match(/@([^\s]+):(\d+):(\d+)$/);
36
+ const match = parened ?? hermes;
37
+ if (!match) continue;
38
+ const path = match[1];
39
+ if (path.includes("react-dom")) continue;
40
+ if (path.includes("react-native/Libraries")) continue;
41
+ if (path.includes("/react/cjs/")) continue;
42
+ if (path.includes("/scheduler/")) continue;
43
+ if (isJsBundlePath(path)) continue;
44
+ return {
45
+ fileName: normalizeStackFramePath(path),
46
+ lineNumber: Number(match[2]),
47
+ columnNumber: Number(match[3])
48
+ };
49
+ }
50
+ return null;
51
+ }
14
52
  function computeCallSiteId(source) {
15
53
  const normPath = normalizeJsxSourcePath(source.fileName);
16
54
  const key = `${normPath}:${source.lineNumber}:${source.columnNumber}`;
@@ -21,16 +59,71 @@ function computeCallSiteId(source) {
21
59
  }
22
60
  return (hash >>> 0).toString(16).padStart(8, "0");
23
61
  }
62
+ var FLOTRACE_SRC_ATTR = "data-flotrace-src";
63
+ function parseDataFlotraceSrc(value) {
64
+ let obj;
65
+ try {
66
+ obj = JSON.parse(value);
67
+ } catch {
68
+ return void 0;
69
+ }
70
+ if (!obj || typeof obj !== "object") return void 0;
71
+ const o = obj;
72
+ if (typeof o.f !== "string" || typeof o.l !== "number" || typeof o.c !== "number") {
73
+ return void 0;
74
+ }
75
+ const fileName = o.f;
76
+ const lineNumber = o.l;
77
+ const columnNumber = o.c;
78
+ return {
79
+ fileName,
80
+ lineNumber,
81
+ columnNumber,
82
+ callSiteId: computeCallSiteId({ fileName, lineNumber, columnNumber })
83
+ };
84
+ }
24
85
  function readJsxSourceFromFiber(fiber) {
25
86
  const props = fiber.memoizedProps;
26
- if (!props) return void 0;
27
- const raw = props[FLOTRACE_SOURCE];
28
- if (!raw || typeof raw !== "object") return void 0;
29
- const obj = raw;
30
- if (typeof obj.fileName !== "string" || typeof obj.lineNumber !== "number" || typeof obj.columnNumber !== "number" || typeof obj.callSiteId !== "string") {
31
- return void 0;
87
+ if (props) {
88
+ const symRaw = props[FLOTRACE_SOURCE];
89
+ if (symRaw && typeof symRaw === "object") {
90
+ const obj = symRaw;
91
+ if (typeof obj.fileName === "string" && typeof obj.lineNumber === "number" && typeof obj.columnNumber === "number" && typeof obj.callSiteId === "string") {
92
+ return symRaw;
93
+ }
94
+ }
95
+ const strRaw = props[FLOTRACE_SRC_ATTR];
96
+ if (typeof strRaw === "string") {
97
+ const parsed = parseDataFlotraceSrc(strRaw);
98
+ if (parsed) return parsed;
99
+ }
100
+ }
101
+ const type = fiber.type;
102
+ if (type !== null && (typeof type === "function" || typeof type === "object")) {
103
+ const typeAttr = type[FLOTRACE_SRC_ATTR];
104
+ if (typeof typeAttr === "string") {
105
+ const parsed = parseDataFlotraceSrc(typeAttr);
106
+ if (parsed) return parsed;
107
+ }
108
+ }
109
+ return void 0;
110
+ }
111
+ function isUserComponent(fiber) {
112
+ const jsxSrc = readJsxSourceFromFiber({
113
+ memoizedProps: fiber.memoizedProps ?? null,
114
+ type: fiber.type
115
+ });
116
+ if (jsxSrc && !jsxSrc.fileName.includes("node_modules")) return true;
117
+ const debugSourcePath = fiber._debugSource?.fileName;
118
+ if (typeof debugSourcePath === "string" && !debugSourcePath.includes("node_modules")) {
119
+ return true;
120
+ }
121
+ const stack = fiber._debugStack?.stack;
122
+ if (typeof stack === "string") {
123
+ const frame = parseFirstNonReactFrame(stack);
124
+ if (frame && !frame.fileName.includes("node_modules")) return true;
32
125
  }
33
- return raw;
126
+ return false;
34
127
  }
35
128
  var callSiteRenders = /* @__PURE__ */ new Map();
36
129
  var RING_BUFFER_MAX = 60;
@@ -166,8 +259,10 @@ export {
166
259
  FLOTRACE_SOURCE,
167
260
  JSX_RUNTIME_ACTIVE_KEY,
168
261
  normalizeJsxSourcePath,
262
+ parseFirstNonReactFrame,
169
263
  computeCallSiteId,
170
264
  readJsxSourceFromFiber,
265
+ isUserComponent,
171
266
  recordCallSiteRender,
172
267
  getCallSiteRenders,
173
268
  getCallSiteRenderRate,
package/dist/index.d.mts CHANGED
@@ -79,6 +79,64 @@ declare function normalizeJsxSourcePath(fileName: string): string;
79
79
  * Not suitable as a security token.
80
80
  */
81
81
  declare function computeCallSiteId(source: JsxSourceArg): string;
82
+ /**
83
+ * Top-priority "is this fiber a user-defined component?" check.
84
+ *
85
+ * Three routes, checked in order of precision (fastest + most reliable
86
+ * first). Returns `true` as soon as ANY route produces positive evidence
87
+ * that the fiber's source code lives outside `node_modules`.
88
+ *
89
+ * Route A — `fiber.type[FLOTRACE_SRC_ATTR]` (babel plugin):
90
+ * The `@flotrace/runtime-core/babel-plugin` declaration-tagging pass
91
+ * writes a JSON `{f,l,c}` payload onto every PascalCase function /
92
+ * class / variable in user code. Works for: React Native (Babel),
93
+ * Vite + React (Babel), CRA, Webpack + Babel, Next.js Pages Router
94
+ * (if Babel is opted in). Doesn't work for: Next.js with SWC.
95
+ *
96
+ * Route B — `fiber._debugSource.fileName` (React 18 + Babel JSX source):
97
+ * The `@babel/plugin-transform-react-jsx-source` plugin (auto-included
98
+ * by RN's preset and CRA/Vite's React preset) injects `__source` on
99
+ * every JSX element, which React 18 captures onto `fiber._debugSource`.
100
+ * Empty under React 19+ (the field was deprecated upstream).
101
+ *
102
+ * Route C — `fiber._debugStack.stack` (React 19+ web — Next.js SWC, Vite):
103
+ * React 19 replaced `_debugSource` with an Error captured at JSX
104
+ * creation time. We parse the first non-React stack frame for a path.
105
+ * This is what makes Next.js (SWC, no babel config possible without
106
+ * losing SWC) work — the React 19 reconciler attaches stack-frame
107
+ * info regardless of the compiler used.
108
+ *
109
+ * For ALL routes the "is user code" criterion is the same: the resolved
110
+ * file path must not include `node_modules`. That string check is enough
111
+ * because every modern bundler resolves third-party deps through a path
112
+ * containing `/node_modules/` — there's no realistic false-negative.
113
+ *
114
+ * Returns `false` for:
115
+ * - Host components (string `fiber.type` like `'View'` / `'div'`)
116
+ * - Library / framework components (paths in `node_modules`, plus
117
+ * bundle-URL frames which are skipped by `parseFirstNonReactFrame`)
118
+ * - Fibers with no source signal at all (degrade to existing heuristics)
119
+ *
120
+ * Used by `fiberTreeWalker.ts` as a top-priority short-circuit before
121
+ * name-list / regex / path heuristics fire. A fiber that returns `true`
122
+ * here is authoritatively user code — every framework/library
123
+ * classification downstream should bypass.
124
+ *
125
+ * Generic over the fiber-like shape so cascadeAnalyzer / propDrillingAnalyzer
126
+ * / valueTraceResolver can all share one definition without importing the
127
+ * walker's `Fiber` type.
128
+ */
129
+ declare function isUserComponent(fiber: {
130
+ type?: unknown;
131
+ memoizedProps?: Record<string, unknown> | null;
132
+ _debugSource?: {
133
+ fileName: string;
134
+ lineNumber?: number;
135
+ } | null;
136
+ _debugStack?: {
137
+ stack?: string;
138
+ } | null;
139
+ }): boolean;
82
140
  /**
83
141
  * Record a render timestamp for a call site. Caller may inject `now` for
84
142
  * deterministic tests; production callers use the default `performance.now()`.
@@ -2074,4 +2132,4 @@ interface FtConsoleApi {
2074
2132
  download(filename?: string): void;
2075
2133
  }
2076
2134
 
2077
- export { type ActionStateEntry, DEFAULT_CONFIG, type DetailedRenderReason, type DetailedRenderReasonType, type DuplicateKeyEvent, type EffectInfo, FLOTRACE_SOURCE, type Fiber$1 as Fiber, type FiberEffect, type FiberHookState, type FiberTreeWalkerOptions, type FloTraceConfig, FloTraceWebSocketClient, type FlotraceJsxSource, type FrameworkInfo, type HookInfo, type HookType, JSX_RUNTIME_ACTIVE_KEY, type LiveTreeNode, type MutationCorrelation, type NetworkRequestEntry, type PropChange, type ReduxStoreApi, type ResolvedFloTraceConfig, type RscCacheStatus, type RuntimeActionStateMessage, type RuntimeCallSiteMetricsMessage, type RuntimeDuplicateKeyMessage, type RuntimeHydrationEventMessage, type RuntimeNextjsContextMessage, type RuntimeOptimisticDiffMessage, type RuntimeRscPayloadMessage, type RuntimeTreeDiffMessage, type RuntimeValueTraceMessage, type SerializedValue, type SourceConfidence, type TanStackMutationInfo, type TanStackQueryClientApi, type TanStackQueryEvent, type TanStackQueryInfo, type TimelineEvent, type TimelineEventType, type TraceConfidence, type TraceStep, type TrackingOptions, type ValueTrace, type ValueTraceInput, type ZustandStoreApi, buildAncestorChain, clearCallSiteRenders, clearFetchOriginTags, computeCallSiteId, computeCallSiteMetricsPayload, describeFiberType, detectInlineLiterals, detectServerComponent, detectWebFramework, disposeWebSocketClient, findFetchOrigin, getCallSiteRenderRate, getCallSiteRenders, getChangedKeys, getComponentNameFromFiber, getCurrentRenderingFiber, getDetailedRenderReason, getFiberRefMap, getNodeEffects, getNodeHooks, getNodeProps, getReduxSnapshot, getTanstackSnapshot, getTimeline, getWebSocketClient, getZustandSnapshot, hasActiveTags, inspectEffects, inspectHooks, installFiberTreeWalker, installReduxTracker, installRscPayloadInterceptor, installTanStackQueryTracker, installTimelineTracker, installZustandTracker, isJsxRuntimeActive, isReduxStore, isTanStackQueryClient, logTreeSnapshot, logTreeSummary, markJsxRuntimeActive, maybeEmitNextjsContext, normalizeJsxSourcePath, recordCallSiteRender, recordTimelineEvent, requestFullSnapshot, requestTreeSnapshot, resetNextjsDetection, resolveValueTrace, serializeProps, serializeValue, setDuplicateKeyEmitter, setFiberDebug, tagFetchData, uninstallFiberTreeWalker, uninstallReduxTracker, uninstallRscPayloadInterceptor, uninstallTanStackQueryTracker, uninstallTimelineTracker, uninstallZustandTracker };
2135
+ export { type ActionStateEntry, DEFAULT_CONFIG, type DetailedRenderReason, type DetailedRenderReasonType, type DuplicateKeyEvent, type EffectInfo, FLOTRACE_SOURCE, type Fiber$1 as Fiber, type FiberEffect, type FiberHookState, type FiberTreeWalkerOptions, type FloTraceConfig, FloTraceWebSocketClient, type FlotraceJsxSource, type FrameworkInfo, type HookInfo, type HookType, JSX_RUNTIME_ACTIVE_KEY, type LiveTreeNode, type MutationCorrelation, type NetworkRequestEntry, type PropChange, type ReduxStoreApi, type ResolvedFloTraceConfig, type RscCacheStatus, type RuntimeActionStateMessage, type RuntimeCallSiteMetricsMessage, type RuntimeDuplicateKeyMessage, type RuntimeHydrationEventMessage, type RuntimeNextjsContextMessage, type RuntimeOptimisticDiffMessage, type RuntimeRscPayloadMessage, type RuntimeTreeDiffMessage, type RuntimeValueTraceMessage, type SerializedValue, type SourceConfidence, type TanStackMutationInfo, type TanStackQueryClientApi, type TanStackQueryEvent, type TanStackQueryInfo, type TimelineEvent, type TimelineEventType, type TraceConfidence, type TraceStep, type TrackingOptions, type ValueTrace, type ValueTraceInput, type ZustandStoreApi, buildAncestorChain, clearCallSiteRenders, clearFetchOriginTags, computeCallSiteId, computeCallSiteMetricsPayload, describeFiberType, detectInlineLiterals, detectServerComponent, detectWebFramework, disposeWebSocketClient, findFetchOrigin, getCallSiteRenderRate, getCallSiteRenders, getChangedKeys, getComponentNameFromFiber, getCurrentRenderingFiber, getDetailedRenderReason, getFiberRefMap, getNodeEffects, getNodeHooks, getNodeProps, getReduxSnapshot, getTanstackSnapshot, getTimeline, getWebSocketClient, getZustandSnapshot, hasActiveTags, inspectEffects, inspectHooks, installFiberTreeWalker, installReduxTracker, installRscPayloadInterceptor, installTanStackQueryTracker, installTimelineTracker, installZustandTracker, isJsxRuntimeActive, isReduxStore, isTanStackQueryClient, isUserComponent, logTreeSnapshot, logTreeSummary, markJsxRuntimeActive, maybeEmitNextjsContext, normalizeJsxSourcePath, recordCallSiteRender, recordTimelineEvent, requestFullSnapshot, requestTreeSnapshot, resetNextjsDetection, resolveValueTrace, serializeProps, serializeValue, setDuplicateKeyEmitter, setFiberDebug, tagFetchData, uninstallFiberTreeWalker, uninstallReduxTracker, uninstallRscPayloadInterceptor, uninstallTanStackQueryTracker, uninstallTimelineTracker, uninstallZustandTracker };
package/dist/index.d.ts CHANGED
@@ -79,6 +79,64 @@ declare function normalizeJsxSourcePath(fileName: string): string;
79
79
  * Not suitable as a security token.
80
80
  */
81
81
  declare function computeCallSiteId(source: JsxSourceArg): string;
82
+ /**
83
+ * Top-priority "is this fiber a user-defined component?" check.
84
+ *
85
+ * Three routes, checked in order of precision (fastest + most reliable
86
+ * first). Returns `true` as soon as ANY route produces positive evidence
87
+ * that the fiber's source code lives outside `node_modules`.
88
+ *
89
+ * Route A — `fiber.type[FLOTRACE_SRC_ATTR]` (babel plugin):
90
+ * The `@flotrace/runtime-core/babel-plugin` declaration-tagging pass
91
+ * writes a JSON `{f,l,c}` payload onto every PascalCase function /
92
+ * class / variable in user code. Works for: React Native (Babel),
93
+ * Vite + React (Babel), CRA, Webpack + Babel, Next.js Pages Router
94
+ * (if Babel is opted in). Doesn't work for: Next.js with SWC.
95
+ *
96
+ * Route B — `fiber._debugSource.fileName` (React 18 + Babel JSX source):
97
+ * The `@babel/plugin-transform-react-jsx-source` plugin (auto-included
98
+ * by RN's preset and CRA/Vite's React preset) injects `__source` on
99
+ * every JSX element, which React 18 captures onto `fiber._debugSource`.
100
+ * Empty under React 19+ (the field was deprecated upstream).
101
+ *
102
+ * Route C — `fiber._debugStack.stack` (React 19+ web — Next.js SWC, Vite):
103
+ * React 19 replaced `_debugSource` with an Error captured at JSX
104
+ * creation time. We parse the first non-React stack frame for a path.
105
+ * This is what makes Next.js (SWC, no babel config possible without
106
+ * losing SWC) work — the React 19 reconciler attaches stack-frame
107
+ * info regardless of the compiler used.
108
+ *
109
+ * For ALL routes the "is user code" criterion is the same: the resolved
110
+ * file path must not include `node_modules`. That string check is enough
111
+ * because every modern bundler resolves third-party deps through a path
112
+ * containing `/node_modules/` — there's no realistic false-negative.
113
+ *
114
+ * Returns `false` for:
115
+ * - Host components (string `fiber.type` like `'View'` / `'div'`)
116
+ * - Library / framework components (paths in `node_modules`, plus
117
+ * bundle-URL frames which are skipped by `parseFirstNonReactFrame`)
118
+ * - Fibers with no source signal at all (degrade to existing heuristics)
119
+ *
120
+ * Used by `fiberTreeWalker.ts` as a top-priority short-circuit before
121
+ * name-list / regex / path heuristics fire. A fiber that returns `true`
122
+ * here is authoritatively user code — every framework/library
123
+ * classification downstream should bypass.
124
+ *
125
+ * Generic over the fiber-like shape so cascadeAnalyzer / propDrillingAnalyzer
126
+ * / valueTraceResolver can all share one definition without importing the
127
+ * walker's `Fiber` type.
128
+ */
129
+ declare function isUserComponent(fiber: {
130
+ type?: unknown;
131
+ memoizedProps?: Record<string, unknown> | null;
132
+ _debugSource?: {
133
+ fileName: string;
134
+ lineNumber?: number;
135
+ } | null;
136
+ _debugStack?: {
137
+ stack?: string;
138
+ } | null;
139
+ }): boolean;
82
140
  /**
83
141
  * Record a render timestamp for a call site. Caller may inject `now` for
84
142
  * deterministic tests; production callers use the default `performance.now()`.
@@ -2074,4 +2132,4 @@ interface FtConsoleApi {
2074
2132
  download(filename?: string): void;
2075
2133
  }
2076
2134
 
2077
- export { type ActionStateEntry, DEFAULT_CONFIG, type DetailedRenderReason, type DetailedRenderReasonType, type DuplicateKeyEvent, type EffectInfo, FLOTRACE_SOURCE, type Fiber$1 as Fiber, type FiberEffect, type FiberHookState, type FiberTreeWalkerOptions, type FloTraceConfig, FloTraceWebSocketClient, type FlotraceJsxSource, type FrameworkInfo, type HookInfo, type HookType, JSX_RUNTIME_ACTIVE_KEY, type LiveTreeNode, type MutationCorrelation, type NetworkRequestEntry, type PropChange, type ReduxStoreApi, type ResolvedFloTraceConfig, type RscCacheStatus, type RuntimeActionStateMessage, type RuntimeCallSiteMetricsMessage, type RuntimeDuplicateKeyMessage, type RuntimeHydrationEventMessage, type RuntimeNextjsContextMessage, type RuntimeOptimisticDiffMessage, type RuntimeRscPayloadMessage, type RuntimeTreeDiffMessage, type RuntimeValueTraceMessage, type SerializedValue, type SourceConfidence, type TanStackMutationInfo, type TanStackQueryClientApi, type TanStackQueryEvent, type TanStackQueryInfo, type TimelineEvent, type TimelineEventType, type TraceConfidence, type TraceStep, type TrackingOptions, type ValueTrace, type ValueTraceInput, type ZustandStoreApi, buildAncestorChain, clearCallSiteRenders, clearFetchOriginTags, computeCallSiteId, computeCallSiteMetricsPayload, describeFiberType, detectInlineLiterals, detectServerComponent, detectWebFramework, disposeWebSocketClient, findFetchOrigin, getCallSiteRenderRate, getCallSiteRenders, getChangedKeys, getComponentNameFromFiber, getCurrentRenderingFiber, getDetailedRenderReason, getFiberRefMap, getNodeEffects, getNodeHooks, getNodeProps, getReduxSnapshot, getTanstackSnapshot, getTimeline, getWebSocketClient, getZustandSnapshot, hasActiveTags, inspectEffects, inspectHooks, installFiberTreeWalker, installReduxTracker, installRscPayloadInterceptor, installTanStackQueryTracker, installTimelineTracker, installZustandTracker, isJsxRuntimeActive, isReduxStore, isTanStackQueryClient, logTreeSnapshot, logTreeSummary, markJsxRuntimeActive, maybeEmitNextjsContext, normalizeJsxSourcePath, recordCallSiteRender, recordTimelineEvent, requestFullSnapshot, requestTreeSnapshot, resetNextjsDetection, resolveValueTrace, serializeProps, serializeValue, setDuplicateKeyEmitter, setFiberDebug, tagFetchData, uninstallFiberTreeWalker, uninstallReduxTracker, uninstallRscPayloadInterceptor, uninstallTanStackQueryTracker, uninstallTimelineTracker, uninstallZustandTracker };
2135
+ export { type ActionStateEntry, DEFAULT_CONFIG, type DetailedRenderReason, type DetailedRenderReasonType, type DuplicateKeyEvent, type EffectInfo, FLOTRACE_SOURCE, type Fiber$1 as Fiber, type FiberEffect, type FiberHookState, type FiberTreeWalkerOptions, type FloTraceConfig, FloTraceWebSocketClient, type FlotraceJsxSource, type FrameworkInfo, type HookInfo, type HookType, JSX_RUNTIME_ACTIVE_KEY, type LiveTreeNode, type MutationCorrelation, type NetworkRequestEntry, type PropChange, type ReduxStoreApi, type ResolvedFloTraceConfig, type RscCacheStatus, type RuntimeActionStateMessage, type RuntimeCallSiteMetricsMessage, type RuntimeDuplicateKeyMessage, type RuntimeHydrationEventMessage, type RuntimeNextjsContextMessage, type RuntimeOptimisticDiffMessage, type RuntimeRscPayloadMessage, type RuntimeTreeDiffMessage, type RuntimeValueTraceMessage, type SerializedValue, type SourceConfidence, type TanStackMutationInfo, type TanStackQueryClientApi, type TanStackQueryEvent, type TanStackQueryInfo, type TimelineEvent, type TimelineEventType, type TraceConfidence, type TraceStep, type TrackingOptions, type ValueTrace, type ValueTraceInput, type ZustandStoreApi, buildAncestorChain, clearCallSiteRenders, clearFetchOriginTags, computeCallSiteId, computeCallSiteMetricsPayload, describeFiberType, detectInlineLiterals, detectServerComponent, detectWebFramework, disposeWebSocketClient, findFetchOrigin, getCallSiteRenderRate, getCallSiteRenders, getChangedKeys, getComponentNameFromFiber, getCurrentRenderingFiber, getDetailedRenderReason, getFiberRefMap, getNodeEffects, getNodeHooks, getNodeProps, getReduxSnapshot, getTanstackSnapshot, getTimeline, getWebSocketClient, getZustandSnapshot, hasActiveTags, inspectEffects, inspectHooks, installFiberTreeWalker, installReduxTracker, installRscPayloadInterceptor, installTanStackQueryTracker, installTimelineTracker, installZustandTracker, isJsxRuntimeActive, isReduxStore, isTanStackQueryClient, isUserComponent, logTreeSnapshot, logTreeSummary, markJsxRuntimeActive, maybeEmitNextjsContext, normalizeJsxSourcePath, recordCallSiteRender, recordTimelineEvent, requestFullSnapshot, requestTreeSnapshot, resetNextjsDetection, resolveValueTrace, serializeProps, serializeValue, setDuplicateKeyEmitter, setFiberDebug, tagFetchData, uninstallFiberTreeWalker, uninstallReduxTracker, uninstallRscPayloadInterceptor, uninstallTanStackQueryTracker, uninstallTimelineTracker, uninstallZustandTracker };
package/dist/index.js CHANGED
@@ -72,6 +72,7 @@ __export(index_exports, {
72
72
  isJsxRuntimeActive: () => isJsxRuntimeActive,
73
73
  isReduxStore: () => isReduxStore,
74
74
  isTanStackQueryClient: () => isTanStackQueryClient,
75
+ isUserComponent: () => isUserComponent,
75
76
  logTreeSnapshot: () => logTreeSnapshot,
76
77
  logTreeSummary: () => logTreeSummary,
77
78
  markJsxRuntimeActive: () => markJsxRuntimeActive,
@@ -133,6 +134,44 @@ function normalizeJsxSourcePath(fileName) {
133
134
  if (/^[a-zA-Z]:[\\/]/.test(p)) p = p[0].toLowerCase() + p.slice(1);
134
135
  return p;
135
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
+ }
136
175
  function computeCallSiteId(source) {
137
176
  const normPath = normalizeJsxSourcePath(source.fileName);
138
177
  const key = `${normPath}:${source.lineNumber}:${source.columnNumber}`;
@@ -143,16 +182,71 @@ function computeCallSiteId(source) {
143
182
  }
144
183
  return (hash >>> 0).toString(16).padStart(8, "0");
145
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
+ }
146
208
  function readJsxSourceFromFiber(fiber) {
147
209
  const props = fiber.memoizedProps;
148
- if (!props) return void 0;
149
- const raw = props[FLOTRACE_SOURCE];
150
- if (!raw || typeof raw !== "object") return void 0;
151
- const obj = raw;
152
- if (typeof obj.fileName !== "string" || typeof obj.lineNumber !== "number" || typeof obj.columnNumber !== "number" || typeof obj.callSiteId !== "string") {
153
- return void 0;
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;
154
248
  }
155
- return raw;
249
+ return false;
156
250
  }
157
251
  var callSiteRenders = /* @__PURE__ */ new Map();
158
252
  var RING_BUFFER_MAX = 60;
@@ -1716,7 +1810,11 @@ function runAnalysis(tree, fiberRefMap2) {
1716
1810
  }
1717
1811
  }
1718
1812
  for (const sourceId of sourceNodeIds) {
1719
- 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) => {
1720
1818
  if (visited.has(currentId)) return;
1721
1819
  visited.add(currentId);
1722
1820
  const outgoing = outEdges.get(currentId);
@@ -1729,7 +1827,7 @@ function runAnalysis(tree, fiberRefMap2) {
1729
1827
  }
1730
1828
  for (const edge of outgoing) {
1731
1829
  const isRename = edge.propKey !== edge.childPropKey;
1732
- dfs2(
1830
+ dfs(
1733
1831
  edge.childNodeId,
1734
1832
  edge.childPropKey,
1735
1833
  [...currentPath, { nodeId: edge.childNodeId, propKey: edge.childPropKey, isRename }],
@@ -1738,12 +1836,7 @@ function runAnalysis(tree, fiberRefMap2) {
1738
1836
  }
1739
1837
  visited.delete(currentId);
1740
1838
  };
1741
- var dfs = dfs2;
1742
- const firstEdge = outEdges.get(sourceId)?.[0];
1743
- if (!firstEdge) continue;
1744
- const sourcePropName = firstEdge.propKey;
1745
- const allPaths = [];
1746
- dfs2(
1839
+ dfs(
1747
1840
  sourceId,
1748
1841
  sourcePropName,
1749
1842
  [{ nodeId: sourceId, propKey: sourcePropName, isRename: false }],
@@ -2573,7 +2666,7 @@ function detectTransitionPending(fiber) {
2573
2666
  }
2574
2667
  return false;
2575
2668
  }
2576
- var MAX_TREE_DEPTH = 100;
2669
+ var MAX_TREE_DEPTH = 3e3;
2577
2670
  var MAX_CHILDREN_PER_NODE = 300;
2578
2671
  var throttleTimer = null;
2579
2672
  var maxWaitTimer = null;
@@ -2633,7 +2726,7 @@ function getComponentName2(fiber) {
2633
2726
  }
2634
2727
  return "Unknown";
2635
2728
  }
2636
- function isUserComponent(fiber) {
2729
+ function isLikelyUserComponent(fiber) {
2637
2730
  if (!USER_COMPONENT_TAGS.has(fiber.tag)) return false;
2638
2731
  const name = getComponentName2(fiber);
2639
2732
  if (name === "Anonymous" || name === "Unknown" || name === "ForwardRef" || name === "Memo")
@@ -2737,17 +2830,28 @@ var FRAMEWORK_PATH_PATTERNS = [
2737
2830
  /react-hook-form/,
2738
2831
  /formik/
2739
2832
  ];
2740
- function resolveEffectiveSourcePath(fiber) {
2833
+ function resolveEffectiveSourceLocation(fiber) {
2741
2834
  const jsxSrc = readJsxSourceFromFiber(fiber);
2742
- if (jsxSrc) return jsxSrc.fileName;
2743
- if (fiber._debugSource?.fileName) return fiber._debugSource.fileName;
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
+ }
2744
2848
  const ownerHit = walkAncestors(
2745
2849
  fiber._debugOwner ?? null,
2746
2850
  3,
2747
2851
  (f) => f._debugOwner ?? null,
2748
2852
  (cur) => cur._debugSource?.fileName ?? void 0
2749
2853
  );
2750
- if (ownerHit) return ownerHit;
2854
+ if (ownerHit) return { fileName: ownerHit };
2751
2855
  const stack = fiber._debugStack?.stack;
2752
2856
  if (typeof stack === "string") {
2753
2857
  const parsed = parseFirstNonReactFrame(stack);
@@ -2755,7 +2859,11 @@ function resolveEffectiveSourcePath(fiber) {
2755
2859
  }
2756
2860
  return null;
2757
2861
  }
2862
+ function resolveEffectiveSourcePath(fiber) {
2863
+ return resolveEffectiveSourceLocation(fiber)?.fileName ?? null;
2864
+ }
2758
2865
  function resolveSourceConfidence(fiber, isFramework, isLibrary, precomputedJsxSource) {
2866
+ if (isUserComponent(fiber)) return "exact";
2759
2867
  if (isFramework || isLibrary) return "package";
2760
2868
  const jsxSrc = precomputedJsxSource ?? readJsxSourceFromFiber(fiber);
2761
2869
  if (jsxSrc) return "exact";
@@ -2773,22 +2881,8 @@ function walkAncestors(start, maxHops, next, visit) {
2773
2881
  }
2774
2882
  return void 0;
2775
2883
  }
2776
- function parseFirstNonReactFrame(stack) {
2777
- const lines = stack.split("\n");
2778
- for (const line of lines) {
2779
- const parened = line.match(/\(([^)]+):\d+:\d+\)/);
2780
- const hermes = line.match(/@([^\s]+):\d+:\d+$/);
2781
- const path = parened?.[1] ?? hermes?.[1];
2782
- if (!path) continue;
2783
- if (path.includes("react-dom")) continue;
2784
- if (path.includes("react-native/Libraries")) continue;
2785
- if (path.includes("/react/cjs/")) continue;
2786
- if (path.includes("/scheduler/")) continue;
2787
- return path;
2788
- }
2789
- return null;
2790
- }
2791
2884
  function isFrameworkComponent(fiber, name) {
2885
+ if (isUserComponent(fiber)) return false;
2792
2886
  if (walkerFilterConfig.frameworkNames.has(name)) return true;
2793
2887
  for (const pattern of walkerFilterConfig.frameworkNamePatterns) {
2794
2888
  if (pattern.test(name)) return true;
@@ -2828,6 +2922,7 @@ var KNOWN_LIBRARY_NAMES = /* @__PURE__ */ new Map([
2828
2922
  ["HeroIcon", "heroicons"]
2829
2923
  ]);
2830
2924
  function detectLibraryName(fiber, name) {
2925
+ if (isUserComponent(fiber)) return void 0;
2831
2926
  if (name.includes(".")) {
2832
2927
  return name.split(".")[0].toLowerCase();
2833
2928
  }
@@ -2898,7 +2993,7 @@ function resolveEffectiveReactKey(fiber) {
2898
2993
  6,
2899
2994
  (f) => f.return ?? null,
2900
2995
  (cur) => {
2901
- if (isUserComponent(cur) && !isFrameworkComponent(cur, getComponentName2(cur))) {
2996
+ if (isLikelyUserComponent(cur) && !isFrameworkComponent(cur, getComponentName2(cur))) {
2902
2997
  return STOP_WALK;
2903
2998
  }
2904
2999
  return typeof cur.key === "string" ? cur.key : void 0;
@@ -2914,7 +3009,7 @@ function walkFiber(fiber, parentId, sharedNameCountMap, depth = 0, inSuspenseFal
2914
3009
  while (current) {
2915
3010
  try {
2916
3011
  const tag = current.tag;
2917
- if (isUserComponent(current)) {
3012
+ if (isLikelyUserComponent(current)) {
2918
3013
  const name = getComponentName2(current);
2919
3014
  const nameCount = nameCountMap.get(name) || 0;
2920
3015
  nameCountMap.set(name, nameCount + 1);
@@ -2944,6 +3039,8 @@ function walkFiber(fiber, parentId, sharedNameCountMap, depth = 0, inSuspenseFal
2944
3039
  libraryName !== void 0,
2945
3040
  jsxSource
2946
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;
2947
3044
  const node = {
2948
3045
  id: nodeId,
2949
3046
  name,
@@ -2952,8 +3049,8 @@ function walkFiber(fiber, parentId, sharedNameCountMap, depth = 0, inSuspenseFal
2952
3049
  renderPhase,
2953
3050
  renderReason,
2954
3051
  renderDuration: current.actualDuration,
2955
- filePath: jsxSource?.fileName ?? current._debugSource?.fileName,
2956
- lineNumber: jsxSource?.lineNumber ?? current._debugSource?.lineNumber,
3052
+ filePath: jsxSource?.fileName ?? current._debugSource?.fileName ?? stackLocation?.fileName,
3053
+ lineNumber: jsxSource?.lineNumber ?? current._debugSource?.lineNumber ?? stackLocation?.lineNumber,
2957
3054
  isFramework: framework,
2958
3055
  reactKey: resolveEffectiveReactKey(current),
2959
3056
  queryHashes,
@@ -4789,6 +4886,7 @@ function detectWebFramework() {
4789
4886
  isJsxRuntimeActive,
4790
4887
  isReduxStore,
4791
4888
  isTanStackQueryClient,
4889
+ isUserComponent,
4792
4890
  logTreeSnapshot,
4793
4891
  logTreeSummary,
4794
4892
  markJsxRuntimeActive,
package/dist/index.mjs CHANGED
@@ -8,12 +8,14 @@ import {
8
8
  getCallSiteRenderRate,
9
9
  getCallSiteRenders,
10
10
  isJsxRuntimeActive,
11
+ isUserComponent,
11
12
  markJsxRuntimeActive,
12
13
  normalizeJsxSourcePath,
14
+ parseFirstNonReactFrame,
13
15
  readJsxSourceFromFiber,
14
16
  recordCallSiteRender,
15
17
  setDuplicateKeyEmitter
16
- } from "./chunk-QLOJU5F2.mjs";
18
+ } from "./chunk-5LSFLPGP.mjs";
17
19
 
18
20
  // src/types.ts
19
21
  var DEFAULT_CONFIG = {
@@ -1510,7 +1512,11 @@ function runAnalysis(tree, fiberRefMap2) {
1510
1512
  }
1511
1513
  }
1512
1514
  for (const sourceId of sourceNodeIds) {
1513
- let dfs2 = function(currentId, currentPropKey, currentPath, visited) {
1515
+ const firstEdge = outEdges.get(sourceId)?.[0];
1516
+ if (!firstEdge) continue;
1517
+ const sourcePropName = firstEdge.propKey;
1518
+ const allPaths = [];
1519
+ const dfs = (currentId, currentPropKey, currentPath, visited) => {
1514
1520
  if (visited.has(currentId)) return;
1515
1521
  visited.add(currentId);
1516
1522
  const outgoing = outEdges.get(currentId);
@@ -1523,7 +1529,7 @@ function runAnalysis(tree, fiberRefMap2) {
1523
1529
  }
1524
1530
  for (const edge of outgoing) {
1525
1531
  const isRename = edge.propKey !== edge.childPropKey;
1526
- dfs2(
1532
+ dfs(
1527
1533
  edge.childNodeId,
1528
1534
  edge.childPropKey,
1529
1535
  [...currentPath, { nodeId: edge.childNodeId, propKey: edge.childPropKey, isRename }],
@@ -1532,12 +1538,7 @@ function runAnalysis(tree, fiberRefMap2) {
1532
1538
  }
1533
1539
  visited.delete(currentId);
1534
1540
  };
1535
- var dfs = dfs2;
1536
- const firstEdge = outEdges.get(sourceId)?.[0];
1537
- if (!firstEdge) continue;
1538
- const sourcePropName = firstEdge.propKey;
1539
- const allPaths = [];
1540
- dfs2(
1541
+ dfs(
1541
1542
  sourceId,
1542
1543
  sourcePropName,
1543
1544
  [{ nodeId: sourceId, propKey: sourcePropName, isRename: false }],
@@ -2367,7 +2368,7 @@ function detectTransitionPending(fiber) {
2367
2368
  }
2368
2369
  return false;
2369
2370
  }
2370
- var MAX_TREE_DEPTH = 100;
2371
+ var MAX_TREE_DEPTH = 3e3;
2371
2372
  var MAX_CHILDREN_PER_NODE = 300;
2372
2373
  var throttleTimer = null;
2373
2374
  var maxWaitTimer = null;
@@ -2427,7 +2428,7 @@ function getComponentName2(fiber) {
2427
2428
  }
2428
2429
  return "Unknown";
2429
2430
  }
2430
- function isUserComponent(fiber) {
2431
+ function isLikelyUserComponent(fiber) {
2431
2432
  if (!USER_COMPONENT_TAGS.has(fiber.tag)) return false;
2432
2433
  const name = getComponentName2(fiber);
2433
2434
  if (name === "Anonymous" || name === "Unknown" || name === "ForwardRef" || name === "Memo")
@@ -2531,17 +2532,28 @@ var FRAMEWORK_PATH_PATTERNS = [
2531
2532
  /react-hook-form/,
2532
2533
  /formik/
2533
2534
  ];
2534
- function resolveEffectiveSourcePath(fiber) {
2535
+ function resolveEffectiveSourceLocation(fiber) {
2535
2536
  const jsxSrc = readJsxSourceFromFiber(fiber);
2536
- if (jsxSrc) return jsxSrc.fileName;
2537
- if (fiber._debugSource?.fileName) return fiber._debugSource.fileName;
2537
+ if (jsxSrc) {
2538
+ return {
2539
+ fileName: jsxSrc.fileName,
2540
+ lineNumber: jsxSrc.lineNumber,
2541
+ columnNumber: jsxSrc.columnNumber
2542
+ };
2543
+ }
2544
+ if (fiber._debugSource?.fileName) {
2545
+ return {
2546
+ fileName: fiber._debugSource.fileName,
2547
+ lineNumber: fiber._debugSource.lineNumber
2548
+ };
2549
+ }
2538
2550
  const ownerHit = walkAncestors(
2539
2551
  fiber._debugOwner ?? null,
2540
2552
  3,
2541
2553
  (f) => f._debugOwner ?? null,
2542
2554
  (cur) => cur._debugSource?.fileName ?? void 0
2543
2555
  );
2544
- if (ownerHit) return ownerHit;
2556
+ if (ownerHit) return { fileName: ownerHit };
2545
2557
  const stack = fiber._debugStack?.stack;
2546
2558
  if (typeof stack === "string") {
2547
2559
  const parsed = parseFirstNonReactFrame(stack);
@@ -2549,7 +2561,11 @@ function resolveEffectiveSourcePath(fiber) {
2549
2561
  }
2550
2562
  return null;
2551
2563
  }
2564
+ function resolveEffectiveSourcePath(fiber) {
2565
+ return resolveEffectiveSourceLocation(fiber)?.fileName ?? null;
2566
+ }
2552
2567
  function resolveSourceConfidence(fiber, isFramework, isLibrary, precomputedJsxSource) {
2568
+ if (isUserComponent(fiber)) return "exact";
2553
2569
  if (isFramework || isLibrary) return "package";
2554
2570
  const jsxSrc = precomputedJsxSource ?? readJsxSourceFromFiber(fiber);
2555
2571
  if (jsxSrc) return "exact";
@@ -2567,22 +2583,8 @@ function walkAncestors(start, maxHops, next, visit) {
2567
2583
  }
2568
2584
  return void 0;
2569
2585
  }
2570
- function parseFirstNonReactFrame(stack) {
2571
- const lines = stack.split("\n");
2572
- for (const line of lines) {
2573
- const parened = line.match(/\(([^)]+):\d+:\d+\)/);
2574
- const hermes = line.match(/@([^\s]+):\d+:\d+$/);
2575
- const path = parened?.[1] ?? hermes?.[1];
2576
- if (!path) continue;
2577
- if (path.includes("react-dom")) continue;
2578
- if (path.includes("react-native/Libraries")) continue;
2579
- if (path.includes("/react/cjs/")) continue;
2580
- if (path.includes("/scheduler/")) continue;
2581
- return path;
2582
- }
2583
- return null;
2584
- }
2585
2586
  function isFrameworkComponent(fiber, name) {
2587
+ if (isUserComponent(fiber)) return false;
2586
2588
  if (walkerFilterConfig.frameworkNames.has(name)) return true;
2587
2589
  for (const pattern of walkerFilterConfig.frameworkNamePatterns) {
2588
2590
  if (pattern.test(name)) return true;
@@ -2622,6 +2624,7 @@ var KNOWN_LIBRARY_NAMES = /* @__PURE__ */ new Map([
2622
2624
  ["HeroIcon", "heroicons"]
2623
2625
  ]);
2624
2626
  function detectLibraryName(fiber, name) {
2627
+ if (isUserComponent(fiber)) return void 0;
2625
2628
  if (name.includes(".")) {
2626
2629
  return name.split(".")[0].toLowerCase();
2627
2630
  }
@@ -2692,7 +2695,7 @@ function resolveEffectiveReactKey(fiber) {
2692
2695
  6,
2693
2696
  (f) => f.return ?? null,
2694
2697
  (cur) => {
2695
- if (isUserComponent(cur) && !isFrameworkComponent(cur, getComponentName2(cur))) {
2698
+ if (isLikelyUserComponent(cur) && !isFrameworkComponent(cur, getComponentName2(cur))) {
2696
2699
  return STOP_WALK;
2697
2700
  }
2698
2701
  return typeof cur.key === "string" ? cur.key : void 0;
@@ -2708,7 +2711,7 @@ function walkFiber(fiber, parentId, sharedNameCountMap, depth = 0, inSuspenseFal
2708
2711
  while (current) {
2709
2712
  try {
2710
2713
  const tag = current.tag;
2711
- if (isUserComponent(current)) {
2714
+ if (isLikelyUserComponent(current)) {
2712
2715
  const name = getComponentName2(current);
2713
2716
  const nameCount = nameCountMap.get(name) || 0;
2714
2717
  nameCountMap.set(name, nameCount + 1);
@@ -2738,6 +2741,8 @@ function walkFiber(fiber, parentId, sharedNameCountMap, depth = 0, inSuspenseFal
2738
2741
  libraryName !== void 0,
2739
2742
  jsxSource
2740
2743
  );
2744
+ const needsStackFallback = (jsxSource?.fileName ?? current._debugSource?.fileName) === void 0 || (jsxSource?.lineNumber ?? current._debugSource?.lineNumber) === void 0;
2745
+ const stackLocation = needsStackFallback ? resolveEffectiveSourceLocation(current) : null;
2741
2746
  const node = {
2742
2747
  id: nodeId,
2743
2748
  name,
@@ -2746,8 +2751,8 @@ function walkFiber(fiber, parentId, sharedNameCountMap, depth = 0, inSuspenseFal
2746
2751
  renderPhase,
2747
2752
  renderReason,
2748
2753
  renderDuration: current.actualDuration,
2749
- filePath: jsxSource?.fileName ?? current._debugSource?.fileName,
2750
- lineNumber: jsxSource?.lineNumber ?? current._debugSource?.lineNumber,
2754
+ filePath: jsxSource?.fileName ?? current._debugSource?.fileName ?? stackLocation?.fileName,
2755
+ lineNumber: jsxSource?.lineNumber ?? current._debugSource?.lineNumber ?? stackLocation?.lineNumber,
2751
2756
  isFramework: framework,
2752
2757
  reactKey: resolveEffectiveReactKey(current),
2753
2758
  queryHashes,
@@ -4582,6 +4587,7 @@ export {
4582
4587
  isJsxRuntimeActive,
4583
4588
  isReduxStore,
4584
4589
  isTanStackQueryClient,
4590
+ isUserComponent,
4585
4591
  logTreeSnapshot,
4586
4592
  logTreeSummary,
4587
4593
  markJsxRuntimeActive,
@@ -6,7 +6,7 @@ import {
6
6
  normalizeJsxSourcePath,
7
7
  recordCallSiteRender,
8
8
  recordJsxKey
9
- } from "./chunk-QLOJU5F2.mjs";
9
+ } from "./chunk-5LSFLPGP.mjs";
10
10
 
11
11
  // src/jsx-dev-runtime.ts
12
12
  import { jsxDEV as origJsxDEV, Fragment } from "react/jsx-dev-runtime";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flotrace/runtime-core",
3
- "version": "2.3.0",
3
+ "version": "2.3.3",
4
4
  "description": "Platform-agnostic core for FloTrace runtime — fiber walker, analyzers, trackers. Shared by @flotrace/runtime (web) and @flotrace/runtime-native (React Native).",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -20,11 +20,17 @@
20
20
  "types": "./dist/jsx-dev-runtime.d.ts",
21
21
  "import": "./dist/jsx-dev-runtime.mjs",
22
22
  "require": "./dist/jsx-dev-runtime.js"
23
+ },
24
+ "./babel-plugin": {
25
+ "types": "./babel-plugin.d.ts",
26
+ "default": "./babel-plugin.js"
23
27
  }
24
28
  },
25
29
  "sideEffects": false,
26
30
  "files": [
27
31
  "dist",
32
+ "babel-plugin.js",
33
+ "babel-plugin.d.ts",
28
34
  "LICENSE",
29
35
  "README.md"
30
36
  ],