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/errors.js ADDED
@@ -0,0 +1,253 @@
1
+ // What Framework - Structured Error System
2
+ // Agent-first error reporting with actionable codes, suggestions, and JSON output.
3
+ // Every error tells an AI agent exactly what went wrong and how to fix it.
4
+
5
+ import { __DEV__ } from './reactive.js';
6
+
7
+ // --- Error Codes ---
8
+ // Each code maps to a specific, well-documented mistake pattern.
9
+
10
+ export const ERROR_CODES = {
11
+ INFINITE_EFFECT: {
12
+ code: 'ERR_INFINITE_EFFECT',
13
+ severity: 'error',
14
+ template: 'Effect "{{effectName}}" exceeded 25 flush iterations — likely an infinite loop.',
15
+ suggestion: 'An effect is writing to a signal it also reads, creating a cycle. Use untrack() to read the signal without subscribing, or restructure so the write and read are in separate effects.',
16
+ codeExample: `// Bad — reads and writes count, creating a cycle:
17
+ effect(() => { count(count() + 1); });
18
+
19
+ // Good — use untrack() so the read doesn't subscribe:
20
+ effect(() => { count(untrack(count) + 1); });
21
+
22
+ // Better — split into separate logic:
23
+ const doubled = computed(() => count() * 2);`,
24
+ },
25
+
26
+ MISSING_SIGNAL_READ: {
27
+ code: 'ERR_MISSING_SIGNAL_READ',
28
+ severity: 'warning',
29
+ template: 'Signal "{{signalName}}" used without calling () — renders as "[Function]" instead of its value.',
30
+ suggestion: 'Signals are functions. Call them to read: count() not count. In JSX: {count()} not {count}.',
31
+ codeExample: `// Bad — signal reference, not value:
32
+ <span>{count}</span> // renders "[Function]"
33
+
34
+ // Good — call the signal:
35
+ <span>{count()}</span> // renders the actual value`,
36
+ },
37
+
38
+ HYDRATION_MISMATCH: {
39
+ code: 'ERR_HYDRATION_MISMATCH',
40
+ severity: 'error',
41
+ template: 'Hydration mismatch in component "{{component}}": server rendered "{{serverHTML}}" but client expects "{{clientHTML}}".',
42
+ suggestion: 'Ensure server and client render identical initial HTML. Avoid reading browser-only APIs (window, localStorage) during the initial render. Use onMount() for client-only logic.',
43
+ codeExample: `// Bad — different on server vs client:
44
+ function App() {
45
+ return <p>{window.innerWidth}</p>;
46
+ }
47
+
48
+ // Good — use onMount for client-only values:
49
+ function App() {
50
+ const width = signal(0);
51
+ onMount(() => width(window.innerWidth));
52
+ return <p>{width()}</p>;
53
+ }`,
54
+ },
55
+
56
+ ORPHAN_EFFECT: {
57
+ code: 'ERR_ORPHAN_EFFECT',
58
+ severity: 'warning',
59
+ template: 'Effect "{{effectName}}" was created outside a reactive root — it will never be cleaned up.',
60
+ suggestion: 'Wrap effect creation in createRoot() or create effects inside component functions where they are automatically tracked.',
61
+ codeExample: `// Bad — orphaned, leaks memory:
62
+ effect(() => console.log(count()));
63
+
64
+ // Good — inside a root with cleanup:
65
+ createRoot(dispose => {
66
+ effect(() => console.log(count()));
67
+ // later: dispose() cleans up
68
+ });`,
69
+ },
70
+
71
+ SIGNAL_WRITE_IN_RENDER: {
72
+ code: 'ERR_SIGNAL_WRITE_IN_RENDER',
73
+ severity: 'error',
74
+ template: 'Signal "{{signalName}}" written during render of component "{{component}}". This triggers re-execution.',
75
+ suggestion: 'Move signal writes into event handlers, effects, or onMount(). The component body should only read signals, not write them.',
76
+ codeExample: `// Bad — write during render:
77
+ function Counter() {
78
+ count(count() + 1); // triggers infinite loop
79
+ return <span>{count()}</span>;
80
+ }
81
+
82
+ // Good — write in event handler:
83
+ function Counter() {
84
+ return <button onclick={() => count(c => c + 1)}>{count()}</button>;
85
+ }`,
86
+ },
87
+
88
+ MISSING_CLEANUP: {
89
+ code: 'ERR_MISSING_CLEANUP',
90
+ severity: 'warning',
91
+ template: 'Effect sets up "{{resource}}" but does not return a cleanup function.',
92
+ suggestion: 'Effects that add event listeners, set timers, or open connections should return a cleanup function to prevent memory leaks.',
93
+ codeExample: `// Bad — no cleanup:
94
+ effect(() => {
95
+ window.addEventListener('resize', handler);
96
+ });
97
+
98
+ // Good — return cleanup:
99
+ effect(() => {
100
+ window.addEventListener('resize', handler);
101
+ return () => window.removeEventListener('resize', handler);
102
+ });`,
103
+ },
104
+
105
+ UNSAFE_INNERHTML: {
106
+ code: 'ERR_UNSAFE_INNERHTML',
107
+ severity: 'warning',
108
+ template: 'innerHTML set on element without using the __html safety marker.',
109
+ suggestion: 'Use the html tagged template literal or pass { __html: content } to mark innerHTML as intentional and reviewed.',
110
+ codeExample: `// Bad — raw innerHTML (XSS risk):
111
+ <div innerHTML={userInput} />
112
+
113
+ // Good — explicit opt-in:
114
+ <div innerHTML={{ __html: sanitizedContent }} />
115
+
116
+ // Better — use the html template literal:
117
+ html\`<div>\${sanitizedContent}</div>\``,
118
+ },
119
+
120
+ MISSING_KEY: {
121
+ code: 'ERR_MISSING_KEY',
122
+ severity: 'warning',
123
+ template: 'List rendered without key prop in component "{{component}}". Items may re-order incorrectly.',
124
+ suggestion: 'Add a unique key prop to each item in a list. Use a stable identifier (like an ID), not the array index.',
125
+ codeExample: `// Bad — no key:
126
+ <For each={items()}>{item => <li>{item.name}</li>}</For>
127
+
128
+ // Good — stable key:
129
+ <For each={items()}>{item => <li key={item.id}>{item.name}</li>}</For>`,
130
+ },
131
+ };
132
+
133
+ // --- WhatError ---
134
+ // Structured error class with full context for agent consumption.
135
+
136
+ export class WhatError extends Error {
137
+ constructor({ code, message, suggestion, file, line, component, signal, effect }) {
138
+ super(message);
139
+ this.name = 'WhatError';
140
+ this.code = code;
141
+ this.suggestion = suggestion;
142
+ this.file = file;
143
+ this.line = line;
144
+ this.component = component;
145
+ this.signal = signal;
146
+ this.effect = effect;
147
+ }
148
+
149
+ toJSON() {
150
+ return {
151
+ code: this.code,
152
+ message: this.message,
153
+ suggestion: this.suggestion,
154
+ file: this.file,
155
+ line: this.line,
156
+ component: this.component,
157
+ signal: this.signal,
158
+ effect: this.effect,
159
+ };
160
+ }
161
+ }
162
+
163
+ // --- Error Factory ---
164
+ // Create WhatError instances from error codes with template interpolation.
165
+
166
+ export function createWhatError(errorCode, context = {}) {
167
+ const def = typeof errorCode === 'string' ? ERROR_CODES[errorCode] : errorCode;
168
+ if (!def) {
169
+ return new WhatError({
170
+ code: 'ERR_UNKNOWN',
171
+ message: `Unknown error: ${errorCode}`,
172
+ suggestion: 'Check the error code and try again.',
173
+ });
174
+ }
175
+
176
+ // Interpolate template with context values
177
+ let message = def.template;
178
+ for (const [key, val] of Object.entries(context)) {
179
+ message = message.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(val));
180
+ }
181
+ // Clean up any unreplaced placeholders
182
+ message = message.replace(/\{\{[^}]+\}\}/g, '(unknown)');
183
+
184
+ return new WhatError({
185
+ code: def.code,
186
+ message,
187
+ suggestion: def.suggestion,
188
+ file: context.file,
189
+ line: context.line,
190
+ component: context.component,
191
+ signal: context.signal || context.signalName,
192
+ effect: context.effect || context.effectName,
193
+ });
194
+ }
195
+
196
+ // --- Error Collector ---
197
+ // Dev-mode error accumulator for agent retrieval.
198
+
199
+ let collectedErrors = [];
200
+ const MAX_COLLECTED = 200;
201
+
202
+ export function collectError(whatError) {
203
+ if (!__DEV__) return;
204
+ collectedErrors.push({
205
+ ...whatError.toJSON(),
206
+ timestamp: Date.now(),
207
+ });
208
+ if (collectedErrors.length > MAX_COLLECTED) {
209
+ collectedErrors = collectedErrors.slice(-MAX_COLLECTED);
210
+ }
211
+ }
212
+
213
+ export function getCollectedErrors(since) {
214
+ if (since) return collectedErrors.filter(e => e.timestamp > since);
215
+ return collectedErrors.slice();
216
+ }
217
+
218
+ export function clearCollectedErrors() {
219
+ collectedErrors = [];
220
+ }
221
+
222
+ // --- Error Classification ---
223
+ // Classify a raw Error into a structured WhatError if possible.
224
+
225
+ export function classifyError(err, context = {}) {
226
+ const msg = err?.message || String(err);
227
+
228
+ // Infinite effect loop
229
+ if (msg.includes('infinite effect loop') || msg.includes('25 iterations')) {
230
+ return createWhatError('INFINITE_EFFECT', context);
231
+ }
232
+
233
+ // Hydration mismatch
234
+ if (msg.includes('hydration') || msg.includes('Hydration')) {
235
+ return createWhatError('HYDRATION_MISMATCH', context);
236
+ }
237
+
238
+ // Signal write in computed
239
+ if (msg.includes('Signal.set() called inside a computed')) {
240
+ return createWhatError('SIGNAL_WRITE_IN_RENDER', {
241
+ ...context,
242
+ signalName: msg.match(/signal: (\w+)/)?.[1] || context.signalName,
243
+ });
244
+ }
245
+
246
+ // Fallback — return a generic WhatError with the original message
247
+ return new WhatError({
248
+ code: 'ERR_RUNTIME',
249
+ message: msg,
250
+ suggestion: 'Check the stack trace and component context for more details.',
251
+ ...context,
252
+ });
253
+ }
@@ -0,0 +1,224 @@
1
+ // What Framework - Agent Guardrails
2
+ // Dev-mode runtime checks that catch common mistakes BEFORE they become bugs.
3
+ // Designed for AI agents: structured warnings with fix suggestions.
4
+
5
+ import { __DEV__ } from './reactive.js';
6
+ import { createWhatError, collectError } from './errors.js';
7
+
8
+ // --- Guardrail Registry ---
9
+ // Each guardrail can be enabled/disabled independently.
10
+
11
+ const guardrails = {
12
+ signalReadDetection: true,
13
+ effectCycleDetection: true,
14
+ componentNaming: true,
15
+ importValidation: true,
16
+ };
17
+
18
+ export function configureGuardrails(overrides) {
19
+ Object.assign(guardrails, overrides);
20
+ }
21
+
22
+ export function getGuardrailConfig() {
23
+ return { ...guardrails };
24
+ }
25
+
26
+ // --- Guardrail 1: Signal Read Detection ---
27
+ // Detect when a signal function reference is used where its value was intended.
28
+ // This catches the pattern: <span>{count}</span> (should be count())
29
+ //
30
+ // At runtime, we can detect this when a signal is coerced to string (via toString/valueOf)
31
+ // and warn that it should be called.
32
+
33
+ export function installSignalReadGuardrail(signalFn, debugName) {
34
+ if (!__DEV__ || !guardrails.signalReadDetection) return signalFn;
35
+
36
+ // Override toString to catch template literal coercion
37
+ const originalToString = signalFn.toString;
38
+ signalFn.toString = function () {
39
+ const err = createWhatError('MISSING_SIGNAL_READ', {
40
+ signalName: debugName || '(unnamed)',
41
+ });
42
+ console.warn(`[what] ${err.message}\n Suggestion: ${err.suggestion}`);
43
+ collectError(err);
44
+ // Still return the value so the app doesn't crash
45
+ return String(signalFn());
46
+ };
47
+
48
+ // Override valueOf for numeric coercion contexts
49
+ signalFn.valueOf = function () {
50
+ const err = createWhatError('MISSING_SIGNAL_READ', {
51
+ signalName: debugName || '(unnamed)',
52
+ });
53
+ console.warn(`[what] ${err.message}\n Suggestion: ${err.suggestion}`);
54
+ collectError(err);
55
+ return signalFn();
56
+ };
57
+
58
+ return signalFn;
59
+ }
60
+
61
+ // --- Guardrail 2: Enhanced Effect Cycle Detection ---
62
+ // Track which signals an effect reads AND writes.
63
+ // If an effect writes to a signal it reads, warn about the specific cycle.
64
+
65
+ const effectWriteTracking = new WeakMap(); // effect -> Set of signal debug names
66
+
67
+ export function trackEffectSignalWrite(effectRef, signalDebugName) {
68
+ if (!__DEV__ || !guardrails.effectCycleDetection) return;
69
+
70
+ if (!effectWriteTracking.has(effectRef)) {
71
+ effectWriteTracking.set(effectRef, new Set());
72
+ }
73
+ effectWriteTracking.get(effectRef).add(signalDebugName);
74
+ }
75
+
76
+ export function checkEffectCycle(effectRef, readSignals) {
77
+ if (!__DEV__ || !guardrails.effectCycleDetection) return null;
78
+
79
+ const writes = effectWriteTracking.get(effectRef);
80
+ if (!writes || writes.size === 0) return null;
81
+
82
+ const overlapping = [];
83
+ for (const sigName of readSignals) {
84
+ if (writes.has(sigName)) {
85
+ overlapping.push(sigName);
86
+ }
87
+ }
88
+
89
+ if (overlapping.length > 0) {
90
+ const err = createWhatError('INFINITE_EFFECT', {
91
+ effectName: effectRef.fn?.name || '(anonymous)',
92
+ signalName: overlapping.join(', '),
93
+ });
94
+ collectError(err);
95
+ return err;
96
+ }
97
+
98
+ return null;
99
+ }
100
+
101
+ // --- Guardrail 3: Component Naming ---
102
+ // Warn if a component function is not PascalCase.
103
+
104
+ export function checkComponentName(name) {
105
+ if (!__DEV__ || !guardrails.componentNaming) return null;
106
+ if (!name) return null;
107
+
108
+ // PascalCase: starts with uppercase letter
109
+ if (/^[A-Z]/.test(name)) return null;
110
+
111
+ const suggestion = `Component "${name}" should use PascalCase (e.g., "${capitalize(name)}"). ` +
112
+ 'PascalCase distinguishes components from HTML elements in JSX and is required by the What Framework compiler.';
113
+
114
+ console.warn(`[what] ${suggestion}`);
115
+ return { code: 'WARN_COMPONENT_NAMING', name, suggestion };
116
+ }
117
+
118
+ function capitalize(str) {
119
+ return str.charAt(0).toUpperCase() + str.slice(1);
120
+ }
121
+
122
+ // --- Guardrail 4: Import Validation ---
123
+ // Verify that all named imports from 'what-framework' are valid exports.
124
+
125
+ const VALID_EXPORTS = new Set([
126
+ // Reactive primitives
127
+ 'signal', 'computed', 'effect', 'signalMemo', 'batch', 'untrack', 'flushSync',
128
+ 'createRoot', 'getOwner', 'runWithOwner', 'onRootCleanup', '__setDevToolsHooks',
129
+ // Rendering
130
+ 'template', '_template', 'svgTemplate', 'insert', 'mapArray', 'spread',
131
+ 'setProp', 'delegateEvents', 'on', 'classList', 'hydrate', 'isHydrating',
132
+ '_$createComponent',
133
+ // JSX
134
+ 'h', 'Fragment', 'html',
135
+ // DOM
136
+ 'mount',
137
+ // Hooks
138
+ 'useState', 'useSignal', 'useComputed', 'useEffect', 'useMemo', 'useCallback',
139
+ 'useRef', 'useContext', 'useReducer', 'createContext', 'onMount', 'onCleanup',
140
+ 'createResource',
141
+ // Components
142
+ 'memo', 'lazy', 'Suspense', 'ErrorBoundary', 'Show', 'For', 'Switch', 'Match', 'Island',
143
+ // Store
144
+ 'createStore', 'derived', 'storeComputed', 'atom',
145
+ // Head
146
+ 'Head', 'clearHead',
147
+ // Utilities
148
+ 'each', 'cls', 'style', 'debounce', 'throttle', 'useMediaQuery',
149
+ 'useLocalStorage', 'useClickOutside', 'Portal', 'transition',
150
+ // Scheduler
151
+ 'scheduleRead', 'scheduleWrite', 'flushScheduler', 'measure', 'mutate',
152
+ 'useScheduledEffect', 'nextFrame', 'raf', 'onResize', 'onIntersect', 'smoothScrollTo',
153
+ // Animation
154
+ 'spring', 'tween', 'easings', 'useTransition', 'useGesture', 'useAnimatedValue',
155
+ 'createTransitionClasses', 'cssTransition',
156
+ // Accessibility
157
+ 'useFocus', 'useFocusRestore', 'useFocusTrap', 'FocusTrap', 'announce',
158
+ 'announceAssertive', 'SkipLink', 'useAriaExpanded', 'useAriaSelected',
159
+ 'useAriaChecked', 'useRovingTabIndex', 'VisuallyHidden', 'LiveRegion',
160
+ 'useId', 'useIds', 'useDescribedBy', 'useLabelledBy', 'Keys', 'onKey', 'onKeys',
161
+ // Skeleton
162
+ 'Skeleton', 'SkeletonText', 'SkeletonAvatar', 'SkeletonCard', 'SkeletonTable',
163
+ 'IslandSkeleton', 'useSkeleton', 'Placeholder', 'LoadingDots', 'Spinner',
164
+ // Data fetching
165
+ 'useFetch', 'useSWR', 'useQuery', 'useInfiniteQuery', 'invalidateQueries',
166
+ 'prefetchQuery', 'setQueryData', 'getQueryData', 'clearCache', '__getCacheSnapshot',
167
+ // Form
168
+ 'useForm', 'useField', 'rules', 'simpleResolver', 'zodResolver', 'yupResolver',
169
+ 'Input', 'Textarea', 'Select', 'Checkbox', 'Radio', 'ErrorMessage',
170
+ ]);
171
+
172
+ export function validateImports(importNames) {
173
+ if (!__DEV__ || !guardrails.importValidation) return [];
174
+
175
+ const invalid = [];
176
+ for (const name of importNames) {
177
+ if (!VALID_EXPORTS.has(name)) {
178
+ invalid.push({
179
+ name,
180
+ message: `"${name}" is not a valid export from what-framework.`,
181
+ suggestion: `Check the API reference. Did you mean: ${findClosest(name)}?`,
182
+ });
183
+ }
184
+ }
185
+ return invalid;
186
+ }
187
+
188
+ // Simple Levenshtein-based closest match
189
+ function findClosest(input) {
190
+ const lower = input.toLowerCase();
191
+ let best = null;
192
+ let bestDist = Infinity;
193
+
194
+ for (const name of VALID_EXPORTS) {
195
+ const dist = levenshtein(lower, name.toLowerCase());
196
+ if (dist < bestDist) {
197
+ bestDist = dist;
198
+ best = name;
199
+ }
200
+ }
201
+
202
+ return bestDist <= 3 ? best : '(no close match found)';
203
+ }
204
+
205
+ function levenshtein(a, b) {
206
+ if (a.length === 0) return b.length;
207
+ if (b.length === 0) return a.length;
208
+
209
+ const matrix = [];
210
+ for (let i = 0; i <= b.length; i++) matrix[i] = [i];
211
+ for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
212
+
213
+ for (let i = 1; i <= b.length; i++) {
214
+ for (let j = 1; j <= a.length; j++) {
215
+ const cost = b[i - 1] === a[j - 1] ? 0 : 1;
216
+ matrix[i][j] = Math.min(
217
+ matrix[i - 1][j] + 1,
218
+ matrix[i][j - 1] + 1,
219
+ matrix[i - 1][j - 1] + cost,
220
+ );
221
+ }
222
+ }
223
+ return matrix[b.length][a.length];
224
+ }
package/src/h.js CHANGED
@@ -1,11 +1,11 @@
1
- // What Framework - Hyperscript / VDOM
2
- // Minimal virtual DOM nodes. No classes, no fibers, just plain objects.
1
+ // What Framework - Element Descriptors
2
+ // Minimal element descriptor objects. No classes, no fibers, just plain objects.
3
3
 
4
4
  // h(tag, props, ...children) -> VNode
5
5
  // h(Component, props, ...children) -> VNode
6
6
  // VNode = { tag, props, children, key }
7
7
 
8
- const EMPTY_OBJ = {};
8
+ const EMPTY_OBJ = Object.create(null);
9
9
  const EMPTY_ARR = [];
10
10
 
11
11
  export function h(tag, props, ...children) {