what-devtools 0.5.3 → 0.5.5

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 ADDED
@@ -0,0 +1,90 @@
1
+ # what-devtools
2
+
3
+ Development tools for [What Framework](https://whatfw.com). Provides runtime instrumentation to inspect signals, effects, and components. Exposes a `window.__WHAT_DEVTOOLS__` global for console-based debugging and a subscribable event system for custom tooling.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install what-devtools --save-dev
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ Call `installDevTools()` once at app startup:
14
+
15
+ ```js
16
+ import { installDevTools } from 'what-devtools';
17
+
18
+ installDevTools();
19
+ ```
20
+
21
+ Then inspect your app in the browser console:
22
+
23
+ ```js
24
+ __WHAT_DEVTOOLS__.signals // All live signals with names and values
25
+ __WHAT_DEVTOOLS__.effects // All active effects
26
+ __WHAT_DEVTOOLS__.components // All mounted components
27
+ __WHAT_DEVTOOLS__.getSnapshot() // Full state snapshot
28
+ ```
29
+
30
+ ## Event Subscription
31
+
32
+ Subscribe to real-time devtools events for custom tooling or a UI panel:
33
+
34
+ ```js
35
+ import { subscribe } from 'what-devtools';
36
+
37
+ const unsub = subscribe((event, data) => {
38
+ console.log(event, data);
39
+ // Events:
40
+ // 'signal:created' { id, name, ref, createdAt }
41
+ // 'signal:updated' { id, name, value }
42
+ // 'signal:disposed' { id }
43
+ // 'effect:created' { id, name, createdAt }
44
+ // 'effect:disposed' { id }
45
+ // 'component:mounted' { id, name, element, mountedAt }
46
+ // 'component:unmounted' { id }
47
+ });
48
+ ```
49
+
50
+ ## DevPanel Component
51
+
52
+ A built-in panel component for visual debugging (import separately):
53
+
54
+ ```js
55
+ import DevPanel from 'what-devtools/panel';
56
+ ```
57
+
58
+ ## API
59
+
60
+ | Export | Description |
61
+ |---|---|
62
+ | `installDevTools(core?)` | Initialize devtools and wire into what-core's hooks |
63
+ | `subscribe(fn)` | Subscribe to devtools events. Returns unsubscribe function |
64
+ | `getSnapshot()` | Get a snapshot of all signals, effects, and components |
65
+ | `registerSignal(sig, name?)` | Manually register a signal |
66
+ | `notifySignalUpdate(sig)` | Notify devtools of a signal value change |
67
+ | `unregisterSignal(sig)` | Unregister a signal |
68
+ | `registerEffect(e, name?)` | Manually register an effect |
69
+ | `unregisterEffect(e)` | Unregister an effect |
70
+ | `registerComponent(name, element)` | Register a component mount |
71
+ | `unregisterComponent(id)` | Unregister a component |
72
+ | `signals` | Map of all tracked signals |
73
+ | `effects` | Map of all tracked effects |
74
+ | `components` | Map of all tracked components |
75
+
76
+ ## Sub-path Exports
77
+
78
+ | Path | Contents |
79
+ |---|---|
80
+ | `what-devtools` | Instrumentation API |
81
+ | `what-devtools/panel` | DevPanel UI component |
82
+
83
+ ## Links
84
+
85
+ - [Documentation](https://whatfw.com)
86
+ - [GitHub](https://github.com/CelsianJs/whatfw)
87
+
88
+ ## License
89
+
90
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-devtools",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Dev tools for What Framework — signal inspector, component tree, effect graph",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -22,14 +22,14 @@
22
22
  "peerDependencies": {
23
23
  "what-core": "^0.5.3"
24
24
  },
25
- "author": "",
25
+ "author": "ZVN DEV (https://zvndev.com)",
26
26
  "license": "MIT",
27
27
  "repository": {
28
28
  "type": "git",
29
- "url": "https://github.com/zvndev/what-fw"
29
+ "url": "https://github.com/CelsianJs/whatfw"
30
30
  },
31
31
  "bugs": {
32
- "url": "https://github.com/zvndev/what-fw/issues"
32
+ "url": "https://github.com/CelsianJs/whatfw/issues"
33
33
  },
34
- "homepage": "https://whatframework.dev"
34
+ "homepage": "https://whatfw.com"
35
35
  }
package/src/DevPanel.jsx CHANGED
@@ -39,9 +39,7 @@ export function DevPanel() {
39
39
  clearInterval(interval);
40
40
  });
41
41
 
42
- const panelStyle = () => isOpen()
43
- ? 'position:fixed;bottom:0;right:0;width:360px;max-height:50vh;z-index:99998;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;background:#1a1a2e;color:#e0e0e0;border:1px solid #2a2a4a;border-radius:12px 0 0 0;box-shadow:0 -4px 24px rgba(0,0,0,0.3);display:flex;flex-direction:column;overflow:hidden;'
44
- : 'display:none;';
42
+ const PANEL_STYLE = 'position:fixed;bottom:0;right:0;width:360px;max-height:50vh;z-index:99998;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;background:#1a1a2e;color:#e0e0e0;border:1px solid #2a2a4a;border-radius:12px 0 0 0;box-shadow:0 -4px 24px rgba(0,0,0,0.3);display:flex;flex-direction:column;overflow:hidden;';
45
43
 
46
44
  const tabStyle = (tab) => () => {
47
45
  const isActive = activeTab() === tab;
@@ -110,44 +108,46 @@ export function DevPanel() {
110
108
  W
111
109
  </button>
112
110
 
113
- {/* Panel */}
114
- <div style={panelStyle}>
115
- {/* Header */}
116
- <div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #2a2a4a;background:#16163a;">
117
- <div style="display:flex;align-items:center;gap:8px;">
118
- <span style="font-weight:700;font-size:12px;color:#818cf8;">What DevTools</span>
111
+ {/* Panel — conditionally rendered */}
112
+ {() => isOpen() ? (
113
+ <div style={PANEL_STYLE}>
114
+ {/* Header */}
115
+ <div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #2a2a4a;background:#16163a;">
116
+ <div style="display:flex;align-items:center;gap:8px;">
117
+ <span style="font-weight:700;font-size:12px;color:#818cf8;">What DevTools</span>
118
+ </div>
119
+ <button
120
+ onclick={() => isOpen(false)}
121
+ style="background:none;border:none;color:#6a6a8a;cursor:pointer;font-size:14px;"
122
+ >
123
+ x
124
+ </button>
119
125
  </div>
120
- <button
121
- onclick={() => isOpen(false)}
122
- style="background:none;border:none;color:#6a6a8a;cursor:pointer;font-size:14px;"
123
- >
124
- x
125
- </button>
126
- </div>
127
126
 
128
- {/* Tabs */}
129
- <div style="display:flex;gap:4px;padding:6px 8px;border-bottom:1px solid #2a2a4a;">
130
- <button style={tabStyle('signals')} onclick={() => activeTab('signals')}>
131
- Signals ({() => snapshot().signals.length})
132
- </button>
133
- <button style={tabStyle('effects')} onclick={() => activeTab('effects')}>
134
- Effects ({() => snapshot().effects.length})
135
- </button>
136
- <button style={tabStyle('components')} onclick={() => activeTab('components')}>
137
- Components ({() => snapshot().components.length})
138
- </button>
139
- </div>
127
+ {/* Tabs */}
128
+ <div style="display:flex;gap:4px;padding:6px 8px;border-bottom:1px solid #2a2a4a;">
129
+ <button style={tabStyle('signals')} onclick={() => activeTab('signals')}>
130
+ Signals ({() => snapshot().signals.length})
131
+ </button>
132
+ <button style={tabStyle('effects')} onclick={() => activeTab('effects')}>
133
+ Effects ({() => snapshot().effects.length})
134
+ </button>
135
+ <button style={tabStyle('components')} onclick={() => activeTab('components')}>
136
+ Components ({() => snapshot().components.length})
137
+ </button>
138
+ </div>
140
139
 
141
- {/* Content */}
142
- <div style="overflow-y:auto;flex:1;">
143
- {() => {
144
- const tab = activeTab();
145
- if (tab === 'signals') return renderSignals();
146
- if (tab === 'effects') return renderEffects();
147
- return renderComponents();
148
- }}
140
+ {/* Content */}
141
+ <div style="overflow-y:auto;flex:1;">
142
+ {() => {
143
+ const tab = activeTab();
144
+ if (tab === 'signals') return renderSignals();
145
+ if (tab === 'effects') return renderEffects();
146
+ return renderComponents();
147
+ }}
148
+ </div>
149
149
  </div>
150
- </div>
150
+ ) : null}
151
151
  </>
152
152
  );
153
153
  }
package/src/index.js CHANGED
@@ -20,9 +20,16 @@ let effectId = 0;
20
20
  let componentId = 0;
21
21
 
22
22
  // Registries
23
- const signals = new Map(); // id → { name, value, subs, createdAt }
24
- const effects = new Map(); // id → { name, deps, createdAt }
25
- const components = new Map(); // id → { name, mountedAt, signals: [], effects: [] }
23
+ const signals = new Map(); // id → { name, ref, createdAt, internal }
24
+ const effects = new Map(); // id → { name, createdAt, depSignalIds, runCount, lastRunAt }
25
+ const components = new Map(); // id → { name, element, mountedAt, parentId }
26
+
27
+ // Reverse lookup: subscriber Set → signal ID (O(1) dep resolution)
28
+ const subsToSignalId = new WeakMap();
29
+
30
+ // Error log (capped at 100)
31
+ const errors = [];
32
+ const MAX_ERRORS = 100;
26
33
 
27
34
  // Event listeners for the DevPanel
28
35
  const listeners = new Set();
@@ -33,6 +40,88 @@ function emit(event, data) {
33
40
  }
34
41
  }
35
42
 
43
+ /**
44
+ * Safely serialize a value for transport (WS, JSON).
45
+ * Handles DOM nodes, functions, circular refs, Maps, Sets, large collections.
46
+ */
47
+ export function safeSerialize(value, depth = 0, seen) {
48
+ if (depth > 6) return '[max depth]';
49
+ if (value === null || value === undefined) return value;
50
+
51
+ const type = typeof value;
52
+ if (type === 'string' || type === 'number' || type === 'boolean') return value;
53
+ if (type === 'function') return `[Function: ${value.name || 'anonymous'}]`;
54
+ if (type === 'symbol') return `[Symbol: ${value.description || ''}]`;
55
+ if (type === 'bigint') return value.toString() + 'n';
56
+
57
+ // DOM nodes
58
+ if (typeof Node !== 'undefined' && value instanceof Node) {
59
+ const tag = value.nodeName?.toLowerCase() || 'node';
60
+ const id = value.id ? `#${value.id}` : '';
61
+ const cls = value.className ? `.${String(value.className).split(' ')[0]}` : '';
62
+ return `[DOM: <${tag}${id}${cls}>]`;
63
+ }
64
+
65
+ if (!seen) seen = new Set();
66
+ if (seen.has(value)) return '[Circular]';
67
+ seen.add(value);
68
+
69
+ // Map
70
+ if (value instanceof Map) {
71
+ if (value.size > 50) return `[Map: ${value.size} entries]`;
72
+ const obj = {};
73
+ for (const [k, v] of value) {
74
+ obj[String(k)] = safeSerialize(v, depth + 1, seen);
75
+ }
76
+ return { __type: 'Map', entries: obj };
77
+ }
78
+
79
+ // Set
80
+ if (value instanceof Set) {
81
+ if (value.size > 50) return `[Set: ${value.size} items]`;
82
+ return { __type: 'Set', values: [...value].map(v => safeSerialize(v, depth + 1, seen)) };
83
+ }
84
+
85
+ // Array
86
+ if (Array.isArray(value)) {
87
+ if (value.length > 100) {
88
+ return [...value.slice(0, 100).map(v => safeSerialize(v, depth + 1, seen)), `... (${value.length} total)`];
89
+ }
90
+ return value.map(v => safeSerialize(v, depth + 1, seen));
91
+ }
92
+
93
+ // Error
94
+ if (value instanceof Error) {
95
+ return { __type: 'Error', name: value.name, message: value.message, stack: value.stack };
96
+ }
97
+
98
+ // Date
99
+ if (value instanceof Date) return { __type: 'Date', iso: value.toISOString() };
100
+
101
+ // RegExp
102
+ if (value instanceof RegExp) return value.toString();
103
+
104
+ // Plain object
105
+ if (type === 'object') {
106
+ const keys = Object.keys(value);
107
+ if (keys.length > 100) {
108
+ const obj = {};
109
+ for (const k of keys.slice(0, 100)) {
110
+ obj[k] = safeSerialize(value[k], depth + 1, seen);
111
+ }
112
+ obj['...'] = `(${keys.length} total keys)`;
113
+ return obj;
114
+ }
115
+ const obj = {};
116
+ for (const k of keys) {
117
+ obj[k] = safeSerialize(value[k], depth + 1, seen);
118
+ }
119
+ return obj;
120
+ }
121
+
122
+ return String(value);
123
+ }
124
+
36
125
  /**
37
126
  * Register a signal with the devtools.
38
127
  * Called from reactive.js __DEV__ hooks.
@@ -42,12 +131,15 @@ export function registerSignal(sig, name) {
42
131
  const id = ++signalId;
43
132
  const entry = {
44
133
  id,
45
- name: name || `signal_${id}`,
134
+ name: sig._debugName || name || `signal_${id}`,
46
135
  ref: sig,
47
136
  createdAt: Date.now(),
137
+ internal: false,
48
138
  };
49
139
  signals.set(id, entry);
50
140
  sig._devId = id;
141
+ // Reverse lookup for O(1) effect dep resolution
142
+ if (sig._subs) subsToSignalId.set(sig._subs, id);
51
143
  emit('signal:created', entry);
52
144
  return id;
53
145
  }
@@ -86,6 +178,9 @@ export function registerEffect(e, name) {
86
178
  id,
87
179
  name: name || e.fn?.name || `effect_${id}`,
88
180
  createdAt: Date.now(),
181
+ depSignalIds: [],
182
+ runCount: 0,
183
+ lastRunAt: null,
89
184
  };
90
185
  effects.set(id, entry);
91
186
  e._devId = id;
@@ -93,6 +188,30 @@ export function registerEffect(e, name) {
93
188
  return id;
94
189
  }
95
190
 
191
+ /**
192
+ * Track effect dependencies and run count after an effect runs.
193
+ */
194
+ function trackEffectRun(e) {
195
+ const id = e._devId;
196
+ if (id == null) return;
197
+ const entry = effects.get(id);
198
+ if (!entry) return;
199
+
200
+ // Resolve deps via WeakMap reverse lookup — O(m) where m = number of deps
201
+ const depSignalIds = [];
202
+ if (e.deps) {
203
+ for (const subSet of e.deps) {
204
+ const sigId = subsToSignalId.get(subSet);
205
+ if (sigId != null) depSignalIds.push(sigId);
206
+ }
207
+ }
208
+
209
+ entry.depSignalIds = depSignalIds;
210
+ entry.runCount = (entry.runCount || 0) + 1;
211
+ entry.lastRunAt = Date.now();
212
+ emit('effect:run', { id, depSignalIds: entry.depSignalIds, runCount: entry.runCount });
213
+ }
214
+
96
215
  /**
97
216
  * Unregister an effect.
98
217
  */
@@ -104,16 +223,33 @@ export function unregisterEffect(e) {
104
223
  emit('effect:disposed', { id });
105
224
  }
106
225
 
226
+ /**
227
+ * Capture a runtime error.
228
+ */
229
+ function captureError(err, context) {
230
+ const entry = {
231
+ message: err?.message || String(err),
232
+ stack: err?.stack || null,
233
+ type: context?.type || 'unknown',
234
+ effectId: context?.effect?._devId || null,
235
+ timestamp: Date.now(),
236
+ };
237
+ errors.push(entry);
238
+ if (errors.length > MAX_ERRORS) errors.shift();
239
+ emit('error:captured', entry);
240
+ }
241
+
107
242
  /**
108
243
  * Register a component mount.
109
244
  */
110
- export function registerComponent(name, element) {
245
+ export function registerComponent(name, element, parentDevId) {
111
246
  if (!installed) return;
112
247
  const id = ++componentId;
113
248
  const entry = {
114
249
  id,
115
250
  name: name || 'Anonymous',
116
251
  element,
252
+ parentId: parentDevId || null,
117
253
  mountedAt: Date.now(),
118
254
  };
119
255
  components.set(id, entry);
@@ -141,10 +277,15 @@ export function subscribe(fn) {
141
277
 
142
278
  /**
143
279
  * Get a snapshot of all tracked state.
280
+ * @param {object} [opts] - Options
281
+ * @param {boolean} [opts.includeInternal=false] - Include framework-internal signals
144
282
  */
145
- export function getSnapshot() {
283
+ export function getSnapshot(opts = {}) {
284
+ const { includeInternal = false } = opts;
285
+
146
286
  const signalList = [];
147
287
  for (const [id, entry] of signals) {
288
+ if (!includeInternal && entry.internal) continue;
148
289
  signalList.push({
149
290
  id,
150
291
  name: entry.name,
@@ -154,51 +295,93 @@ export function getSnapshot() {
154
295
 
155
296
  const effectList = [];
156
297
  for (const [id, entry] of effects) {
157
- effectList.push({ id, name: entry.name });
298
+ effectList.push({
299
+ id,
300
+ name: entry.name,
301
+ depSignalIds: entry.depSignalIds || [],
302
+ runCount: entry.runCount || 0,
303
+ lastRunAt: entry.lastRunAt || null,
304
+ });
158
305
  }
159
306
 
160
307
  const componentList = [];
161
308
  for (const [id, entry] of components) {
162
- componentList.push({ id, name: entry.name });
309
+ componentList.push({ id, name: entry.name, parentId: entry.parentId });
163
310
  }
164
311
 
165
- return { signals: signalList, effects: effectList, components: componentList };
312
+ return {
313
+ signals: signalList,
314
+ effects: effectList,
315
+ components: componentList,
316
+ errors: errors.slice(),
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Get captured errors.
322
+ * @param {object} [opts]
323
+ * @param {number} [opts.since] - Only errors after this timestamp
324
+ */
325
+ export function getErrors(opts = {}) {
326
+ const { since } = opts;
327
+ if (since) return errors.filter(e => e.timestamp > since);
328
+ return errors.slice();
166
329
  }
167
330
 
168
331
  /**
169
332
  * Install devtools. Call once at app startup.
170
333
  * Wires into what-core's __DEV__ hooks and exposes `window.__WHAT_DEVTOOLS__`.
334
+ *
335
+ * @param {object} [core] - Optional what-core module. If not provided, attempts dynamic import.
171
336
  */
172
- export function installDevTools() {
337
+ export function installDevTools(core) {
173
338
  if (installed) return;
174
339
  installed = true;
175
340
 
341
+ const hooks = {
342
+ onSignalCreate: (sig) => registerSignal(sig),
343
+ onSignalUpdate: (sig) => notifySignalUpdate(sig),
344
+ onEffectCreate: (e) => registerEffect(e),
345
+ onEffectDispose: (e) => unregisterEffect(e),
346
+ onEffectRun: (e) => trackEffectRun(e),
347
+ onError: (err, context) => captureError(err, context),
348
+ onComponentMount: (ctx) => {
349
+ const name = ctx.Component?.displayName || ctx.Component?.name || 'Anonymous';
350
+ const parentDevId = ctx._parentCtx?._devId || null;
351
+ const id = registerComponent(name, ctx._wrapper, parentDevId);
352
+ ctx._devId = id;
353
+ },
354
+ onComponentUnmount: (ctx) => {
355
+ if (ctx._devId != null) unregisterComponent(ctx._devId);
356
+ },
357
+ };
358
+
176
359
  // Wire into what-core's reactive system
177
- try {
178
- import('what-core').then(core => {
179
- if (core.__setDevToolsHooks) {
180
- core.__setDevToolsHooks({
181
- onSignalCreate: (sig) => registerSignal(sig),
182
- onSignalUpdate: (sig) => notifySignalUpdate(sig),
183
- onEffectCreate: (e) => registerEffect(e),
184
- onEffectDispose: (e) => unregisterEffect(e),
185
- });
186
- }
187
- }).catch(() => {
188
- // what-core not available — devtools still work via manual registration
189
- });
190
- } catch {}
360
+ if (core && core.__setDevToolsHooks) {
361
+ core.__setDevToolsHooks(hooks);
362
+ if (typeof window !== 'undefined') window.__WHAT_CORE__ = core;
363
+ } else {
364
+ try {
365
+ import('what-core').then(mod => {
366
+ if (mod.__setDevToolsHooks) mod.__setDevToolsHooks(hooks);
367
+ if (typeof window !== 'undefined') window.__WHAT_CORE__ = mod;
368
+ }).catch(() => {});
369
+ } catch {}
370
+ }
191
371
 
192
372
  if (typeof window !== 'undefined') {
193
373
  window.__WHAT_DEVTOOLS__ = {
194
374
  get signals() { return getSnapshot().signals; },
195
375
  get effects() { return getSnapshot().effects; },
196
376
  get components() { return getSnapshot().components; },
377
+ get errors() { return getErrors(); },
197
378
  getSnapshot,
379
+ getErrors,
198
380
  subscribe,
199
- _registries: { signals, effects, components },
381
+ safeSerialize,
382
+ _registries: { signals, effects, components, errors },
200
383
  };
201
384
  }
202
385
  }
203
386
 
204
- export { signals, effects, components };
387
+ export { signals, effects, components, errors };