@flotrace/runtime-core 2.2.4 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -0
- package/babel-plugin.d.ts +36 -0
- package/babel-plugin.js +302 -0
- package/dist/chunk-5LSFLPGP.mjs +276 -0
- package/dist/index.d.mts +435 -17
- package/dist/index.d.ts +435 -17
- package/dist/index.js +801 -120
- package/dist/index.mjs +585 -120
- package/dist/jsx-dev-runtime.d.mts +64 -0
- package/dist/jsx-dev-runtime.d.ts +64 -0
- package/dist/jsx-dev-runtime.js +179 -0
- package/dist/jsx-dev-runtime.mjs +46 -0
- package/dist/jsx-runtime.d.mts +1 -0
- package/dist/jsx-runtime.d.ts +1 -0
- package/dist/jsx-runtime.js +34 -0
- package/dist/jsx-runtime.mjs +7 -0
- package/package.json +18 -1
package/README.md
CHANGED
|
@@ -50,6 +50,100 @@ your React app ←→ @flotrace/runtime[-native] ←→ ws://localhost:3457
|
|
|
50
50
|
| `serializer` | Safe JSON serialization (depth 5, circular-ref guard, truncation). |
|
|
51
51
|
| `websocketClient` | Singleton WS client with exponential backoff reconnect, message batching, optional auth token. |
|
|
52
52
|
|
|
53
|
+
## Optional: JSX runtime opt-in (source attribution upgrade)
|
|
54
|
+
|
|
55
|
+
`runtime-core` ships two additional subpath entries — `./jsx-runtime` and `./jsx-dev-runtime` — that you can opt into via a one-line `tsconfig.json` change. Doing so lets FloTrace attribute every JSX call site to its exact `file:line:column` with 100% confidence, even on stacks where React no longer carries that signal (Next.js 15 + SWC, React 19 with `_debugSource` removed).
|
|
56
|
+
|
|
57
|
+
**The opt-in is free, additive, and reverts cleanly** — your app continues to work in production unchanged, and the existing heuristic ladder still runs when the opt-in is off.
|
|
58
|
+
|
|
59
|
+
### Setup
|
|
60
|
+
|
|
61
|
+
```jsonc
|
|
62
|
+
// tsconfig.json
|
|
63
|
+
{
|
|
64
|
+
"compilerOptions": {
|
|
65
|
+
"jsx": "react-jsx",
|
|
66
|
+
"jsxImportSource": "@flotrace/runtime-core"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Restart your dev server. That's it.
|
|
72
|
+
|
|
73
|
+
### What you get
|
|
74
|
+
|
|
75
|
+
- **Click any node in FloTrace → IDE jumps to the exact JSX line** (column included).
|
|
76
|
+
- **Per-call-site render metrics** instead of per-component-type. `<Button/>` at `Header.tsx:23` and `<Button/>` at `Footer.tsx:7` are now separate rows in the Hot Call Sites tab — only the actually-hot one is flagged.
|
|
77
|
+
- **Inline-literal detection** — the runtime sees props *before* React processes them and tags fresh-each-render `onClick={() => ...}`, `style={{}}`, `items={[...]}` at the call site that created them. This signal is impossible to recover after React commits.
|
|
78
|
+
- **Conditional-render visibility** — a callsite nested in `{cond && <X/>}` gets a `~N%` chip showing how often it actually renders.
|
|
79
|
+
- **Duplicate-key warnings** with exact source location instead of grep-the-codebase.
|
|
80
|
+
- **Privacy-safe Copy-as-Prompt** — every component reference cites `(src/components/Header.tsx:42)`, project-relative, never `/Users/foo/...`.
|
|
81
|
+
- **Watches, Resolution Tracker, Value Lineage, Cascade, Prop Drilling** all gain `(file:line)` annotations and HMR-stable call-site identity.
|
|
82
|
+
|
|
83
|
+
### Bundler matrix
|
|
84
|
+
|
|
85
|
+
| Bundler | Zero-config? | Notes |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| **Vite (+ SWC or Babel)** | ✅ | Honors `jsxImportSource` from tsconfig out of the box. |
|
|
88
|
+
| **Next.js 15 (Webpack)** | ✅ | SWC reads tsconfig. |
|
|
89
|
+
| **Next.js 15 (Turbopack)** | ✅ | Same. |
|
|
90
|
+
| **Remix / Vinxi** | ✅ | Same. |
|
|
91
|
+
| **Expo SDK 50+ (Metro + Babel)** | ✅ | Metro's Babel preset honors `jsxImportSource`. |
|
|
92
|
+
| **Bun** | ✅ | Built-in JSX transformer reads tsconfig. |
|
|
93
|
+
| **Create React App** | ⚠ Needs Babel snippet | CRA's locked Babel config doesn't honor `jsxImportSource` from tsconfig. Use CRACO (or `react-app-rewired`) to inject a Babel preset override. See snippet below. |
|
|
94
|
+
|
|
95
|
+
For CRA via CRACO, add to `craco.config.js`:
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
module.exports = {
|
|
99
|
+
babel: {
|
|
100
|
+
presets: [
|
|
101
|
+
['@babel/preset-react', {
|
|
102
|
+
runtime: 'automatic',
|
|
103
|
+
importSource: '@flotrace/runtime-core',
|
|
104
|
+
development: true, // emits jsxDEV instead of jsx in dev builds
|
|
105
|
+
}],
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### How to verify it's working
|
|
112
|
+
|
|
113
|
+
1. **Run your app in dev mode**, open browser DevTools → Console.
|
|
114
|
+
2. Paste: `globalThis[Symbol.for('flotrace.jsx-runtime-active')]` → should return `true`.
|
|
115
|
+
3. **Open React DevTools → Components tab**, select any user component, then click the wrench icon to view its props in the Console. Run:
|
|
116
|
+
```js
|
|
117
|
+
$r.memoizedProps[Symbol.for('flotrace.source')]
|
|
118
|
+
```
|
|
119
|
+
You should see `{ fileName, lineNumber, columnNumber, callSiteId }`.
|
|
120
|
+
|
|
121
|
+
If step 2 returns `undefined`, your bundler isn't picking up the tsconfig setting — see the bundler matrix above. The connection-status tooltip inside the FloTrace desktop app also shows **"JSX runtime: active ✓"** once the opt-in is wired up correctly.
|
|
122
|
+
|
|
123
|
+
If step 2 returns `true` but step 3 returns `undefined` on a specific component, that fiber was created via a path that bypasses `jsxDEV` (e.g. `React.createElement` direct call inside a vendored dependency, or a server-rendered component hydrated client-side). User-authored Client Components in `.tsx` / `.jsx` files always pass through.
|
|
124
|
+
|
|
125
|
+
### Performance budget
|
|
126
|
+
|
|
127
|
+
| Cost | Target | Notes |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| Per-`jsxDEV` call overhead | **<10µs median** | Microbenchmark on V8. Path normalization + hash + inline detection + symbol-prop allocation. |
|
|
130
|
+
| Per-commit cumulative overhead | **<55ms** | For a 5000-element user-component tree. Well below React's own commit cost. |
|
|
131
|
+
| Symbol-prop memory | ~200 bytes per user-component fiber | ~1 MB for 5000 fibers. GC'd with the fiber on unmount. |
|
|
132
|
+
| Ring buffer memory | 60 timestamps × callSiteId count | ~2.4 MB worst case for 5000 distinct callsites. Cleared on walker uninstall. |
|
|
133
|
+
|
|
134
|
+
### Privacy
|
|
135
|
+
|
|
136
|
+
The runtime captures **absolute paths** (needed so click-to-IDE can resolve files reliably across symlinks and workspaces). Absolute paths NEVER leave your machine via:
|
|
137
|
+
|
|
138
|
+
- **WebSocket** → bound to `127.0.0.1` only; the desktop app is also local.
|
|
139
|
+
- **Copy-as-Prompt** → the prompt builder calls `relativizePath(absPath, projectRoot)` before serialization. Output cites `src/Header.tsx:42`, never `/Users/foo/proj/src/Header.tsx:42`.
|
|
140
|
+
|
|
141
|
+
The opt-in is **dev-only** by design. The production entry (`@flotrace/runtime-core/jsx-runtime`) is a pure passthrough to `react/jsx-runtime` — zero runtime overhead, no symbol injection, no metadata captured.
|
|
142
|
+
|
|
143
|
+
### Reverting
|
|
144
|
+
|
|
145
|
+
Delete the `jsxImportSource` line from `tsconfig.json` and restart your dev server. FloTrace falls back to its existing heuristic ladder (`_debugSource` → `_debugOwner` → `_debugStack`). No code changes needed in your app.
|
|
146
|
+
|
|
53
147
|
## Version compatibility
|
|
54
148
|
|
|
55
149
|
`@flotrace/runtime-core@2.x` is the companion release for:
|
|
@@ -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;
|