@keyframerdev/vitals 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Catalin Miron
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,97 @@
1
- # Temporary Holding Version
1
+ # @keyframerdev/vitals
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Motion Vitals for React Native: slow frames and janky animations from production, by screen and by
4
+ the line of code that started the animation. Reports go to your project on
5
+ [keyframer.dev](https://keyframer.dev/inspector/docs/vitals).
6
+
7
+ - Safe for store builds: it never connects to Metro or a relay, never changes an animation, and does
8
+ no work while nothing moves.
9
+ - JavaScript only: no native module and no config plugin. Works in Expo apps, Expo Go included.
10
+ - Never sends screenshots, text, values, touches, identifiers or absolute times.
11
+
12
+ Needs Reanimated 4.1 or later with `react-native-worklets` 0.5 or later (React Native 0.78 or later, New
13
+ Architecture).
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ npm i @keyframerdev/vitals
19
+ ```
20
+
21
+ A regular dependency, not a dev dependency: it ships in your release build.
22
+
23
+ ## Start it
24
+
25
+ At the app's entry, with the project's field key (`kfp_live_…`, public and write-only, safe to ship):
26
+
27
+ ```ts
28
+ import { startVitals } from '@keyframerdev/vitals'
29
+ import AsyncStorage from '@react-native-async-storage/async-storage'
30
+ import * as Application from 'expo-application'
31
+
32
+ startVitals({
33
+ key: 'kfp_live_…',
34
+ appVersion: Application.nativeApplicationVersion!,
35
+ build: Application.nativeBuildVersion ?? undefined,
36
+ navigationRef, // React Navigation's createNavigationContainerRef() or expo-router's useNavigationContainerRef()
37
+ storage: AsyncStorage, // optional: checkpoints, retries and the daily cap survive restarts
38
+ })
39
+ ```
40
+
41
+ Nothing is measured or sent before `startVitals`. If your app asks for consent, call it after.
42
+
43
+ | Option | Default | What it does |
44
+ | --- | --- | --- |
45
+ | `key` | required | The project's field key |
46
+ | `appVersion` | required | The version people see (`2.4.1`) |
47
+ | `build` | none | The build number (`812`) |
48
+ | `environment` | `'development'` when `__DEV__`, else `'production'` | Development reports are kept only when the project accepts development builds |
49
+ | `sampleRate` | `0.1` | The share of sessions that report, until the project's settings say otherwise |
50
+ | `navigationRef` | none | Screens are named from its routes |
51
+ | `screenName` | the route's name | `(route) => string`, to name routes yourself (still masked) |
52
+ | `storage` | none | Anything with `getItem`, `setItem` and `removeItem` (AsyncStorage, an MMKV wrapper) |
53
+ | `device` | none | `{ model, memoryBytes, memoryGb, tier, displayHz }`, read by the app (for example from `expo-device`) |
54
+ | `appId` | none | The bundle id or package name |
55
+ | `endpoint` | `https://vitals.keyframer.dev` | The ingest server |
56
+
57
+ ## Call sites with file and line
58
+
59
+ Add the Babel plugin, beside Reanimated's (react-native-worklets') plugin, in any order:
60
+
61
+ ```js
62
+ // babel.config.js
63
+ module.exports = function (api) {
64
+ api.cache(true)
65
+ return {
66
+ presets: ['babel-preset-expo'],
67
+ plugins: ['@keyframerdev/vitals/babel'],
68
+ }
69
+ }
70
+ ```
71
+
72
+ It tags each Reanimated animation, layout animation and animated scroll handler with where it is
73
+ written, so the dashboard shows `app/feed.tsx:42:9 withSpring`, not only the screen. Without it, frames
74
+ are still counted per screen. Restart Metro with a clean cache after changing Babel:
75
+ `npx expo start -c`.
76
+
77
+ To keep file paths out of the bundle, pass `{ hashSites: true, salt: 'kfp_live_…' }` and upload the
78
+ sites file the build writes with
79
+ [`@keyframerdev/vitals-cli`](https://www.npmjs.com/package/@keyframerdev/vitals-cli).
80
+
81
+ ## More
82
+
83
+ ```ts
84
+ import { setVitalsEnabled, setVitalsScreen, VitalsRoot } from '@keyframerdev/vitals'
85
+
86
+ setVitalsScreen('Checkout') // where navigation is not automatic
87
+ setVitalsEnabled(false) // stops now and drops what is pending; true starts again
88
+ <VitalsRoot>{app}</VitalsRoot> // optional: touches open motion windows, so plain lists are measured while they scroll
89
+ ```
90
+
91
+ Docs: [React Native setup](https://keyframer.dev/inspector/docs/vitals/react-native),
92
+ [consent and privacy](https://keyframer.dev/inspector/docs/vitals/consent),
93
+ [store privacy answers](https://keyframer.dev/inspector/docs/vitals/store-privacy).
94
+
95
+ ## License
96
+
97
+ MIT
package/dist/babel.cjs ADDED
@@ -0,0 +1,451 @@
1
+ var __create = Object.create;
2
+ var __getProtoOf = Object.getPrototypeOf;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ function __accessProp(key) {
8
+ return this[key];
9
+ }
10
+ var __toESMCache_node;
11
+ var __toESMCache_esm;
12
+ var __toESM = (mod, isNodeMode, target) => {
13
+ var canCache = mod != null && typeof mod === "object";
14
+ if (canCache) {
15
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
16
+ var cached = cache.get(mod);
17
+ if (cached)
18
+ return cached;
19
+ }
20
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
21
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
22
+ for (let key of __getOwnPropNames(mod))
23
+ if (!__hasOwnProp.call(to, key))
24
+ __defProp(to, key, {
25
+ get: __accessProp.bind(mod, key),
26
+ enumerable: true
27
+ });
28
+ if (canCache)
29
+ cache.set(mod, to);
30
+ return to;
31
+ };
32
+ var __toCommonJS = (from) => {
33
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
34
+ if (entry)
35
+ return entry;
36
+ entry = __defProp({}, "__esModule", { value: true });
37
+ if (from && typeof from === "object" || typeof from === "function") {
38
+ for (var key of __getOwnPropNames(from))
39
+ if (!__hasOwnProp.call(entry, key))
40
+ __defProp(entry, key, {
41
+ get: __accessProp.bind(from, key),
42
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
43
+ });
44
+ }
45
+ __moduleCache.set(from, entry);
46
+ return entry;
47
+ };
48
+ var __moduleCache;
49
+ var __returnValue = (v) => v;
50
+ function __exportSetter(name, newValue) {
51
+ this[name] = __returnValue.bind(null, newValue);
52
+ }
53
+ var __export = (target, all) => {
54
+ for (var name in all)
55
+ __defProp(target, name, {
56
+ get: all[name],
57
+ enumerable: true,
58
+ configurable: true,
59
+ set: __exportSetter.bind(all, name)
60
+ });
61
+ };
62
+
63
+ // src/babel.ts
64
+ var exports_babel = {};
65
+ __export(exports_babel, {
66
+ writeManifest: () => writeManifest,
67
+ manifestPath: () => manifestPath,
68
+ default: () => keyframerVitals,
69
+ appIdentity: () => appIdentity,
70
+ RUNTIME_MODULE: () => RUNTIME_MODULE,
71
+ PLUGIN_NAME: () => PLUGIN_NAME,
72
+ ANIMATION_FNS: () => ANIMATION_FNS
73
+ });
74
+ module.exports = __toCommonJS(exports_babel);
75
+ var import_node_crypto = require("node:crypto");
76
+ var import_node_fs = require("node:fs");
77
+ var import_node_path = __toESM(require("node:path"));
78
+ var PLUGIN_NAME = "keyframer-vitals";
79
+ var RUNTIME_MODULE = "@keyframerdev/vitals/runtime";
80
+ var INSPECTOR_PLUGIN = "keyframer-inspector";
81
+ var INSPECTOR_RUNTIME = "@keyframerdev/inspector/runtime";
82
+ var REANIMATED = "react-native-reanimated";
83
+ var ANIMATION_FNS = new Set(["withTiming", "withSpring", "withDecay", "withRepeat", "withSequence", "withDelay", "withClamp"]);
84
+ var LAYOUT_PROPS = ["entering", "exiting", "layout"];
85
+ var SCROLL_EVENTS = new Set(["onScroll", "onBeginDrag", "onEndDrag", "onMomentumBegin", "onMomentumEnd"]);
86
+ function keyName(key) {
87
+ if (key.type === "Identifier")
88
+ return key.name;
89
+ if (key.type === "StringLiteral")
90
+ return key.value;
91
+ return null;
92
+ }
93
+ function relativeFile(state) {
94
+ const file = state.filename ?? "unknown";
95
+ const root = state.opts.root ?? state.cwd ?? "";
96
+ const rel = root && file.startsWith(root) ? file.slice(root.length).replace(/^[/\\]+/, "") : file;
97
+ return rel.replace(/\\/g, "/");
98
+ }
99
+ function enclosingName(path2) {
100
+ let fallback = null;
101
+ for (let p = path2.parentPath;p; p = p.parentPath) {
102
+ let name = null;
103
+ if (p.isFunctionDeclaration() && p.node.id)
104
+ name = p.node.id.name;
105
+ else if ((p.isArrowFunctionExpression() || p.isFunctionExpression()) && p.parentPath?.isVariableDeclarator()) {
106
+ const id = p.parentPath.node.id;
107
+ if (id.type === "Identifier")
108
+ name = id.name;
109
+ }
110
+ if (!name)
111
+ continue;
112
+ if (/^[A-Z]/.test(name) || /^use[A-Z]/.test(name))
113
+ return name;
114
+ fallback ??= name;
115
+ }
116
+ return fallback;
117
+ }
118
+ var NESTED_STYLE_KEYS = new Set(["shadowOffset", "textShadowOffset"]);
119
+ function nameOfObject(obj) {
120
+ return obj.type === "Identifier" ? obj.name : obj.type === "MemberExpression" && !obj.computed ? keyName(obj.property) : null;
121
+ }
122
+ function propOf(path2) {
123
+ let prop = null;
124
+ let child = path2;
125
+ for (let p = path2.parentPath;p; child = p, p = p.parentPath) {
126
+ if (p.isFunction() || p.isStatement())
127
+ break;
128
+ if (p.isObjectProperty() && prop === null)
129
+ prop = keyName(p.node.key);
130
+ else if (p.isObjectProperty() && NESTED_STYLE_KEYS.has(keyName(p.node.key) ?? ""))
131
+ prop = keyName(p.node.key);
132
+ if (p.isCallExpression() && p.node.arguments[0] === child.node) {
133
+ const callee = p.node.callee;
134
+ if (callee.type === "MemberExpression" && !callee.computed && keyName(callee.property) === "set")
135
+ return prop ?? nameOfObject(callee.object);
136
+ }
137
+ if (p.isAssignmentExpression()) {
138
+ const left = p.node.left;
139
+ if (left.type === "MemberExpression" && keyName(left.property) === "value" && !left.computed)
140
+ return prop ?? nameOfObject(left.object);
141
+ break;
142
+ }
143
+ }
144
+ return prop;
145
+ }
146
+ var hex12 = (s) => import_node_crypto.createHash("sha256").update(s).digest("hex").slice(0, 12);
147
+ function readJson(file) {
148
+ try {
149
+ return JSON.parse(import_node_fs.readFileSync(file, "utf8"));
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+ function appIdentity(root, opts) {
155
+ const appJson = readJson(import_node_path.default.join(root, "app.json"));
156
+ const expo = appJson?.expo ?? appJson;
157
+ const pkg = readJson(import_node_path.default.join(root, "package.json"));
158
+ const version = opts.appVersion ?? process.env.KEYFRAMER_APP_VERSION ?? expo?.version ?? pkg?.version ?? "0.0.0";
159
+ const build = opts.build ?? process.env.KEYFRAMER_APP_BUILD ?? expo?.ios?.buildNumber ?? (expo?.android?.versionCode !== undefined ? String(expo.android.versionCode) : undefined);
160
+ return build ? { version, build } : { version };
161
+ }
162
+ function manifestPath(root, opts) {
163
+ const app = appIdentity(root, opts);
164
+ const name = app.build ? `${app.version}+${app.build}` : app.version;
165
+ return import_node_path.default.join(opts.manifestDir ?? import_node_path.default.join(root, ".keyframer"), `sites-${name.replace(/[^\w.+-]/g, "_")}.json`);
166
+ }
167
+ var sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
168
+ function writeManifest(file, source, sites, app) {
169
+ import_node_fs.mkdirSync(import_node_path.default.dirname(file), { recursive: true });
170
+ const lock = `${file}.lock`;
171
+ let fd = -1;
172
+ for (let i = 0;i < 400 && fd < 0; i++) {
173
+ try {
174
+ fd = import_node_fs.openSync(lock, "wx");
175
+ } catch {
176
+ try {
177
+ if (Date.now() - import_node_fs.statSync(lock).mtimeMs > 1e4)
178
+ import_node_fs.unlinkSync(lock);
179
+ } catch {}
180
+ sleep(5);
181
+ }
182
+ }
183
+ try {
184
+ const prev = readJson(file);
185
+ const merged = {};
186
+ for (const [hash, s] of Object.entries(prev?.sites ?? {}))
187
+ if (s.file !== source)
188
+ merged[hash] = s;
189
+ Object.assign(merged, sites);
190
+ const sorted = Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a < b ? -1 : 1));
191
+ const tmp = `${file}.${process.pid}.tmp`;
192
+ import_node_fs.writeFileSync(tmp, JSON.stringify({ format: "keyframer-vitals-sites", version: 1, app, sites: sorted }, null, 2) + `
193
+ `);
194
+ import_node_fs.renameSync(tmp, file);
195
+ } finally {
196
+ if (fd >= 0) {
197
+ import_node_fs.closeSync(fd);
198
+ try {
199
+ import_node_fs.unlinkSync(lock);
200
+ } catch {}
201
+ }
202
+ }
203
+ }
204
+ function keyframerVitals({ types: t }) {
205
+ const literal = (value) => t.valueToNode(value);
206
+ const markWorklet = (fn) => {
207
+ if (fn.type !== "ArrowFunctionExpression" && fn.type !== "FunctionExpression")
208
+ return;
209
+ if (fn.body.type !== "BlockStatement")
210
+ fn.body = t.blockStatement([t.returnStatement(fn.body)]);
211
+ if (!fn.body.directives.some((d) => d.value.value === "worklet"))
212
+ fn.body.directives.unshift(t.directive(t.directiveLiteral("worklet")));
213
+ };
214
+ const helper = (fs, path2, name) => {
215
+ let id = fs.helpers.get(name);
216
+ if (!id) {
217
+ id = path2.scope.getProgramParent().generateUidIdentifier(name.replace(/^__/, ""));
218
+ fs.helpers.set(name, id);
219
+ }
220
+ return t.cloneNode(id);
221
+ };
222
+ const isHelper = (fs, e, name) => e?.type === "CallExpression" && e.callee.type === "Identifier" && e.callee.name === fs.helpers.get(name)?.name;
223
+ const isModuleBinding = (path2, name) => path2.scope.getBinding(name)?.kind === "module";
224
+ const site = (state, fs, at, fn, component, prop) => {
225
+ const group = `${fs.file}#${component ?? ""}#${fn}#${prop ?? ""}`;
226
+ const n = (fs.ordinals.get(group) ?? 0) + 1;
227
+ fs.ordinals.set(group, n);
228
+ const key = `${group}#${n}`;
229
+ if (state.opts.hashSites) {
230
+ const salt = state.opts.salt ?? "";
231
+ const hash = hex12(`${salt}
232
+ ${fs.file}:${at.line}:${at.column}
233
+ ${key}`);
234
+ fs.sites[hash] = { file: fs.file, line: at.line, column: at.column, fn, component, prop, key };
235
+ return literal({ ref: { kind: "hashed", hash }, key: `k_${hex12(`${salt}
236
+ ${key}`)}`, fn, component: null, prop: null });
237
+ }
238
+ return literal({ ref: { kind: "source", file: fs.file, line: at.line, column: at.column }, key, fn, component, prop });
239
+ };
240
+ const inspectorPosition = (node) => {
241
+ if (!node || node.type !== "ObjectExpression")
242
+ return null;
243
+ let line = null;
244
+ let column = null;
245
+ for (const p of node.properties) {
246
+ if (p.type !== "ObjectProperty" || p.value.type !== "NumericLiteral")
247
+ continue;
248
+ if (keyName(p.key) === "line")
249
+ line = p.value.value;
250
+ if (keyName(p.key) === "column")
251
+ column = p.value.value;
252
+ }
253
+ return line !== null ? { line, column: column ?? 0 } : null;
254
+ };
255
+ const reportScroll = (fs, path2, fn) => {
256
+ if (!fn || fs.wrapped.has(fn))
257
+ return;
258
+ let body = null;
259
+ if (fn.type === "ArrowFunctionExpression" || fn.type === "FunctionExpression") {
260
+ markWorklet(fn);
261
+ body = fn.body;
262
+ } else if (fn.type === "ObjectMethod")
263
+ body = fn.body;
264
+ if (!body)
265
+ return;
266
+ fs.wrapped.add(fn);
267
+ body.body.unshift(t.expressionStatement(t.callExpression(helper(fs, path2, "__kfvScroll"), [])));
268
+ };
269
+ const nodes = {
270
+ CallExpression(path2, state) {
271
+ const { fs } = state;
272
+ const { node } = path2;
273
+ if (fs.wrapped.has(node) || node.callee.type !== "Identifier")
274
+ return;
275
+ const name = node.callee.name;
276
+ if (fs.scrollHookLocals.has(name) && isModuleBinding(path2, name)) {
277
+ const [first2] = node.arguments;
278
+ if (first2?.type === "ObjectExpression") {
279
+ for (const p of first2.properties) {
280
+ if (p.type === "ObjectProperty" && SCROLL_EVENTS.has(keyName(p.key) ?? ""))
281
+ reportScroll(fs, path2, p.value);
282
+ else if (p.type === "ObjectMethod" && SCROLL_EVENTS.has(keyName(p.key) ?? ""))
283
+ reportScroll(fs, path2, p);
284
+ }
285
+ } else
286
+ reportScroll(fs, path2, first2);
287
+ return;
288
+ }
289
+ const fn = fs.animLocals.get(name);
290
+ if (fn && isModuleBinding(path2, name) && node.loc) {
291
+ const at = { line: node.loc.start.line, column: node.loc.start.column };
292
+ node.arguments.forEach(markWorklet);
293
+ const call = t.callExpression(helper(fs, path2, "__kfvAnim"), [t.identifier(name), site(state, fs, at, fn, enclosingName(path2), propOf(path2)), ...node.arguments]);
294
+ fs.wrapped.add(call);
295
+ path2.replaceWith(call);
296
+ return;
297
+ }
298
+ const viaLocal = fs.inspector.get("__kfAnim");
299
+ const [first, isite, ...rest] = node.arguments;
300
+ if (viaLocal && name === viaLocal && first?.type === "Identifier" && fs.animLocals.has(first.name) && isModuleBinding(path2, first.name)) {
301
+ const at = inspectorPosition(isite) ?? (node.loc ? { line: node.loc.start.line, column: node.loc.start.column } : null);
302
+ if (!at || !isite)
303
+ return;
304
+ const call = t.callExpression(helper(fs, path2, "__kfvAnimVia"), [
305
+ t.identifier(name),
306
+ site(state, fs, at, fs.animLocals.get(first.name), enclosingName(path2), propOf(path2)),
307
+ first,
308
+ isite,
309
+ ...rest
310
+ ]);
311
+ fs.wrapped.add(call);
312
+ path2.replaceWith(call);
313
+ }
314
+ },
315
+ JSXOpeningElement(path2, state) {
316
+ const { fs } = state;
317
+ const el = path2.node;
318
+ const attr = (n) => el.attributes.find((a) => a.type === "JSXAttribute" && a.name.type === "JSXIdentifier" && a.name.name === n);
319
+ const value = (a) => a?.value?.type === "JSXExpressionContainer" && a.value.expression.type !== "JSXEmptyExpression" ? a.value.expression : null;
320
+ const setAttr = (n, expr) => {
321
+ const a = attr(n);
322
+ if (a) {
323
+ a.value = t.jsxExpressionContainer(expr);
324
+ return;
325
+ }
326
+ const added = t.jsxAttribute(t.jsxIdentifier(n), t.jsxExpressionContainer(expr));
327
+ const spread = el.attributes.findIndex((x) => x.type === "JSXSpreadAttribute");
328
+ if (spread >= 0)
329
+ el.attributes.splice(spread, 0, added);
330
+ else
331
+ el.attributes.push(added);
332
+ };
333
+ const loc = el.loc;
334
+ if (loc) {
335
+ const at = { line: loc.start.line, column: loc.start.column + 1 };
336
+ const component = enclosingName(path2);
337
+ const kinds = {};
338
+ for (const kind of LAYOUT_PROPS) {
339
+ const a = attr(kind);
340
+ const v2 = value(a);
341
+ if (!a || !v2 || isHelper(fs, v2, "__kfvLayout"))
342
+ continue;
343
+ const s = site(state, fs, at, kind, component, null);
344
+ kinds[kind] = s;
345
+ a.value = t.jsxExpressionContainer(t.callExpression(helper(fs, path2, "__kfvLayout"), [v2, t.stringLiteral(kind), s]));
346
+ }
347
+ if (kinds.entering || kinds.exiting) {
348
+ const prev = value(attr("ref"));
349
+ setAttr("ref", t.callExpression(helper(fs, path2, "__kfvMotionRef"), [kinds.entering ? t.cloneNode(kinds.entering) : t.nullLiteral(), kinds.exiting ? t.cloneNode(kinds.exiting) : t.nullLiteral(), ...prev ? [prev] : []]));
350
+ }
351
+ if (kinds.layout) {
352
+ const prev = value(attr("onLayout"));
353
+ setAttr("onLayout", t.callExpression(helper(fs, path2, "__kfvLayoutMoved"), [t.cloneNode(kinds.layout), ...prev ? [prev] : []]));
354
+ }
355
+ }
356
+ const onScroll = attr("onScroll");
357
+ const v = value(onScroll);
358
+ if (!onScroll || !v || isHelper(fs, v, "__kfvScrollJs"))
359
+ return;
360
+ if (v.type !== "Identifier" && v.type !== "MemberExpression" && v.type !== "ArrowFunctionExpression" && v.type !== "FunctionExpression" && v.type !== "CallExpression")
361
+ return;
362
+ if (v.type === "Identifier" && v.name === "undefined")
363
+ return;
364
+ const callOf = (e) => {
365
+ if (e.type !== "Identifier")
366
+ return e;
367
+ const decl = path2.scope.getBinding(e.name)?.path.node;
368
+ return decl?.type === "VariableDeclarator" ? decl.init : null;
369
+ };
370
+ const init = callOf(v);
371
+ if (init?.type === "CallExpression" && init.callee.type === "Identifier" && fs.scrollHookLocals.has(init.callee.name))
372
+ return;
373
+ onScroll.value = t.jsxExpressionContainer(t.callExpression(helper(fs, path2, "__kfvScrollJs"), [v]));
374
+ }
375
+ };
376
+ const run = (program, state) => {
377
+ if (state.done)
378
+ return;
379
+ state.done = true;
380
+ const fs = {
381
+ file: relativeFile(state),
382
+ animLocals: new Map,
383
+ scrollHookLocals: new Set,
384
+ inspector: new Map,
385
+ helpers: new Map,
386
+ ordinals: new Map,
387
+ wrapped: new WeakSet,
388
+ sites: {}
389
+ };
390
+ for (const stmt of program.node.body) {
391
+ if (stmt.type !== "ImportDeclaration" || stmt.importKind === "type")
392
+ continue;
393
+ for (const spec of stmt.specifiers) {
394
+ if (spec.type !== "ImportSpecifier")
395
+ continue;
396
+ const imported = spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value;
397
+ if (stmt.source.value === REANIMATED) {
398
+ if (ANIMATION_FNS.has(imported))
399
+ fs.animLocals.set(spec.local.name, imported);
400
+ if (imported === "useAnimatedScrollHandler")
401
+ fs.scrollHookLocals.add(spec.local.name);
402
+ } else if (stmt.source.value === INSPECTOR_RUNTIME)
403
+ fs.inspector.set(imported, spec.local.name);
404
+ }
405
+ }
406
+ if (!fs.animLocals.size && !fs.scrollHookLocals.size && !/\b(entering|exiting|layout|onScroll)\s*=/.test(state.file.code ?? ""))
407
+ return;
408
+ program.traverse(nodes, Object.assign(state, { fs }));
409
+ if (fs.helpers.size) {
410
+ const specifiers = [...fs.helpers].map(([exported, local]) => t.importSpecifier(local, t.identifier(exported)));
411
+ program.unshiftContainer("body", t.importDeclaration(specifiers, t.stringLiteral(RUNTIME_MODULE)));
412
+ program.scope.crawl();
413
+ }
414
+ if (state.opts.hashSites) {
415
+ const root = state.opts.root ?? state.cwd ?? process.cwd();
416
+ if (Object.keys(fs.sites).length || import_node_fs.existsSync(manifestPath(root, state.opts))) {
417
+ writeManifest(manifestPath(root, state.opts), fs.file, fs.sites, appIdentity(root, state.opts));
418
+ }
419
+ }
420
+ };
421
+ return {
422
+ name: PLUGIN_NAME,
423
+ visitor: {
424
+ Program: {
425
+ enter(program, state) {
426
+ const file = (state.filename ?? "").replace(/\\/g, "/");
427
+ state.skip = !state.opts.includeNodeModules && file.includes("/node_modules/") || /\/packages\/vitals\/(src|dist)\//.test(file);
428
+ state.done = false;
429
+ state.program = program;
430
+ if (state.skip) {
431
+ state.pending = false;
432
+ return;
433
+ }
434
+ const keys = (state.file.opts.plugins ?? []).map((p) => p.key);
435
+ const me = keys.indexOf(PLUGIN_NAME);
436
+ const inspector = keys.indexOf(INSPECTOR_PLUGIN);
437
+ state.pending = me >= 0 && inspector > me;
438
+ if (!state.pending)
439
+ run(program, state);
440
+ }
441
+ },
442
+ "Statement|Directive"(_path, state) {
443
+ if (!state.pending)
444
+ return;
445
+ state.pending = false;
446
+ if (state.program)
447
+ run(state.program, state);
448
+ }
449
+ }
450
+ };
451
+ }