react-native-mcp-kit 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -10
- package/dist/bin/ios-hid +0 -0
- package/dist/client/contexts/McpContext/McpProvider.d.ts.map +1 -1
- package/dist/client/contexts/McpContext/McpProvider.js +5 -5
- package/dist/client/contexts/McpContext/McpProvider.js.map +1 -1
- package/dist/client/core/McpClient.d.ts.map +1 -1
- package/dist/client/core/McpClient.js +26 -32
- package/dist/client/core/McpClient.js.map +1 -1
- package/dist/modules/alert/alert.d.ts.map +1 -1
- package/dist/modules/alert/alert.js +2 -5
- package/dist/modules/alert/alert.js.map +1 -1
- package/dist/modules/console/console.d.ts.map +1 -1
- package/dist/modules/console/console.js +32 -107
- package/dist/modules/console/console.js.map +1 -1
- package/dist/modules/console/types.d.ts +1 -0
- package/dist/modules/console/types.d.ts.map +1 -1
- package/dist/modules/device/device.d.ts.map +1 -1
- package/dist/modules/device/device.js +224 -133
- package/dist/modules/device/device.js.map +1 -1
- package/dist/modules/errors/errors.d.ts.map +1 -1
- package/dist/modules/errors/errors.js +19 -36
- package/dist/modules/errors/errors.js.map +1 -1
- package/dist/modules/fiberTree/children.d.ts +49 -0
- package/dist/modules/fiberTree/children.d.ts.map +1 -0
- package/dist/modules/fiberTree/children.js +182 -0
- package/dist/modules/fiberTree/children.js.map +1 -0
- package/dist/modules/fiberTree/constants.d.ts +13 -0
- package/dist/modules/fiberTree/constants.d.ts.map +1 -0
- package/dist/modules/fiberTree/constants.js +24 -0
- package/dist/modules/fiberTree/constants.js.map +1 -0
- package/dist/modules/fiberTree/fiberTree.d.ts +3 -6
- package/dist/modules/fiberTree/fiberTree.d.ts.map +1 -1
- package/dist/modules/fiberTree/fiberTree.js +219 -1080
- package/dist/modules/fiberTree/fiberTree.js.map +1 -1
- package/dist/modules/fiberTree/finder.d.ts +60 -0
- package/dist/modules/fiberTree/finder.d.ts.map +1 -0
- package/dist/modules/fiberTree/finder.js +107 -0
- package/dist/modules/fiberTree/finder.js.map +1 -0
- package/dist/modules/fiberTree/hooks.d.ts +103 -0
- package/dist/modules/fiberTree/hooks.d.ts.map +1 -0
- package/dist/modules/fiberTree/hooks.js +532 -0
- package/dist/modules/fiberTree/hooks.js.map +1 -0
- package/dist/modules/fiberTree/projection.d.ts +49 -0
- package/dist/modules/fiberTree/projection.d.ts.map +1 -0
- package/dist/modules/fiberTree/projection.js +82 -0
- package/dist/modules/fiberTree/projection.js.map +1 -0
- package/dist/modules/fiberTree/query.d.ts +56 -0
- package/dist/modules/fiberTree/query.d.ts.map +1 -0
- package/dist/modules/fiberTree/query.js +151 -0
- package/dist/modules/fiberTree/query.js.map +1 -0
- package/dist/modules/fiberTree/redact.d.ts +24 -0
- package/dist/modules/fiberTree/redact.d.ts.map +1 -0
- package/dist/modules/fiberTree/redact.js +51 -0
- package/dist/modules/fiberTree/redact.js.map +1 -0
- package/dist/modules/fiberTree/types.d.ts +7 -0
- package/dist/modules/fiberTree/types.d.ts.map +1 -1
- package/dist/modules/fiberTree/utils.d.ts +8 -2
- package/dist/modules/fiberTree/utils.d.ts.map +1 -1
- package/dist/modules/fiberTree/utils.js +79 -78
- package/dist/modules/fiberTree/utils.js.map +1 -1
- package/dist/modules/fiberTree/viewport.d.ts +28 -0
- package/dist/modules/fiberTree/viewport.d.ts.map +1 -0
- package/dist/modules/fiberTree/viewport.js +50 -0
- package/dist/modules/fiberTree/viewport.js.map +1 -0
- package/dist/modules/fiberTree/waitFor.d.ts +52 -0
- package/dist/modules/fiberTree/waitFor.d.ts.map +1 -0
- package/dist/modules/fiberTree/waitFor.js +98 -0
- package/dist/modules/fiberTree/waitFor.js.map +1 -0
- package/dist/modules/logBox/logBox.d.ts.map +1 -1
- package/dist/modules/logBox/logBox.js +59 -66
- package/dist/modules/logBox/logBox.js.map +1 -1
- package/dist/modules/navigation/navigation.d.ts.map +1 -1
- package/dist/modules/navigation/navigation.js +115 -114
- package/dist/modules/navigation/navigation.js.map +1 -1
- package/dist/modules/network/network.d.ts.map +1 -1
- package/dist/modules/network/network.js +78 -197
- package/dist/modules/network/network.js.map +1 -1
- package/dist/modules/network/types.d.ts +23 -27
- package/dist/modules/network/types.d.ts.map +1 -1
- package/dist/modules/reactQuery/reactQuery.d.ts.map +1 -1
- package/dist/modules/reactQuery/reactQuery.js +46 -52
- package/dist/modules/reactQuery/reactQuery.js.map +1 -1
- package/dist/modules/storage/storage.d.ts.map +1 -1
- package/dist/modules/storage/storage.js +20 -3
- package/dist/modules/storage/storage.js.map +1 -1
- package/dist/server/host/tools/input.js +2 -2
- package/dist/server/host/tools/input.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +33 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcpServer.d.ts.map +1 -1
- package/dist/server/mcpServer.js +43 -3
- package/dist/server/mcpServer.js.map +1 -1
- package/dist/server/metro/eventCapture.d.ts +0 -2
- package/dist/server/metro/eventCapture.d.ts.map +1 -1
- package/dist/server/metro/eventCapture.js +1 -2
- package/dist/server/metro/eventCapture.js.map +1 -1
- package/dist/server/metro/tools/events.d.ts.map +1 -1
- package/dist/server/metro/tools/events.js +11 -11
- package/dist/server/metro/tools/events.js.map +1 -1
- package/dist/shared/projection/projectValue.d.ts +90 -0
- package/dist/shared/projection/projectValue.d.ts.map +1 -0
- package/dist/shared/projection/projectValue.js +322 -0
- package/dist/shared/projection/projectValue.js.map +1 -0
- package/dist/shared/projection/redact.d.ts +31 -0
- package/dist/shared/projection/redact.d.ts.map +1 -0
- package/dist/shared/projection/redact.js +78 -0
- package/dist/shared/projection/redact.js.map +1 -0
- package/dist/shared/projection/resolvePath.d.ts +45 -0
- package/dist/shared/projection/resolvePath.d.ts.map +1 -0
- package/dist/shared/projection/resolvePath.js +211 -0
- package/dist/shared/projection/resolvePath.js.map +1 -0
- package/dist/shared/rn/core.d.ts +48 -0
- package/dist/shared/rn/core.d.ts.map +1 -0
- package/dist/shared/rn/core.js +100 -0
- package/dist/shared/rn/core.js.map +1 -0
- package/dist/shared/rn/deviceInfo.d.ts +40 -0
- package/dist/shared/rn/deviceInfo.d.ts.map +1 -0
- package/dist/shared/rn/deviceInfo.js +78 -0
- package/dist/shared/rn/deviceInfo.js.map +1 -0
- package/package.json +2 -2
- package/dist/client/hooks/useMcpState.d.ts +0 -3
- package/dist/client/hooks/useMcpState.d.ts.map +0 -1
- package/dist/client/hooks/useMcpState.js +0 -20
- package/dist/client/hooks/useMcpState.js.map +0 -1
- package/dist/server/canonicalize.d.ts +0 -8
- package/dist/server/canonicalize.d.ts.map +0 -1
- package/dist/server/canonicalize.js +0 -23
- package/dist/server/canonicalize.js.map +0 -1
- package/dist/server/host/modules/screenshot.d.ts +0 -4
- package/dist/server/host/modules/screenshot.d.ts.map +0 -1
- package/dist/server/host/modules/screenshot.js +0 -615
- package/dist/server/host/modules/screenshot.js.map +0 -1
- package/dist/server/host/tools/connectionStatus.d.ts +0 -9
- package/dist/server/host/tools/connectionStatus.d.ts.map +0 -1
- package/dist/server/host/tools/connectionStatus.js +0 -39
- package/dist/server/host/tools/connectionStatus.js.map +0 -1
- package/dist/server/host/tools/symbolicate.d.ts +0 -3
- package/dist/server/host/tools/symbolicate.d.ts.map +0 -1
- package/dist/server/host/tools/symbolicate.js +0 -209
- package/dist/server/host/tools/symbolicate.js.map +0 -1
- package/dist/server/host/wda.d.ts +0 -15
- package/dist/server/host/wda.d.ts.map +0 -1
- package/dist/server/host/wda.js +0 -100
- package/dist/server/host/wda.js.map +0 -1
- package/dist/server/inputSchemaToZod.d.ts +0 -19
- package/dist/server/inputSchemaToZod.d.ts.map +0 -1
- package/dist/server/inputSchemaToZod.js +0 -89
- package/dist/server/inputSchemaToZod.js.map +0 -1
- package/dist/server/metro/tools/openUrl.d.ts +0 -3
- package/dist/server/metro/tools/openUrl.d.ts.map +0 -1
- package/dist/server/metro/tools/openUrl.js +0 -71
- package/dist/server/metro/tools/openUrl.js.map +0 -1
- package/dist/shared/slice.d.ts +0 -16
- package/dist/shared/slice.d.ts.map +0 -1
- package/dist/shared/slice.js +0 -29
- package/dist/shared/slice.js.map +0 -1
|
@@ -1,643 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.fiberTreeModule = void 0;
|
|
4
|
+
const projectValue_1 = require("../../shared/projection/projectValue");
|
|
5
|
+
const children_1 = require("./children");
|
|
6
|
+
const constants_1 = require("./constants");
|
|
7
|
+
const finder_1 = require("./finder");
|
|
8
|
+
const hooks_1 = require("./hooks");
|
|
9
|
+
const projection_1 = require("./projection");
|
|
10
|
+
const query_1 = require("./query");
|
|
11
|
+
const redact_1 = require("./redact");
|
|
4
12
|
const utils_1 = require("./utils");
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const WAIT_INTERVAL_MIN = 100;
|
|
13
|
-
// Parse a name pattern: `/regex/flags` → RegExp matcher; anything else →
|
|
14
|
-
// exact-string matcher. Same convention as log_box__ignore.
|
|
15
|
-
const parseNamePattern = (raw) => {
|
|
16
|
-
const m = raw.match(/^\/(.+)\/([gimsuy]*)$/);
|
|
17
|
-
if (m && m[1] !== undefined) {
|
|
18
|
-
try {
|
|
19
|
-
const rx = new RegExp(m[1], m[2] ?? '');
|
|
20
|
-
return (n) => {
|
|
21
|
-
return rx.test(n);
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return (n) => {
|
|
26
|
-
return n === raw;
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return (n) => {
|
|
31
|
-
return n === raw;
|
|
32
|
-
};
|
|
33
|
-
};
|
|
34
|
-
const HOOK_DEFAULT_MAX_DEPTH = 3;
|
|
35
|
-
// Try to treat `current` as a React component / native view instance and
|
|
36
|
-
// collapse it to a compact identifier. Native views expose
|
|
37
|
-
// `_internalFiberInstanceHandleDEV` / `_reactInternals`; class instances
|
|
38
|
-
// expose `_reactInternals`. If we find a fiber, we surface its mcpId /
|
|
39
|
-
// testID / component name so the agent can follow up via `query` if needed.
|
|
40
|
-
// If it doesn't look like a component, return null to signal "serialize
|
|
41
|
-
// normally".
|
|
42
|
-
const resolveComponentRef = (current) => {
|
|
43
|
-
if (current === null || current === undefined)
|
|
44
|
-
return null;
|
|
45
|
-
if (typeof current !== 'object')
|
|
46
|
-
return null;
|
|
47
|
-
const obj = current;
|
|
48
|
-
// Pick up a fiber from the typical internal fields used by RN / React.
|
|
49
|
-
const fiber = obj._reactInternals ??
|
|
50
|
-
obj._reactInternalFiber ??
|
|
51
|
-
obj._internalFiberInstanceHandleDEV ??
|
|
52
|
-
obj._internalInstanceHandle;
|
|
53
|
-
const hasNativeTag = '_nativeTag' in obj;
|
|
54
|
-
if (!fiber && !hasNativeTag)
|
|
55
|
-
return null;
|
|
56
|
-
const out = { __componentRef: true };
|
|
57
|
-
if (fiber) {
|
|
58
|
-
const props = fiber.memoizedProps;
|
|
59
|
-
const mcpId = props?.['data-mcp-id'];
|
|
60
|
-
const testID = props?.testID;
|
|
61
|
-
if (mcpId)
|
|
62
|
-
out.mcpId = mcpId;
|
|
63
|
-
if (testID)
|
|
64
|
-
out.testID = testID;
|
|
65
|
-
const typeName = fiber.type?.displayName ??
|
|
66
|
-
fiber.type?.name;
|
|
67
|
-
if (typeName)
|
|
68
|
-
out.componentName = typeName;
|
|
69
|
-
}
|
|
70
|
-
if (hasNativeTag) {
|
|
71
|
-
out.nativeTag = obj._nativeTag;
|
|
72
|
-
const viewConfig = obj.viewConfig;
|
|
73
|
-
if (viewConfig?.uiViewClassName)
|
|
74
|
-
out.viewClass = viewConfig.uiViewClassName;
|
|
75
|
-
}
|
|
76
|
-
return out;
|
|
77
|
-
};
|
|
78
|
-
// Recognise React's effect-record shape: `{ tag: number, create: function,
|
|
79
|
-
// deps: null | unknown[] }` with optional `inst` / `destroy` / `next`.
|
|
80
|
-
// useState / useReducer / useContext memoizedState values of this exact
|
|
81
|
-
// shape in real user code are astronomically unlikely, so we treat this as
|
|
82
|
-
// a reliable "definitely not a state slot" signal.
|
|
83
|
-
const looksLikeEffectRecord = (raw) => {
|
|
84
|
-
if (!raw || typeof raw !== 'object')
|
|
85
|
-
return false;
|
|
86
|
-
const r = raw;
|
|
87
|
-
return (typeof r.tag === 'number' &&
|
|
88
|
-
typeof r.create === 'function' &&
|
|
89
|
-
(r.deps === null || r.deps === undefined || Array.isArray(r.deps)));
|
|
90
|
-
};
|
|
91
|
-
// Recognise the useRef shape: `{ current: X }` with NO other keys. A useState
|
|
92
|
-
// value that is literally an object whose sole own-key is "current" is so
|
|
93
|
-
// improbable in real code that we treat it as a reliable "this slot is a
|
|
94
|
-
// ref, not a state" signal — lets State/Custom skip ref slots that leaked in
|
|
95
|
-
// through custom-hook internals.
|
|
96
|
-
const looksLikeRefShape = (raw) => {
|
|
97
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
98
|
-
return false;
|
|
99
|
-
const keys = Object.keys(raw);
|
|
100
|
-
return keys.length === 1 && keys[0] === 'current';
|
|
101
|
-
};
|
|
102
|
-
// Shape-verify a hook slot's memoizedState against its expected kind. When
|
|
103
|
-
// a custom hook internally uses multiple built-in hooks, our static metadata
|
|
104
|
-
// understates the number of slots — every subsequent pairing drifts. By
|
|
105
|
-
// requiring a structural match before consuming a metadata entry we can
|
|
106
|
-
// swallow "internal" slots and keep the rest aligned. Permissive kinds
|
|
107
|
-
// (State / Reducer / Context / Custom) reject only obvious mis-matches
|
|
108
|
-
// (currently: the effect-record shape).
|
|
109
|
-
const shapeMatchesKind = (raw, kind) => {
|
|
110
|
-
switch (kind) {
|
|
111
|
-
case 'Ref':
|
|
112
|
-
return !!raw && typeof raw === 'object' && 'current' in raw;
|
|
113
|
-
case 'Memo':
|
|
114
|
-
case 'Callback':
|
|
115
|
-
return Array.isArray(raw) && raw.length === 2 && (raw[1] === null || Array.isArray(raw[1]));
|
|
116
|
-
case 'Effect':
|
|
117
|
-
case 'LayoutEffect':
|
|
118
|
-
case 'InsertionEffect':
|
|
119
|
-
return looksLikeEffectRecord(raw);
|
|
120
|
-
case 'Transition':
|
|
121
|
-
return Array.isArray(raw) && raw.length === 2;
|
|
122
|
-
case 'State':
|
|
123
|
-
case 'Reducer':
|
|
124
|
-
case 'Context':
|
|
125
|
-
case 'Custom':
|
|
126
|
-
// Permissive but not blind — drop obvious effect-node and ref-shape
|
|
127
|
-
// slots so State/Custom metadata doesn't swallow internals of
|
|
128
|
-
// preceding custom hooks.
|
|
129
|
-
return !looksLikeEffectRecord(raw) && !looksLikeRefShape(raw);
|
|
130
|
-
default:
|
|
131
|
-
return true;
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
const serializeHookValue = (raw, kind, maxDepth) => {
|
|
135
|
-
const walk = (v) => {
|
|
136
|
-
return (0, utils_1.serializeValue)(v, new WeakSet(), 0, maxDepth);
|
|
137
|
-
};
|
|
138
|
-
switch (kind) {
|
|
139
|
-
case 'Ref': {
|
|
140
|
-
if (!raw || typeof raw !== 'object' || !('current' in raw)) {
|
|
141
|
-
return walk(raw);
|
|
142
|
-
}
|
|
143
|
-
const current = raw.current;
|
|
144
|
-
const componentRef = resolveComponentRef(current);
|
|
145
|
-
if (componentRef) {
|
|
146
|
-
return { current: componentRef };
|
|
147
|
-
}
|
|
148
|
-
return { current: walk(current) };
|
|
149
|
-
}
|
|
150
|
-
case 'Memo':
|
|
151
|
-
case 'Callback':
|
|
152
|
-
if (Array.isArray(raw) && raw.length === 2) {
|
|
153
|
-
return { deps: raw[1], value: walk(raw[0]) };
|
|
154
|
-
}
|
|
155
|
-
return walk(raw);
|
|
156
|
-
case 'Effect':
|
|
157
|
-
case 'LayoutEffect':
|
|
158
|
-
case 'InsertionEffect': {
|
|
159
|
-
// React's hook slot for effects holds { tag, create, destroy, deps, next }.
|
|
160
|
-
// Only `deps` is safe and useful to surface.
|
|
161
|
-
const effect = raw;
|
|
162
|
-
return effect && typeof effect === 'object' && 'deps' in effect
|
|
163
|
-
? { deps: effect.deps ?? null }
|
|
164
|
-
: null;
|
|
165
|
-
}
|
|
166
|
-
default:
|
|
167
|
-
return walk(raw);
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
// Estimate how many slots in fiber.memoizedState a hook function consumes.
|
|
171
|
-
// Used by the slot-walker to advance past unannotated black-box library
|
|
172
|
-
// hooks (e.g. `useSelector` from react-redux) which our babel plugin couldn't
|
|
173
|
-
// expand statically — without this they'd consume only one fiber slot during
|
|
174
|
-
// alignment, drifting the rest of the metadata out of sync.
|
|
175
|
-
//
|
|
176
|
-
// Three cascading strategies, falling through on miss:
|
|
177
|
-
// 1. `fn.__mcp_hooks` recursive — accurate for any hook our plugin saw.
|
|
178
|
-
// Custom sub-entries recurse; built-in entries count as 1.
|
|
179
|
-
// 2. `fn.toString()` regex — counts `useXxx(` calls in the source. Works
|
|
180
|
-
// even after Metro bundling because hook references survive as
|
|
181
|
-
// property accesses (`(0, _react.useState)(...)`) — property names
|
|
182
|
-
// aren't mangled. Underestimates when nested customs themselves expand
|
|
183
|
-
// to multiple slots, but better than 1.
|
|
184
|
-
// 3. Default 1 — native functions, bound functions, or sources we can't
|
|
185
|
-
// parse. Same as the original behavior.
|
|
186
|
-
//
|
|
187
|
-
// Cached per-function via WeakMap so cost is paid once per hook fn per
|
|
188
|
-
// session.
|
|
189
|
-
const HOOK_SLOTS_CACHE = new WeakMap();
|
|
190
|
-
const HOOK_NAME_RE = /\buse[A-Z]\w*\s*\(/g;
|
|
191
|
-
const STRING_LITERAL_RE = /(['"`])(?:\\.|(?!\1).)*\1/g;
|
|
192
|
-
const BLOCK_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
|
|
193
|
-
const LINE_COMMENT_RE = /\/\/[^\n]*/g;
|
|
194
|
-
const countHookSlots = (fn, depth = 0, seen) => {
|
|
195
|
-
if (depth > 8)
|
|
196
|
-
return 1;
|
|
197
|
-
if (typeof fn !== 'function')
|
|
198
|
-
return 1;
|
|
199
|
-
const fnObj = fn;
|
|
200
|
-
const cached = HOOK_SLOTS_CACHE.get(fnObj);
|
|
201
|
-
if (cached !== undefined)
|
|
202
|
-
return cached;
|
|
203
|
-
const localSeen = seen ?? new WeakSet();
|
|
204
|
-
if (localSeen.has(fnObj))
|
|
205
|
-
return 1;
|
|
206
|
-
localSeen.add(fnObj);
|
|
207
|
-
// (1) Annotated metadata: recurse into sub-entries, summing their slot
|
|
208
|
-
// counts. Each built-in entry contributes 1; each Custom contributes
|
|
209
|
-
// however many its own fn does (recurse).
|
|
210
|
-
const annotated = fn.__mcp_hooks;
|
|
211
|
-
if (Array.isArray(annotated)) {
|
|
212
|
-
let total = 0;
|
|
213
|
-
for (const entry of annotated) {
|
|
214
|
-
if (entry && entry.kind === 'Custom' && typeof entry.fn === 'function') {
|
|
215
|
-
total += countHookSlots(entry.fn, depth + 1, localSeen);
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
total += 1;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
const result = Math.max(total, 1);
|
|
222
|
-
HOOK_SLOTS_CACHE.set(fnObj, result);
|
|
223
|
-
return result;
|
|
224
|
-
}
|
|
225
|
-
// (2) toString-based parsing. Strip strings/comments first to avoid
|
|
226
|
-
// matching `'useState'` inside literals. Each `useXxx(` occurrence in
|
|
227
|
-
// remaining source counts as one hook call (≥ 1 slot). Custom hook calls
|
|
228
|
-
// we encounter here can't be resolved further (no scope binding from the
|
|
229
|
-
// outer function), so we bottom out at 1 slot per occurrence — still
|
|
230
|
-
// beats the original constant-1 fallback when the source has multiple
|
|
231
|
-
// hook calls (e.g. `useSyncExternalStoreWithSelector` internals).
|
|
232
|
-
let src;
|
|
233
|
-
try {
|
|
234
|
-
src = Function.prototype.toString.call(fn);
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
HOOK_SLOTS_CACHE.set(fnObj, 1);
|
|
238
|
-
return 1;
|
|
239
|
-
}
|
|
240
|
-
if (!src || src.includes('[native code]')) {
|
|
241
|
-
HOOK_SLOTS_CACHE.set(fnObj, 1);
|
|
242
|
-
return 1;
|
|
243
|
-
}
|
|
244
|
-
const stripped = src
|
|
245
|
-
.replace(STRING_LITERAL_RE, '""')
|
|
246
|
-
.replace(BLOCK_COMMENT_RE, '')
|
|
247
|
-
.replace(LINE_COMMENT_RE, '');
|
|
248
|
-
HOOK_NAME_RE.lastIndex = 0;
|
|
249
|
-
const matches = stripped.match(HOOK_NAME_RE);
|
|
250
|
-
const result = matches ? matches.length : 1;
|
|
251
|
-
HOOK_SLOTS_CACHE.set(fnObj, result);
|
|
252
|
-
return result;
|
|
253
|
-
};
|
|
254
|
-
// Flatten a metadata array by recursively inlining custom-hook sub-metadata.
|
|
255
|
-
// Stops on cycles (hook references itself) and on Custom entries whose `fn`
|
|
256
|
-
// isn't annotated (library hooks that bypassed the babel plugin — usually
|
|
257
|
-
// pre-compiled node_modules). Such unannotated entries stay as single
|
|
258
|
-
// records and rely on shape-check for alignment.
|
|
259
|
-
//
|
|
260
|
-
// Custom entries with annotated `fn` produce TWO records: a parent (marked
|
|
261
|
-
// `expanded: true`, no slot consumption) followed by all flattened
|
|
262
|
-
// children. This keeps the call-site visible in the output — without it
|
|
263
|
-
// the agent would see e.g. `wrapperAnimStyle.areAnimationsActive` deep
|
|
264
|
-
// in `via:` but never the `wrapperAnimStyle = useAnimatedStyle(...)`
|
|
265
|
-
// invocation that owns those slots.
|
|
266
|
-
const flattenHookMeta = (meta, via = [], seen = new WeakSet(), maxDepth = Infinity) => {
|
|
267
|
-
const out = [];
|
|
268
|
-
for (const entry of meta) {
|
|
269
|
-
const fn = entry.fn;
|
|
270
|
-
const sub = fn && typeof fn === 'function' && Array.isArray(fn.__mcp_hooks) ? fn.__mcp_hooks : undefined;
|
|
271
|
-
// Stop expanding once `via.length` would reach the cap — at that point
|
|
272
|
-
// the current entry is treated as a leaf (Custom record without
|
|
273
|
-
// children). The slot-walker still pairs it with one slot, so output
|
|
274
|
-
// stays internally consistent.
|
|
275
|
-
if (sub && !seen.has(fn) && via.length < maxDepth) {
|
|
276
|
-
seen.add(fn);
|
|
277
|
-
out.push({ ...entry, expanded: true, via });
|
|
278
|
-
out.push(...flattenHookMeta(sub, [...via, entry.name], seen, maxDepth));
|
|
279
|
-
seen.delete(fn);
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
out.push({ ...entry, via });
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
return out;
|
|
286
|
-
};
|
|
287
|
-
const flatHooksToTree = (flat) => {
|
|
288
|
-
const root = [];
|
|
289
|
-
// Stack of currently-open parents, indexed by their depth (= via.length
|
|
290
|
-
// of THEIR own entry, since their children sit at via.length + 1).
|
|
291
|
-
const parents = [];
|
|
292
|
-
for (const entry of flat) {
|
|
293
|
-
const depth = entry.via?.length ?? 0;
|
|
294
|
-
while (parents.length > depth)
|
|
295
|
-
parents.pop();
|
|
296
|
-
const node = { kind: entry.kind, name: entry.name };
|
|
297
|
-
if (entry.hook !== undefined)
|
|
298
|
-
node.hook = entry.hook;
|
|
299
|
-
if (entry.value !== undefined)
|
|
300
|
-
node.value = entry.value;
|
|
301
|
-
if (parents.length === 0) {
|
|
302
|
-
root.push(node);
|
|
303
|
-
}
|
|
304
|
-
else {
|
|
305
|
-
const parent = parents[parents.length - 1];
|
|
306
|
-
if (parent) {
|
|
307
|
-
parent.children = parent.children ?? [];
|
|
308
|
-
parent.children.push(node);
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
root.push(node);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
if (entry.expanded) {
|
|
315
|
-
// Push as the new active parent at this depth. Subsequent entries
|
|
316
|
-
// with via.length > depth become this node's descendants.
|
|
317
|
-
parents.push(node);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
return root;
|
|
321
|
-
};
|
|
322
|
-
const extractHooks = (fiber, filter) => {
|
|
323
|
-
// React's wrapper machinery makes "where does metadata live" depend on
|
|
324
|
-
// the exact HOC chain. We try the most likely homes in order:
|
|
325
|
-
//
|
|
326
|
-
// 1. `fiber.type.__mcp_hooks` — bare components, FunctionDeclarations,
|
|
327
|
-
// and the outer memo fiber when the chain is just memo(fn).
|
|
328
|
-
// 2. `fiber.elementType.__mcp_hooks` — memo(fn) without compare.
|
|
329
|
-
// React converts the fiber to SimpleMemoComponent and rewrites
|
|
330
|
-
// `fiber.type` to the inner function (see `updateMemoComponent` in
|
|
331
|
-
// ReactFabric); our metadata sits on the outer memo wrapper, which
|
|
332
|
-
// survives only on `elementType`.
|
|
333
|
-
// 3. `fiber.type.render.__mcp_hooks` — forwardRef wrapper. React lays
|
|
334
|
-
// out memo(forwardRef(fn)) as three fibers (memo → forwardRef →
|
|
335
|
-
// function). When the user queries by displayName they tend to
|
|
336
|
-
// match the middle ForwardRef fiber (whose displayName resolves
|
|
337
|
-
// via `render.displayName`); fiber.type there is the forwardRef
|
|
338
|
-
// wrapper, which holds the inner fn at `.render`. The babel plugin
|
|
339
|
-
// put plain metadata on that fn via the FunctionDecl visitor.
|
|
340
|
-
// 4. `fiber.type.type.__mcp_hooks` — memo wrapper around forwardRef
|
|
341
|
-
// (or any non-function inner). Not the SimpleMemoComponent path,
|
|
342
|
-
// so `fiber.type` stays as the memo wrapper and `.type` is the
|
|
343
|
-
// inner forwardRef / class / etc. This catches getter installations
|
|
344
|
-
// on the wrapper layer too.
|
|
345
|
-
const candidates = [
|
|
346
|
-
fiber.type,
|
|
347
|
-
fiber.elementType,
|
|
348
|
-
fiber.type?.render,
|
|
349
|
-
fiber.type?.type,
|
|
350
|
-
];
|
|
351
|
-
let rawMeta;
|
|
352
|
-
for (const c of candidates) {
|
|
353
|
-
const m = c?.__mcp_hooks;
|
|
354
|
-
if (Array.isArray(m)) {
|
|
355
|
-
rawMeta = m;
|
|
356
|
-
break;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
if (!Array.isArray(rawMeta))
|
|
360
|
-
return null;
|
|
361
|
-
const meta = flattenHookMeta(rawMeta, [], new WeakSet(), filter.expansionDepth);
|
|
362
|
-
const out = [];
|
|
363
|
-
let state = fiber.memoizedState;
|
|
364
|
-
let metaIdx = 0;
|
|
365
|
-
// Walk the fiber's hook chain and pair each slot with the next metadata
|
|
366
|
-
// entry whose `kind` is shape-compatible with the slot. Strongly-shaped
|
|
367
|
-
// kinds (Ref / Memo / Callback / Effect) match only the corresponding
|
|
368
|
-
// React hook shape. Permissive kinds (State / Reducer / Context / Custom)
|
|
369
|
-
// reject obvious mismatches (effect-record, ref-shape) so they don't
|
|
370
|
-
// swallow slots belonging to preceding custom hooks.
|
|
371
|
-
//
|
|
372
|
-
// For Custom-leaf entries — typically black-box library hooks
|
|
373
|
-
// (`useSelector`, `useQuery`, etc.) where flattenHookMeta couldn't expand
|
|
374
|
-
// because `fn.__mcp_hooks` was missing — we estimate slot count via
|
|
375
|
-
// `countHookSlots` and advance the fiber chain by that many slots in one
|
|
376
|
-
// step. Without this, a hook that consumes 3 internal slots only
|
|
377
|
-
// advances by 1 in the walker, drifting all trailing entries off the end
|
|
378
|
-
// of the chain.
|
|
379
|
-
const emitEntry = (entry, rawValueSlot) => {
|
|
380
|
-
const { hook, kind, name, via } = entry;
|
|
381
|
-
const passesKind = !filter.kindsSet || filter.kindsSet.has(kind);
|
|
382
|
-
const passesName = !filter.nameMatchers ||
|
|
383
|
-
filter.nameMatchers.some((m) => {
|
|
384
|
-
return m(name);
|
|
385
|
-
});
|
|
386
|
-
if (!(passesKind && passesName))
|
|
387
|
-
return;
|
|
388
|
-
const record = { kind, name };
|
|
389
|
-
// Prefer the babel-emitted hook name; fall back to fn.name for entries
|
|
390
|
-
// produced by older bundles that predate the `hook` field.
|
|
391
|
-
const resolvedHook = hook ?? (typeof entry.fn === 'function' ? entry.fn.name : undefined);
|
|
392
|
-
if (resolvedHook)
|
|
393
|
-
record.hook = resolvedHook;
|
|
394
|
-
if (filter.withValues && rawValueSlot !== undefined) {
|
|
395
|
-
// Redaction guard: mask the value (but keep kind/name/hook visible)
|
|
396
|
-
// when the entry's name OR any ancestor in `via` matches a redact
|
|
397
|
-
// pattern. Catches both direct sensitive hooks (`password` State)
|
|
398
|
-
// and leaves nested under sensitive customs (a `value` field of
|
|
399
|
-
// `useCredentials()` won't slip through).
|
|
400
|
-
const isRedacted = matchesAnyRedactPattern(name, filter.redactPatterns) ||
|
|
401
|
-
(via?.some((v) => {
|
|
402
|
-
return matchesAnyRedactPattern(v, filter.redactPatterns);
|
|
403
|
-
}) ??
|
|
404
|
-
false);
|
|
405
|
-
if (isRedacted) {
|
|
406
|
-
record.value = REDACTED_VALUE;
|
|
407
|
-
}
|
|
408
|
-
else {
|
|
409
|
-
let value;
|
|
410
|
-
try {
|
|
411
|
-
value = serializeHookValue(rawValueSlot, kind, filter.maxDepth);
|
|
412
|
-
// Final cycle / non-serialisable check — the MCP bridge will
|
|
413
|
-
// stringify this for transport, so bail now rather than killing
|
|
414
|
-
// the whole response if one stray value carries a cycle past our
|
|
415
|
-
// WeakSet (e.g. Proxy, lazy getter, native-bridged object).
|
|
416
|
-
JSON.stringify(value);
|
|
417
|
-
}
|
|
418
|
-
catch {
|
|
419
|
-
value = '[Unserialisable value]';
|
|
420
|
-
}
|
|
421
|
-
record.value = value;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
if (via && via.length > 0)
|
|
425
|
-
record.via = via;
|
|
426
|
-
if (entry.expanded)
|
|
427
|
-
record.expanded = true;
|
|
428
|
-
out.push(record);
|
|
429
|
-
};
|
|
430
|
-
const advanceState = (steps) => {
|
|
431
|
-
for (let i = 0; i < steps && state; i++)
|
|
432
|
-
state = state.next;
|
|
433
|
-
};
|
|
434
|
-
while (state && metaIdx < meta.length) {
|
|
435
|
-
const entry = meta[metaIdx];
|
|
436
|
-
if (!entry) {
|
|
437
|
-
metaIdx++;
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
// Expanded parent (synthetic, marks the call-site of a recursively
|
|
441
|
-
// expanded custom hook). Emit without consuming any fiber slot — the
|
|
442
|
-
// children that follow are the real slot-bearing entries.
|
|
443
|
-
if (entry.expanded) {
|
|
444
|
-
emitEntry(entry, undefined);
|
|
445
|
-
metaIdx++;
|
|
446
|
-
continue;
|
|
447
|
-
}
|
|
448
|
-
// Custom-leaf with a known fn → recurse into source to estimate slot
|
|
449
|
-
// count, then consume that many slots at once. The first slot's value
|
|
450
|
-
// is exposed (best approximation of "this hook's value"); the rest are
|
|
451
|
-
// skipped silently as internals of the library hook.
|
|
452
|
-
if (entry.kind === 'Custom' && typeof entry.fn === 'function') {
|
|
453
|
-
const slots = countHookSlots(entry.fn);
|
|
454
|
-
if (slots > 1) {
|
|
455
|
-
emitEntry(entry, state.memoizedState);
|
|
456
|
-
advanceState(slots);
|
|
457
|
-
metaIdx++;
|
|
458
|
-
continue;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
if (!shapeMatchesKind(state.memoizedState, entry.kind)) {
|
|
462
|
-
state = state.next;
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
emitEntry(entry, state.memoizedState);
|
|
466
|
-
metaIdx++;
|
|
467
|
-
state = state.next;
|
|
468
|
-
}
|
|
469
|
-
// Any metadata entries left after the fiber chain ran dry didn't get a
|
|
470
|
-
// slot match. Emit them anyway (without value) so the agent at least
|
|
471
|
-
// sees the hook exists — this is strictly better than silently dropping,
|
|
472
|
-
// and helps debug alignment issues. Common cause: a preceding Custom
|
|
473
|
-
// hook consumed more slots than countHookSlots estimated.
|
|
474
|
-
while (metaIdx < meta.length) {
|
|
475
|
-
const entry = meta[metaIdx];
|
|
476
|
-
if (entry)
|
|
477
|
-
emitEntry(entry, undefined);
|
|
478
|
-
metaIdx++;
|
|
479
|
-
}
|
|
480
|
-
return filter.format === 'tree' ? flatHooksToTree(out) : out;
|
|
481
|
-
};
|
|
482
|
-
const FIND_SCHEMA = {
|
|
483
|
-
index: {
|
|
484
|
-
description: '0-based index when several components match (default: 0).',
|
|
485
|
-
type: 'number',
|
|
486
|
-
},
|
|
487
|
-
mcpId: { description: 'Stable data-mcp-id to match.', type: 'string' },
|
|
488
|
-
name: { description: 'Component name to match.', type: 'string' },
|
|
489
|
-
testID: { description: 'testID to match.', type: 'string' },
|
|
490
|
-
text: { description: 'Rendered text substring (not prop values).', type: 'string' },
|
|
491
|
-
within: {
|
|
492
|
-
description: 'Parent component path. "/" nests, ":N" picks index.',
|
|
493
|
-
examples: ['LoginForm', 'Button:1/Pressable', 'TabBar/TabBarItem:2'],
|
|
494
|
-
type: 'string',
|
|
495
|
-
},
|
|
496
|
-
};
|
|
497
|
-
// Default redact patterns — applied to a hook's `name` AND every entry in
|
|
498
|
-
// its `via` chain so values stay masked even when nested under a sensitive
|
|
499
|
-
// custom hook (e.g. a leaf `value` inside a `useAuth()` expansion). Tuned
|
|
500
|
-
// to match real-world variable names without over-matching innocent ones:
|
|
501
|
-
// `Pin$` is anchored so it doesn't catch "Spinner"; broad terms like
|
|
502
|
-
// `auth` are deliberately omitted (would catch `isAuthenticated`).
|
|
503
|
-
const DEFAULT_REDACT_HOOK_NAMES = [
|
|
504
|
-
/password/i,
|
|
505
|
-
/token/i,
|
|
506
|
-
/jwt/i,
|
|
507
|
-
/secret/i,
|
|
508
|
-
/Pin$/,
|
|
509
|
-
/credential/i,
|
|
510
|
-
/apiKey/i,
|
|
511
|
-
/authorization/i,
|
|
512
|
-
];
|
|
513
|
-
const REDACTED_VALUE = '[redacted]';
|
|
514
|
-
const escapeRegExp = (s) => {
|
|
515
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
516
|
-
};
|
|
517
|
-
const compileRedactPatterns = (raw) => {
|
|
518
|
-
return raw.map((p) => {
|
|
519
|
-
return typeof p === 'string' ? new RegExp(escapeRegExp(p), 'i') : p;
|
|
520
|
-
});
|
|
521
|
-
};
|
|
522
|
-
const matchesAnyRedactPattern = (name, patterns) => {
|
|
523
|
-
for (const p of patterns) {
|
|
524
|
-
if (p.test(name))
|
|
525
|
-
return true;
|
|
526
|
-
}
|
|
527
|
-
return false;
|
|
528
|
-
};
|
|
529
|
-
const resolveScreenFiber = (runtime) => {
|
|
530
|
-
const nav = runtime.navigationRef;
|
|
531
|
-
if (!nav || typeof nav.getCurrentRoute !== 'function')
|
|
532
|
-
return null;
|
|
533
|
-
const route = nav.getCurrentRoute();
|
|
534
|
-
const key = route && typeof route.key === 'string' ? route.key : undefined;
|
|
535
|
-
if (!key)
|
|
536
|
-
return null;
|
|
537
|
-
return (0, utils_1.findScreenFiberByRouteKey)(runtime.root, key);
|
|
538
|
-
};
|
|
539
|
-
const collectByScope = (fiber, scope, runtime) => {
|
|
540
|
-
switch (scope) {
|
|
541
|
-
case 'self':
|
|
542
|
-
return [fiber];
|
|
543
|
-
case 'parent':
|
|
544
|
-
return fiber.return ? [fiber.return] : [];
|
|
545
|
-
case 'ancestors':
|
|
546
|
-
return (0, utils_1.getAncestors)(fiber);
|
|
547
|
-
case 'children':
|
|
548
|
-
return (0, utils_1.getDirectChildren)(fiber);
|
|
549
|
-
case 'siblings':
|
|
550
|
-
return (0, utils_1.getSiblings)(fiber);
|
|
551
|
-
case 'nearest_host': {
|
|
552
|
-
const host = (0, utils_1.findHostFiber)(fiber);
|
|
553
|
-
return host ? [host] : [];
|
|
554
|
-
}
|
|
555
|
-
case 'screen': {
|
|
556
|
-
const screen = resolveScreenFiber(runtime);
|
|
557
|
-
if (!screen)
|
|
558
|
-
return [];
|
|
559
|
-
return (0, utils_1.findAllByQuery)(screen, {}).filter((f) => {
|
|
560
|
-
return f !== screen;
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
case 'descendants':
|
|
564
|
-
default:
|
|
565
|
-
return (0, utils_1.findAllByQuery)(fiber, {}).filter((f) => {
|
|
566
|
-
return f !== fiber;
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
};
|
|
570
|
-
const runQueryChain = (runtime, steps) => {
|
|
571
|
-
let current = [runtime.root];
|
|
572
|
-
for (const step of steps) {
|
|
573
|
-
const scope = step.scope ?? 'descendants';
|
|
574
|
-
const seen = new Set();
|
|
575
|
-
const collected = [];
|
|
576
|
-
for (const fiber of current) {
|
|
577
|
-
for (const candidate of collectByScope(fiber, scope, runtime)) {
|
|
578
|
-
if (!seen.has(candidate)) {
|
|
579
|
-
seen.add(candidate);
|
|
580
|
-
collected.push(candidate);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
const filtered = collected.filter((f) => {
|
|
585
|
-
return (0, utils_1.matchesQuery)(f, step);
|
|
586
|
-
});
|
|
587
|
-
if (typeof step.index === 'number') {
|
|
588
|
-
const picked = filtered[step.index];
|
|
589
|
-
current = picked ? [picked] : [];
|
|
590
|
-
}
|
|
591
|
-
else {
|
|
592
|
-
current = filtered;
|
|
593
|
-
}
|
|
594
|
-
if (current.length === 0)
|
|
595
|
-
return [];
|
|
596
|
-
}
|
|
597
|
-
return current;
|
|
598
|
-
};
|
|
599
|
-
// Keep only fibers whose ancestor chain contains no other match. Removes
|
|
600
|
-
// wrapper cascades (PressableView → Pressable → View → RCTView) while keeping
|
|
601
|
-
// independent siblings with overlapping bounds (e.g. absolute-positioned
|
|
602
|
-
// overlays). Preserves original DFS order.
|
|
603
|
-
const dedupAncestors = (matches) => {
|
|
604
|
-
if (matches.length < 2)
|
|
605
|
-
return matches;
|
|
606
|
-
const matchSet = new Set(matches);
|
|
607
|
-
return matches.filter((fiber) => {
|
|
608
|
-
let p = fiber.return;
|
|
609
|
-
while (p) {
|
|
610
|
-
if (matchSet.has(p))
|
|
611
|
-
return false;
|
|
612
|
-
p = p.return;
|
|
613
|
-
}
|
|
614
|
-
return true;
|
|
615
|
-
});
|
|
616
|
-
};
|
|
617
|
-
// Window dimensions → physical-pixel bounds rectangle for `onlyVisible` filter.
|
|
618
|
-
const getVisibleRect = () => {
|
|
619
|
-
try {
|
|
620
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
621
|
-
const RN = require('react-native');
|
|
622
|
-
const { Dimensions, PixelRatio } = RN;
|
|
623
|
-
const window = Dimensions?.get?.('window');
|
|
624
|
-
const ratio = PixelRatio?.get?.() ?? 1;
|
|
625
|
-
if (!window || !Number.isFinite(window.width) || !Number.isFinite(window.height))
|
|
626
|
-
return null;
|
|
627
|
-
return {
|
|
628
|
-
height: window.height * ratio,
|
|
629
|
-
width: window.width * ratio,
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
catch {
|
|
633
|
-
return null;
|
|
634
|
-
}
|
|
635
|
-
};
|
|
636
|
-
const intersectsRect = (bounds, rect) => {
|
|
637
|
-
return (bounds.x + bounds.width > 0 &&
|
|
638
|
-
bounds.y + bounds.height > 0 &&
|
|
639
|
-
bounds.x < rect.width &&
|
|
640
|
-
bounds.y < rect.height);
|
|
13
|
+
const viewport_1 = require("./viewport");
|
|
14
|
+
const waitFor_1 = require("./waitFor");
|
|
15
|
+
const PROJECTION_SCHEMA = (0, projectValue_1.makeProjectionSchema)(constants_1.FIBER_DEFAULT_DEPTH);
|
|
16
|
+
// Module-local 2-arg wrapper around the shared `applyProjection` so handlers
|
|
17
|
+
// don't have to repeat `projectFiberValue` + default-depth on every call.
|
|
18
|
+
const applyProjection = (result, args) => {
|
|
19
|
+
return (0, projectValue_1.applyProjection)(result, args, utils_1.projectFiberValue, constants_1.FIBER_DEFAULT_DEPTH);
|
|
641
20
|
};
|
|
642
21
|
const fiberTreeModule = (options) => {
|
|
643
22
|
if (options?.rootRef) {
|
|
@@ -649,9 +28,9 @@ const fiberTreeModule = (options) => {
|
|
|
649
28
|
// - `additionalRedactHookNames` provided → defaults + user's.
|
|
650
29
|
// - Neither → defaults.
|
|
651
30
|
// Pass `redactHookNames: []` to disable redaction entirely.
|
|
652
|
-
const redactPatterns = compileRedactPatterns(options?.redactHookNames !== undefined
|
|
31
|
+
const redactPatterns = (0, redact_1.compileRedactPatterns)(options?.redactHookNames !== undefined
|
|
653
32
|
? options.redactHookNames
|
|
654
|
-
: [...DEFAULT_REDACT_HOOK_NAMES, ...(options?.additionalRedactHookNames ?? [])]);
|
|
33
|
+
: [...redact_1.DEFAULT_REDACT_HOOK_NAMES, ...(options?.additionalRedactHookNames ?? [])]);
|
|
655
34
|
// Root-version keyed cache for `runQueryChain`. When React commits, the
|
|
656
35
|
// HostRoot fiber swaps — so a mismatched pointer is proof the tree changed
|
|
657
36
|
// and the cached match set for the same steps is no longer valid.
|
|
@@ -660,7 +39,7 @@ const fiberTreeModule = (options) => {
|
|
|
660
39
|
const cacheEntries = new Map();
|
|
661
40
|
const runCachedQuery = (runtime, steps, useCache) => {
|
|
662
41
|
if (!useCache)
|
|
663
|
-
return runQueryChain(runtime, steps);
|
|
42
|
+
return (0, query_1.runQueryChain)(runtime, steps);
|
|
664
43
|
if (cacheRoot !== runtime.root) {
|
|
665
44
|
cacheRoot = runtime.root;
|
|
666
45
|
cacheEntries.clear();
|
|
@@ -669,78 +48,25 @@ const fiberTreeModule = (options) => {
|
|
|
669
48
|
const hit = cacheEntries.get(key);
|
|
670
49
|
if (hit)
|
|
671
50
|
return hit;
|
|
672
|
-
const result = runQueryChain(runtime, steps);
|
|
51
|
+
const result = (0, query_1.runQueryChain)(runtime, steps);
|
|
673
52
|
cacheEntries.set(key, result);
|
|
674
53
|
return result;
|
|
675
54
|
};
|
|
676
|
-
const findInRoot = (root, segment) => {
|
|
677
|
-
if (!root)
|
|
678
|
-
return null;
|
|
679
|
-
// Support "Name:index" format, e.g. "Button:1"
|
|
680
|
-
const [name, indexStr] = segment.split(':');
|
|
681
|
-
if (!name)
|
|
682
|
-
return null;
|
|
683
|
-
const idx = indexStr ? parseInt(indexStr, 10) : 0;
|
|
684
|
-
const allByMcpId = (0, utils_1.findAllByQuery)(root, { mcpId: name });
|
|
685
|
-
if (allByMcpId.length > 0)
|
|
686
|
-
return allByMcpId[idx] ?? null;
|
|
687
|
-
const allByTestID = (0, utils_1.findAllByQuery)(root, { testID: name });
|
|
688
|
-
if (allByTestID.length > 0)
|
|
689
|
-
return allByTestID[idx] ?? null;
|
|
690
|
-
const allByName = (0, utils_1.findAllByQuery)(root, { name });
|
|
691
|
-
return allByName[idx] ?? null;
|
|
692
|
-
};
|
|
693
|
-
const findComponent = (args) => {
|
|
694
|
-
let root = (0, utils_1.getFiberRoot)();
|
|
695
|
-
if (!root)
|
|
696
|
-
return null;
|
|
697
|
-
// "within" supports recursive path with index: "Parent/Child:1/GrandChild"
|
|
698
|
-
if (args.within) {
|
|
699
|
-
const path = args.within.split('/');
|
|
700
|
-
for (const segment of path) {
|
|
701
|
-
root = findInRoot(root, segment);
|
|
702
|
-
if (!root)
|
|
703
|
-
return null;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
const index = args.index ?? 0;
|
|
707
|
-
if (args.mcpId) {
|
|
708
|
-
const all = (0, utils_1.findAllByQuery)(root, { mcpId: args.mcpId });
|
|
709
|
-
return all[index] ?? null;
|
|
710
|
-
}
|
|
711
|
-
if (args.testID) {
|
|
712
|
-
const all = (0, utils_1.findAllByQuery)(root, { testID: args.testID });
|
|
713
|
-
return all[index] ?? null;
|
|
714
|
-
}
|
|
715
|
-
if (args.name) {
|
|
716
|
-
const all = (0, utils_1.findAllByQuery)(root, { name: args.name });
|
|
717
|
-
return all[index] ?? null;
|
|
718
|
-
}
|
|
719
|
-
if (args.text) {
|
|
720
|
-
const all = (0, utils_1.findAllByQuery)(root, { text: args.text });
|
|
721
|
-
return all[index] ?? null;
|
|
722
|
-
}
|
|
723
|
-
return null;
|
|
724
|
-
};
|
|
725
|
-
const requireRoot = () => {
|
|
726
|
-
const root = (0, utils_1.getFiberRoot)();
|
|
727
|
-
if (!root) {
|
|
728
|
-
return { error: 'Fiber root not available. The app may not have rendered yet.' };
|
|
729
|
-
}
|
|
730
|
-
return null;
|
|
731
|
-
};
|
|
732
55
|
return {
|
|
733
56
|
description: `React fiber tree inspection and interaction.
|
|
734
57
|
|
|
735
58
|
SCOPES (query steps)
|
|
736
59
|
descendants (default) / children / parent / ancestors / siblings / self
|
|
737
|
-
/ screen / nearest_host.
|
|
60
|
+
/ root / screen / nearest_host.
|
|
61
|
+
· root — the React fiber root, regardless of the previous step's
|
|
62
|
+
match. Use as the first step to start from the top of the tree
|
|
63
|
+
(e.g. dump the whole tree via select: [{ children: 5 }]).
|
|
738
64
|
· screen — descendants of the currently focused React Navigation
|
|
739
65
|
screen fiber. Available when the library was initialized with a
|
|
740
66
|
navigationRef. Lets a first step skip "find current screen first".
|
|
741
67
|
· nearest_host — walks down to the first mounted HOST_COMPONENT
|
|
742
|
-
fiber. Useful before
|
|
743
|
-
a host instance.
|
|
68
|
+
fiber. Useful before \`call({ method })\` (focus/blur/measure)
|
|
69
|
+
which requires a host instance.
|
|
744
70
|
|
|
745
71
|
STEP CRITERIA
|
|
746
72
|
name / mcpId / testID — strict equality.
|
|
@@ -759,15 +85,27 @@ STEP CRITERIA
|
|
|
759
85
|
index — pick N-th match from this step; otherwise all matches fan out into the next step.
|
|
760
86
|
|
|
761
87
|
SELECT (output fields)
|
|
762
|
-
Default ["mcpId", "name", "testID"] — props, bounds, hooks
|
|
88
|
+
Default ["mcpId", "name", "testID"] — props, bounds, hooks,
|
|
89
|
+
refMethods, children are opt-in.
|
|
763
90
|
bounds: { x, y, width, height, centerX, centerY } in PHYSICAL pixels,
|
|
764
91
|
top-left origin. null when the fiber has no mounted host view. centerX/
|
|
765
92
|
centerY feed straight into host__tap.
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
93
|
+
refMethods: list of native-ref method names (focus, blur, measure,
|
|
94
|
+
scrollTo, ...) available on the fiber's host instance. null when
|
|
95
|
+
there is no native instance (composite wrapper, unmounted,
|
|
96
|
+
virtualized). Feeds directly into fiber_tree__call({ method }).
|
|
97
|
+
props: per-field projection — \`{ props: { path?, depth?, maxBytes? } }\`.
|
|
98
|
+
hooks: filtered + projected — \`{ hooks: { kinds?, names?, withValues?,
|
|
99
|
+
expansionDepth?, format?, path?, depth?, maxBytes? } }\`. Each entry
|
|
100
|
+
{ kind, name, hook?, via?, expanded?, value? }.
|
|
101
|
+
children: recursive light-only walker for tree-of-tree navigation —
|
|
102
|
+
short form { children: 5 } (treeDepth=5) or object form
|
|
103
|
+
{ children: { treeDepth, select?, itemsCap? } }. select inside
|
|
104
|
+
children may include only mcpId / name / testID / bounds / nested
|
|
105
|
+
children — props/hooks throw at parse time. Use a second query against
|
|
106
|
+
a child mcpId to inspect its props/hooks. treeDepth max 16, itemsCap
|
|
107
|
+
default 50; overflow inserts a \`\${truncated}\` sentinel as the first
|
|
108
|
+
array item.
|
|
771
109
|
|
|
772
110
|
RESPONSE
|
|
773
111
|
{ matches: [...], total, truncated? } — total is the unrestricted match
|
|
@@ -782,9 +120,9 @@ RESPONSE
|
|
|
782
120
|
|
|
783
121
|
TIPS
|
|
784
122
|
mcpId format "ComponentName:file:line" — stable across renders.
|
|
785
|
-
Use query to locate, then
|
|
786
|
-
with bounds (real OS touch) to act. For one-shot
|
|
787
|
-
collapses both steps into a single call.
|
|
123
|
+
Use query to locate, then call({ prop } or { method }) (bypasses gesture
|
|
124
|
+
pipeline) or host__tap with bounds (real OS touch) to act. For one-shot
|
|
125
|
+
real taps, tap_fiber collapses both steps into a single call.
|
|
788
126
|
When stepping up via scope: "ancestors", prefer filtering by name (or
|
|
789
127
|
testID/mcpId) over guessing an index — ancestors count is brittle and
|
|
790
128
|
varies across RN versions.
|
|
@@ -793,37 +131,54 @@ TIPS
|
|
|
793
131
|
{ contains: "Search" } }\`.`,
|
|
794
132
|
name: 'fiber_tree',
|
|
795
133
|
tools: {
|
|
796
|
-
|
|
797
|
-
description: "
|
|
134
|
+
call: {
|
|
135
|
+
description: "Imperative action on a fiber — invoke a prop callback OR a native-ref method. Pass `prop: 'onPress'` to call a callback prop, or `method: 'focus'` to call a method on the host instance's native ref. For simulating user taps, prefer `host__tap_fiber` — it goes through the real OS gesture pipeline so Pressable feedback / gesture responders / hit-test all behave as under a real finger. `call` is for non-gesture callbacks, off-screen / virtualised components, or imperative ref methods (focus / blur / measure / scrollTo / ...). Use `query` with `select: ['refMethods']` first to see what methods are available on a fiber.",
|
|
798
136
|
handler: (args) => {
|
|
799
|
-
const rootError = requireRoot();
|
|
137
|
+
const rootError = (0, finder_1.requireRoot)();
|
|
800
138
|
if (rootError)
|
|
801
139
|
return rootError;
|
|
802
|
-
const fiber = findComponent(args);
|
|
140
|
+
const fiber = (0, finder_1.findComponent)(args);
|
|
803
141
|
if (!fiber)
|
|
804
142
|
return { error: 'Component not found' };
|
|
143
|
+
const propName = typeof args.prop === 'string' ? args.prop : undefined;
|
|
144
|
+
const methodName = typeof args.method === 'string' ? args.method : undefined;
|
|
145
|
+
if ((propName && methodName) || (!propName && !methodName)) {
|
|
146
|
+
return {
|
|
147
|
+
error: 'call requires exactly one of `prop` (callback name) or `method` (ref method name).',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const callArgs = args.args ?? [];
|
|
151
|
+
// Prop-callback path: read prop from memoizedProps, call directly.
|
|
152
|
+
if (propName) {
|
|
153
|
+
const callback = fiber.memoizedProps?.[propName];
|
|
154
|
+
if (typeof callback !== 'function') {
|
|
155
|
+
const availableProps = Object.keys(fiber.memoizedProps ?? {}).filter((key) => {
|
|
156
|
+
return typeof fiber.memoizedProps[key] === 'function';
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
availableProps,
|
|
160
|
+
error: `Component "${(0, utils_1.getComponentName)(fiber)}" has no "${propName}" callback prop`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const result = callback(...callArgs);
|
|
164
|
+
return applyProjection({ component: (0, utils_1.getComponentName)(fiber), prop: propName, result, success: true }, args);
|
|
165
|
+
}
|
|
166
|
+
// Ref-method path: resolve native instance, call method on it.
|
|
805
167
|
const instance = (0, utils_1.getNativeInstance)(fiber);
|
|
806
168
|
if (!instance) {
|
|
807
169
|
return { error: `Component "${(0, utils_1.getComponentName)(fiber)}" has no native instance` };
|
|
808
170
|
}
|
|
809
|
-
const methodName = args.method;
|
|
810
|
-
const methodArgs = args.args;
|
|
811
171
|
const method = instance[methodName];
|
|
812
172
|
if (typeof method !== 'function') {
|
|
813
173
|
return {
|
|
814
174
|
availableMethods: (0, utils_1.getAvailableMethods)(instance),
|
|
815
|
-
error: `No method "${methodName}" on native instance`,
|
|
175
|
+
error: `No method "${methodName}" on native instance of "${(0, utils_1.getComponentName)(fiber)}"`,
|
|
816
176
|
};
|
|
817
177
|
}
|
|
818
178
|
try {
|
|
819
179
|
const bound = method.bind(instance);
|
|
820
|
-
const result = bound(...
|
|
821
|
-
return {
|
|
822
|
-
component: (0, utils_1.getComponentName)(fiber),
|
|
823
|
-
method: methodName,
|
|
824
|
-
result,
|
|
825
|
-
success: true,
|
|
826
|
-
};
|
|
180
|
+
const result = bound(...callArgs);
|
|
181
|
+
return applyProjection({ component: (0, utils_1.getComponentName)(fiber), method: methodName, result, success: true }, args);
|
|
827
182
|
}
|
|
828
183
|
catch (e) {
|
|
829
184
|
return {
|
|
@@ -832,168 +187,21 @@ TIPS
|
|
|
832
187
|
}
|
|
833
188
|
},
|
|
834
189
|
inputSchema: {
|
|
835
|
-
...FIND_SCHEMA,
|
|
836
|
-
|
|
837
|
-
method: {
|
|
838
|
-
description: 'Method name to call.',
|
|
839
|
-
examples: ['focus', 'blur', 'measure'],
|
|
840
|
-
type: 'string',
|
|
841
|
-
},
|
|
842
|
-
},
|
|
843
|
-
},
|
|
844
|
-
get_children: {
|
|
845
|
-
description: 'Get the children subtree of a single component.',
|
|
846
|
-
handler: (args) => {
|
|
847
|
-
const rootError = requireRoot();
|
|
848
|
-
if (rootError)
|
|
849
|
-
return rootError;
|
|
850
|
-
const fiber = findComponent(args);
|
|
851
|
-
if (!fiber)
|
|
852
|
-
return { error: 'Component not found' };
|
|
853
|
-
const depth = args.depth || DEFAULT_DEPTH;
|
|
854
|
-
const serialized = (0, utils_1.serializeFiber)(fiber, depth);
|
|
855
|
-
return serialized?.children ?? [];
|
|
856
|
-
},
|
|
857
|
-
inputSchema: {
|
|
858
|
-
...FIND_SCHEMA,
|
|
859
|
-
depth: { description: 'Max traversal depth (default: 10).', type: 'number' },
|
|
860
|
-
},
|
|
861
|
-
},
|
|
862
|
-
get_component: {
|
|
863
|
-
description: 'Find one component and return its details with children subtree (deep inspection). Use `query` for a flat list of matches.',
|
|
864
|
-
handler: async (args) => {
|
|
865
|
-
const rootError = requireRoot();
|
|
866
|
-
if (rootError)
|
|
867
|
-
return rootError;
|
|
868
|
-
const root = (0, utils_1.getFiberRoot)();
|
|
869
|
-
let fiber = null;
|
|
870
|
-
if (args.mcpId) {
|
|
871
|
-
fiber = (0, utils_1.findByMcpId)(root, args.mcpId);
|
|
872
|
-
}
|
|
873
|
-
else if (args.testID) {
|
|
874
|
-
fiber = (0, utils_1.findByTestID)(root, args.testID);
|
|
875
|
-
}
|
|
876
|
-
else if (args.name) {
|
|
877
|
-
fiber = (0, utils_1.findByName)(root, args.name);
|
|
878
|
-
}
|
|
879
|
-
else if (args.text) {
|
|
880
|
-
fiber = (0, utils_1.findByText)(root, args.text);
|
|
881
|
-
}
|
|
882
|
-
if (!fiber)
|
|
883
|
-
return { error: 'Component not found' };
|
|
884
|
-
const depth = args.depth || DEFAULT_DEPTH;
|
|
885
|
-
const serialized = (0, utils_1.serializeFiber)(fiber, depth);
|
|
886
|
-
if (serialized && Array.isArray(args.select)) {
|
|
887
|
-
const fields = new Set(args.select);
|
|
888
|
-
if (fields.has('bounds')) {
|
|
889
|
-
const bounds = await (0, utils_1.measureFiber)(fiber);
|
|
890
|
-
if (bounds) {
|
|
891
|
-
serialized.bounds = bounds;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
if (!fields.has('props')) {
|
|
895
|
-
serialized.props = {};
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
return serialized;
|
|
899
|
-
},
|
|
900
|
-
inputSchema: {
|
|
901
|
-
depth: { description: 'Max child traversal depth (default: 10).', type: 'number' },
|
|
902
|
-
mcpId: { description: 'Stable data-mcp-id to match.', type: 'string' },
|
|
903
|
-
name: { description: 'Component name to match.', type: 'string' },
|
|
904
|
-
select: {
|
|
905
|
-
description: 'Fields to include on the root node. Available: name, props, bounds. Children are always included.',
|
|
906
|
-
examples: [['name', 'bounds']],
|
|
907
|
-
type: 'array',
|
|
908
|
-
},
|
|
909
|
-
testID: { description: 'testID to match.', type: 'string' },
|
|
910
|
-
text: { description: 'Rendered text substring.', type: 'string' },
|
|
911
|
-
},
|
|
912
|
-
},
|
|
913
|
-
get_props: {
|
|
914
|
-
description: 'Get all props of one component.',
|
|
915
|
-
handler: (args) => {
|
|
916
|
-
const rootError = requireRoot();
|
|
917
|
-
if (rootError)
|
|
918
|
-
return rootError;
|
|
919
|
-
const fiber = findComponent(args);
|
|
920
|
-
if (!fiber)
|
|
921
|
-
return { error: 'Component not found' };
|
|
922
|
-
return {
|
|
923
|
-
name: (0, utils_1.getComponentName)(fiber),
|
|
924
|
-
props: (0, utils_1.serializeProps)(fiber.memoizedProps),
|
|
925
|
-
};
|
|
926
|
-
},
|
|
927
|
-
inputSchema: FIND_SCHEMA,
|
|
928
|
-
},
|
|
929
|
-
get_ref_methods: {
|
|
930
|
-
description: "List available methods on a component's native ref.",
|
|
931
|
-
handler: (args) => {
|
|
932
|
-
const rootError = requireRoot();
|
|
933
|
-
if (rootError)
|
|
934
|
-
return rootError;
|
|
935
|
-
const fiber = findComponent(args);
|
|
936
|
-
if (!fiber)
|
|
937
|
-
return { error: 'Component not found' };
|
|
938
|
-
const instance = (0, utils_1.getNativeInstance)(fiber);
|
|
939
|
-
if (!instance) {
|
|
940
|
-
return { error: `Component "${(0, utils_1.getComponentName)(fiber)}" has no native instance` };
|
|
941
|
-
}
|
|
942
|
-
return {
|
|
943
|
-
component: (0, utils_1.getComponentName)(fiber),
|
|
944
|
-
methods: (0, utils_1.getAvailableMethods)(instance),
|
|
945
|
-
};
|
|
946
|
-
},
|
|
947
|
-
inputSchema: FIND_SCHEMA,
|
|
948
|
-
},
|
|
949
|
-
get_tree: {
|
|
950
|
-
description: 'Dump the full React component tree from the root fiber.',
|
|
951
|
-
handler: (args) => {
|
|
952
|
-
const rootError = requireRoot();
|
|
953
|
-
if (rootError)
|
|
954
|
-
return rootError;
|
|
955
|
-
const root = (0, utils_1.getFiberRoot)();
|
|
956
|
-
const depth = args.depth || DEFAULT_DEPTH;
|
|
957
|
-
return (0, utils_1.serializeFiber)(root, depth);
|
|
958
|
-
},
|
|
959
|
-
inputSchema: {
|
|
960
|
-
depth: { description: 'Max traversal depth (default: 10).', type: 'number' },
|
|
961
|
-
},
|
|
962
|
-
},
|
|
963
|
-
invoke: {
|
|
964
|
-
description: "Call a prop's callback function directly from JS. For simulating a user tap, prefer host__tap_fiber — it runs the real OS gesture pipeline so Pressable feedback, gesture responders, and hit-test behave as under a real finger. invoke still works for any callback when you specifically want the JS-only path (component off-screen, skipping the gesture recognizer, or driving a non-gesture prop), but it is not the default for user-behavior simulation.",
|
|
965
|
-
handler: (args) => {
|
|
966
|
-
const rootError = requireRoot();
|
|
967
|
-
if (rootError)
|
|
968
|
-
return rootError;
|
|
969
|
-
const fiber = findComponent(args);
|
|
970
|
-
if (!fiber)
|
|
971
|
-
return { error: 'Component not found' };
|
|
972
|
-
const callbackName = args.callback;
|
|
973
|
-
const callbackArgs = args.args;
|
|
974
|
-
const callback = fiber.memoizedProps?.[callbackName];
|
|
975
|
-
if (typeof callback !== 'function') {
|
|
976
|
-
const availableCallbacks = Object.keys(fiber.memoizedProps ?? {}).filter((key) => {
|
|
977
|
-
return typeof fiber.memoizedProps[key] === 'function';
|
|
978
|
-
});
|
|
979
|
-
return {
|
|
980
|
-
availableCallbacks,
|
|
981
|
-
error: `Component "${(0, utils_1.getComponentName)(fiber)}" has no "${callbackName}" callback`,
|
|
982
|
-
};
|
|
983
|
-
}
|
|
984
|
-
const result = callback(...(callbackArgs ?? []));
|
|
985
|
-
return { component: (0, utils_1.getComponentName)(fiber), result, success: true };
|
|
986
|
-
},
|
|
987
|
-
inputSchema: {
|
|
988
|
-
...FIND_SCHEMA,
|
|
190
|
+
...finder_1.FIND_SCHEMA,
|
|
191
|
+
...PROJECTION_SCHEMA,
|
|
989
192
|
args: {
|
|
990
|
-
description: 'Arguments passed to the callback.',
|
|
193
|
+
description: 'Arguments passed to the callback / method.',
|
|
991
194
|
examples: [[true], ['text']],
|
|
992
195
|
type: 'array',
|
|
993
196
|
},
|
|
994
|
-
|
|
995
|
-
description: '
|
|
996
|
-
examples: ['
|
|
197
|
+
method: {
|
|
198
|
+
description: 'Native-ref method name. Mutually exclusive with `prop`.',
|
|
199
|
+
examples: ['focus', 'blur', 'measure', 'scrollTo'],
|
|
200
|
+
type: 'string',
|
|
201
|
+
},
|
|
202
|
+
prop: {
|
|
203
|
+
description: 'Callback prop name. Mutually exclusive with `method`.',
|
|
204
|
+
examples: ['onPress', 'onSkip', 'onChangeText'],
|
|
997
205
|
type: 'string',
|
|
998
206
|
},
|
|
999
207
|
},
|
|
@@ -1001,187 +209,130 @@ TIPS
|
|
|
1001
209
|
query: {
|
|
1002
210
|
description: 'Chain-based fiber search. Each step narrows the result set via `scope` + criteria; multiple matches fan out into the next step. Returns { matches, total, truncated? }. Pass `waitFor` to poll until an element appears or disappears (optionally requiring stability for N ms) instead of a single-shot read. See the module description for scope, criteria, select and response reference.',
|
|
1003
211
|
handler: async (args) => {
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
const limit = typeof args.limit === 'number' && args.limit > 0
|
|
1013
|
-
? Math.min(Math.floor(args.limit), QUERY_LIMIT_MAX)
|
|
1014
|
-
: QUERY_LIMIT_DEFAULT;
|
|
1015
|
-
const dedup = args.dedup !== false;
|
|
1016
|
-
const useCacheDefault = args.cache !== false;
|
|
1017
|
-
const onlyVisible = args.onlyVisible === true;
|
|
1018
|
-
const fields = new Set(Array.isArray(args.select) ? args.select : QUERY_DEFAULT_FIELDS);
|
|
1019
|
-
const propsInclude = Array.isArray(args.propsInclude)
|
|
1020
|
-
? new Set(args.propsInclude)
|
|
1021
|
-
: null;
|
|
1022
|
-
const hooksIncludeRaw = args.hooksInclude;
|
|
1023
|
-
const hookKindsSet = hooksIncludeRaw && Array.isArray(hooksIncludeRaw.kinds)
|
|
1024
|
-
? new Set(hooksIncludeRaw.kinds)
|
|
1025
|
-
: null;
|
|
1026
|
-
const hookNameMatchers = hooksIncludeRaw && Array.isArray(hooksIncludeRaw.names)
|
|
1027
|
-
? hooksIncludeRaw.names.map(parseNamePattern)
|
|
1028
|
-
: null;
|
|
1029
|
-
const hookWithValues = hooksIncludeRaw?.withValues === true;
|
|
1030
|
-
const hookMaxDepth = typeof hooksIncludeRaw?.maxDepthInValues === 'number' &&
|
|
1031
|
-
hooksIncludeRaw.maxDepthInValues >= 0
|
|
1032
|
-
? Math.min(Math.floor(hooksIncludeRaw.maxDepthInValues), 8)
|
|
1033
|
-
: HOOK_DEFAULT_MAX_DEPTH;
|
|
1034
|
-
const hookExpansionDepth = typeof hooksIncludeRaw?.expansionDepth === 'number' &&
|
|
1035
|
-
hooksIncludeRaw.expansionDepth >= 0
|
|
1036
|
-
? Math.floor(hooksIncludeRaw.expansionDepth)
|
|
1037
|
-
: Infinity;
|
|
1038
|
-
const hookFormat = hooksIncludeRaw?.format === 'tree' ? 'tree' : 'flat';
|
|
1039
|
-
const runtime = { navigationRef, root };
|
|
1040
|
-
const runOnce = async (useCache) => {
|
|
1041
|
-
const rawMatches = runCachedQuery(runtime, steps, useCache);
|
|
1042
|
-
let all = dedup ? dedupAncestors(rawMatches) : rawMatches;
|
|
1043
|
-
const boundsCache = new Map();
|
|
1044
|
-
const measure = async (fiber) => {
|
|
1045
|
-
if (boundsCache.has(fiber))
|
|
1046
|
-
return boundsCache.get(fiber) ?? null;
|
|
1047
|
-
const b = await (0, utils_1.measureFiber)(fiber);
|
|
1048
|
-
boundsCache.set(fiber, b);
|
|
1049
|
-
return b;
|
|
1050
|
-
};
|
|
1051
|
-
if (onlyVisible) {
|
|
1052
|
-
const visibleRect = getVisibleRect();
|
|
1053
|
-
if (visibleRect) {
|
|
1054
|
-
const rect = visibleRect;
|
|
1055
|
-
const measured = await Promise.all(all.map(async (fiber) => {
|
|
1056
|
-
return { bounds: await measure(fiber), fiber };
|
|
1057
|
-
}));
|
|
1058
|
-
all = measured
|
|
1059
|
-
.filter(({ bounds }) => {
|
|
1060
|
-
return bounds && intersectsRect(bounds, rect);
|
|
1061
|
-
})
|
|
1062
|
-
.map(({ fiber }) => {
|
|
1063
|
-
return fiber;
|
|
1064
|
-
});
|
|
1065
|
-
}
|
|
212
|
+
const inner = async () => {
|
|
213
|
+
const rootError = (0, finder_1.requireRoot)();
|
|
214
|
+
if (rootError)
|
|
215
|
+
return rootError;
|
|
216
|
+
const root = (0, utils_1.getFiberRoot)();
|
|
217
|
+
const steps = args.steps;
|
|
218
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
219
|
+
return { error: 'query requires a non-empty `steps` array' };
|
|
1066
220
|
}
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
result.name = (0, utils_1.getComponentName)(fiber);
|
|
1080
|
-
}
|
|
1081
|
-
if (fields.has('props')) {
|
|
1082
|
-
const full = (0, utils_1.serializeProps)(fiber.memoizedProps);
|
|
1083
|
-
if (propsInclude) {
|
|
1084
|
-
const filtered = {};
|
|
1085
|
-
for (const key of propsInclude) {
|
|
1086
|
-
if (key in full)
|
|
1087
|
-
filtered[key] = full[key];
|
|
1088
|
-
}
|
|
1089
|
-
result.props = filtered;
|
|
1090
|
-
}
|
|
1091
|
-
else {
|
|
1092
|
-
result.props = full;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
if (fields.has('testID')) {
|
|
1096
|
-
result.testID = fiber.memoizedProps?.testID;
|
|
1097
|
-
}
|
|
1098
|
-
if (fields.has('hooks')) {
|
|
1099
|
-
result.hooks = extractHooks(fiber, {
|
|
1100
|
-
expansionDepth: hookExpansionDepth,
|
|
1101
|
-
format: hookFormat,
|
|
1102
|
-
kindsSet: hookKindsSet,
|
|
1103
|
-
maxDepth: hookMaxDepth,
|
|
1104
|
-
nameMatchers: hookNameMatchers,
|
|
1105
|
-
redactPatterns,
|
|
1106
|
-
withValues: hookWithValues,
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
return result;
|
|
1110
|
-
}));
|
|
1111
|
-
return truncated ? { matches, total, truncated: true } : { matches, total };
|
|
1112
|
-
};
|
|
1113
|
-
const waitForRaw = args.waitFor;
|
|
1114
|
-
if (!waitForRaw || typeof waitForRaw !== 'object') {
|
|
1115
|
-
return runOnce(useCacheDefault);
|
|
1116
|
-
}
|
|
1117
|
-
const until = waitForRaw.until;
|
|
1118
|
-
if (until !== 'appear' && until !== 'disappear') {
|
|
1119
|
-
return { error: 'waitFor.until must be "appear" or "disappear"' };
|
|
1120
|
-
}
|
|
1121
|
-
const waitUntil = until;
|
|
1122
|
-
const timeout = Math.min(WAIT_TIMEOUT_MAX, Math.max(0, waitForRaw.timeout ?? WAIT_TIMEOUT_DEFAULT));
|
|
1123
|
-
const interval = Math.max(WAIT_INTERVAL_MIN, waitForRaw.interval ?? WAIT_INTERVAL_DEFAULT);
|
|
1124
|
-
const stable = Math.max(0, waitForRaw.stable ?? 0);
|
|
1125
|
-
const predicate = (total) => {
|
|
1126
|
-
return waitUntil === 'appear' ? total >= 1 : total === 0;
|
|
1127
|
-
};
|
|
1128
|
-
const startedAt = Date.now();
|
|
1129
|
-
const deadline = startedAt + timeout;
|
|
1130
|
-
let attempts = 0;
|
|
1131
|
-
let stableSince = null;
|
|
1132
|
-
let lastResult = await runOnce(false);
|
|
1133
|
-
attempts++;
|
|
1134
|
-
// eslint-disable-next-line no-constant-condition
|
|
1135
|
-
while (true) {
|
|
1136
|
-
const now = Date.now();
|
|
1137
|
-
const elapsedMs = now - startedAt;
|
|
1138
|
-
const met = predicate(lastResult.total);
|
|
1139
|
-
if (met) {
|
|
1140
|
-
if (stable === 0) {
|
|
1141
|
-
return {
|
|
1142
|
-
...lastResult,
|
|
1143
|
-
attempts,
|
|
1144
|
-
elapsedMs,
|
|
1145
|
-
timedOut: false,
|
|
1146
|
-
until: waitUntil,
|
|
1147
|
-
waited: true,
|
|
1148
|
-
};
|
|
1149
|
-
}
|
|
1150
|
-
if (stableSince === null)
|
|
1151
|
-
stableSince = now;
|
|
1152
|
-
if (now - stableSince >= stable) {
|
|
1153
|
-
return {
|
|
1154
|
-
...lastResult,
|
|
1155
|
-
attempts,
|
|
1156
|
-
elapsedMs,
|
|
1157
|
-
stableFor: now - stableSince,
|
|
1158
|
-
timedOut: false,
|
|
1159
|
-
until: waitUntil,
|
|
1160
|
-
waited: true,
|
|
1161
|
-
};
|
|
1162
|
-
}
|
|
221
|
+
const stepError = (0, query_1.validateSteps)(steps);
|
|
222
|
+
if (stepError)
|
|
223
|
+
return { error: stepError };
|
|
224
|
+
const limit = typeof args.limit === 'number' && args.limit > 0
|
|
225
|
+
? Math.min(Math.floor(args.limit), constants_1.QUERY_LIMIT_MAX)
|
|
226
|
+
: constants_1.QUERY_LIMIT_DEFAULT;
|
|
227
|
+
const dedup = args.dedup !== false;
|
|
228
|
+
const useCacheDefault = args.cache !== false;
|
|
229
|
+
const onlyVisible = args.onlyVisible === true;
|
|
230
|
+
let projection;
|
|
231
|
+
try {
|
|
232
|
+
projection = (0, projection_1.parseProjection)(args.select);
|
|
1163
233
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
234
|
+
catch (e) {
|
|
235
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
1166
236
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
237
|
+
const { children: childrenOpts, fields, hooks: hookOpts, props: propsOpts, } = projection;
|
|
238
|
+
const runtime = { navigationRef, root };
|
|
239
|
+
const runOnce = async (useCache) => {
|
|
240
|
+
const rawMatches = runCachedQuery(runtime, steps, useCache);
|
|
241
|
+
let all = dedup ? (0, query_1.dedupAncestors)(rawMatches) : rawMatches;
|
|
242
|
+
const boundsCache = new Map();
|
|
243
|
+
const measure = async (fiber) => {
|
|
244
|
+
if (boundsCache.has(fiber))
|
|
245
|
+
return boundsCache.get(fiber) ?? null;
|
|
246
|
+
const b = await (0, utils_1.measureFiber)(fiber);
|
|
247
|
+
boundsCache.set(fiber, b);
|
|
248
|
+
return b;
|
|
1175
249
|
};
|
|
250
|
+
if (onlyVisible) {
|
|
251
|
+
const visibleRect = (0, viewport_1.getVisibleRect)();
|
|
252
|
+
if (visibleRect) {
|
|
253
|
+
const rect = visibleRect;
|
|
254
|
+
const measured = await Promise.all(all.map(async (fiber) => {
|
|
255
|
+
return { bounds: await measure(fiber), fiber };
|
|
256
|
+
}));
|
|
257
|
+
all = measured
|
|
258
|
+
.filter(({ bounds }) => {
|
|
259
|
+
return bounds && (0, viewport_1.intersectsRect)(bounds, rect);
|
|
260
|
+
})
|
|
261
|
+
.map(({ fiber }) => {
|
|
262
|
+
return fiber;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const total = all.length;
|
|
267
|
+
const truncated = total > limit;
|
|
268
|
+
const picked = truncated ? all.slice(0, limit) : all;
|
|
269
|
+
const matches = await Promise.all(picked.map(async (fiber) => {
|
|
270
|
+
const result = {};
|
|
271
|
+
if (fields.has('bounds')) {
|
|
272
|
+
result.bounds = await measure(fiber);
|
|
273
|
+
}
|
|
274
|
+
if (fields.has('mcpId')) {
|
|
275
|
+
result.mcpId = fiber.memoizedProps?.['data-mcp-id'];
|
|
276
|
+
}
|
|
277
|
+
if (fields.has('name')) {
|
|
278
|
+
result.name = (0, utils_1.getComponentName)(fiber);
|
|
279
|
+
}
|
|
280
|
+
if (fields.has('props')) {
|
|
281
|
+
// Heavy field — projected here via select.props options
|
|
282
|
+
// (path/depth/maxBytes). Top-level response stays raw
|
|
283
|
+
// so mcpId/name/etc are always visible without
|
|
284
|
+
// projection.
|
|
285
|
+
result.props = (0, utils_1.projectFiberValue)(fiber.memoizedProps ?? {}, {
|
|
286
|
+
depth: propsOpts.depth ?? 1,
|
|
287
|
+
maxBytes: propsOpts.maxBytes,
|
|
288
|
+
path: propsOpts.path,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
if (fields.has('refMethods')) {
|
|
292
|
+
// List of native-ref methods available on the fiber's
|
|
293
|
+
// host instance (focus, blur, measure, scrollTo, ...).
|
|
294
|
+
// null when the fiber has no native instance (composite
|
|
295
|
+
// wrappers, unmounted, virtualized). Feeds directly into
|
|
296
|
+
// `fiber_tree__call({ method })`.
|
|
297
|
+
const instance = (0, utils_1.getNativeInstance)(fiber);
|
|
298
|
+
result.refMethods = instance ? (0, utils_1.getAvailableMethods)(instance) : null;
|
|
299
|
+
}
|
|
300
|
+
if (fields.has('testID')) {
|
|
301
|
+
result.testID = fiber.memoizedProps?.testID;
|
|
302
|
+
}
|
|
303
|
+
if (fields.has('hooks')) {
|
|
304
|
+
result.hooks = (0, hooks_1.extractHooks)(fiber, {
|
|
305
|
+
...hookOpts,
|
|
306
|
+
redactPatterns,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
if (childrenOpts) {
|
|
310
|
+
result.children = await (0, children_1.walkChildren)(fiber, childrenOpts, childrenOpts.treeDepth, measure);
|
|
311
|
+
}
|
|
312
|
+
return result;
|
|
313
|
+
}));
|
|
314
|
+
return truncated ? { matches, total, truncated: true } : { matches, total };
|
|
315
|
+
};
|
|
316
|
+
const waitForRaw = args.waitFor;
|
|
317
|
+
if (!waitForRaw || typeof waitForRaw !== 'object') {
|
|
318
|
+
return runOnce(useCacheDefault);
|
|
1176
319
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
320
|
+
// Cache is always bypassed inside the polling loop — `runOnce`
|
|
321
|
+
// with cache:true would just keep returning the stale match
|
|
322
|
+
// set the cache captured pre-mount.
|
|
323
|
+
return (0, waitFor_1.runWaitForLoop)(waitForRaw, () => {
|
|
324
|
+
return runOnce(false);
|
|
1181
325
|
});
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
}
|
|
326
|
+
};
|
|
327
|
+
// No top-level projection on the query response — the response
|
|
328
|
+
// shell ({ matches, total, ... }) is light by construction;
|
|
329
|
+
// heavy values inside `props` / `hooks` are already collapsed to
|
|
330
|
+
// markers by per-field projection in `inner`, and the
|
|
331
|
+
// `select.children` walker self-bounds via treeDepth/itemsCap.
|
|
332
|
+
// Top-level path/depth/maxBytes are not exposed on `query` —
|
|
333
|
+
// drill happens via `select.props.path` / `select.hooks.path`,
|
|
334
|
+
// and tree-shape navigation via `select.children`.
|
|
335
|
+
return inner();
|
|
1185
336
|
},
|
|
1186
337
|
inputSchema: {
|
|
1187
338
|
cache: {
|
|
@@ -1192,40 +343,28 @@ TIPS
|
|
|
1192
343
|
description: 'Drop wrapper cascades — a fiber is removed when any of its ancestors is also in the match set (PressableView → Pressable → View → RCTView collapses to the topmost). Independent siblings with overlapping bounds are kept. Default true; pass false to keep every match.',
|
|
1193
344
|
type: 'boolean',
|
|
1194
345
|
},
|
|
1195
|
-
hooksInclude: {
|
|
1196
|
-
description: 'When select includes "hooks": `kinds` (State | Reducer | Memo | Callback | Ref | Effect | LayoutEffect | InsertionEffect | Context | Transition | DeferredValue | Id | SyncExternalStore | ImperativeHandle | Custom) and `names` (exact or `/regex/flags`) filter the list. Each entry carries { kind, name, hook?, via?, expanded? } — `hook` is the source-level hook function (`useState`, `useAnimatedStyle`); `expanded: true` marks a parent custom-hook call whose sub-hooks follow. Pass `withValues: true` for resolved values. `maxDepthInValues` caps value recursion (default 3, max 8). `expansionDepth` caps how deep custom hooks recurse — 0 = no expansion (top-level only), 1 = one level, default Infinity. `format: "tree"` returns nested `children:` instead of flat `via:`.',
|
|
1197
|
-
examples: [
|
|
1198
|
-
{ kinds: ['State', 'Memo'] },
|
|
1199
|
-
{ names: ['count', 'scrollRef'], withValues: true },
|
|
1200
|
-
{ kinds: ['State'], names: ['/^is/'], withValues: true },
|
|
1201
|
-
{ expansionDepth: 0 },
|
|
1202
|
-
{ format: 'tree', withValues: true },
|
|
1203
|
-
{ expansionDepth: 1, format: 'tree' },
|
|
1204
|
-
],
|
|
1205
|
-
type: 'object',
|
|
1206
|
-
},
|
|
1207
346
|
limit: {
|
|
1208
|
-
description: `Max matches to return (default ${QUERY_LIMIT_DEFAULT}, max ${QUERY_LIMIT_MAX}). truncated: true is added when total exceeds limit.`,
|
|
347
|
+
description: `Max matches to return (default ${constants_1.QUERY_LIMIT_DEFAULT}, max ${constants_1.QUERY_LIMIT_MAX}). truncated: true is added when total exceeds limit.`,
|
|
1209
348
|
type: 'number',
|
|
1210
349
|
},
|
|
1211
350
|
onlyVisible: {
|
|
1212
351
|
description: 'Drop matches whose measured bounds do not intersect the current window rectangle (physical pixels). Also drops fibers with no measurable host view — usually virtualized or unmounted. Halves results on long lists.',
|
|
1213
352
|
type: 'boolean',
|
|
1214
353
|
},
|
|
1215
|
-
propsInclude: {
|
|
1216
|
-
description: 'When select includes "props", keep only these prop names. Unknown keys are silently dropped. Omit for full serialization.',
|
|
1217
|
-
examples: [
|
|
1218
|
-
['placeholder', 'value'],
|
|
1219
|
-
['title', 'disabled'],
|
|
1220
|
-
],
|
|
1221
|
-
type: 'array',
|
|
1222
|
-
},
|
|
1223
354
|
select: {
|
|
1224
|
-
description: `Output fields: mcpId, name, testID, props, bounds, hooks. Default ${JSON.stringify(QUERY_DEFAULT_FIELDS)}
|
|
355
|
+
description: `Output fields: mcpId, name, testID, props, bounds, hooks, refMethods, children. Default ${JSON.stringify(projection_1.QUERY_DEFAULT_FIELDS)}. Each entry is either a string ("mcpId" — include with defaults) or an object whose keys are field names. Object values are \`true\` / \`false\` / per-field options.\n\nLight fields (mcpId, name, testID, bounds, refMethods) — no options, just toggle. refMethods is the list of native-ref methods (focus, blur, measure, scrollTo, ...) available on the fiber's host instance; null when the fiber has no native instance. Feeds directly into \`fiber_tree__call({ method })\`.\n\nHeavy fields (props, hooks) — per-field projection via shared \`projectValue\` so nested heavy values become \`\${...}\`-keyed markers. Each takes its own \`path\` / \`depth\` / \`maxBytes\`.\n\nprops options: \`{ path?, depth?, maxBytes? }\`.\n\nhooks options: \`{ kinds?, names?, withValues?, expansionDepth?, format?, path?, depth?, maxBytes? }\`. \`kinds\`: State | Reducer | Memo | Callback | Ref | Effect | LayoutEffect | InsertionEffect | Context | Transition | DeferredValue | Id | SyncExternalStore | ImperativeHandle | Custom. \`names\`: exact or \`/regex/flags\`. \`withValues:true\` adds resolved values. \`expansionDepth\` caps custom-hook recursion (default Infinity). \`format:"tree"\` returns nested children instead of flat \`via\`.\n\nchildren — recursive light-only walker for tree-of-tree dumps.\n Short form: \`{ children: 5 }\` → treeDepth=5, default fields ['mcpId','name'].\n Object form: \`{ children: { treeDepth?, select?, itemsCap? } }\`.\n treeDepth max 16; itemsCap default 50; overflow inserts \`\${truncated}\` as the first item.\n select inside children may include only mcpId / name / testID / bounds / nested children. props/hooks throw at parse time — run a second query against a child's mcpId to inspect them.\n\nEach hook entry carries \`{ kind, name, hook?, via?, expanded? }\`.`,
|
|
1225
356
|
examples: [
|
|
1226
357
|
['mcpId', 'name', 'bounds'],
|
|
1227
|
-
['mcpId', '
|
|
1228
|
-
['mcpId', '
|
|
358
|
+
['mcpId', 'refMethods'],
|
|
359
|
+
['mcpId', { props: { path: 'style' } }],
|
|
360
|
+
['mcpId', { props: { depth: 3 } }],
|
|
361
|
+
[{ hooks: { kinds: ['State'], withValues: true }, mcpId: true }],
|
|
362
|
+
[{ children: 5 }],
|
|
363
|
+
[
|
|
364
|
+
'mcpId',
|
|
365
|
+
'name',
|
|
366
|
+
{ children: { select: ['mcpId', 'name', 'testID'], treeDepth: 3 } },
|
|
367
|
+
],
|
|
1229
368
|
],
|
|
1230
369
|
type: 'array',
|
|
1231
370
|
},
|
|
@@ -1240,7 +379,7 @@ TIPS
|
|
|
1240
379
|
type: 'array',
|
|
1241
380
|
},
|
|
1242
381
|
waitFor: {
|
|
1243
|
-
description: `Poll the query until a predicate holds, instead of reading once. \`until\` selects the target state: "appear" waits for \`total >= 1\`, "disappear" waits for \`total === 0\`. \`timeout\` (default ${WAIT_TIMEOUT_DEFAULT}ms, max ${WAIT_TIMEOUT_MAX}ms) caps the wait. \`interval\` (default ${WAIT_INTERVAL_DEFAULT}ms, min ${WAIT_INTERVAL_MIN}ms) is the gap between polls. \`stable\` (default 0) requires the predicate to hold continuously for this many ms before returning — useful to ignore transient matches during screen transitions. Cache is always bypassed while polling. On success the response carries the usual query fields plus \`{ waited: true, until, attempts, elapsedMs, timedOut: false, stableFor? }\`; on timeout \`timedOut: true\` with the last observed matches.`,
|
|
382
|
+
description: `Poll the query until a predicate holds, instead of reading once. \`until\` selects the target state: "appear" waits for \`total >= 1\`, "disappear" waits for \`total === 0\`. \`timeout\` (default ${constants_1.WAIT_TIMEOUT_DEFAULT}ms, max ${constants_1.WAIT_TIMEOUT_MAX}ms) caps the wait. \`interval\` (default ${constants_1.WAIT_INTERVAL_DEFAULT}ms, min ${constants_1.WAIT_INTERVAL_MIN}ms) is the gap between polls. \`stable\` (default 0) requires the predicate to hold continuously for this many ms before returning — useful to ignore transient matches during screen transitions. Cache is always bypassed while polling. On success the response carries the usual query fields plus \`{ waited: true, until, attempts, elapsedMs, timedOut: false, stableFor? }\`; on timeout \`timedOut: true\` with the last observed matches.`,
|
|
1244
383
|
examples: [
|
|
1245
384
|
{ until: 'appear' },
|
|
1246
385
|
{ timeout: 5000, until: 'disappear' },
|