agent-react-devtools 0.0.0 → 0.1.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/CHANGELOG.md +34 -0
- package/dist/cli.js +584 -0
- package/dist/cli.js.map +1 -0
- package/dist/daemon.js +1091 -0
- package/dist/daemon.js.map +1 -0
- package/package.json +35 -1
- package/src/__tests__/cli-parser.test.ts +76 -0
- package/src/__tests__/component-tree.test.ts +229 -0
- package/src/__tests__/formatters.test.ts +189 -0
- package/src/__tests__/profiler.test.ts +264 -0
- package/src/cli.ts +315 -0
- package/src/component-tree.ts +495 -0
- package/src/daemon-client.ts +144 -0
- package/src/daemon.ts +275 -0
- package/src/devtools-bridge.ts +391 -0
- package/src/formatters.ts +270 -0
- package/src/profiler.ts +356 -0
- package/src/types.ts +126 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +17 -0
- package/vitest.config.ts +7 -0
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,1091 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/daemon.ts
|
|
4
|
+
import net from "net";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
|
|
8
|
+
// src/devtools-bridge.ts
|
|
9
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
10
|
+
var DevToolsBridge = class {
|
|
11
|
+
wss = null;
|
|
12
|
+
connections = /* @__PURE__ */ new Set();
|
|
13
|
+
port;
|
|
14
|
+
tree;
|
|
15
|
+
profiler;
|
|
16
|
+
pendingInspections = /* @__PURE__ */ new Map();
|
|
17
|
+
pendingProfilingCollect = null;
|
|
18
|
+
rendererIds = /* @__PURE__ */ new Set();
|
|
19
|
+
/** Track which root fiber IDs belong to each WebSocket connection */
|
|
20
|
+
connectionRoots = /* @__PURE__ */ new Map();
|
|
21
|
+
constructor(port2, tree, profiler) {
|
|
22
|
+
this.port = port2;
|
|
23
|
+
this.tree = tree;
|
|
24
|
+
this.profiler = profiler;
|
|
25
|
+
}
|
|
26
|
+
async start() {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
this.wss = new WebSocketServer({ port: this.port }, () => {
|
|
29
|
+
resolve();
|
|
30
|
+
});
|
|
31
|
+
this.wss.on("error", (err) => {
|
|
32
|
+
reject(err);
|
|
33
|
+
});
|
|
34
|
+
this.wss.on("connection", (ws) => {
|
|
35
|
+
this.connections.add(ws);
|
|
36
|
+
ws.on("message", (data) => {
|
|
37
|
+
try {
|
|
38
|
+
const msg = JSON.parse(data.toString());
|
|
39
|
+
this.handleMessage(ws, msg);
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
ws.on("close", () => {
|
|
44
|
+
this.cleanupConnection(ws);
|
|
45
|
+
});
|
|
46
|
+
ws.on("error", () => {
|
|
47
|
+
this.cleanupConnection(ws);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
stop() {
|
|
53
|
+
for (const conn of this.connections) {
|
|
54
|
+
conn.close();
|
|
55
|
+
}
|
|
56
|
+
this.connections.clear();
|
|
57
|
+
if (this.wss) {
|
|
58
|
+
this.wss.close();
|
|
59
|
+
this.wss = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
getConnectedAppCount() {
|
|
63
|
+
return this.connections.size;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Request detailed inspection of a specific element.
|
|
67
|
+
* Sends a request to the React app and waits for the response.
|
|
68
|
+
*/
|
|
69
|
+
inspectElement(id) {
|
|
70
|
+
const node = this.tree.getNode(id);
|
|
71
|
+
if (!node) return Promise.resolve(null);
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
const timer = setTimeout(() => {
|
|
74
|
+
this.pendingInspections.delete(id);
|
|
75
|
+
resolve(null);
|
|
76
|
+
}, 5e3);
|
|
77
|
+
this.pendingInspections.set(id, { resolve, timer });
|
|
78
|
+
this.sendToAll({
|
|
79
|
+
event: "inspectElement",
|
|
80
|
+
payload: {
|
|
81
|
+
id,
|
|
82
|
+
rendererID: node.rendererId,
|
|
83
|
+
forceFullData: true,
|
|
84
|
+
requestID: id,
|
|
85
|
+
path: null
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
startProfiling() {
|
|
91
|
+
this.sendToAll({
|
|
92
|
+
event: "startProfiling",
|
|
93
|
+
payload: { recordChangeDescriptions: true }
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Stop profiling and request data from each renderer.
|
|
98
|
+
* Returns a promise that resolves when profilingData arrives (or 5s timeout).
|
|
99
|
+
*/
|
|
100
|
+
stopProfilingAndCollect() {
|
|
101
|
+
this.sendToAll({
|
|
102
|
+
event: "stopProfiling",
|
|
103
|
+
payload: void 0
|
|
104
|
+
});
|
|
105
|
+
if (this.rendererIds.size === 0) {
|
|
106
|
+
return Promise.resolve();
|
|
107
|
+
}
|
|
108
|
+
for (const rendererID of this.rendererIds) {
|
|
109
|
+
this.sendToAll({
|
|
110
|
+
event: "getProfilingData",
|
|
111
|
+
payload: { rendererID }
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const expected = this.rendererIds.size;
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
const timer = setTimeout(() => {
|
|
117
|
+
this.pendingProfilingCollect = null;
|
|
118
|
+
resolve();
|
|
119
|
+
}, 5e3);
|
|
120
|
+
this.pendingProfilingCollect = { resolve, timer, remaining: expected };
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
handleMessage(ws, msg) {
|
|
124
|
+
switch (msg.event) {
|
|
125
|
+
case "backendInitialized":
|
|
126
|
+
this.sendTo(ws, { event: "getBridgeProtocol", payload: void 0 });
|
|
127
|
+
this.sendTo(ws, { event: "getBackendVersion", payload: void 0 });
|
|
128
|
+
this.sendTo(ws, { event: "getIfHasUnsupportedRendererVersion", payload: void 0 });
|
|
129
|
+
this.sendTo(ws, { event: "getHookSettings", payload: void 0 });
|
|
130
|
+
this.sendTo(ws, { event: "getProfilingStatus", payload: void 0 });
|
|
131
|
+
break;
|
|
132
|
+
case "bridgeProtocol":
|
|
133
|
+
case "backendVersion":
|
|
134
|
+
case "profilingStatus":
|
|
135
|
+
case "overrideComponentFilters":
|
|
136
|
+
break;
|
|
137
|
+
case "operations":
|
|
138
|
+
this.handleOperations(ws, msg.payload);
|
|
139
|
+
break;
|
|
140
|
+
case "inspectedElement":
|
|
141
|
+
this.handleInspectedElement(msg.payload);
|
|
142
|
+
break;
|
|
143
|
+
case "profilingData":
|
|
144
|
+
this.handleProfilingData(msg.payload);
|
|
145
|
+
break;
|
|
146
|
+
case "renderer": {
|
|
147
|
+
const payload = msg.payload;
|
|
148
|
+
this.rendererIds.add(payload.id);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case "rendererAttached": {
|
|
152
|
+
const payload = msg.payload;
|
|
153
|
+
this.rendererIds.add(payload.id);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case "shutdown":
|
|
157
|
+
ws.close();
|
|
158
|
+
break;
|
|
159
|
+
// Silently ignore known but unhandled events
|
|
160
|
+
case "hookSettings":
|
|
161
|
+
case "isBackendStorageAPISupported":
|
|
162
|
+
case "isReactNativeEnvironment":
|
|
163
|
+
case "isReloadAndProfileSupportedByBackend":
|
|
164
|
+
case "isSynchronousXHRSupported":
|
|
165
|
+
case "syncSelectionFromNativeElementsPanel":
|
|
166
|
+
case "unsupportedRendererVersion":
|
|
167
|
+
break;
|
|
168
|
+
default:
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
handleOperations(ws, operations) {
|
|
173
|
+
if (operations.length >= 2) {
|
|
174
|
+
this.rendererIds.add(operations[0]);
|
|
175
|
+
const rootFiberId = operations[1];
|
|
176
|
+
let roots = this.connectionRoots.get(ws);
|
|
177
|
+
if (!roots) {
|
|
178
|
+
roots = /* @__PURE__ */ new Set();
|
|
179
|
+
this.connectionRoots.set(ws, roots);
|
|
180
|
+
}
|
|
181
|
+
roots.add(rootFiberId);
|
|
182
|
+
}
|
|
183
|
+
const added = this.tree.applyOperations(operations);
|
|
184
|
+
if (this.profiler.isActive()) {
|
|
185
|
+
for (const node of added) {
|
|
186
|
+
this.profiler.trackComponent(node.id, node.displayName);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
cleanupConnection(ws) {
|
|
191
|
+
this.connections.delete(ws);
|
|
192
|
+
const roots = this.connectionRoots.get(ws);
|
|
193
|
+
if (roots) {
|
|
194
|
+
for (const rootId of roots) {
|
|
195
|
+
this.tree.removeRoot(rootId);
|
|
196
|
+
}
|
|
197
|
+
this.connectionRoots.delete(ws);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
handleInspectedElement(payload) {
|
|
201
|
+
const data = payload;
|
|
202
|
+
if (data.type !== "full-data" && data.type !== "hydrated-path") {
|
|
203
|
+
const pending2 = this.pendingInspections.get(data.id);
|
|
204
|
+
if (pending2) {
|
|
205
|
+
clearTimeout(pending2.timer);
|
|
206
|
+
this.pendingInspections.delete(data.id);
|
|
207
|
+
pending2.resolve(null);
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const pending = this.pendingInspections.get(data.id);
|
|
212
|
+
if (!pending || !data.value) return;
|
|
213
|
+
clearTimeout(pending.timer);
|
|
214
|
+
this.pendingInspections.delete(data.id);
|
|
215
|
+
const node = this.tree.getNode(data.id);
|
|
216
|
+
const inspected = {
|
|
217
|
+
id: data.id,
|
|
218
|
+
displayName: data.value.displayName || node?.displayName || "Unknown",
|
|
219
|
+
type: node?.type || "other",
|
|
220
|
+
key: data.value.key,
|
|
221
|
+
props: cleanDehydrated(data.value.props),
|
|
222
|
+
state: data.value.state ? cleanDehydrated(data.value.state) : null,
|
|
223
|
+
hooks: data.value.hooks ? parseHooks(data.value.hooks) : null,
|
|
224
|
+
renderedAt: null
|
|
225
|
+
};
|
|
226
|
+
pending.resolve(inspected);
|
|
227
|
+
}
|
|
228
|
+
handleProfilingData(payload) {
|
|
229
|
+
this.profiler.processProfilingData(payload);
|
|
230
|
+
if (this.pendingProfilingCollect) {
|
|
231
|
+
this.pendingProfilingCollect.remaining--;
|
|
232
|
+
if (this.pendingProfilingCollect.remaining <= 0) {
|
|
233
|
+
clearTimeout(this.pendingProfilingCollect.timer);
|
|
234
|
+
const pending = this.pendingProfilingCollect;
|
|
235
|
+
this.pendingProfilingCollect = null;
|
|
236
|
+
pending.resolve();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
sendTo(ws, msg) {
|
|
241
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
242
|
+
ws.send(JSON.stringify(msg));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
sendToAll(msg) {
|
|
246
|
+
const raw = JSON.stringify(msg);
|
|
247
|
+
for (const conn of this.connections) {
|
|
248
|
+
if (conn.readyState === WebSocket.OPEN) {
|
|
249
|
+
conn.send(raw);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
function cleanDehydrated(obj) {
|
|
255
|
+
if (obj === null || obj === void 0) return obj;
|
|
256
|
+
if (typeof obj !== "object") return obj;
|
|
257
|
+
if (Array.isArray(obj)) return obj.map(cleanDehydrated);
|
|
258
|
+
const record = obj;
|
|
259
|
+
if ("type" in record && "preview_short" in record) {
|
|
260
|
+
return record["preview_short"];
|
|
261
|
+
}
|
|
262
|
+
const cleaned = {};
|
|
263
|
+
for (const [key, value] of Object.entries(record)) {
|
|
264
|
+
cleaned[key] = cleanDehydrated(value);
|
|
265
|
+
}
|
|
266
|
+
return cleaned;
|
|
267
|
+
}
|
|
268
|
+
function parseHooks(hooks) {
|
|
269
|
+
return hooks.map((hook) => {
|
|
270
|
+
const h = hook;
|
|
271
|
+
const result = {
|
|
272
|
+
name: h.name,
|
|
273
|
+
value: cleanDehydrated(h.value)
|
|
274
|
+
};
|
|
275
|
+
if (h.subHooks && h.subHooks.length > 0) {
|
|
276
|
+
result.subHooks = parseHooks(h.subHooks);
|
|
277
|
+
}
|
|
278
|
+
return result;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/component-tree.ts
|
|
283
|
+
var TREE_OPERATION_ADD = 1;
|
|
284
|
+
var TREE_OPERATION_REMOVE = 2;
|
|
285
|
+
var TREE_OPERATION_REORDER_CHILDREN = 3;
|
|
286
|
+
var TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
|
|
287
|
+
var TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS = 5;
|
|
288
|
+
var TREE_OPERATION_REMOVE_ROOT = 6;
|
|
289
|
+
var TREE_OPERATION_SET_SUBTREE_MODE = 7;
|
|
290
|
+
var SUSPENSE_TREE_OPERATION_ADD = 8;
|
|
291
|
+
var SUSPENSE_TREE_OPERATION_REMOVE = 9;
|
|
292
|
+
var SUSPENSE_TREE_OPERATION_REORDER_CHILDREN = 10;
|
|
293
|
+
var SUSPENSE_TREE_OPERATION_RESIZE = 11;
|
|
294
|
+
var SUSPENSE_TREE_OPERATION_SUSPENDERS = 12;
|
|
295
|
+
var TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE = 13;
|
|
296
|
+
var ELEMENT_TYPE_CLASS = 1;
|
|
297
|
+
var ELEMENT_TYPE_FUNCTION = 5;
|
|
298
|
+
var ELEMENT_TYPE_FORWARD_REF = 6;
|
|
299
|
+
var ELEMENT_TYPE_HOST = 7;
|
|
300
|
+
var ELEMENT_TYPE_MEMO = 8;
|
|
301
|
+
var ELEMENT_TYPE_PROFILER = 10;
|
|
302
|
+
var ELEMENT_TYPE_ROOT = 11;
|
|
303
|
+
var ELEMENT_TYPE_SUSPENSE = 12;
|
|
304
|
+
function toComponentType(elementType) {
|
|
305
|
+
switch (elementType) {
|
|
306
|
+
case ELEMENT_TYPE_CLASS:
|
|
307
|
+
return "class";
|
|
308
|
+
case ELEMENT_TYPE_FUNCTION:
|
|
309
|
+
return "function";
|
|
310
|
+
case ELEMENT_TYPE_FORWARD_REF:
|
|
311
|
+
return "forwardRef";
|
|
312
|
+
case ELEMENT_TYPE_HOST:
|
|
313
|
+
return "host";
|
|
314
|
+
case ELEMENT_TYPE_MEMO:
|
|
315
|
+
return "memo";
|
|
316
|
+
case ELEMENT_TYPE_PROFILER:
|
|
317
|
+
return "profiler";
|
|
318
|
+
case ELEMENT_TYPE_SUSPENSE:
|
|
319
|
+
return "suspense";
|
|
320
|
+
case ELEMENT_TYPE_ROOT:
|
|
321
|
+
return "other";
|
|
322
|
+
// roots are internal, map to 'other'
|
|
323
|
+
default:
|
|
324
|
+
return "other";
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function skipRects(operations, i) {
|
|
328
|
+
const count = operations[i++];
|
|
329
|
+
if (count === -1) return i;
|
|
330
|
+
return i + count * 4;
|
|
331
|
+
}
|
|
332
|
+
var ComponentTree = class {
|
|
333
|
+
nodes = /* @__PURE__ */ new Map();
|
|
334
|
+
roots = [];
|
|
335
|
+
/** Index: lowercase display name → set of node ids */
|
|
336
|
+
nameIndex = /* @__PURE__ */ new Map();
|
|
337
|
+
/** Label → real node ID (e.g., "@c1" → 10) */
|
|
338
|
+
labelToId = /* @__PURE__ */ new Map();
|
|
339
|
+
/** Real node ID → label */
|
|
340
|
+
idToLabel = /* @__PURE__ */ new Map();
|
|
341
|
+
/**
|
|
342
|
+
* Whether the backend uses the extended ADD format (8 fields with namePropStringID).
|
|
343
|
+
* Auto-detected from the presence of SUSPENSE_TREE_OPERATION opcodes.
|
|
344
|
+
*/
|
|
345
|
+
extendedAddFormat = false;
|
|
346
|
+
applyOperations(operations) {
|
|
347
|
+
if (operations.length < 2) return [];
|
|
348
|
+
const added = [];
|
|
349
|
+
const rendererId = operations[0];
|
|
350
|
+
let i = 2;
|
|
351
|
+
const stringTable = [null];
|
|
352
|
+
const stringTableSize = operations[i++];
|
|
353
|
+
const stringTableEnd = i + stringTableSize;
|
|
354
|
+
while (i < stringTableEnd) {
|
|
355
|
+
const strLen = operations[i++];
|
|
356
|
+
let str = "";
|
|
357
|
+
for (let j = 0; j < strLen; j++) {
|
|
358
|
+
str += String.fromCodePoint(operations[i++]);
|
|
359
|
+
}
|
|
360
|
+
stringTable.push(str);
|
|
361
|
+
}
|
|
362
|
+
while (i < operations.length) {
|
|
363
|
+
const op = operations[i];
|
|
364
|
+
switch (op) {
|
|
365
|
+
case TREE_OPERATION_ADD: {
|
|
366
|
+
const id = operations[i + 1];
|
|
367
|
+
const elementType = operations[i + 2];
|
|
368
|
+
i += 3;
|
|
369
|
+
if (elementType === ELEMENT_TYPE_ROOT) {
|
|
370
|
+
i += 4;
|
|
371
|
+
const node = {
|
|
372
|
+
id,
|
|
373
|
+
displayName: "Root",
|
|
374
|
+
type: "other",
|
|
375
|
+
key: null,
|
|
376
|
+
parentId: null,
|
|
377
|
+
children: [],
|
|
378
|
+
rendererId
|
|
379
|
+
};
|
|
380
|
+
this.nodes.set(id, node);
|
|
381
|
+
added.push({ id, displayName: node.displayName });
|
|
382
|
+
if (!this.roots.includes(id)) {
|
|
383
|
+
this.roots.push(id);
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
const parentId = operations[i++];
|
|
387
|
+
i++;
|
|
388
|
+
const displayNameStringId = operations[i++];
|
|
389
|
+
const keyStringId = operations[i++];
|
|
390
|
+
if (this.extendedAddFormat) {
|
|
391
|
+
i++;
|
|
392
|
+
}
|
|
393
|
+
const displayName = (displayNameStringId > 0 ? stringTable[displayNameStringId] : null) || (elementType === ELEMENT_TYPE_HOST ? "HostComponent" : "Anonymous");
|
|
394
|
+
const key = keyStringId > 0 ? stringTable[keyStringId] || null : null;
|
|
395
|
+
const node = {
|
|
396
|
+
id,
|
|
397
|
+
displayName,
|
|
398
|
+
type: toComponentType(elementType),
|
|
399
|
+
key,
|
|
400
|
+
parentId: parentId === 0 ? null : parentId,
|
|
401
|
+
children: [],
|
|
402
|
+
rendererId
|
|
403
|
+
};
|
|
404
|
+
this.nodes.set(id, node);
|
|
405
|
+
added.push({ id, displayName });
|
|
406
|
+
if (parentId === 0) {
|
|
407
|
+
if (!this.roots.includes(id)) {
|
|
408
|
+
this.roots.push(id);
|
|
409
|
+
}
|
|
410
|
+
} else {
|
|
411
|
+
const parent = this.nodes.get(parentId);
|
|
412
|
+
if (parent) {
|
|
413
|
+
parent.children.push(id);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (displayName) {
|
|
417
|
+
const lower = displayName.toLowerCase();
|
|
418
|
+
let set = this.nameIndex.get(lower);
|
|
419
|
+
if (!set) {
|
|
420
|
+
set = /* @__PURE__ */ new Set();
|
|
421
|
+
this.nameIndex.set(lower, set);
|
|
422
|
+
}
|
|
423
|
+
set.add(id);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
case TREE_OPERATION_REMOVE: {
|
|
429
|
+
const numRemoved = operations[i + 1];
|
|
430
|
+
for (let j = 0; j < numRemoved; j++) {
|
|
431
|
+
const id = operations[i + 2 + j];
|
|
432
|
+
this.removeNode(id);
|
|
433
|
+
}
|
|
434
|
+
i += 2 + numRemoved;
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
case TREE_OPERATION_REORDER_CHILDREN: {
|
|
438
|
+
const id = operations[i + 1];
|
|
439
|
+
const numChildren = operations[i + 2];
|
|
440
|
+
const newChildren = [];
|
|
441
|
+
for (let j = 0; j < numChildren; j++) {
|
|
442
|
+
newChildren.push(operations[i + 3 + j]);
|
|
443
|
+
}
|
|
444
|
+
const node = this.nodes.get(id);
|
|
445
|
+
if (node) {
|
|
446
|
+
node.children = newChildren;
|
|
447
|
+
}
|
|
448
|
+
i += 3 + numChildren;
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
case TREE_OPERATION_UPDATE_TREE_BASE_DURATION: {
|
|
452
|
+
i += 3;
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS: {
|
|
456
|
+
i += 4;
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
case TREE_OPERATION_REMOVE_ROOT: {
|
|
460
|
+
const rootId = operations[i + 1];
|
|
461
|
+
this.removeNode(rootId);
|
|
462
|
+
i += 2;
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
case TREE_OPERATION_SET_SUBTREE_MODE: {
|
|
466
|
+
i += 3;
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
// ── Suspense tree operations (newer backends) ──
|
|
470
|
+
case SUSPENSE_TREE_OPERATION_ADD: {
|
|
471
|
+
this.extendedAddFormat = true;
|
|
472
|
+
i += 5;
|
|
473
|
+
i = skipRects(operations, i);
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
case SUSPENSE_TREE_OPERATION_REMOVE: {
|
|
477
|
+
this.extendedAddFormat = true;
|
|
478
|
+
const numIds = operations[i + 1];
|
|
479
|
+
i += 2 + numIds;
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
case SUSPENSE_TREE_OPERATION_REORDER_CHILDREN: {
|
|
483
|
+
this.extendedAddFormat = true;
|
|
484
|
+
const numSuspenseChildren = operations[i + 2];
|
|
485
|
+
i += 3 + numSuspenseChildren;
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
case SUSPENSE_TREE_OPERATION_RESIZE: {
|
|
489
|
+
this.extendedAddFormat = true;
|
|
490
|
+
i += 2;
|
|
491
|
+
i = skipRects(operations, i);
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
case SUSPENSE_TREE_OPERATION_SUSPENDERS: {
|
|
495
|
+
this.extendedAddFormat = true;
|
|
496
|
+
const numChanges = operations[i + 1];
|
|
497
|
+
i += 2 + numChanges * 4;
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
case TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE: {
|
|
501
|
+
this.extendedAddFormat = true;
|
|
502
|
+
i += 2;
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
default:
|
|
506
|
+
i++;
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return added;
|
|
511
|
+
}
|
|
512
|
+
removeNode(id) {
|
|
513
|
+
const node = this.nodes.get(id);
|
|
514
|
+
if (!node) return;
|
|
515
|
+
if (node.parentId !== null) {
|
|
516
|
+
const parent = this.nodes.get(node.parentId);
|
|
517
|
+
if (parent) {
|
|
518
|
+
parent.children = parent.children.filter((c) => c !== id);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
this.roots = this.roots.filter((r) => r !== id);
|
|
522
|
+
if (node.displayName) {
|
|
523
|
+
const lower = node.displayName.toLowerCase();
|
|
524
|
+
const set = this.nameIndex.get(lower);
|
|
525
|
+
if (set) {
|
|
526
|
+
set.delete(id);
|
|
527
|
+
if (set.size === 0) this.nameIndex.delete(lower);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
for (const childId of node.children) {
|
|
531
|
+
this.removeNode(childId);
|
|
532
|
+
}
|
|
533
|
+
this.nodes.delete(id);
|
|
534
|
+
}
|
|
535
|
+
getNode(id) {
|
|
536
|
+
return this.nodes.get(id);
|
|
537
|
+
}
|
|
538
|
+
getTree(maxDepth) {
|
|
539
|
+
const result = [];
|
|
540
|
+
this.labelToId.clear();
|
|
541
|
+
this.idToLabel.clear();
|
|
542
|
+
let labelCounter = 1;
|
|
543
|
+
const walk = (id, depth) => {
|
|
544
|
+
const node = this.nodes.get(id);
|
|
545
|
+
if (!node) return;
|
|
546
|
+
if (maxDepth !== void 0 && depth > maxDepth) return;
|
|
547
|
+
const label = `@c${labelCounter++}`;
|
|
548
|
+
this.labelToId.set(label, node.id);
|
|
549
|
+
this.idToLabel.set(node.id, label);
|
|
550
|
+
result.push({
|
|
551
|
+
id: node.id,
|
|
552
|
+
label,
|
|
553
|
+
displayName: node.displayName,
|
|
554
|
+
type: node.type,
|
|
555
|
+
key: node.key,
|
|
556
|
+
parentId: node.parentId,
|
|
557
|
+
children: node.children,
|
|
558
|
+
depth
|
|
559
|
+
});
|
|
560
|
+
for (const childId of node.children) {
|
|
561
|
+
walk(childId, depth + 1);
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
for (const rootId of this.roots) {
|
|
565
|
+
walk(rootId, 0);
|
|
566
|
+
}
|
|
567
|
+
return result;
|
|
568
|
+
}
|
|
569
|
+
findByName(name, exact) {
|
|
570
|
+
const results = [];
|
|
571
|
+
if (exact) {
|
|
572
|
+
const lower = name.toLowerCase();
|
|
573
|
+
const ids = this.nameIndex.get(lower);
|
|
574
|
+
if (ids) {
|
|
575
|
+
for (const id of ids) {
|
|
576
|
+
const node = this.nodes.get(id);
|
|
577
|
+
if (node && node.displayName.toLowerCase() === lower) {
|
|
578
|
+
results.push(this.toTreeNode(node));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
} else {
|
|
583
|
+
const lower = name.toLowerCase();
|
|
584
|
+
for (const [indexName, ids] of this.nameIndex) {
|
|
585
|
+
if (indexName.includes(lower)) {
|
|
586
|
+
for (const id of ids) {
|
|
587
|
+
const node = this.nodes.get(id);
|
|
588
|
+
if (node) {
|
|
589
|
+
results.push(this.toTreeNode(node));
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return results;
|
|
596
|
+
}
|
|
597
|
+
getComponentCount() {
|
|
598
|
+
return this.nodes.size;
|
|
599
|
+
}
|
|
600
|
+
getCountByType() {
|
|
601
|
+
const counts = {};
|
|
602
|
+
for (const node of this.nodes.values()) {
|
|
603
|
+
counts[node.type] = (counts[node.type] || 0) + 1;
|
|
604
|
+
}
|
|
605
|
+
return counts;
|
|
606
|
+
}
|
|
607
|
+
getAllNodeIds() {
|
|
608
|
+
return Array.from(this.nodes.keys());
|
|
609
|
+
}
|
|
610
|
+
getRootIds() {
|
|
611
|
+
return [...this.roots];
|
|
612
|
+
}
|
|
613
|
+
removeRoot(rootId) {
|
|
614
|
+
this.removeNode(rootId);
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Resolve a label like "@c3" to a real node ID.
|
|
618
|
+
* Returns undefined if label not found.
|
|
619
|
+
*/
|
|
620
|
+
resolveLabel(label) {
|
|
621
|
+
return this.labelToId.get(label);
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Resolve either a label string ("@c3") or a numeric ID to a real node ID.
|
|
625
|
+
*/
|
|
626
|
+
resolveId(id) {
|
|
627
|
+
if (typeof id === "number") return id;
|
|
628
|
+
if (id.startsWith("@c")) return this.labelToId.get(id);
|
|
629
|
+
const num = parseInt(id, 10);
|
|
630
|
+
return isNaN(num) ? void 0 : num;
|
|
631
|
+
}
|
|
632
|
+
toTreeNode(node) {
|
|
633
|
+
let depth = 0;
|
|
634
|
+
let current = node;
|
|
635
|
+
while (current.parentId !== null) {
|
|
636
|
+
depth++;
|
|
637
|
+
const parent = this.nodes.get(current.parentId);
|
|
638
|
+
if (!parent) break;
|
|
639
|
+
current = parent;
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
id: node.id,
|
|
643
|
+
label: this.idToLabel.get(node.id) || `@c?`,
|
|
644
|
+
displayName: node.displayName,
|
|
645
|
+
type: node.type,
|
|
646
|
+
key: node.key,
|
|
647
|
+
parentId: node.parentId,
|
|
648
|
+
children: node.children,
|
|
649
|
+
depth
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
// src/profiler.ts
|
|
655
|
+
var Profiler = class {
|
|
656
|
+
session = null;
|
|
657
|
+
/** Display names captured during profiling (survives unmounts) */
|
|
658
|
+
displayNames = /* @__PURE__ */ new Map();
|
|
659
|
+
isActive() {
|
|
660
|
+
return this.session !== null && this.session.stoppedAt === null;
|
|
661
|
+
}
|
|
662
|
+
start(name) {
|
|
663
|
+
this.displayNames.clear();
|
|
664
|
+
this.session = {
|
|
665
|
+
name: name || `session-${Date.now()}`,
|
|
666
|
+
startedAt: Date.now(),
|
|
667
|
+
stoppedAt: null,
|
|
668
|
+
commits: []
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
/** Cache a component's display name (call during profiling to survive unmounts) */
|
|
672
|
+
trackComponent(id, displayName) {
|
|
673
|
+
this.displayNames.set(id, displayName);
|
|
674
|
+
}
|
|
675
|
+
stop(tree) {
|
|
676
|
+
if (!this.session) return null;
|
|
677
|
+
this.session.stoppedAt = Date.now();
|
|
678
|
+
const duration = this.session.stoppedAt - this.session.startedAt;
|
|
679
|
+
const renderCounts = /* @__PURE__ */ new Map();
|
|
680
|
+
for (const commit of this.session.commits) {
|
|
681
|
+
for (const [id] of commit.fiberActualDurations) {
|
|
682
|
+
renderCounts.set(id, (renderCounts.get(id) || 0) + 1);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const componentRenderCounts = Array.from(renderCounts.entries()).map(([id, count]) => ({
|
|
686
|
+
id,
|
|
687
|
+
displayName: tree?.getNode(id)?.displayName || this.displayNames.get(id) || "",
|
|
688
|
+
count
|
|
689
|
+
})).sort((a, b) => b.count - a.count);
|
|
690
|
+
return {
|
|
691
|
+
name: this.session.name,
|
|
692
|
+
duration,
|
|
693
|
+
commitCount: this.session.commits.length,
|
|
694
|
+
componentRenderCounts
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Process profiling data sent from React DevTools.
|
|
699
|
+
*
|
|
700
|
+
* The data format varies between React versions. We handle the common
|
|
701
|
+
* format where each commit contains:
|
|
702
|
+
* - commitTime
|
|
703
|
+
* - duration
|
|
704
|
+
* - fiberActualDurations: [id, duration, ...]
|
|
705
|
+
* - fiberSelfDurations: [id, duration, ...]
|
|
706
|
+
* - changeDescriptions: Map<id, description>
|
|
707
|
+
*/
|
|
708
|
+
processProfilingData(payload) {
|
|
709
|
+
if (!this.session || this.session.stoppedAt !== null) return;
|
|
710
|
+
const data = payload;
|
|
711
|
+
const roots = data?.dataForRoots;
|
|
712
|
+
if (roots) {
|
|
713
|
+
for (const root of roots) {
|
|
714
|
+
if (root.commitData) {
|
|
715
|
+
for (const commitData of root.commitData) {
|
|
716
|
+
this.processCommitData(commitData);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (data?.commitData) {
|
|
723
|
+
for (const commitData of data.commitData) {
|
|
724
|
+
this.processCommitData(commitData);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
processCommitData(commitData) {
|
|
729
|
+
const commit = {
|
|
730
|
+
timestamp: commitData.timestamp || Date.now(),
|
|
731
|
+
duration: commitData.duration || 0,
|
|
732
|
+
fiberActualDurations: /* @__PURE__ */ new Map(),
|
|
733
|
+
fiberSelfDurations: /* @__PURE__ */ new Map(),
|
|
734
|
+
changeDescriptions: /* @__PURE__ */ new Map()
|
|
735
|
+
};
|
|
736
|
+
if (commitData.fiberActualDurations) {
|
|
737
|
+
parseDurations(commitData.fiberActualDurations, commit.fiberActualDurations);
|
|
738
|
+
}
|
|
739
|
+
if (commitData.fiberSelfDurations) {
|
|
740
|
+
parseDurations(commitData.fiberSelfDurations, commit.fiberSelfDurations);
|
|
741
|
+
}
|
|
742
|
+
if (commitData.changeDescriptions) {
|
|
743
|
+
const entries = commitData.changeDescriptions instanceof Map ? commitData.changeDescriptions.entries() : commitData.changeDescriptions[Symbol.iterator]();
|
|
744
|
+
for (const [id, desc] of entries) {
|
|
745
|
+
const d = desc;
|
|
746
|
+
commit.changeDescriptions.set(id, {
|
|
747
|
+
didHooksChange: d.didHooksChange || false,
|
|
748
|
+
isFirstMount: d.isFirstMount || false,
|
|
749
|
+
props: d.props || null,
|
|
750
|
+
state: d.state || null,
|
|
751
|
+
hooks: d.hooks || null
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
this.session.commits.push(commit);
|
|
756
|
+
}
|
|
757
|
+
getReport(componentId, tree) {
|
|
758
|
+
if (!this.session) return null;
|
|
759
|
+
const node = tree.getNode(componentId);
|
|
760
|
+
let renderCount = 0;
|
|
761
|
+
let totalDuration = 0;
|
|
762
|
+
let maxDuration = 0;
|
|
763
|
+
const causeSet = /* @__PURE__ */ new Set();
|
|
764
|
+
for (const commit of this.session.commits) {
|
|
765
|
+
const duration = commit.fiberActualDurations.get(componentId);
|
|
766
|
+
if (duration !== void 0) {
|
|
767
|
+
renderCount++;
|
|
768
|
+
totalDuration += duration;
|
|
769
|
+
if (duration > maxDuration) maxDuration = duration;
|
|
770
|
+
const desc = commit.changeDescriptions.get(componentId);
|
|
771
|
+
if (desc) {
|
|
772
|
+
for (const cause of describeCauses(desc)) {
|
|
773
|
+
causeSet.add(cause);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (renderCount === 0) return null;
|
|
779
|
+
return {
|
|
780
|
+
id: componentId,
|
|
781
|
+
displayName: node?.displayName || this.displayNames.get(componentId) || `Component#${componentId}`,
|
|
782
|
+
renderCount,
|
|
783
|
+
totalDuration,
|
|
784
|
+
avgDuration: totalDuration / renderCount,
|
|
785
|
+
maxDuration,
|
|
786
|
+
causes: Array.from(causeSet)
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
getSlowest(tree, limit = 10) {
|
|
790
|
+
return this.getAllReports(tree).sort((a, b) => b.avgDuration - a.avgDuration).slice(0, limit);
|
|
791
|
+
}
|
|
792
|
+
getMostRerenders(tree, limit = 10) {
|
|
793
|
+
return this.getAllReports(tree).sort((a, b) => b.renderCount - a.renderCount).slice(0, limit);
|
|
794
|
+
}
|
|
795
|
+
getCommitDetails(index, tree, limit = 10) {
|
|
796
|
+
if (!this.session) return null;
|
|
797
|
+
if (index < 0 || index >= this.session.commits.length) return null;
|
|
798
|
+
const commit = this.session.commits[index];
|
|
799
|
+
const components = [];
|
|
800
|
+
for (const [id, actualDuration] of commit.fiberActualDurations) {
|
|
801
|
+
const selfDuration = commit.fiberSelfDurations.get(id) || 0;
|
|
802
|
+
const desc = commit.changeDescriptions.get(id);
|
|
803
|
+
components.push({
|
|
804
|
+
id,
|
|
805
|
+
displayName: tree.getNode(id)?.displayName || this.displayNames.get(id) || `Component#${id}`,
|
|
806
|
+
actualDuration,
|
|
807
|
+
selfDuration,
|
|
808
|
+
causes: desc ? describeCauses(desc) : []
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
components.sort((a, b) => b.selfDuration - a.selfDuration);
|
|
812
|
+
const totalCount = components.length;
|
|
813
|
+
return {
|
|
814
|
+
index,
|
|
815
|
+
timestamp: commit.timestamp,
|
|
816
|
+
duration: commit.duration,
|
|
817
|
+
components: limit > 0 ? components.slice(0, limit) : components,
|
|
818
|
+
totalComponents: totalCount
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
getTimeline(limit) {
|
|
822
|
+
if (!this.session) return [];
|
|
823
|
+
const entries = this.session.commits.map((commit, index) => ({
|
|
824
|
+
index,
|
|
825
|
+
timestamp: commit.timestamp,
|
|
826
|
+
duration: commit.duration,
|
|
827
|
+
componentCount: commit.fiberActualDurations.size
|
|
828
|
+
}));
|
|
829
|
+
if (limit) return entries.slice(0, limit);
|
|
830
|
+
return entries;
|
|
831
|
+
}
|
|
832
|
+
getAllReports(tree) {
|
|
833
|
+
if (!this.session) return [];
|
|
834
|
+
const componentIds = /* @__PURE__ */ new Set();
|
|
835
|
+
for (const commit of this.session.commits) {
|
|
836
|
+
for (const id of commit.fiberActualDurations.keys()) {
|
|
837
|
+
componentIds.add(id);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
const reports = [];
|
|
841
|
+
for (const id of componentIds) {
|
|
842
|
+
const report = this.getReport(id, tree);
|
|
843
|
+
if (report) reports.push(report);
|
|
844
|
+
}
|
|
845
|
+
return reports;
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
function parseDurations(raw, target) {
|
|
849
|
+
if (raw.length === 0) return;
|
|
850
|
+
if (Array.isArray(raw[0])) {
|
|
851
|
+
for (const [id, duration] of raw) {
|
|
852
|
+
target.set(id, duration);
|
|
853
|
+
}
|
|
854
|
+
} else {
|
|
855
|
+
const flat = raw;
|
|
856
|
+
for (let i = 0; i < flat.length; i += 2) {
|
|
857
|
+
target.set(flat[i], flat[i + 1]);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
function describeCauses(desc) {
|
|
862
|
+
const causes = [];
|
|
863
|
+
if (desc.isFirstMount) {
|
|
864
|
+
causes.push("first-mount");
|
|
865
|
+
return causes;
|
|
866
|
+
}
|
|
867
|
+
if (desc.props && desc.props.length > 0) causes.push("props-changed");
|
|
868
|
+
if (desc.state && desc.state.length > 0) causes.push("state-changed");
|
|
869
|
+
if (desc.didHooksChange) causes.push("hooks-changed");
|
|
870
|
+
if (causes.length === 0) causes.push("parent-rendered");
|
|
871
|
+
return causes;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// src/daemon.ts
|
|
875
|
+
var DEFAULT_STATE_DIR = path.join(
|
|
876
|
+
process.env.HOME || process.env.USERPROFILE || "/tmp",
|
|
877
|
+
".agent-react-devtools"
|
|
878
|
+
);
|
|
879
|
+
var STATE_DIR = DEFAULT_STATE_DIR;
|
|
880
|
+
function getSocketPath() {
|
|
881
|
+
return path.join(STATE_DIR, "daemon.sock");
|
|
882
|
+
}
|
|
883
|
+
function getDaemonInfoPath() {
|
|
884
|
+
return path.join(STATE_DIR, "daemon.json");
|
|
885
|
+
}
|
|
886
|
+
var Daemon = class {
|
|
887
|
+
ipcServer = null;
|
|
888
|
+
bridge;
|
|
889
|
+
tree;
|
|
890
|
+
profiler;
|
|
891
|
+
port;
|
|
892
|
+
startedAt = Date.now();
|
|
893
|
+
constructor(port2) {
|
|
894
|
+
this.port = port2;
|
|
895
|
+
this.tree = new ComponentTree();
|
|
896
|
+
this.profiler = new Profiler();
|
|
897
|
+
this.bridge = new DevToolsBridge(port2, this.tree, this.profiler);
|
|
898
|
+
}
|
|
899
|
+
async start() {
|
|
900
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
901
|
+
const socketPath = getSocketPath();
|
|
902
|
+
if (fs.existsSync(socketPath)) {
|
|
903
|
+
try {
|
|
904
|
+
fs.unlinkSync(socketPath);
|
|
905
|
+
} catch {
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
await this.bridge.start();
|
|
909
|
+
await this.startIpc(socketPath);
|
|
910
|
+
const info = {
|
|
911
|
+
pid: process.pid,
|
|
912
|
+
port: this.port,
|
|
913
|
+
socketPath,
|
|
914
|
+
startedAt: this.startedAt
|
|
915
|
+
};
|
|
916
|
+
fs.writeFileSync(getDaemonInfoPath(), JSON.stringify(info, null, 2));
|
|
917
|
+
console.log(`Daemon started (pid=${process.pid}, port=${this.port})`);
|
|
918
|
+
const shutdown = () => {
|
|
919
|
+
this.stop();
|
|
920
|
+
process.exit(0);
|
|
921
|
+
};
|
|
922
|
+
process.on("SIGTERM", shutdown);
|
|
923
|
+
process.on("SIGINT", shutdown);
|
|
924
|
+
}
|
|
925
|
+
startIpc(socketPath) {
|
|
926
|
+
return new Promise((resolve, reject) => {
|
|
927
|
+
this.ipcServer = net.createServer((conn) => {
|
|
928
|
+
let buffer = "";
|
|
929
|
+
conn.on("data", (chunk) => {
|
|
930
|
+
buffer += chunk.toString();
|
|
931
|
+
let newlineIdx;
|
|
932
|
+
while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
|
|
933
|
+
const line = buffer.slice(0, newlineIdx);
|
|
934
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
935
|
+
try {
|
|
936
|
+
const cmd = JSON.parse(line);
|
|
937
|
+
this.handleCommand(cmd).then((response) => {
|
|
938
|
+
conn.write(JSON.stringify(response) + "\n");
|
|
939
|
+
});
|
|
940
|
+
} catch {
|
|
941
|
+
const response = {
|
|
942
|
+
ok: false,
|
|
943
|
+
error: "Invalid JSON"
|
|
944
|
+
};
|
|
945
|
+
conn.write(JSON.stringify(response) + "\n");
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
this.ipcServer.on("error", reject);
|
|
951
|
+
this.ipcServer.listen(socketPath, () => {
|
|
952
|
+
resolve();
|
|
953
|
+
});
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
async handleCommand(cmd) {
|
|
957
|
+
try {
|
|
958
|
+
switch (cmd.type) {
|
|
959
|
+
case "ping":
|
|
960
|
+
return { ok: true, data: "pong" };
|
|
961
|
+
case "status":
|
|
962
|
+
return {
|
|
963
|
+
ok: true,
|
|
964
|
+
data: {
|
|
965
|
+
daemonRunning: true,
|
|
966
|
+
port: this.port,
|
|
967
|
+
connectedApps: this.bridge.getConnectedAppCount(),
|
|
968
|
+
componentCount: this.tree.getComponentCount(),
|
|
969
|
+
profilingActive: this.profiler.isActive(),
|
|
970
|
+
uptime: Date.now() - this.startedAt
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
case "get-tree":
|
|
974
|
+
return {
|
|
975
|
+
ok: true,
|
|
976
|
+
data: this.tree.getTree(cmd.depth)
|
|
977
|
+
};
|
|
978
|
+
case "get-component": {
|
|
979
|
+
const resolvedId = this.tree.resolveId(cmd.id);
|
|
980
|
+
if (resolvedId === void 0) {
|
|
981
|
+
return { ok: false, error: `Component ${cmd.id} not found` };
|
|
982
|
+
}
|
|
983
|
+
const element = await this.bridge.inspectElement(resolvedId);
|
|
984
|
+
if (!element) {
|
|
985
|
+
return { ok: false, error: `Component ${cmd.id} not found` };
|
|
986
|
+
}
|
|
987
|
+
const label = typeof cmd.id === "string" ? cmd.id : void 0;
|
|
988
|
+
return { ok: true, data: element, label };
|
|
989
|
+
}
|
|
990
|
+
case "find":
|
|
991
|
+
return {
|
|
992
|
+
ok: true,
|
|
993
|
+
data: this.tree.findByName(cmd.name, cmd.exact)
|
|
994
|
+
};
|
|
995
|
+
case "count":
|
|
996
|
+
return {
|
|
997
|
+
ok: true,
|
|
998
|
+
data: this.tree.getCountByType()
|
|
999
|
+
};
|
|
1000
|
+
case "profile-start":
|
|
1001
|
+
this.profiler.start(cmd.name);
|
|
1002
|
+
for (const id of this.tree.getAllNodeIds()) {
|
|
1003
|
+
const node = this.tree.getNode(id);
|
|
1004
|
+
if (node) this.profiler.trackComponent(id, node.displayName);
|
|
1005
|
+
}
|
|
1006
|
+
this.bridge.startProfiling();
|
|
1007
|
+
return { ok: true, data: "Profiling started" };
|
|
1008
|
+
case "profile-stop": {
|
|
1009
|
+
await this.bridge.stopProfilingAndCollect();
|
|
1010
|
+
const session = this.profiler.stop(this.tree);
|
|
1011
|
+
if (!session) {
|
|
1012
|
+
return { ok: false, error: "No active profiling session" };
|
|
1013
|
+
}
|
|
1014
|
+
return { ok: true, data: session };
|
|
1015
|
+
}
|
|
1016
|
+
case "profile-report": {
|
|
1017
|
+
const resolvedCompId = this.tree.resolveId(cmd.componentId);
|
|
1018
|
+
if (resolvedCompId === void 0) {
|
|
1019
|
+
return { ok: false, error: `Component ${cmd.componentId} not found` };
|
|
1020
|
+
}
|
|
1021
|
+
const report = this.profiler.getReport(resolvedCompId, this.tree);
|
|
1022
|
+
if (!report) {
|
|
1023
|
+
return {
|
|
1024
|
+
ok: false,
|
|
1025
|
+
error: `No profiling data for component ${cmd.componentId}`
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
const compLabel = typeof cmd.componentId === "string" ? cmd.componentId : void 0;
|
|
1029
|
+
return { ok: true, data: report, label: compLabel };
|
|
1030
|
+
}
|
|
1031
|
+
case "profile-slow":
|
|
1032
|
+
return {
|
|
1033
|
+
ok: true,
|
|
1034
|
+
data: this.profiler.getSlowest(this.tree, cmd.limit)
|
|
1035
|
+
};
|
|
1036
|
+
case "profile-rerenders":
|
|
1037
|
+
return {
|
|
1038
|
+
ok: true,
|
|
1039
|
+
data: this.profiler.getMostRerenders(this.tree, cmd.limit)
|
|
1040
|
+
};
|
|
1041
|
+
case "profile-timeline":
|
|
1042
|
+
return {
|
|
1043
|
+
ok: true,
|
|
1044
|
+
data: this.profiler.getTimeline(cmd.limit)
|
|
1045
|
+
};
|
|
1046
|
+
case "profile-commit": {
|
|
1047
|
+
const detail = this.profiler.getCommitDetails(cmd.index, this.tree, cmd.limit);
|
|
1048
|
+
if (!detail) {
|
|
1049
|
+
return { ok: false, error: `Commit #${cmd.index} not found` };
|
|
1050
|
+
}
|
|
1051
|
+
return { ok: true, data: detail };
|
|
1052
|
+
}
|
|
1053
|
+
default:
|
|
1054
|
+
return { ok: false, error: `Unknown command: ${cmd.type}` };
|
|
1055
|
+
}
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
return {
|
|
1058
|
+
ok: false,
|
|
1059
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
stop() {
|
|
1064
|
+
this.bridge.stop();
|
|
1065
|
+
if (this.ipcServer) {
|
|
1066
|
+
this.ipcServer.close();
|
|
1067
|
+
this.ipcServer = null;
|
|
1068
|
+
}
|
|
1069
|
+
try {
|
|
1070
|
+
fs.unlinkSync(getSocketPath());
|
|
1071
|
+
} catch {
|
|
1072
|
+
}
|
|
1073
|
+
try {
|
|
1074
|
+
fs.unlinkSync(getDaemonInfoPath());
|
|
1075
|
+
} catch {
|
|
1076
|
+
}
|
|
1077
|
+
console.log("Daemon stopped");
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
var portArg = process.argv.find((a) => a.startsWith("--port="));
|
|
1081
|
+
var port = portArg ? parseInt(portArg.split("=")[1], 10) : 8097;
|
|
1082
|
+
var stateDirArg = process.argv.find((a) => a.startsWith("--state-dir="));
|
|
1083
|
+
if (stateDirArg) {
|
|
1084
|
+
STATE_DIR = stateDirArg.split("=")[1];
|
|
1085
|
+
}
|
|
1086
|
+
var daemon = new Daemon(port);
|
|
1087
|
+
daemon.start().catch((err) => {
|
|
1088
|
+
console.error("Failed to start daemon:", err);
|
|
1089
|
+
process.exit(1);
|
|
1090
|
+
});
|
|
1091
|
+
//# sourceMappingURL=daemon.js.map
|