what-devtools 0.8.4 → 0.10.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/package.json +2 -2
- package/src/index.js +149 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "what-devtools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Dev tools for What Framework — signal inspector, component tree, effect graph",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"inspector"
|
|
21
21
|
],
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"what-core": "^0.
|
|
23
|
+
"what-core": "^0.10.0"
|
|
24
24
|
},
|
|
25
25
|
"author": "ZVN DEV (https://zvndev.com)",
|
|
26
26
|
"license": "MIT",
|
package/src/index.js
CHANGED
|
@@ -27,6 +27,63 @@ const components = new Map(); // id → { name, element, mountedAt, parentId }
|
|
|
27
27
|
// Reverse lookup: subscriber Set → signal ID (O(1) dep resolution)
|
|
28
28
|
const subsToSignalId = new WeakMap();
|
|
29
29
|
|
|
30
|
+
// Set of known component function names (added on registerComponent, kept
|
|
31
|
+
// forever even after unmount so post-mortem signals still attribute).
|
|
32
|
+
// Used by attributeToComponent() to recognise component frames in stack traces.
|
|
33
|
+
const knownComponentNames = new Set();
|
|
34
|
+
|
|
35
|
+
// name -> Set<componentId>. When multiple components share a name we cannot
|
|
36
|
+
// disambiguate via stack; the heuristic falls back to the most-recently-mounted.
|
|
37
|
+
const componentsByName = new Map();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Heuristic attribution: parse new Error().stack to find the most recently
|
|
41
|
+
* called component frame. Returns a componentId or null.
|
|
42
|
+
*
|
|
43
|
+
* Why a stack heuristic instead of a hook:
|
|
44
|
+
* - We cannot add bracketing hooks to packages/core (compiler agent owns it).
|
|
45
|
+
* - `onComponentMount` fires before the component body runs; there's no
|
|
46
|
+
* `onComponentRendered` paired event.
|
|
47
|
+
* - Signal/effect creation happens DURING the component body, so its
|
|
48
|
+
* stack always contains a frame named after the component function.
|
|
49
|
+
*
|
|
50
|
+
* Limitations:
|
|
51
|
+
* - Anonymous components (no function name) cannot be matched.
|
|
52
|
+
* - Arrow components bound to a const get the const name in V8 stacks.
|
|
53
|
+
* - In production builds without source maps, function names may be mangled
|
|
54
|
+
* — but installDevTools should only run in dev anyway.
|
|
55
|
+
*/
|
|
56
|
+
function attributeToComponent() {
|
|
57
|
+
if (knownComponentNames.size === 0) return null;
|
|
58
|
+
let stack;
|
|
59
|
+
try { throw new Error(); } catch (e) { stack = e.stack; }
|
|
60
|
+
if (!stack) return null;
|
|
61
|
+
// Walk stack from innermost to outermost. The FIRST component frame we
|
|
62
|
+
// hit is the deepest (currently-running) component.
|
|
63
|
+
const lines = stack.split('\n');
|
|
64
|
+
for (const rawLine of lines) {
|
|
65
|
+
const line = rawLine.trim();
|
|
66
|
+
if (!line.startsWith('at ')) continue;
|
|
67
|
+
// Extract function name token. Handles `at Foo`, `at Object.Foo`,
|
|
68
|
+
// `at new Foo`, `at Foo.bar`, `at Foo (file:line:col)`.
|
|
69
|
+
const m = line.match(/^at\s+(?:new\s+)?([\w$.]+)/);
|
|
70
|
+
if (!m) continue;
|
|
71
|
+
const tokens = m[1].split('.');
|
|
72
|
+
for (const tok of tokens) {
|
|
73
|
+
if (knownComponentNames.has(tok)) {
|
|
74
|
+
const ids = componentsByName.get(tok);
|
|
75
|
+
if (ids && ids.size > 0) {
|
|
76
|
+
// Most-recently-mounted wins.
|
|
77
|
+
let max = -1;
|
|
78
|
+
for (const id of ids) if (id > max) max = id;
|
|
79
|
+
return max;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
30
87
|
// Error log (capped at 100)
|
|
31
88
|
const errors = [];
|
|
32
89
|
const MAX_ERRORS = 100;
|
|
@@ -135,6 +192,9 @@ export function registerSignal(sig, name) {
|
|
|
135
192
|
ref: sig,
|
|
136
193
|
createdAt: Date.now(),
|
|
137
194
|
internal: false,
|
|
195
|
+
// P1-6: attribute to the component currently executing, so what_explain
|
|
196
|
+
// can show component-local signals instead of always returning [].
|
|
197
|
+
componentId: attributeToComponent(),
|
|
138
198
|
};
|
|
139
199
|
signals.set(id, entry);
|
|
140
200
|
sig._devId = id;
|
|
@@ -181,6 +241,8 @@ export function registerEffect(e, name) {
|
|
|
181
241
|
depSignalIds: [],
|
|
182
242
|
runCount: 0,
|
|
183
243
|
lastRunAt: null,
|
|
244
|
+
// P1-6: attribute to the component currently executing.
|
|
245
|
+
componentId: attributeToComponent(),
|
|
184
246
|
};
|
|
185
247
|
effects.set(id, entry);
|
|
186
248
|
e._devId = id;
|
|
@@ -241,6 +303,23 @@ function captureError(err, context) {
|
|
|
241
303
|
|
|
242
304
|
/**
|
|
243
305
|
* Register a component mount.
|
|
306
|
+
*
|
|
307
|
+
* TODO(P2-7) — stable component IDs across remounts.
|
|
308
|
+
* Currently `componentId` is a monotonic counter, so a view switch (or any
|
|
309
|
+
* conditional render) produces fresh IDs even when the same component
|
|
310
|
+
* remounts in the same slot. Agents that cache IDs across calls get burned.
|
|
311
|
+
*
|
|
312
|
+
* A stable scheme would key by (parent stable id + position + name), but
|
|
313
|
+
* that requires:
|
|
314
|
+
* - Tracking position within parent (currently only parentDevId is known).
|
|
315
|
+
* - Resolving collisions when two siblings have the same name.
|
|
316
|
+
* - Deciding how recursion (component-renders-itself) is handled.
|
|
317
|
+
* - Migrating every code path that assumes monotonic numeric IDs (devtools
|
|
318
|
+
* panel, registries, MCP bridge serialization, snapshot diffing).
|
|
319
|
+
*
|
|
320
|
+
* Verdict: the registry rewrite is invasive enough to warrant a dedicated
|
|
321
|
+
* change. For now, the MCP tool descriptions document the ephemerality
|
|
322
|
+
* (see what_components / what_explain) so agents re-query before using IDs.
|
|
244
323
|
*/
|
|
245
324
|
export function registerComponent(name, element, parentDevId) {
|
|
246
325
|
if (!installed) return;
|
|
@@ -253,6 +332,13 @@ export function registerComponent(name, element, parentDevId) {
|
|
|
253
332
|
mountedAt: Date.now(),
|
|
254
333
|
};
|
|
255
334
|
components.set(id, entry);
|
|
335
|
+
// Index by name so attributeToComponent() can match stack frames.
|
|
336
|
+
if (name) {
|
|
337
|
+
knownComponentNames.add(name);
|
|
338
|
+
let set = componentsByName.get(name);
|
|
339
|
+
if (!set) { set = new Set(); componentsByName.set(name, set); }
|
|
340
|
+
set.add(id);
|
|
341
|
+
}
|
|
256
342
|
emit('component:mounted', entry);
|
|
257
343
|
return id;
|
|
258
344
|
}
|
|
@@ -262,7 +348,17 @@ export function registerComponent(name, element, parentDevId) {
|
|
|
262
348
|
*/
|
|
263
349
|
export function unregisterComponent(id) {
|
|
264
350
|
if (!installed) return;
|
|
351
|
+
const entry = components.get(id);
|
|
265
352
|
components.delete(id);
|
|
353
|
+
if (entry?.name) {
|
|
354
|
+
const set = componentsByName.get(entry.name);
|
|
355
|
+
if (set) {
|
|
356
|
+
set.delete(id);
|
|
357
|
+
// Keep knownComponentNames populated even after unmount — a re-mount
|
|
358
|
+
// of the same component should still attribute correctly, and
|
|
359
|
+
// attribution costs nothing when no live components match.
|
|
360
|
+
}
|
|
361
|
+
}
|
|
266
362
|
emit('component:unmounted', { id });
|
|
267
363
|
}
|
|
268
364
|
|
|
@@ -290,6 +386,7 @@ export function getSnapshot(opts = {}) {
|
|
|
290
386
|
id,
|
|
291
387
|
name: entry.name,
|
|
292
388
|
value: entry.ref.peek(),
|
|
389
|
+
componentId: entry.componentId || null,
|
|
293
390
|
});
|
|
294
391
|
}
|
|
295
392
|
|
|
@@ -301,6 +398,7 @@ export function getSnapshot(opts = {}) {
|
|
|
301
398
|
depSignalIds: entry.depSignalIds || [],
|
|
302
399
|
runCount: entry.runCount || 0,
|
|
303
400
|
lastRunAt: entry.lastRunAt || null,
|
|
401
|
+
componentId: entry.componentId || null,
|
|
304
402
|
});
|
|
305
403
|
}
|
|
306
404
|
|
|
@@ -357,15 +455,60 @@ export function installDevTools(core) {
|
|
|
357
455
|
};
|
|
358
456
|
|
|
359
457
|
// Wire into what-core's reactive system
|
|
458
|
+
function installInto(mod) {
|
|
459
|
+
if (!mod) return;
|
|
460
|
+
if (mod.__setDevToolsHooks) mod.__setDevToolsHooks(hooks);
|
|
461
|
+
if (typeof window !== 'undefined') window.__WHAT_CORE__ = mod;
|
|
462
|
+
|
|
463
|
+
// P1-9: drain anything created BEFORE installDevTools was called
|
|
464
|
+
// (e.g. module-scope signals in store.js imported before app.js).
|
|
465
|
+
// The placeholder hooks in reactive.js buffered creations; we now
|
|
466
|
+
// register them with the live devtools so what_signals can see them.
|
|
467
|
+
if (typeof mod.__drainPreinstallBuffer === 'function') {
|
|
468
|
+
try {
|
|
469
|
+
const drained = mod.__drainPreinstallBuffer();
|
|
470
|
+
for (const sig of drained.signals || []) {
|
|
471
|
+
// Avoid double-register if the placeholder somehow already passed
|
|
472
|
+
// through to hooks (defensive — should be impossible given the
|
|
473
|
+
// ordering in reactive.js).
|
|
474
|
+
if (sig._devId == null) registerSignal(sig);
|
|
475
|
+
}
|
|
476
|
+
for (const e of drained.effects || []) {
|
|
477
|
+
if (e._devId == null) {
|
|
478
|
+
registerEffect(e);
|
|
479
|
+
// The effect already ran once before install; populate its deps
|
|
480
|
+
// now so what_dependency_graph shows the edges immediately,
|
|
481
|
+
// instead of waiting for the next run to re-track.
|
|
482
|
+
const entry = effects.get(e._devId);
|
|
483
|
+
if (entry && Array.isArray(e.deps)) {
|
|
484
|
+
const depSignalIds = [];
|
|
485
|
+
for (const subSet of e.deps) {
|
|
486
|
+
const sigId = subsToSignalId.get(subSet);
|
|
487
|
+
if (sigId != null) depSignalIds.push(sigId);
|
|
488
|
+
}
|
|
489
|
+
entry.depSignalIds = depSignalIds;
|
|
490
|
+
entry.runCount = 1;
|
|
491
|
+
entry.lastRunAt = Date.now();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
for (const ctx of drained.components || []) {
|
|
496
|
+
if (ctx._devId == null) hooks.onComponentMount(ctx);
|
|
497
|
+
}
|
|
498
|
+
} catch (err) {
|
|
499
|
+
// Non-fatal — pre-install drain is best-effort.
|
|
500
|
+
if (typeof console !== 'undefined') {
|
|
501
|
+
console.warn('[what-devtools] pre-install drain failed:', err);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
360
507
|
if (core && core.__setDevToolsHooks) {
|
|
361
|
-
core
|
|
362
|
-
if (typeof window !== 'undefined') window.__WHAT_CORE__ = core;
|
|
508
|
+
installInto(core);
|
|
363
509
|
} else {
|
|
364
510
|
try {
|
|
365
|
-
import('what-core').then(
|
|
366
|
-
if (mod.__setDevToolsHooks) mod.__setDevToolsHooks(hooks);
|
|
367
|
-
if (typeof window !== 'undefined') window.__WHAT_CORE__ = mod;
|
|
368
|
-
}).catch(() => {});
|
|
511
|
+
import('what-core').then(installInto).catch(() => {});
|
|
369
512
|
} catch {}
|
|
370
513
|
}
|
|
371
514
|
|