what-devtools-mcp 0.6.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 +223 -0
- package/package.json +33 -0
- package/src/bridge.js +171 -0
- package/src/client-commands.js +206 -0
- package/src/client.js +283 -0
- package/src/index.js +637 -0
- package/src/tools-agent.js +818 -0
- package/src/tools-extended.js +789 -0
- package/src/tools.js +670 -0
- package/src/vite-plugin.js +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# what-devtools-mcp
|
|
2
|
+
|
|
3
|
+
AI-powered debugging for What Framework apps. Lets Claude Code, Cursor, Windsurf, and other AI agents inspect your app's live state — signals, effects, components, errors — in real time.
|
|
4
|
+
|
|
5
|
+
> **What is MCP?** [Model Context Protocol](https://modelcontextprotocol.io) is an open standard that lets AI coding assistants call tools in external systems. This package exposes your app's runtime state as MCP tools that your AI assistant can call while helping you debug.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Browser (What App + devtools) ──WebSocket:9229──▶ Node.js Process
|
|
11
|
+
├── WS Bridge (state + events)
|
|
12
|
+
└── MCP Server (stdio) ◀── Claude Code / Cursor
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install --save-dev what-devtools what-devtools-mcp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
### Step 1: Add the Vite plugin
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
// vite.config.js
|
|
27
|
+
import what from 'what-compiler/vite';
|
|
28
|
+
import whatDevToolsMCP from 'what-devtools-mcp/vite-plugin';
|
|
29
|
+
|
|
30
|
+
export default {
|
|
31
|
+
plugins: [what(), whatDevToolsMCP()],
|
|
32
|
+
};
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The plugin auto-injects the devtools client in dev mode only (`apply: 'serve'`). It never runs during `vite build`.
|
|
36
|
+
|
|
37
|
+
### Step 2: Configure your AI tool
|
|
38
|
+
|
|
39
|
+
**Claude Code** — add to `.claude/mcp.json` in your project root:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"mcpServers": {
|
|
44
|
+
"what-devtools": {
|
|
45
|
+
"command": "npx",
|
|
46
|
+
"args": ["what-devtools-mcp"]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Cursor** — add to `.cursor/mcp.json` in your project root:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"mcpServers": {
|
|
57
|
+
"what-devtools": {
|
|
58
|
+
"command": "npx",
|
|
59
|
+
"args": ["what-devtools-mcp"]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Other MCP clients** — any client that supports stdio transport can connect with:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
command: npx what-devtools-mcp
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Step 3: Start your app
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm run dev
|
|
75
|
+
# In another terminal (or your AI tool starts it automatically):
|
|
76
|
+
# The MCP server runs via your AI tool's MCP config — no manual start needed.
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
You should see in the browser console:
|
|
80
|
+
```
|
|
81
|
+
⚡ What DevTools MCP Client v0.1.0
|
|
82
|
+
MCP Connecting to bridge on ws://localhost:9229
|
|
83
|
+
MCP 🟢 Connected to bridge — AI agent can now inspect this app
|
|
84
|
+
MCP Sent initial snapshot — 5 signals, 3 effects, 2 components
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## What Can Your AI Do Now?
|
|
88
|
+
|
|
89
|
+
Here's a real debugging session with Claude Code:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
You: "The counter isn't updating when I click the button"
|
|
93
|
+
|
|
94
|
+
Claude: Let me check your app's reactive state.
|
|
95
|
+
[calls what_signals { filter: "count" }]
|
|
96
|
+
|
|
97
|
+
I can see signal 'count' exists with value 0. Let me watch
|
|
98
|
+
what happens when you click.
|
|
99
|
+
[calls what_watch { duration: 5000, filter: "signal:updated" }]
|
|
100
|
+
|
|
101
|
+
No signal updates detected in 5 seconds. The click handler
|
|
102
|
+
isn't writing to the signal. Let me check the component.
|
|
103
|
+
[calls what_dom_inspect { componentId: 2 }]
|
|
104
|
+
|
|
105
|
+
Found it — the button's onclick calls increment() but that
|
|
106
|
+
function uses count.peek() instead of count.set(). peek()
|
|
107
|
+
reads without triggering reactivity. Here's the fix...
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The AI sees your app's internals in real time — no console.log, no breakpoints, no guesswork.
|
|
111
|
+
|
|
112
|
+
## Available Tools
|
|
113
|
+
|
|
114
|
+
### Read Tools (inspect state)
|
|
115
|
+
|
|
116
|
+
| Tool | Description |
|
|
117
|
+
|------|-------------|
|
|
118
|
+
| `what_connection_status` | Check if browser is connected, get counts |
|
|
119
|
+
| `what_signals` | List signals with values. Filter by name regex or ID |
|
|
120
|
+
| `what_effects` | List effects with deps, run counts, timing. Filter by name or dep signal |
|
|
121
|
+
| `what_components` | List mounted components. Filter by name |
|
|
122
|
+
| `what_snapshot` | Full state snapshot (signals + effects + components + errors) |
|
|
123
|
+
| `what_errors` | Runtime errors with effect context and stack traces |
|
|
124
|
+
| `what_cache` | SWR/useQuery cache entries. Filter by key |
|
|
125
|
+
| `what_component_tree` | Hierarchical component tree with parent-child relationships |
|
|
126
|
+
| `what_dependency_graph` | Reactive dependency graph — which signals feed which effects |
|
|
127
|
+
| `what_dom_inspect` | See a component's rendered DOM output |
|
|
128
|
+
| `what_route` | Current route info (path, params, query, matched pattern) |
|
|
129
|
+
|
|
130
|
+
### Write Tools (modify state)
|
|
131
|
+
|
|
132
|
+
| Tool | Description |
|
|
133
|
+
|------|-------------|
|
|
134
|
+
| `what_set_signal` | Set a signal's value and see what changes |
|
|
135
|
+
| `what_invalidate_cache` | Force-refresh a cache key |
|
|
136
|
+
| `what_eval` | Execute arbitrary JS in the browser context |
|
|
137
|
+
| `what_navigate` | Navigate to a different route |
|
|
138
|
+
|
|
139
|
+
### Observe Tools (watch changes)
|
|
140
|
+
|
|
141
|
+
| Tool | Description |
|
|
142
|
+
|------|-------------|
|
|
143
|
+
| `what_watch` | Collect reactive events over a time window |
|
|
144
|
+
| `what_diff_snapshot` | Save a baseline, take action, diff the changes |
|
|
145
|
+
|
|
146
|
+
### Diagnostic Tools
|
|
147
|
+
|
|
148
|
+
| Tool | Description |
|
|
149
|
+
|------|-------------|
|
|
150
|
+
| `what_diagnose` | Multi-check diagnostic — errors, performance, reactivity in one call |
|
|
151
|
+
|
|
152
|
+
## Signal Debug Names
|
|
153
|
+
|
|
154
|
+
For best debugging output, add names to your signals:
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
// Without name: shows as "signal_1" in devtools
|
|
158
|
+
const count = signal(0);
|
|
159
|
+
|
|
160
|
+
// With name: shows as "count" in devtools
|
|
161
|
+
const count = signal(0, 'count');
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
A compiler transform to auto-inject names from variable declarations is planned.
|
|
165
|
+
|
|
166
|
+
## Manual Setup (without Vite plugin)
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
import * as core from 'what-core';
|
|
170
|
+
import { installDevTools } from 'what-devtools';
|
|
171
|
+
import { connectDevToolsMCP } from 'what-devtools-mcp/client';
|
|
172
|
+
|
|
173
|
+
installDevTools(core);
|
|
174
|
+
connectDevToolsMCP({ port: 9229 });
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Configuration
|
|
178
|
+
|
|
179
|
+
| Option | Default | Description |
|
|
180
|
+
|--------|---------|-------------|
|
|
181
|
+
| `WHAT_MCP_PORT` env var | `9229` | WebSocket bridge port |
|
|
182
|
+
| Vite plugin `{ port }` | `9229` | Same, via plugin option |
|
|
183
|
+
|
|
184
|
+
Port 9229 matches Node.js `--inspect` convention. If you're also debugging Node.js, set a different port:
|
|
185
|
+
|
|
186
|
+
```js
|
|
187
|
+
whatDevToolsMCP({ port: 9230 })
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"mcpServers": {
|
|
193
|
+
"what-devtools": {
|
|
194
|
+
"command": "npx",
|
|
195
|
+
"args": ["what-devtools-mcp"],
|
|
196
|
+
"env": { "WHAT_MCP_PORT": "9230" }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Troubleshooting
|
|
203
|
+
|
|
204
|
+
**"No browser connected"**
|
|
205
|
+
Your app isn't running or the Vite plugin isn't configured. Start your dev server and check the browser console for the MCP connection banner.
|
|
206
|
+
|
|
207
|
+
**"No snapshot available"**
|
|
208
|
+
The browser connected but hasn't sent state yet. Refresh the page.
|
|
209
|
+
|
|
210
|
+
**WebSocket connection refused**
|
|
211
|
+
Port 9229 may be in use. Check with `lsof -i :9229` and either kill the process or use a different port.
|
|
212
|
+
|
|
213
|
+
**Multiple browser tabs**
|
|
214
|
+
Only the most recently connected tab's state is tracked. Close extra tabs or use the one you're debugging.
|
|
215
|
+
|
|
216
|
+
**Empty signal names (signal_1, signal_2)**
|
|
217
|
+
Add debug names: `signal(0, 'count')`. See "Signal Debug Names" above.
|
|
218
|
+
|
|
219
|
+
## WebMCP (Future)
|
|
220
|
+
|
|
221
|
+
What Framework is designed to support WebMCP — running the MCP server directly in the browser, eliminating the WebSocket bridge entirely. The tool handlers are being built transport-agnostic so they can run in either Node.js or browser context.
|
|
222
|
+
|
|
223
|
+
See `DESIGN.md` for the full WebMCP architecture plan.
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "what-devtools-mcp",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "MCP server bridging AI agents to live What Framework app state via WebSocket",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"what-devtools-mcp": "./src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.js",
|
|
11
|
+
"./client": "./src/client.js",
|
|
12
|
+
"./vite-plugin": "./src/vite-plugin.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
19
|
+
"ws": "^8.0.0",
|
|
20
|
+
"zod": "^3.25.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"what-devtools": ">=0.6.0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"what-framework",
|
|
27
|
+
"devtools",
|
|
28
|
+
"mcp",
|
|
29
|
+
"model-context-protocol",
|
|
30
|
+
"ai-debugging"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT"
|
|
33
|
+
}
|
package/src/bridge.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js WebSocket server + state bridge.
|
|
3
|
+
* Receives state snapshots and events from the browser client,
|
|
4
|
+
* provides query API for the MCP tools.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { WebSocketServer } from 'ws';
|
|
8
|
+
|
|
9
|
+
const MAX_EVENT_LOG = 1000;
|
|
10
|
+
const MAX_ERROR_LOG = 100;
|
|
11
|
+
|
|
12
|
+
export function createBridge({ port = 9229 } = {}) {
|
|
13
|
+
let latestSnapshot = null;
|
|
14
|
+
const eventLog = [];
|
|
15
|
+
const errorLog = [];
|
|
16
|
+
let browserSocket = null;
|
|
17
|
+
let correlationCounter = 0;
|
|
18
|
+
const pendingCommands = new Map();
|
|
19
|
+
|
|
20
|
+
// Snapshot dedup cache (100ms)
|
|
21
|
+
let cachedSnapshot = null;
|
|
22
|
+
let cacheTime = 0;
|
|
23
|
+
const SNAPSHOT_CACHE_MS = 100;
|
|
24
|
+
|
|
25
|
+
// Baseline snapshot for diff tool
|
|
26
|
+
let baselineSnapshot = null;
|
|
27
|
+
|
|
28
|
+
const wss = new WebSocketServer({ port });
|
|
29
|
+
|
|
30
|
+
wss.on('connection', (ws) => {
|
|
31
|
+
browserSocket = ws;
|
|
32
|
+
|
|
33
|
+
ws.on('message', (raw) => {
|
|
34
|
+
let msg;
|
|
35
|
+
try { msg = JSON.parse(raw); } catch { return; }
|
|
36
|
+
|
|
37
|
+
switch (msg.type) {
|
|
38
|
+
case 'snapshot':
|
|
39
|
+
latestSnapshot = msg.data;
|
|
40
|
+
break;
|
|
41
|
+
case 'event':
|
|
42
|
+
eventLog.push({ event: msg.event, data: msg.data, timestamp: Date.now() });
|
|
43
|
+
if (eventLog.length > MAX_EVENT_LOG) eventLog.shift();
|
|
44
|
+
// Track errors separately
|
|
45
|
+
if (msg.event === 'error:captured') {
|
|
46
|
+
errorLog.push({ ...msg.data, timestamp: Date.now() });
|
|
47
|
+
if (errorLog.length > MAX_ERROR_LOG) errorLog.shift();
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
case 'events':
|
|
51
|
+
for (const item of msg.batch || []) {
|
|
52
|
+
eventLog.push({ event: item.event, data: item.data, timestamp: Date.now() });
|
|
53
|
+
if (eventLog.length > MAX_EVENT_LOG) eventLog.shift();
|
|
54
|
+
if (item.event === 'error:captured') {
|
|
55
|
+
errorLog.push({ ...item.data, timestamp: Date.now() });
|
|
56
|
+
if (errorLog.length > MAX_ERROR_LOG) errorLog.shift();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
case 'response': {
|
|
61
|
+
const pending = pendingCommands.get(msg.correlationId);
|
|
62
|
+
if (pending) {
|
|
63
|
+
pendingCommands.delete(msg.correlationId);
|
|
64
|
+
pending.resolve(msg.data);
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
ws.on('close', () => {
|
|
72
|
+
if (browserSocket === ws) browserSocket = null;
|
|
73
|
+
// Reject all pending commands
|
|
74
|
+
for (const [id, pending] of pendingCommands) {
|
|
75
|
+
pending.reject(new Error('Browser disconnected'));
|
|
76
|
+
pendingCommands.delete(id);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function isConnected() {
|
|
82
|
+
return browserSocket !== null && browserSocket.readyState === 1; // WebSocket.OPEN
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sendCommand(command, args = {}, timeout = 5000) {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
if (!isConnected()) {
|
|
88
|
+
return reject(new Error('No browser connected'));
|
|
89
|
+
}
|
|
90
|
+
const correlationId = `cmd_${++correlationCounter}`;
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
pendingCommands.delete(correlationId);
|
|
93
|
+
reject(new Error(`Command '${command}' timed out after ${timeout}ms`));
|
|
94
|
+
}, timeout);
|
|
95
|
+
|
|
96
|
+
pendingCommands.set(correlationId, {
|
|
97
|
+
resolve: (data) => { clearTimeout(timer); resolve(data); },
|
|
98
|
+
reject: (err) => { clearTimeout(timer); reject(err); },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
browserSocket.send(JSON.stringify({ command, correlationId, args }));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function refreshSnapshot() {
|
|
106
|
+
const data = await sendCommand('get-snapshot');
|
|
107
|
+
latestSnapshot = data;
|
|
108
|
+
return data;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function getOrRefreshSnapshot() {
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
if (cachedSnapshot && now - cacheTime < SNAPSHOT_CACHE_MS) return cachedSnapshot;
|
|
114
|
+
try {
|
|
115
|
+
cachedSnapshot = await refreshSnapshot();
|
|
116
|
+
cacheTime = now;
|
|
117
|
+
return cachedSnapshot;
|
|
118
|
+
} catch {
|
|
119
|
+
return latestSnapshot;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function getCacheSnapshot() {
|
|
124
|
+
return sendCommand('get-cache');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getSnapshot() {
|
|
128
|
+
return latestSnapshot;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function saveBaseline() {
|
|
132
|
+
baselineSnapshot = latestSnapshot ? JSON.parse(JSON.stringify(latestSnapshot)) : null;
|
|
133
|
+
return !!baselineSnapshot;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getBaseline() {
|
|
137
|
+
return baselineSnapshot;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getEvents(since) {
|
|
141
|
+
if (since) return eventLog.filter(e => e.timestamp > since);
|
|
142
|
+
return eventLog.slice();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getErrors(since) {
|
|
146
|
+
if (since) return errorLog.filter(e => e.timestamp > since);
|
|
147
|
+
return errorLog.slice();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function close() {
|
|
151
|
+
for (const [id, pending] of pendingCommands) {
|
|
152
|
+
pending.reject(new Error('Bridge closing'));
|
|
153
|
+
pendingCommands.delete(id);
|
|
154
|
+
}
|
|
155
|
+
wss.close();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
getSnapshot,
|
|
160
|
+
getOrRefreshSnapshot,
|
|
161
|
+
getEvents,
|
|
162
|
+
getErrors,
|
|
163
|
+
isConnected,
|
|
164
|
+
sendCommand,
|
|
165
|
+
refreshSnapshot,
|
|
166
|
+
getCacheSnapshot,
|
|
167
|
+
saveBaseline,
|
|
168
|
+
getBaseline,
|
|
169
|
+
close,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extended command handlers for the browser client.
|
|
3
|
+
* Handles: eval, dom-inspect, get-route, navigate
|
|
4
|
+
*
|
|
5
|
+
* Usage in client.js:
|
|
6
|
+
* import { handleExtendedCommand } from './client-commands.js';
|
|
7
|
+
*
|
|
8
|
+
* // Inside handleCommand(), before the default case:
|
|
9
|
+
* const extResult = handleExtendedCommand(command, args, devtools);
|
|
10
|
+
* if (extResult !== null) { result = extResult; break; }
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Handle extended commands sent from the MCP server via the bridge.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} command - The command name
|
|
17
|
+
* @param {object} args - Command arguments
|
|
18
|
+
* @param {object|null} devtools - window.__WHAT_DEVTOOLS__ reference
|
|
19
|
+
* @returns {object|null} Result object, or null if command not handled
|
|
20
|
+
*/
|
|
21
|
+
export function handleExtendedCommand(command, args, devtools) {
|
|
22
|
+
switch (command) {
|
|
23
|
+
|
|
24
|
+
// -------------------------------------------------------------------------
|
|
25
|
+
// eval — Execute arbitrary JS in the browser context
|
|
26
|
+
// -------------------------------------------------------------------------
|
|
27
|
+
case 'eval': {
|
|
28
|
+
const start = performance.now();
|
|
29
|
+
try {
|
|
30
|
+
// Use Function constructor to execute in global scope
|
|
31
|
+
// eslint-disable-next-line no-new-func
|
|
32
|
+
const fn = new Function(args.code);
|
|
33
|
+
const raw = fn();
|
|
34
|
+
const elapsed = performance.now() - start;
|
|
35
|
+
return {
|
|
36
|
+
result: devtools?.safeSerialize ? devtools.safeSerialize(raw) : raw,
|
|
37
|
+
type: typeof raw,
|
|
38
|
+
executionTime: Math.round(elapsed * 100) / 100,
|
|
39
|
+
};
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return {
|
|
42
|
+
error: e.message,
|
|
43
|
+
stack: e.stack,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// -------------------------------------------------------------------------
|
|
49
|
+
// dom-inspect — Serialize a component's rendered DOM
|
|
50
|
+
// -------------------------------------------------------------------------
|
|
51
|
+
case 'dom-inspect': {
|
|
52
|
+
const { componentId, depth = 3 } = args || {};
|
|
53
|
+
const registries = devtools?._registries;
|
|
54
|
+
|
|
55
|
+
if (!registries?.components) {
|
|
56
|
+
return { error: 'DevTools registries not available' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const entry = registries.components.get(componentId);
|
|
60
|
+
if (!entry) {
|
|
61
|
+
return { error: `Component ${componentId} not found` };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const el = entry.element;
|
|
65
|
+
if (!el) {
|
|
66
|
+
return { error: `Component "${entry.name}" (id: ${componentId}) has no DOM element` };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Recursively serialize a DOM node into a plain object.
|
|
71
|
+
* Respects the max depth limit to avoid huge payloads.
|
|
72
|
+
*/
|
|
73
|
+
function serializeDOM(node, currentDepth) {
|
|
74
|
+
if (currentDepth > depth) {
|
|
75
|
+
return { tag: '...', text: '(truncated)' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Text node
|
|
79
|
+
if (node.nodeType === 3) {
|
|
80
|
+
const text = node.textContent?.trim() || '';
|
|
81
|
+
if (!text) return null; // skip empty text nodes
|
|
82
|
+
return { text };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Skip non-element, non-text nodes (comments, etc.)
|
|
86
|
+
if (node.nodeType !== 1) return null;
|
|
87
|
+
|
|
88
|
+
const result = {
|
|
89
|
+
tag: node.tagName.toLowerCase(),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Include common identifying attributes
|
|
93
|
+
if (node.id) result.id = node.id;
|
|
94
|
+
if (node.className && typeof node.className === 'string') {
|
|
95
|
+
result.class = node.className;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Include data attributes (often useful for debugging)
|
|
99
|
+
const dataAttrs = {};
|
|
100
|
+
for (const attr of node.attributes) {
|
|
101
|
+
if (attr.name.startsWith('data-')) {
|
|
102
|
+
dataAttrs[attr.name] = attr.value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (Object.keys(dataAttrs).length > 0) {
|
|
106
|
+
result.dataAttributes = dataAttrs;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Recurse into children
|
|
110
|
+
const children = [];
|
|
111
|
+
for (const child of node.childNodes) {
|
|
112
|
+
const serialized = serializeDOM(child, currentDepth + 1);
|
|
113
|
+
if (serialized && (serialized.tag || serialized.text)) {
|
|
114
|
+
children.push(serialized);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (children.length) result.children = children;
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const structure = serializeDOM(el, 0);
|
|
123
|
+
// Cap HTML to 5000 chars to prevent huge payloads
|
|
124
|
+
const html = el.innerHTML?.substring(0, 5000) || '';
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
componentName: entry.name,
|
|
128
|
+
componentId,
|
|
129
|
+
html,
|
|
130
|
+
structure,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// -------------------------------------------------------------------------
|
|
135
|
+
// get-route — Return current route information
|
|
136
|
+
// -------------------------------------------------------------------------
|
|
137
|
+
case 'get-route': {
|
|
138
|
+
const loc = typeof window !== 'undefined' ? window.location : {};
|
|
139
|
+
const result = {
|
|
140
|
+
path: loc.pathname || '/',
|
|
141
|
+
query: Object.fromEntries(new URLSearchParams(loc.search || '')),
|
|
142
|
+
hash: loc.hash || '',
|
|
143
|
+
fullUrl: loc.href || '',
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Try to get What Router state if available
|
|
147
|
+
try {
|
|
148
|
+
const core = window.__WHAT_CORE__;
|
|
149
|
+
if (core?.routerState) {
|
|
150
|
+
const state = typeof core.routerState === 'function'
|
|
151
|
+
? core.routerState()
|
|
152
|
+
: core.routerState;
|
|
153
|
+
if (state) {
|
|
154
|
+
result.params = state.params || {};
|
|
155
|
+
result.matchedRoute = state.pattern || state.route || null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// Router state not available — that's fine, we have window.location
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// -------------------------------------------------------------------------
|
|
166
|
+
// navigate — Programmatically change the route
|
|
167
|
+
// -------------------------------------------------------------------------
|
|
168
|
+
case 'navigate': {
|
|
169
|
+
const { path, replace } = args || {};
|
|
170
|
+
|
|
171
|
+
if (!path) {
|
|
172
|
+
return { error: 'No path provided' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
// Prefer What Router's navigate() if available
|
|
177
|
+
const core = window.__WHAT_CORE__;
|
|
178
|
+
if (core?.navigate) {
|
|
179
|
+
core.navigate(path, { replace: !!replace });
|
|
180
|
+
} else if (replace) {
|
|
181
|
+
history.replaceState(null, '', path);
|
|
182
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
183
|
+
} else {
|
|
184
|
+
history.pushState(null, '', path);
|
|
185
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
navigatedTo: path,
|
|
190
|
+
currentPath: window.location.pathname,
|
|
191
|
+
method: replace ? 'replaceState' : 'pushState',
|
|
192
|
+
usedWhatRouter: !!(core?.navigate),
|
|
193
|
+
success: true,
|
|
194
|
+
};
|
|
195
|
+
} catch (e) {
|
|
196
|
+
return { error: e.message };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// -------------------------------------------------------------------------
|
|
201
|
+
// Not handled — return null so caller falls through
|
|
202
|
+
// -------------------------------------------------------------------------
|
|
203
|
+
default:
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|