@flotrace/runtime-core 2.3.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/babel-plugin.d.ts +36 -0
- package/babel-plugin.js +302 -0
- package/dist/{chunk-QLOJU5F2.mjs → chunk-5LSFLPGP.mjs} +102 -7
- package/dist/index.d.mts +59 -1
- package/dist/index.d.ts +59 -1
- package/dist/index.js +137 -39
- package/dist/index.mjs +39 -33
- package/dist/jsx-dev-runtime.mjs +1 -1
- package/package.json +7 -1
|
@@ -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;
|
package/babel-plugin.js
ADDED
|
@@ -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 (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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 (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 }],
|
|
@@ -2633,7 +2726,7 @@ function getComponentName2(fiber) {
|
|
|
2633
2726
|
}
|
|
2634
2727
|
return "Unknown";
|
|
2635
2728
|
}
|
|
2636
|
-
function
|
|
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
|
|
2833
|
+
function resolveEffectiveSourceLocation(fiber) {
|
|
2741
2834
|
const jsxSrc = readJsxSourceFromFiber(fiber);
|
|
2742
|
-
if (jsxSrc)
|
|
2743
|
-
|
|
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 (
|
|
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 (
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 }],
|
|
@@ -2427,7 +2428,7 @@ function getComponentName2(fiber) {
|
|
|
2427
2428
|
}
|
|
2428
2429
|
return "Unknown";
|
|
2429
2430
|
}
|
|
2430
|
-
function
|
|
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
|
|
2535
|
+
function resolveEffectiveSourceLocation(fiber) {
|
|
2535
2536
|
const jsxSrc = readJsxSourceFromFiber(fiber);
|
|
2536
|
-
if (jsxSrc)
|
|
2537
|
-
|
|
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 (
|
|
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 (
|
|
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,
|
package/dist/jsx-dev-runtime.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flotrace/runtime-core",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1",
|
|
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
|
],
|