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/src/index.js
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* what-devtools-mcp — MCP server entry point.
|
|
4
|
+
* Creates WS bridge, registers tools + resources, connects MCP stdio transport.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
|
+
import { createBridge } from './bridge.js';
|
|
10
|
+
import { registerTools } from './tools.js';
|
|
11
|
+
|
|
12
|
+
const port = parseInt(process.env.WHAT_MCP_PORT || '9229', 10);
|
|
13
|
+
|
|
14
|
+
const bridge = createBridge({ port });
|
|
15
|
+
|
|
16
|
+
const server = new McpServer({
|
|
17
|
+
name: 'what-devtools-mcp',
|
|
18
|
+
version: '0.2.0',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
registerTools(server, bridge);
|
|
22
|
+
|
|
23
|
+
// --- MCP Resources: static docs for agent context ---
|
|
24
|
+
|
|
25
|
+
server.resource(
|
|
26
|
+
'reactivity-model',
|
|
27
|
+
'what://docs/reactivity-model',
|
|
28
|
+
{ description: 'How What Framework reactivity works — signals, effects, computed, batch' },
|
|
29
|
+
async () => ({
|
|
30
|
+
contents: [{
|
|
31
|
+
uri: 'what://docs/reactivity-model',
|
|
32
|
+
mimeType: 'text/markdown',
|
|
33
|
+
text: `# What Framework Reactivity Model
|
|
34
|
+
|
|
35
|
+
## Signals
|
|
36
|
+
A signal is a reactive value. Read with \`sig()\`, write with \`sig(newValue)\` or \`sig(prev => next)\`.
|
|
37
|
+
Signals track which effects read them and notify those effects when they change.
|
|
38
|
+
|
|
39
|
+
\`\`\`js
|
|
40
|
+
const count = signal(0, 'count'); // second arg is optional debug name
|
|
41
|
+
count() // read (returns 0)
|
|
42
|
+
count(5) // write (sets to 5, notifies subscribers)
|
|
43
|
+
count.peek() // read without tracking (no effect subscription)
|
|
44
|
+
\`\`\`
|
|
45
|
+
|
|
46
|
+
## Effects
|
|
47
|
+
An effect runs a function and auto-tracks which signals it reads. When any tracked signal changes, the effect re-runs.
|
|
48
|
+
|
|
49
|
+
\`\`\`js
|
|
50
|
+
effect(() => {
|
|
51
|
+
console.log('Count is:', count()); // auto-tracks count
|
|
52
|
+
});
|
|
53
|
+
\`\`\`
|
|
54
|
+
|
|
55
|
+
Effects flush asynchronously via microtask, NOT synchronously.
|
|
56
|
+
|
|
57
|
+
## Computed
|
|
58
|
+
Derived signal. Lazy — only recomputes when deps change AND it's read.
|
|
59
|
+
|
|
60
|
+
\`\`\`js
|
|
61
|
+
const doubled = computed(() => count() * 2);
|
|
62
|
+
\`\`\`
|
|
63
|
+
|
|
64
|
+
## Batch
|
|
65
|
+
Group signal writes; effects run once at the end.
|
|
66
|
+
|
|
67
|
+
\`\`\`js
|
|
68
|
+
batch(() => {
|
|
69
|
+
name('Alice');
|
|
70
|
+
age(30);
|
|
71
|
+
// effects that read name or age run once after batch, not twice
|
|
72
|
+
});
|
|
73
|
+
\`\`\`
|
|
74
|
+
|
|
75
|
+
## Common Bugs
|
|
76
|
+
- **Signal read in event handler**: Event handlers are wrapped in untrack(). Reading a signal in onclick doesn't create a subscription.
|
|
77
|
+
- **Effect writes to signal it reads**: Creates an infinite loop. Use untrack() to read without subscribing.
|
|
78
|
+
- **Stale closure**: Effect function captures old value. Read the signal inside the effect, not outside.
|
|
79
|
+
`,
|
|
80
|
+
}],
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
server.resource(
|
|
85
|
+
'debugging-guide',
|
|
86
|
+
'what://docs/debugging-guide',
|
|
87
|
+
{ description: 'How to debug What Framework apps using the MCP devtools' },
|
|
88
|
+
async () => ({
|
|
89
|
+
contents: [{
|
|
90
|
+
uri: 'what://docs/debugging-guide',
|
|
91
|
+
mimeType: 'text/markdown',
|
|
92
|
+
text: `# Debugging What Framework Apps
|
|
93
|
+
|
|
94
|
+
## Step 1: Check connection
|
|
95
|
+
Call \`what_connection_status\` to verify the app is connected.
|
|
96
|
+
|
|
97
|
+
## Step 2: Get the lay of the land
|
|
98
|
+
Call \`what_diagnose\` for a comprehensive health check, or \`what_snapshot\` for raw data.
|
|
99
|
+
|
|
100
|
+
## Step 3: Investigate specific issues
|
|
101
|
+
|
|
102
|
+
### "UI isn't updating"
|
|
103
|
+
1. \`what_signals { filter: "relevant_name" }\` — is the signal value what you expect?
|
|
104
|
+
2. \`what_watch { duration: 5000 }\` — ask user to trigger the action. Do signal:updated events appear?
|
|
105
|
+
3. If no updates: the event handler isn't calling sig(newValue). Check the source code.
|
|
106
|
+
4. If updates appear but UI doesn't change: the component isn't reading the signal reactively. Check for stale closures or peek() usage.
|
|
107
|
+
|
|
108
|
+
### "Infinite re-render / effect loop"
|
|
109
|
+
1. \`what_effects { minRunCount: 50 }\` — find effects with high run counts
|
|
110
|
+
2. \`what_dependency_graph { effectId: N }\` — see what signals this effect reads and writes
|
|
111
|
+
3. If an effect reads and writes the same signal: use untrack() for the read
|
|
112
|
+
|
|
113
|
+
### "Slow performance"
|
|
114
|
+
1. \`what_diagnose { focus: "performance" }\` — identify hot effects
|
|
115
|
+
2. \`what_effects { minRunCount: 20 }\` — find frequently-running effects
|
|
116
|
+
3. Consider using batch() to group signal writes
|
|
117
|
+
|
|
118
|
+
### "Component shows wrong data"
|
|
119
|
+
1. \`what_component_tree\` — verify component hierarchy
|
|
120
|
+
2. \`what_dom_inspect { componentId: N }\` — see actual rendered output
|
|
121
|
+
3. \`what_signals\` — check if signal values are correct
|
|
122
|
+
|
|
123
|
+
### "Route not working"
|
|
124
|
+
1. \`what_route\` — check current path, params, matched pattern
|
|
125
|
+
2. \`what_navigate { path: "/expected" }\` — test navigation programmatically
|
|
126
|
+
`,
|
|
127
|
+
}],
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
server.resource(
|
|
132
|
+
'api-reference',
|
|
133
|
+
'what://docs/api-reference',
|
|
134
|
+
{ description: 'Complete API reference for What Framework core, hooks, components, data, stores, forms, and utilities' },
|
|
135
|
+
async () => ({
|
|
136
|
+
contents: [{
|
|
137
|
+
uri: 'what://docs/api-reference',
|
|
138
|
+
mimeType: 'text/markdown',
|
|
139
|
+
text: `# What Framework API Reference
|
|
140
|
+
|
|
141
|
+
## Reactive Primitives
|
|
142
|
+
- \`signal(initial, debugName?)\` — create reactive value. Read: \`sig()\`. Write: \`sig(newVal)\` or \`sig(prev => next)\`.
|
|
143
|
+
- \`computed(fn)\` — derived value (lazy, only recomputes when deps change AND it's read)
|
|
144
|
+
- \`memo(fn)\` — derived value (eager, deduped — only propagates when value actually changes)
|
|
145
|
+
- \`effect(fn)\` — side effect, auto-tracks deps. Returns dispose function. Flushes async via microtask.
|
|
146
|
+
- \`batch(fn)\` — group writes, effects run once after
|
|
147
|
+
- \`untrack(fn)\` — read signals without subscribing
|
|
148
|
+
- \`flushSync()\` — force pending effects to run synchronously
|
|
149
|
+
- \`createRoot(fn)\` — isolated reactive scope with ownership tree
|
|
150
|
+
- \`getOwner()\` — get current ownership context
|
|
151
|
+
- \`runWithOwner(owner, fn)\` — run within a specific owner context
|
|
152
|
+
- \`onCleanup(fn)\` — register cleanup with current root (alias: onRootCleanup)
|
|
153
|
+
|
|
154
|
+
## Component Hooks
|
|
155
|
+
- \`useState(initial)\` — React-compatible state hook (returns [getter, setter])
|
|
156
|
+
- \`useSignal(initial)\` — signal scoped to component
|
|
157
|
+
- \`useComputed(fn)\` — computed scoped to component
|
|
158
|
+
- \`useEffect(fn, deps?)\` — effect with optional dep array
|
|
159
|
+
- \`useRef(initial)\` — mutable ref that persists across renders
|
|
160
|
+
- \`useMemo(fn, deps)\` — memoized value
|
|
161
|
+
- \`useCallback(fn, deps)\` — memoized callback
|
|
162
|
+
- \`useContext(Context)\` — read context value
|
|
163
|
+
- \`useReducer(reducer, initial)\` — reducer pattern
|
|
164
|
+
- \`createContext(defaultValue)\` — create a context
|
|
165
|
+
- \`onMount(fn)\` — run once after component mounts (client-only)
|
|
166
|
+
- \`onCleanup(fn)\` — run when component unmounts
|
|
167
|
+
- \`createResource(fetcher)\` — async resource with loading/error states
|
|
168
|
+
|
|
169
|
+
## Built-in Components
|
|
170
|
+
- \`<Show when={signal()} fallback={...}>\` — conditional rendering
|
|
171
|
+
- \`<For each={items()}>{item => ...}</For>\` — list rendering (keyed)
|
|
172
|
+
- \`<Switch><Match when={...}>...</Match></Switch>\` — multi-branch conditional
|
|
173
|
+
- \`<Suspense fallback={...}>\` — async loading boundary
|
|
174
|
+
- \`<ErrorBoundary fallback={...}>\` — error catching boundary
|
|
175
|
+
- \`<Island>\` — client-hydrated island
|
|
176
|
+
- \`lazy(() => import(...))\` — code-split component
|
|
177
|
+
- \`memo(Component)\` — memoized component (no-op in run-once model)
|
|
178
|
+
|
|
179
|
+
## Data Fetching
|
|
180
|
+
- \`useSWR(key, fetcher)\` — returns \`{ data(), isLoading(), error() }\` as signal getters
|
|
181
|
+
- \`useQuery(key, fetcher, opts?)\` — query with caching and revalidation
|
|
182
|
+
- \`useFetch(url, opts?)\` — simple fetch wrapper
|
|
183
|
+
- \`useInfiniteQuery(key, fetcher)\` — paginated data
|
|
184
|
+
- \`invalidateQueries(key)\` — force refetch
|
|
185
|
+
- \`prefetchQuery(key, fetcher)\` — prefetch data
|
|
186
|
+
- \`setQueryData(key, data)\` / \`getQueryData(key)\` — manual cache manipulation
|
|
187
|
+
- \`clearCache()\` — clear all cached queries
|
|
188
|
+
|
|
189
|
+
## DOM
|
|
190
|
+
- \`mount(vnode, selector)\` — mount app to DOM
|
|
191
|
+
- \`h(tag, props, ...children)\` — create virtual node (internal — use JSX)
|
|
192
|
+
- \`html\\\`...\\\`\` — tagged template literal for HTML
|
|
193
|
+
- \`Fragment\` — fragment component
|
|
194
|
+
- Event handlers: lowercase in h() (\`onclick\`), camelCase in JSX (\`onClick\`)
|
|
195
|
+
|
|
196
|
+
## Stores
|
|
197
|
+
- \`createStore(initialState)\` — reactive store with \`{ state, set, derived }\`
|
|
198
|
+
- \`derived(fn)\` — derive from store state
|
|
199
|
+
- \`storeComputed(fn)\` — computed from store
|
|
200
|
+
- \`atom(initial)\` — lightweight single-value store
|
|
201
|
+
|
|
202
|
+
## Forms
|
|
203
|
+
- \`useForm({ fields, onSubmit })\` — form management with validation
|
|
204
|
+
- \`useField(name, rules)\` — individual field management
|
|
205
|
+
- \`rules.required(msg)\` / \`rules.email(msg)\` / \`rules.minLength(n, msg)\` etc.
|
|
206
|
+
- \`zodResolver(schema)\` / \`yupResolver(schema)\` — schema validation adapters
|
|
207
|
+
- \`<Input />\` / \`<Textarea />\` / \`<Select />\` / \`<Checkbox />\` / \`<Radio />\` — form components
|
|
208
|
+
|
|
209
|
+
## Head Management
|
|
210
|
+
- \`<Head><title>...</title></Head>\` — manage document head
|
|
211
|
+
- \`clearHead()\` — reset head tags
|
|
212
|
+
|
|
213
|
+
## Animation
|
|
214
|
+
- \`spring(target, config?)\` — spring-based animation
|
|
215
|
+
- \`tween(from, to, config?)\` — tween animation
|
|
216
|
+
- \`useTransition(signal, config?)\` — transition between values
|
|
217
|
+
- \`useGesture(element, handlers)\` — gesture recognition
|
|
218
|
+
- \`cssTransition(classes)\` — CSS-based transitions
|
|
219
|
+
|
|
220
|
+
## Accessibility
|
|
221
|
+
- \`useFocus()\` / \`useFocusTrap()\` / \`useFocusRestore()\` — focus management
|
|
222
|
+
- \`useRovingTabIndex()\` — keyboard navigation
|
|
223
|
+
- \`announce(msg)\` / \`announceAssertive(msg)\` — screen reader announcements
|
|
224
|
+
- \`<SkipLink />\` / \`<VisuallyHidden />\` / \`<LiveRegion />\` — a11y components
|
|
225
|
+
- \`useAriaExpanded()\` / \`useAriaSelected()\` / \`useAriaChecked()\` — ARIA state
|
|
226
|
+
- \`Keys\` / \`onKey()\` / \`onKeys()\` — keyboard event helpers
|
|
227
|
+
- \`useId()\` / \`useIds()\` — unique ID generation
|
|
228
|
+
|
|
229
|
+
## Scheduler
|
|
230
|
+
- \`scheduleRead(fn)\` / \`scheduleWrite(fn)\` — prevent layout thrashing
|
|
231
|
+
- \`measure(fn)\` / \`mutate(fn)\` — batch DOM reads and writes
|
|
232
|
+
- \`nextFrame(fn)\` / \`raf(fn)\` — animation frame scheduling
|
|
233
|
+
- \`onResize(el, fn)\` / \`onIntersect(el, fn)\` — observer utilities
|
|
234
|
+
|
|
235
|
+
## Utilities
|
|
236
|
+
- \`cls(...args)\` — conditional class names
|
|
237
|
+
- \`style(obj)\` — reactive style object
|
|
238
|
+
- \`debounce(fn, ms)\` / \`throttle(fn, ms)\` — rate limiting
|
|
239
|
+
- \`useMediaQuery(query)\` — reactive media query
|
|
240
|
+
- \`useLocalStorage(key, initial)\` — persistent signal
|
|
241
|
+
- \`useClickOutside(ref, fn)\` — detect outside clicks
|
|
242
|
+
- \`<Portal target={selector}>\` — render into a different DOM node
|
|
243
|
+
|
|
244
|
+
## Error System (Agent-First)
|
|
245
|
+
- \`WhatError\` — structured error class with code, suggestion, context
|
|
246
|
+
- \`ERROR_CODES\` — all error code definitions
|
|
247
|
+
- \`createWhatError(code, context)\` — create a structured error
|
|
248
|
+
- \`classifyError(err, context)\` — classify a raw Error into WhatError
|
|
249
|
+
- \`getCollectedErrors(since?)\` — retrieve accumulated errors (dev mode)
|
|
250
|
+
|
|
251
|
+
## Agent Guardrails
|
|
252
|
+
- \`configureGuardrails(overrides)\` — enable/disable specific guardrails
|
|
253
|
+
- \`validateImports(names)\` — check that import names are valid exports
|
|
254
|
+
- \`checkComponentName(name)\` — verify PascalCase naming
|
|
255
|
+
|
|
256
|
+
## Agent Context
|
|
257
|
+
- \`installAgentContext()\` — expose \`window.__WHAT_AGENT__\` for AI agents
|
|
258
|
+
- \`getHealth()\` — health check: cycle risk, orphan effects, signal leaks, memory pressure
|
|
259
|
+
`,
|
|
260
|
+
}],
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// --- Agent Guide Resource ---
|
|
265
|
+
server.resource(
|
|
266
|
+
'agent-guide',
|
|
267
|
+
'what://docs/agent-guide',
|
|
268
|
+
{ description: 'Complete guide for AI coding agents working with What Framework' },
|
|
269
|
+
async () => ({
|
|
270
|
+
contents: [{
|
|
271
|
+
uri: 'what://docs/agent-guide',
|
|
272
|
+
mimeType: 'text/markdown',
|
|
273
|
+
text: `# What Framework Agent Guide
|
|
274
|
+
|
|
275
|
+
## Overview
|
|
276
|
+
What Framework is the first framework built for AI agents. It uses fine-grained reactivity (signals + effects) instead of virtual DOM diffing. Components run ONCE — signals handle all updates directly.
|
|
277
|
+
|
|
278
|
+
## Key Mental Model
|
|
279
|
+
1. **Components run once.** The function body executes a single time. There is no "re-render."
|
|
280
|
+
2. **Signals are the state.** Read with \`sig()\`, write with \`sig(newVal)\`. Signals auto-track which effects read them.
|
|
281
|
+
3. **Effects handle side-effects.** They auto-track signal reads and re-run when those signals change.
|
|
282
|
+
4. **DOM updates are fine-grained.** When a signal changes, only the specific DOM node that reads it updates — no diffing, no reconciliation.
|
|
283
|
+
|
|
284
|
+
## The #1 Mistake: Missing Signal Calls
|
|
285
|
+
Signals are functions. You MUST call them to read the value:
|
|
286
|
+
\`\`\`js
|
|
287
|
+
// WRONG — renders "[Function]"
|
|
288
|
+
<span>{count}</span>
|
|
289
|
+
|
|
290
|
+
// CORRECT — renders the actual value
|
|
291
|
+
<span>{count()}</span>
|
|
292
|
+
\`\`\`
|
|
293
|
+
|
|
294
|
+
## Creating Components
|
|
295
|
+
\`\`\`js
|
|
296
|
+
import { signal, effect, onMount } from 'what-framework';
|
|
297
|
+
|
|
298
|
+
function Counter() {
|
|
299
|
+
const count = signal(0, 'count');
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<div>
|
|
303
|
+
<span>{count()}</span>
|
|
304
|
+
<button onclick={() => count(c => c + 1)}>+1</button>
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
\`\`\`
|
|
309
|
+
|
|
310
|
+
## MCP Devtools Workflow
|
|
311
|
+
1. \`what_connection_status\` — verify app is connected
|
|
312
|
+
2. \`what_diagnose\` — comprehensive health check
|
|
313
|
+
3. \`what_lint { code: "..." }\` — static analysis on code you write
|
|
314
|
+
4. \`what_scaffold { type: "component", name: "MyComponent" }\` — generate boilerplate
|
|
315
|
+
5. \`what_snapshot { diff: true }\` — see what changed after an action
|
|
316
|
+
6. \`what_fix { error: "ERR_INFINITE_EFFECT" }\` — get fix for any error code
|
|
317
|
+
7. \`what_perf\` — performance snapshot with hot effects and memory estimate
|
|
318
|
+
|
|
319
|
+
## Common Patterns
|
|
320
|
+
|
|
321
|
+
### Conditional rendering
|
|
322
|
+
\`\`\`js
|
|
323
|
+
<Show when={isLoggedIn()} fallback={<LoginForm />}>
|
|
324
|
+
<Dashboard />
|
|
325
|
+
</Show>
|
|
326
|
+
\`\`\`
|
|
327
|
+
|
|
328
|
+
### List rendering
|
|
329
|
+
\`\`\`js
|
|
330
|
+
<For each={items()}>{(item) =>
|
|
331
|
+
<li key={item.id}>{item.name}</li>
|
|
332
|
+
}</For>
|
|
333
|
+
\`\`\`
|
|
334
|
+
|
|
335
|
+
### Data fetching
|
|
336
|
+
\`\`\`js
|
|
337
|
+
const { data, isLoading, error } = useSWR('/api/users', fetchJSON);
|
|
338
|
+
// data(), isLoading(), error() are signals — call them!
|
|
339
|
+
\`\`\`
|
|
340
|
+
|
|
341
|
+
### Derived values
|
|
342
|
+
\`\`\`js
|
|
343
|
+
const total = computed(() => items().reduce((sum, i) => sum + i.price, 0));
|
|
344
|
+
\`\`\`
|
|
345
|
+
|
|
346
|
+
### Event handlers
|
|
347
|
+
\`\`\`js
|
|
348
|
+
// JSX uses camelCase — compiler transforms to lowercase
|
|
349
|
+
<button onClick={() => count(c => c + 1)}>Add</button>
|
|
350
|
+
|
|
351
|
+
// In h() calls, use lowercase
|
|
352
|
+
h('button', { onclick: () => count(c => c + 1) }, 'Add')
|
|
353
|
+
\`\`\`
|
|
354
|
+
|
|
355
|
+
## Error Codes Quick Reference
|
|
356
|
+
| Code | What it means | Fix |
|
|
357
|
+
|------|---------------|-----|
|
|
358
|
+
| ERR_INFINITE_EFFECT | Effect reads and writes same signal | Use untrack() for the read |
|
|
359
|
+
| ERR_MISSING_SIGNAL_READ | Signal used without () | Add () to read: count() |
|
|
360
|
+
| ERR_HYDRATION_MISMATCH | Server/client HTML differ | Use onMount() for client-only code |
|
|
361
|
+
| ERR_ORPHAN_EFFECT | Effect outside reactive root | Wrap in createRoot() |
|
|
362
|
+
| ERR_SIGNAL_WRITE_IN_RENDER | Signal written in component body | Move write to event handler |
|
|
363
|
+
| ERR_MISSING_CLEANUP | Effect has no cleanup return | Return cleanup function |
|
|
364
|
+
| ERR_UNSAFE_INNERHTML | innerHTML without __html marker | Use { __html: content } |
|
|
365
|
+
| ERR_MISSING_KEY | List without key prop | Add key={item.id} |
|
|
366
|
+
`,
|
|
367
|
+
}],
|
|
368
|
+
})
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
// --- Error Codes Resource ---
|
|
372
|
+
server.resource(
|
|
373
|
+
'error-codes',
|
|
374
|
+
'what://docs/error-codes',
|
|
375
|
+
{ description: 'All What Framework error codes with explanations, fixes, and code examples' },
|
|
376
|
+
async () => ({
|
|
377
|
+
contents: [{
|
|
378
|
+
uri: 'what://docs/error-codes',
|
|
379
|
+
mimeType: 'text/markdown',
|
|
380
|
+
text: `# What Framework Error Codes
|
|
381
|
+
|
|
382
|
+
## ERR_INFINITE_EFFECT
|
|
383
|
+
**Severity:** error
|
|
384
|
+
**Cause:** An effect reads and writes the same signal, creating a cycle. Each write triggers a re-run, which reads again, which writes again...
|
|
385
|
+
**Fix:** Use \`untrack()\` to read the signal without subscribing:
|
|
386
|
+
\`\`\`js
|
|
387
|
+
// Before (broken):
|
|
388
|
+
effect(() => { count(count() + 1); });
|
|
389
|
+
|
|
390
|
+
// After (fixed):
|
|
391
|
+
effect(() => { count(untrack(count) + 1); });
|
|
392
|
+
\`\`\`
|
|
393
|
+
|
|
394
|
+
## ERR_MISSING_SIGNAL_READ
|
|
395
|
+
**Severity:** warning
|
|
396
|
+
**Cause:** A signal function reference is used where its VALUE was intended. Signals are functions that must be called.
|
|
397
|
+
**Fix:** Add \`()\` after the signal name:
|
|
398
|
+
\`\`\`js
|
|
399
|
+
// Before (broken — renders "[Function]"):
|
|
400
|
+
<span>{count}</span>
|
|
401
|
+
|
|
402
|
+
// After (fixed):
|
|
403
|
+
<span>{count()}</span>
|
|
404
|
+
\`\`\`
|
|
405
|
+
|
|
406
|
+
## ERR_HYDRATION_MISMATCH
|
|
407
|
+
**Severity:** error
|
|
408
|
+
**Cause:** Server-rendered HTML differs from what the client expects. Usually caused by reading browser APIs during initial render.
|
|
409
|
+
**Fix:** Use \`onMount()\` for client-only logic:
|
|
410
|
+
\`\`\`js
|
|
411
|
+
// Before (broken):
|
|
412
|
+
function App() { return <p>{window.innerWidth}</p>; }
|
|
413
|
+
|
|
414
|
+
// After (fixed):
|
|
415
|
+
function App() {
|
|
416
|
+
const width = signal(0);
|
|
417
|
+
onMount(() => width(window.innerWidth));
|
|
418
|
+
return <p>{width()}</p>;
|
|
419
|
+
}
|
|
420
|
+
\`\`\`
|
|
421
|
+
|
|
422
|
+
## ERR_ORPHAN_EFFECT
|
|
423
|
+
**Severity:** warning
|
|
424
|
+
**Cause:** An effect created outside any reactive root or component function. It will never be cleaned up.
|
|
425
|
+
**Fix:** Create effects inside components or wrap in \`createRoot()\`.
|
|
426
|
+
|
|
427
|
+
## ERR_SIGNAL_WRITE_IN_RENDER
|
|
428
|
+
**Severity:** error
|
|
429
|
+
**Cause:** A signal is written during the component function body (render phase), causing immediate re-execution.
|
|
430
|
+
**Fix:** Move writes to event handlers, effects, or \`onMount()\`.
|
|
431
|
+
|
|
432
|
+
## ERR_MISSING_CLEANUP
|
|
433
|
+
**Severity:** warning
|
|
434
|
+
**Cause:** An effect sets up a resource (listener, timer, subscription) but returns no cleanup.
|
|
435
|
+
**Fix:** Return a cleanup function:
|
|
436
|
+
\`\`\`js
|
|
437
|
+
effect(() => {
|
|
438
|
+
window.addEventListener('resize', handler);
|
|
439
|
+
return () => window.removeEventListener('resize', handler);
|
|
440
|
+
});
|
|
441
|
+
\`\`\`
|
|
442
|
+
|
|
443
|
+
## ERR_UNSAFE_INNERHTML
|
|
444
|
+
**Severity:** warning
|
|
445
|
+
**Cause:** innerHTML set without the __html safety marker. XSS risk.
|
|
446
|
+
**Fix:** Use \`{ __html: content }\` or the \`html\` tagged template.
|
|
447
|
+
|
|
448
|
+
## ERR_MISSING_KEY
|
|
449
|
+
**Severity:** warning
|
|
450
|
+
**Cause:** List items rendered without unique key props, causing incorrect reordering.
|
|
451
|
+
**Fix:** Add \`key={item.id}\` using a stable identifier.
|
|
452
|
+
`,
|
|
453
|
+
}],
|
|
454
|
+
})
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
// --- Patterns Resource ---
|
|
458
|
+
server.resource(
|
|
459
|
+
'patterns',
|
|
460
|
+
'what://docs/patterns',
|
|
461
|
+
{ description: 'Common What Framework patterns: component, form, store, list, island' },
|
|
462
|
+
async () => ({
|
|
463
|
+
contents: [{
|
|
464
|
+
uri: 'what://docs/patterns',
|
|
465
|
+
mimeType: 'text/markdown',
|
|
466
|
+
text: `# What Framework Common Patterns
|
|
467
|
+
|
|
468
|
+
## Component Pattern
|
|
469
|
+
\`\`\`js
|
|
470
|
+
import { signal, onMount } from 'what-framework';
|
|
471
|
+
|
|
472
|
+
function MyComponent({ title }) {
|
|
473
|
+
const isActive = signal(false, 'isActive');
|
|
474
|
+
|
|
475
|
+
onMount(() => {
|
|
476
|
+
// Client-only initialization
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
return (
|
|
480
|
+
<div class={cls({ active: isActive() })}>
|
|
481
|
+
<h2>{title}</h2>
|
|
482
|
+
<button onclick={() => isActive(v => !v)}>Toggle</button>
|
|
483
|
+
</div>
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
\`\`\`
|
|
487
|
+
|
|
488
|
+
## Form Pattern
|
|
489
|
+
\`\`\`js
|
|
490
|
+
import { useForm, Input, ErrorMessage, rules } from 'what-framework';
|
|
491
|
+
|
|
492
|
+
function LoginForm() {
|
|
493
|
+
const { fields, handleSubmit, isSubmitting } = useForm({
|
|
494
|
+
fields: {
|
|
495
|
+
email: { initial: '', rules: [rules.required(), rules.email()] },
|
|
496
|
+
password: { initial: '', rules: [rules.required(), rules.minLength(8)] },
|
|
497
|
+
},
|
|
498
|
+
onSubmit: async (values) => {
|
|
499
|
+
await login(values.email, values.password);
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
return (
|
|
504
|
+
<form onsubmit={handleSubmit}>
|
|
505
|
+
<Input field={fields.email} type="email" placeholder="Email" />
|
|
506
|
+
<ErrorMessage field={fields.email} />
|
|
507
|
+
<Input field={fields.password} type="password" placeholder="Password" />
|
|
508
|
+
<ErrorMessage field={fields.password} />
|
|
509
|
+
<button type="submit" disabled={isSubmitting()}>
|
|
510
|
+
{isSubmitting() ? 'Logging in...' : 'Login'}
|
|
511
|
+
</button>
|
|
512
|
+
</form>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
\`\`\`
|
|
516
|
+
|
|
517
|
+
## Store Pattern
|
|
518
|
+
\`\`\`js
|
|
519
|
+
import { createStore, derived } from 'what-framework';
|
|
520
|
+
|
|
521
|
+
const store = createStore({
|
|
522
|
+
items: [],
|
|
523
|
+
filter: 'all',
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
const filteredItems = derived(state =>
|
|
527
|
+
state.filter === 'all' ? state.items : state.items.filter(i => i.status === state.filter)
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
export function addItem(item) {
|
|
531
|
+
store.set(s => ({ ...s, items: [...s.items, item] }));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function setFilter(filter) {
|
|
535
|
+
store.set(s => ({ ...s, filter }));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export { store, filteredItems };
|
|
539
|
+
\`\`\`
|
|
540
|
+
|
|
541
|
+
## List Pattern
|
|
542
|
+
\`\`\`js
|
|
543
|
+
import { signal, For } from 'what-framework';
|
|
544
|
+
|
|
545
|
+
function TodoList() {
|
|
546
|
+
const todos = signal([], 'todos');
|
|
547
|
+
const input = signal('', 'input');
|
|
548
|
+
|
|
549
|
+
const addTodo = () => {
|
|
550
|
+
if (input().trim()) {
|
|
551
|
+
todos(t => [...t, { id: Date.now(), text: input(), done: false }]);
|
|
552
|
+
input('');
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
return (
|
|
557
|
+
<div>
|
|
558
|
+
<input
|
|
559
|
+
value={input()}
|
|
560
|
+
oninput={e => input(e.target.value)}
|
|
561
|
+
onkeydown={e => e.key === 'Enter' && addTodo()}
|
|
562
|
+
/>
|
|
563
|
+
<For each={todos()}>
|
|
564
|
+
{todo => <li key={todo.id}>{todo.text}</li>}
|
|
565
|
+
</For>
|
|
566
|
+
</div>
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
\`\`\`
|
|
570
|
+
|
|
571
|
+
## Island Pattern
|
|
572
|
+
\`\`\`js
|
|
573
|
+
// islands/InteractiveWidget.jsx
|
|
574
|
+
import { signal, onMount } from 'what-framework';
|
|
575
|
+
|
|
576
|
+
function InteractiveWidget({ initialData }) {
|
|
577
|
+
const data = signal(initialData, 'data');
|
|
578
|
+
|
|
579
|
+
onMount(() => {
|
|
580
|
+
// Hydrate with fresh data from API
|
|
581
|
+
fetch('/api/widget').then(r => r.json()).then(data);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
return (
|
|
585
|
+
<div data-island="interactive-widget">
|
|
586
|
+
<span>{data()?.title}</span>
|
|
587
|
+
</div>
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
InteractiveWidget.island = true;
|
|
592
|
+
export default InteractiveWidget;
|
|
593
|
+
\`\`\`
|
|
594
|
+
|
|
595
|
+
## Data Fetching Pattern
|
|
596
|
+
\`\`\`js
|
|
597
|
+
import { useSWR, Show } from 'what-framework';
|
|
598
|
+
|
|
599
|
+
function UserProfile({ userId }) {
|
|
600
|
+
const { data, isLoading, error } = useSWR(
|
|
601
|
+
\`/api/users/\${userId}\`,
|
|
602
|
+
(url) => fetch(url).then(r => r.json())
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
return (
|
|
606
|
+
<Show when={!isLoading()} fallback={<Spinner />}>
|
|
607
|
+
<Show when={!error()} fallback={<p>Error: {error()?.message}</p>}>
|
|
608
|
+
<h1>{data()?.name}</h1>
|
|
609
|
+
<p>{data()?.email}</p>
|
|
610
|
+
</Show>
|
|
611
|
+
</Show>
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
\`\`\`
|
|
615
|
+
`,
|
|
616
|
+
}],
|
|
617
|
+
})
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
// Import and register extended tools if available
|
|
621
|
+
try {
|
|
622
|
+
const { registerExtendedTools } = await import('./tools-extended.js');
|
|
623
|
+
registerExtendedTools(server, bridge);
|
|
624
|
+
} catch {
|
|
625
|
+
// Extended tools not yet available — that's fine
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Import and register agent-first tools
|
|
629
|
+
try {
|
|
630
|
+
const { registerAgentTools } = await import('./tools-agent.js');
|
|
631
|
+
registerAgentTools(server, bridge);
|
|
632
|
+
} catch {
|
|
633
|
+
// Agent tools not yet available — that's fine
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const transport = new StdioServerTransport();
|
|
637
|
+
await server.connect(transport);
|