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/src/hooks.js CHANGED
@@ -1,15 +1,18 @@
1
1
  // What Framework - Hooks
2
- // React-familiar hooks backed by signals. Zero overhead when deps don't change.
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
- '[what] Hooks must be called inside a component function. ' +
12
- 'If you need reactive state outside a component, use signal() directly.'
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 [value, setter]. Setter triggers re-render of this component only.
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(), s.set];
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 render. Cleanup function returned by fn is called
72
- // before re-running and on unmount.
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] = { deps: undefined, cleanup: null };
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
- if (depsChanged(hook.deps, deps)) {
85
- // Schedule after current render
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
- if (hook.cleanup) hook.cleanup();
89
- hook.cleanup = fn() || null;
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. Only recomputes when deps change.
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] = { value: undefined, deps: undefined };
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 hook.value;
179
+ return ctx.hooks[index].computed;
123
180
  }
124
181
 
125
182
  // --- useCallback ---
126
- // Memoized callback. Identity-stable when deps don't change.
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
- return useMemo(() => fn, deps);
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 re-renders, effects, and event handlers.
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 (like React).
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(), hook.dispatch];
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
- // Virtual DOM
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';