what-core 0.5.6 → 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 +8 -6
- package/dist/components.js +1 -1
- package/dist/dom.js +127 -451
- package/dist/h.js +1 -1
- package/dist/hooks.js +4 -0
- package/dist/index.js +5919 -123
- package/dist/index.js.map +7 -0
- package/dist/index.min.js +123 -0
- package/dist/index.min.js.map +7 -0
- package/dist/jsx-dev-runtime.js +51 -0
- package/dist/jsx-dev-runtime.js.map +7 -0
- package/dist/jsx-dev-runtime.min.js +2 -0
- package/dist/jsx-dev-runtime.min.js.map +7 -0
- package/dist/jsx-runtime.js +49 -0
- package/dist/jsx-runtime.js.map +7 -0
- package/dist/jsx-runtime.min.js +2 -0
- package/dist/jsx-runtime.min.js.map +7 -0
- package/dist/reactive.js +175 -11
- package/dist/render.js +1502 -273
- package/dist/render.js.map +7 -0
- package/dist/render.min.js +2 -0
- package/dist/render.min.js.map +7 -0
- package/dist/testing.js +1204 -144
- package/dist/testing.js.map +7 -0
- package/dist/testing.min.js +2 -0
- package/dist/testing.min.js.map +7 -0
- package/dist/what.js +3 -2
- package/package.json +9 -4
- package/src/agent-context.js +126 -0
- package/src/components.js +10 -34
- package/src/dom.js +225 -745
- package/src/errors.js +253 -0
- package/src/guardrails.js +224 -0
- package/src/h.js +3 -3
- package/src/hooks.js +121 -52
- package/src/index.js +38 -4
- package/src/reactive.js +389 -41
- package/src/render.js +445 -14
- package/src/testing.js +169 -1
- package/src/warnings.js +110 -0
package/src/hooks.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
// What Framework - Hooks
|
|
2
|
-
// React-familiar hooks backed by signals
|
|
2
|
+
// React-familiar hooks backed by signals for run-once component model.
|
|
3
|
+
// Components run ONCE. Hooks return signal accessors (functions) so the
|
|
4
|
+
// fine-grained runtime handles reactive updates automatically via effects.
|
|
3
5
|
|
|
4
|
-
import { signal, computed, effect, batch, untrack, __DEV__ } from './reactive.js';
|
|
6
|
+
import { signal, computed, effect, batch, untrack, createRoot, __DEV__ } from './reactive.js';
|
|
5
7
|
import { getCurrentComponent } from './dom.js';
|
|
6
8
|
|
|
7
|
-
function getCtx() {
|
|
9
|
+
function getCtx(hookName) {
|
|
8
10
|
const ctx = getCurrentComponent();
|
|
9
11
|
if (!ctx) {
|
|
10
12
|
throw new Error(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
`[what] ${hookName || 'Hook'}() can only be called inside a component function. ` +
|
|
14
|
+
`Did you call it outside of a component or in an async callback? ` +
|
|
15
|
+
`If you need reactive state outside a component, use signal() directly.`
|
|
13
16
|
);
|
|
14
17
|
}
|
|
15
18
|
return ctx;
|
|
@@ -20,13 +23,14 @@ function getHook(ctx) {
|
|
|
20
23
|
return { index, exists: index < ctx.hooks.length };
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
let _useMemoNoDepsWarned = false;
|
|
24
|
-
|
|
25
26
|
// --- useState ---
|
|
26
|
-
// Returns [
|
|
27
|
+
// Returns [signalAccessor, setter]. The accessor is a function — call it to read.
|
|
28
|
+
// In JSX, {count} (where count is the signal function) auto-binds reactively
|
|
29
|
+
// via insert()'s effect wrapper. For interpolation, use count() explicitly.
|
|
30
|
+
// This matches Solid's API and works with the run-once component model.
|
|
27
31
|
|
|
28
32
|
export function useState(initial) {
|
|
29
|
-
const ctx = getCtx();
|
|
33
|
+
const ctx = getCtx('useState');
|
|
30
34
|
const { index, exists } = getHook(ctx);
|
|
31
35
|
|
|
32
36
|
if (!exists) {
|
|
@@ -35,7 +39,7 @@ export function useState(initial) {
|
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
const s = ctx.hooks[index];
|
|
38
|
-
return [s
|
|
42
|
+
return [s, s.set];
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
// --- useSignal ---
|
|
@@ -43,7 +47,7 @@ export function useState(initial) {
|
|
|
43
47
|
// Avoids array destructuring overhead.
|
|
44
48
|
|
|
45
49
|
export function useSignal(initial) {
|
|
46
|
-
const ctx = getCtx();
|
|
50
|
+
const ctx = getCtx('useSignal');
|
|
47
51
|
const { index, exists } = getHook(ctx);
|
|
48
52
|
|
|
49
53
|
if (!exists) {
|
|
@@ -57,7 +61,7 @@ export function useSignal(initial) {
|
|
|
57
61
|
// Derived value. Only recomputes when signal deps change.
|
|
58
62
|
|
|
59
63
|
export function useComputed(fn) {
|
|
60
|
-
const ctx = getCtx();
|
|
64
|
+
const ctx = getCtx('useComputed');
|
|
61
65
|
const { index, exists } = getHook(ctx);
|
|
62
66
|
|
|
63
67
|
if (!exists) {
|
|
@@ -68,72 +72,135 @@ export function useComputed(fn) {
|
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
// --- useEffect ---
|
|
71
|
-
// Side effect that runs after
|
|
72
|
-
//
|
|
75
|
+
// Side effect that runs after mount. In the run-once model, deps-based
|
|
76
|
+
// re-running only works if deps are signal accessors. The implementation:
|
|
77
|
+
// - No deps (undefined): wrap in effect() that auto-tracks signals
|
|
78
|
+
// - Empty deps []: run once on mount, cleanup on unmount
|
|
79
|
+
// - Deps [a, b]: create effect() that reads each dep (calling signal functions
|
|
80
|
+
// to establish tracking), then runs the callback when any dep changes
|
|
73
81
|
|
|
74
82
|
export function useEffect(fn, deps) {
|
|
75
|
-
const ctx = getCtx();
|
|
83
|
+
const ctx = getCtx('useEffect');
|
|
76
84
|
const { index, exists } = getHook(ctx);
|
|
77
85
|
|
|
78
86
|
if (!exists) {
|
|
79
|
-
ctx.hooks[index] = {
|
|
87
|
+
ctx.hooks[index] = { cleanup: null, dispose: null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Dev-mode: warn when deps array contains non-function values that look like signals
|
|
91
|
+
if (__DEV__ && Array.isArray(deps) && deps.length > 0) {
|
|
92
|
+
for (let i = 0; i < deps.length; i++) {
|
|
93
|
+
const dep = deps[i];
|
|
94
|
+
if (dep != null && typeof dep !== 'function') {
|
|
95
|
+
console.warn(
|
|
96
|
+
`[what] useEffect dep at index ${i} is not a function. ` +
|
|
97
|
+
`Did you mean to pass a signal? Use count instead of count()`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
80
101
|
}
|
|
81
102
|
|
|
82
103
|
const hook = ctx.hooks[index];
|
|
83
104
|
|
|
84
|
-
|
|
85
|
-
|
|
105
|
+
// Only set up once — component runs once
|
|
106
|
+
if (hook.dispose) return;
|
|
107
|
+
|
|
108
|
+
if (deps === undefined) {
|
|
109
|
+
// No deps array: wrap in effect() that auto-tracks signal reads inside fn
|
|
86
110
|
queueMicrotask(() => {
|
|
87
111
|
if (ctx.disposed) return;
|
|
88
|
-
|
|
89
|
-
|
|
112
|
+
hook.dispose = effect(() => {
|
|
113
|
+
if (hook.cleanup) {
|
|
114
|
+
try { hook.cleanup(); } catch (e) { /* cleanup error */ }
|
|
115
|
+
hook.cleanup = null;
|
|
116
|
+
}
|
|
117
|
+
const result = fn();
|
|
118
|
+
if (typeof result === 'function') hook.cleanup = result;
|
|
119
|
+
});
|
|
120
|
+
// Register disposal with component lifecycle
|
|
121
|
+
ctx.effects = ctx.effects || [];
|
|
122
|
+
ctx.effects.push(hook.dispose);
|
|
123
|
+
});
|
|
124
|
+
} else if (deps.length === 0) {
|
|
125
|
+
// Empty deps []: run once on mount, cleanup on unmount
|
|
126
|
+
queueMicrotask(() => {
|
|
127
|
+
if (ctx.disposed) return;
|
|
128
|
+
const result = fn();
|
|
129
|
+
if (typeof result === 'function') hook.cleanup = result;
|
|
130
|
+
});
|
|
131
|
+
// Mark as set up so we don't re-run
|
|
132
|
+
hook.dispose = true;
|
|
133
|
+
} else {
|
|
134
|
+
// Deps array with values: create a reactive effect that reads each dep.
|
|
135
|
+
// If a dep is a signal function, calling it establishes tracking.
|
|
136
|
+
// When any tracked signal changes, the effect re-runs the callback.
|
|
137
|
+
queueMicrotask(() => {
|
|
138
|
+
if (ctx.disposed) return;
|
|
139
|
+
hook.dispose = effect(() => {
|
|
140
|
+
// Read all deps to establish signal tracking
|
|
141
|
+
for (let i = 0; i < deps.length; i++) {
|
|
142
|
+
const dep = deps[i];
|
|
143
|
+
if (typeof dep === 'function' && dep._signal) {
|
|
144
|
+
dep(); // Read signal to track it
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Run cleanup from previous execution
|
|
149
|
+
if (hook.cleanup) {
|
|
150
|
+
try { hook.cleanup(); } catch (e) { /* cleanup error */ }
|
|
151
|
+
hook.cleanup = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Run the effect callback
|
|
155
|
+
const result = untrack(() => fn());
|
|
156
|
+
if (typeof result === 'function') hook.cleanup = result;
|
|
157
|
+
});
|
|
158
|
+
// Register disposal with component lifecycle
|
|
159
|
+
ctx.effects = ctx.effects || [];
|
|
160
|
+
ctx.effects.push(hook.dispose);
|
|
90
161
|
});
|
|
91
|
-
hook.deps = deps;
|
|
92
162
|
}
|
|
93
163
|
}
|
|
94
164
|
|
|
95
165
|
// --- useMemo ---
|
|
96
|
-
// Memoized value.
|
|
166
|
+
// Memoized computed value. Uses computed() for automatic signal tracking.
|
|
167
|
+
// The deps array is accepted for API compatibility but ignored internally —
|
|
168
|
+
// computed() auto-tracks signal dependencies.
|
|
169
|
+
// Returns a computed signal function (call it to read the value).
|
|
97
170
|
|
|
98
171
|
export function useMemo(fn, deps) {
|
|
99
|
-
const ctx = getCtx();
|
|
172
|
+
const ctx = getCtx('useMemo');
|
|
100
173
|
const { index, exists } = getHook(ctx);
|
|
101
174
|
|
|
102
|
-
if (__DEV__ && deps === undefined && !_useMemoNoDepsWarned) {
|
|
103
|
-
_useMemoNoDepsWarned = true;
|
|
104
|
-
console.warn(
|
|
105
|
-
'[what] useMemo() called without a deps array. ' +
|
|
106
|
-
'This recomputes every render. Use useComputed() for signal-derived values, ' +
|
|
107
|
-
'or pass deps to useMemo().'
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
175
|
if (!exists) {
|
|
112
|
-
ctx.hooks[index] = {
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const hook = ctx.hooks[index];
|
|
116
|
-
|
|
117
|
-
if (depsChanged(hook.deps, deps)) {
|
|
118
|
-
hook.value = fn();
|
|
119
|
-
hook.deps = deps;
|
|
176
|
+
ctx.hooks[index] = { computed: computed(fn) };
|
|
120
177
|
}
|
|
121
178
|
|
|
122
|
-
return
|
|
179
|
+
return ctx.hooks[index].computed;
|
|
123
180
|
}
|
|
124
181
|
|
|
125
182
|
// --- useCallback ---
|
|
126
|
-
// Memoized callback.
|
|
183
|
+
// Memoized callback. In the run-once model, the component function only
|
|
184
|
+
// executes once, so the callback reference is inherently stable.
|
|
185
|
+
// Simply store and return the function on first call.
|
|
127
186
|
|
|
128
187
|
export function useCallback(fn, deps) {
|
|
129
|
-
|
|
188
|
+
const ctx = getCtx('useCallback');
|
|
189
|
+
const { index, exists } = getHook(ctx);
|
|
190
|
+
|
|
191
|
+
if (!exists) {
|
|
192
|
+
ctx.hooks[index] = { callback: fn };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return ctx.hooks[index].callback;
|
|
130
196
|
}
|
|
131
197
|
|
|
132
198
|
// --- useRef ---
|
|
133
199
|
// Mutable ref object. Does NOT trigger re-renders.
|
|
200
|
+
// Works correctly in run-once model — the ref persists for the component lifetime.
|
|
134
201
|
|
|
135
202
|
export function useRef(initial) {
|
|
136
|
-
const ctx = getCtx();
|
|
203
|
+
const ctx = getCtx('useRef');
|
|
137
204
|
const { index, exists } = getHook(ctx);
|
|
138
205
|
|
|
139
206
|
if (!exists) {
|
|
@@ -146,7 +213,8 @@ export function useRef(initial) {
|
|
|
146
213
|
// --- useContext ---
|
|
147
214
|
// Read from the nearest Provider in the component tree, or the default value.
|
|
148
215
|
// Uses _parentCtx chain (persistent tree) instead of componentStack (runtime stack)
|
|
149
|
-
// so context works correctly in
|
|
216
|
+
// so context works correctly in effects and event handlers.
|
|
217
|
+
// Returns the signal itself (not its value) so that consumers get reactive updates.
|
|
150
218
|
|
|
151
219
|
export function useContext(context) {
|
|
152
220
|
// Walk up the _parentCtx chain to find the nearest provider
|
|
@@ -178,7 +246,7 @@ export function createContext(defaultValue) {
|
|
|
178
246
|
const context = {
|
|
179
247
|
_defaultValue: defaultValue,
|
|
180
248
|
Provider: ({ value, children }) => {
|
|
181
|
-
const ctx = getCtx();
|
|
249
|
+
const ctx = getCtx('Context.Provider');
|
|
182
250
|
if (!ctx._contextValues) ctx._contextValues = new Map();
|
|
183
251
|
if (!ctx._contextSignals) ctx._contextSignals = new Map();
|
|
184
252
|
|
|
@@ -202,10 +270,11 @@ export function createContext(defaultValue) {
|
|
|
202
270
|
}
|
|
203
271
|
|
|
204
272
|
// --- useReducer ---
|
|
205
|
-
// State management with a reducer function
|
|
273
|
+
// State management with a reducer function.
|
|
274
|
+
// Returns [signalAccessor, dispatch] — accessor is a signal function.
|
|
206
275
|
|
|
207
276
|
export function useReducer(reducer, initialState, init) {
|
|
208
|
-
const ctx = getCtx();
|
|
277
|
+
const ctx = getCtx('useReducer');
|
|
209
278
|
const { index, exists } = getHook(ctx);
|
|
210
279
|
|
|
211
280
|
if (!exists) {
|
|
@@ -218,14 +287,14 @@ export function useReducer(reducer, initialState, init) {
|
|
|
218
287
|
}
|
|
219
288
|
|
|
220
289
|
const hook = ctx.hooks[index];
|
|
221
|
-
return [hook.signal
|
|
290
|
+
return [hook.signal, hook.dispatch];
|
|
222
291
|
}
|
|
223
292
|
|
|
224
293
|
// --- onMount ---
|
|
225
294
|
// Run callback once when component mounts. SolidJS-style lifecycle.
|
|
226
295
|
|
|
227
296
|
export function onMount(fn) {
|
|
228
|
-
const ctx = getCtx();
|
|
297
|
+
const ctx = getCtx('onMount');
|
|
229
298
|
if (!ctx.mounted) {
|
|
230
299
|
ctx._mountCallbacks = ctx._mountCallbacks || [];
|
|
231
300
|
ctx._mountCallbacks.push(fn);
|
|
@@ -236,7 +305,7 @@ export function onMount(fn) {
|
|
|
236
305
|
// Register cleanup function to run when component unmounts.
|
|
237
306
|
|
|
238
307
|
export function onCleanup(fn) {
|
|
239
|
-
const ctx = getCtx();
|
|
308
|
+
const ctx = getCtx('onCleanup');
|
|
240
309
|
ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
|
|
241
310
|
ctx._cleanupCallbacks.push(fn);
|
|
242
311
|
}
|
|
@@ -302,7 +371,7 @@ export function createResource(fetcher, options = {}) {
|
|
|
302
371
|
return [data, { loading, error, refetch, mutate }];
|
|
303
372
|
}
|
|
304
373
|
|
|
305
|
-
// --- Dep comparison ---
|
|
374
|
+
// --- Dep comparison (kept for potential external use) ---
|
|
306
375
|
|
|
307
376
|
function depsChanged(oldDeps, newDeps) {
|
|
308
377
|
if (oldDeps === undefined) return true;
|
package/src/index.js
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
// The closest framework to vanilla JS.
|
|
3
3
|
|
|
4
4
|
// Reactive primitives
|
|
5
|
-
export { signal, computed, effect, memo as signalMemo, batch, untrack, flushSync, createRoot, __setDevToolsHooks } from './reactive.js';
|
|
5
|
+
export { signal, computed, effect, memo as signalMemo, batch, untrack, flushSync, createRoot, getOwner, runWithOwner, onCleanup as onRootCleanup, __setDevToolsHooks } from './reactive.js';
|
|
6
6
|
|
|
7
7
|
// Fine-grained rendering primitives
|
|
8
|
-
export { template, insert, mapArray, spread, setProp, delegateEvents, on, classList } from './render.js';
|
|
8
|
+
export { template, _template, _$template, svgTemplate, insert, mapArray, spread, setProp, delegateEvents, on, classList, hydrate, isHydrating, _$createComponent } from './render.js';
|
|
9
9
|
|
|
10
|
-
//
|
|
10
|
+
// JSX factory — Fragment and html tagged template are public APIs.
|
|
11
|
+
// h is exported for internal package use only (jsx-runtime, server, router, react-compat).
|
|
12
|
+
// It is NOT a public API — users should write JSX, which the compiler transforms directly.
|
|
11
13
|
export { h, Fragment, html } from './h.js';
|
|
12
14
|
|
|
13
|
-
// DOM mounting & rendering
|
|
15
|
+
// DOM mounting & rendering (fine-grained, no VDOM reconciler)
|
|
14
16
|
export { mount } from './dom.js';
|
|
15
17
|
|
|
16
18
|
// Hooks (React-compatible API)
|
|
@@ -150,3 +152,35 @@ export {
|
|
|
150
152
|
Radio,
|
|
151
153
|
ErrorMessage,
|
|
152
154
|
} from './form.js';
|
|
155
|
+
|
|
156
|
+
// Structured error system (agent-first)
|
|
157
|
+
export {
|
|
158
|
+
WhatError,
|
|
159
|
+
ERROR_CODES,
|
|
160
|
+
createWhatError,
|
|
161
|
+
classifyError,
|
|
162
|
+
collectError,
|
|
163
|
+
getCollectedErrors,
|
|
164
|
+
clearCollectedErrors,
|
|
165
|
+
} from './errors.js';
|
|
166
|
+
|
|
167
|
+
// Agent guardrails (dev-mode runtime checks)
|
|
168
|
+
export {
|
|
169
|
+
configureGuardrails,
|
|
170
|
+
getGuardrailConfig,
|
|
171
|
+
installSignalReadGuardrail,
|
|
172
|
+
checkComponentName,
|
|
173
|
+
validateImports,
|
|
174
|
+
} from './guardrails.js';
|
|
175
|
+
|
|
176
|
+
// Agent context (global inspection API)
|
|
177
|
+
export {
|
|
178
|
+
installAgentContext,
|
|
179
|
+
registerComponent,
|
|
180
|
+
unregisterComponent,
|
|
181
|
+
getMountedComponents,
|
|
182
|
+
registerSignal,
|
|
183
|
+
unregisterSignal,
|
|
184
|
+
getActiveSignals,
|
|
185
|
+
getHealth,
|
|
186
|
+
} from './agent-context.js';
|