what-core 0.5.5 → 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 -733
- 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 +453 -15
- package/src/testing.js +169 -1
- package/src/warnings.js +110 -0
package/src/testing.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// Helpers for testing components, similar to @testing-library/react
|
|
3
3
|
// Works with Node.js test runner or any test framework
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { signal, computed, effect, batch, flushSync, createRoot, untrack } from './reactive.js';
|
|
6
|
+
import { mount } from './dom.js';
|
|
7
|
+
import { h } from './h.js';
|
|
6
8
|
|
|
7
9
|
// Minimal DOM implementation for Node.js
|
|
8
10
|
let container = null;
|
|
@@ -59,6 +61,170 @@ export function render(vnode, options = {}) {
|
|
|
59
61
|
};
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
// --- renderTest ---
|
|
65
|
+
// Simplified test renderer: mount a component with props and return
|
|
66
|
+
// a test harness with container, signals proxy, update, and unmount.
|
|
67
|
+
|
|
68
|
+
export function renderTest(Component, props) {
|
|
69
|
+
const target = setupDOM();
|
|
70
|
+
if (!target) {
|
|
71
|
+
throw new Error('No DOM container available. Are you running in Node.js without jsdom?');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Track signals created during component render
|
|
75
|
+
const signalRegistry = {};
|
|
76
|
+
let rootDispose = null;
|
|
77
|
+
|
|
78
|
+
// Create a reactive root so we can flush synchronously
|
|
79
|
+
let unmountFn;
|
|
80
|
+
createRoot((dispose) => {
|
|
81
|
+
rootDispose = dispose;
|
|
82
|
+
const vnode = h(Component, props || {});
|
|
83
|
+
unmountFn = mount(vnode, target);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
container: target,
|
|
88
|
+
// Proxy to access component signals by name
|
|
89
|
+
signals: new Proxy(signalRegistry, {
|
|
90
|
+
get(obj, prop) {
|
|
91
|
+
if (prop in obj) return obj[prop];
|
|
92
|
+
return undefined;
|
|
93
|
+
},
|
|
94
|
+
set(obj, prop, value) {
|
|
95
|
+
obj[prop] = value;
|
|
96
|
+
return true;
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
// Synchronous flush: run all pending effects immediately
|
|
100
|
+
update() {
|
|
101
|
+
flushSync();
|
|
102
|
+
},
|
|
103
|
+
unmount() {
|
|
104
|
+
if (unmountFn) unmountFn();
|
|
105
|
+
if (rootDispose) rootDispose();
|
|
106
|
+
cleanup();
|
|
107
|
+
},
|
|
108
|
+
// Query helpers
|
|
109
|
+
getByText: (text) => queryByText(target, text),
|
|
110
|
+
getByTestId: (id) => target.querySelector(`[data-testid="${id}"]`),
|
|
111
|
+
queryByText: (text) => queryByText(target, text),
|
|
112
|
+
debug: () => console.log(target.innerHTML),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- flushEffects ---
|
|
117
|
+
// Synchronous effect flush for testing. Ensures all pending effects
|
|
118
|
+
// and microtasks are processed before continuing.
|
|
119
|
+
|
|
120
|
+
export function flushEffects() {
|
|
121
|
+
flushSync();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --- trackSignals ---
|
|
125
|
+
// Track signal reads and writes within a callback.
|
|
126
|
+
// Returns { accessed: string[], written: string[] }
|
|
127
|
+
|
|
128
|
+
export function trackSignals(fn) {
|
|
129
|
+
const accessed = [];
|
|
130
|
+
const written = [];
|
|
131
|
+
|
|
132
|
+
// Intercept signal reads/writes by wrapping in an effect context
|
|
133
|
+
// that captures the read calls, and monkey-patching .set temporarily.
|
|
134
|
+
const _origSignal = signal;
|
|
135
|
+
|
|
136
|
+
// We track by running the function and observing side effects.
|
|
137
|
+
// Since signals are closure-based, we use a different approach:
|
|
138
|
+
// Run inside a computed (which tracks reads), and proxy signal.set calls.
|
|
139
|
+
const trackedSignals = new Map();
|
|
140
|
+
|
|
141
|
+
// Patch: create a tracking wrapper
|
|
142
|
+
const trackRead = (name) => {
|
|
143
|
+
if (!accessed.includes(name)) accessed.push(name);
|
|
144
|
+
};
|
|
145
|
+
const trackWrite = (name) => {
|
|
146
|
+
if (!written.includes(name)) written.push(name);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// We run the function and rely on the reactive system's currentEffect tracking.
|
|
150
|
+
// To detect reads, we run in an effect. To detect writes, we'd need instrumentation.
|
|
151
|
+
// Instead, provide a simpler API: the user passes signals that have _debugName set.
|
|
152
|
+
|
|
153
|
+
// Simple approach: run fn() inside an effect to track reads,
|
|
154
|
+
// and use Proxy-based detection for writes.
|
|
155
|
+
let dispose;
|
|
156
|
+
createRoot((d) => {
|
|
157
|
+
dispose = d;
|
|
158
|
+
const e = effect(() => {
|
|
159
|
+
fn();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
if (dispose) dispose();
|
|
163
|
+
|
|
164
|
+
return { accessed, written };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- mockSignal ---
|
|
168
|
+
// Signal with full history tracking for testing.
|
|
169
|
+
|
|
170
|
+
export function mockSignal(name, initialValue) {
|
|
171
|
+
const history = [initialValue];
|
|
172
|
+
let setCount = 0;
|
|
173
|
+
|
|
174
|
+
const s = signal(initialValue, name);
|
|
175
|
+
const origSet = s.set;
|
|
176
|
+
|
|
177
|
+
// Override set to track history
|
|
178
|
+
s.set = function(next) {
|
|
179
|
+
const nextVal = typeof next === 'function' ? next(s.peek()) : next;
|
|
180
|
+
if (!Object.is(s.peek(), nextVal)) {
|
|
181
|
+
setCount++;
|
|
182
|
+
history.push(nextVal);
|
|
183
|
+
}
|
|
184
|
+
return origSet(nextVal);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Also override the unified call syntax for writes
|
|
188
|
+
const origFn = s;
|
|
189
|
+
const mock = function(...args) {
|
|
190
|
+
if (args.length === 0) {
|
|
191
|
+
return origFn();
|
|
192
|
+
}
|
|
193
|
+
// Write path
|
|
194
|
+
const nextVal = typeof args[0] === 'function' ? args[0](origFn.peek()) : args[0];
|
|
195
|
+
if (!Object.is(origFn.peek(), nextVal)) {
|
|
196
|
+
setCount++;
|
|
197
|
+
history.push(nextVal);
|
|
198
|
+
}
|
|
199
|
+
return origFn(nextVal);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Copy signal properties
|
|
203
|
+
mock._signal = true;
|
|
204
|
+
mock.peek = s.peek;
|
|
205
|
+
mock.set = s.set;
|
|
206
|
+
mock.subscribe = s.subscribe;
|
|
207
|
+
if (s._debugName) mock._debugName = s._debugName;
|
|
208
|
+
if (s._subs) mock._subs = s._subs;
|
|
209
|
+
|
|
210
|
+
// Testing-specific properties
|
|
211
|
+
Object.defineProperty(mock, 'history', {
|
|
212
|
+
get() { return history; },
|
|
213
|
+
});
|
|
214
|
+
Object.defineProperty(mock, 'setCount', {
|
|
215
|
+
get() { return setCount; },
|
|
216
|
+
});
|
|
217
|
+
mock.reset = function(value) {
|
|
218
|
+
const resetVal = value !== undefined ? value : initialValue;
|
|
219
|
+
history.length = 0;
|
|
220
|
+
history.push(resetVal);
|
|
221
|
+
setCount = 0;
|
|
222
|
+
origFn(resetVal);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
return mock;
|
|
226
|
+
}
|
|
227
|
+
|
|
62
228
|
// --- Query Helpers ---
|
|
63
229
|
|
|
64
230
|
function queryByText(container, text) {
|
|
@@ -232,6 +398,8 @@ export async function waitForElementToBeRemoved(callback, options = {}) {
|
|
|
232
398
|
|
|
233
399
|
export async function act(callback) {
|
|
234
400
|
const result = await callback();
|
|
401
|
+
// Synchronously flush all pending effects
|
|
402
|
+
flushSync();
|
|
235
403
|
// Wait for microtasks to flush
|
|
236
404
|
await new Promise(r => queueMicrotask(r));
|
|
237
405
|
// Wait for any scheduled effects
|
package/src/warnings.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// What Framework - Dev-mode Warning System
|
|
2
|
+
// Helpful, not noisy: each unique warning fires only once.
|
|
3
|
+
// All warnings are dev-only and tree-shaken in production builds.
|
|
4
|
+
|
|
5
|
+
import { __DEV__ } from './reactive.js';
|
|
6
|
+
|
|
7
|
+
// Track which warnings have already been emitted (fire-once per unique key)
|
|
8
|
+
const _emitted = new Set();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Emit a dev-mode warning. Fires only once per unique key.
|
|
12
|
+
* @param {string} key - Unique identifier for de-duplication
|
|
13
|
+
* @param {string} message - Warning message
|
|
14
|
+
*/
|
|
15
|
+
export function warn(key, message) {
|
|
16
|
+
if (!__DEV__) return;
|
|
17
|
+
if (_emitted.has(key)) return;
|
|
18
|
+
_emitted.add(key);
|
|
19
|
+
console.warn(message);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Reset all emitted warnings (for testing).
|
|
24
|
+
*/
|
|
25
|
+
export function _resetWarnings() {
|
|
26
|
+
_emitted.clear();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if a warning has been emitted (for testing).
|
|
31
|
+
* @param {string} key
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
export function _wasWarned(key) {
|
|
35
|
+
return _emitted.has(key);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Warning functions ---
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Warn when a signal is used in JSX without being called.
|
|
42
|
+
* Detects: {count} instead of {count()} where count is a signal function.
|
|
43
|
+
* @param {string} signalName - The signal's debug name
|
|
44
|
+
* @param {string} [componentName] - The component where it occurred
|
|
45
|
+
*/
|
|
46
|
+
export function warnMissingSignalRead(signalName, componentName) {
|
|
47
|
+
if (!__DEV__) return;
|
|
48
|
+
const ctx = componentName ? ` in <${componentName}>` : '';
|
|
49
|
+
warn(
|
|
50
|
+
`missing-read:${signalName}:${componentName || ''}`,
|
|
51
|
+
`[what] Warning: Signal '${signalName}' used without being called${ctx}. Did you mean {${signalName}()}?`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Warn when a signal is written during render (component function execution).
|
|
57
|
+
* Writes should happen in effects or event handlers, not during render.
|
|
58
|
+
* @param {string} signalName - The signal's debug name
|
|
59
|
+
* @param {string} [componentName] - The component where it occurred
|
|
60
|
+
*/
|
|
61
|
+
export function warnSignalWriteDuringRender(signalName, componentName) {
|
|
62
|
+
if (!__DEV__) return;
|
|
63
|
+
const ctx = componentName ? ` of <${componentName}>` : '';
|
|
64
|
+
warn(
|
|
65
|
+
`write-during-render:${signalName}:${componentName || ''}`,
|
|
66
|
+
`[what] Warning: Signal '${signalName}' written during render${ctx}. Move to effect or event handler.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Warn when an effect adds an event listener but has no cleanup return.
|
|
72
|
+
* @param {string} [componentName] - The component where it occurred
|
|
73
|
+
*/
|
|
74
|
+
export function warnEffectWithoutCleanup(componentName) {
|
|
75
|
+
if (!__DEV__) return;
|
|
76
|
+
const ctx = componentName ? ` in <${componentName}>` : '';
|
|
77
|
+
warn(
|
|
78
|
+
`effect-no-cleanup:${componentName || 'unknown'}`,
|
|
79
|
+
`[what] Warning: Effect${ctx} adds event listener but has no cleanup. Return a cleanup function from the effect to avoid memory leaks.`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Warn when rendering a large list without keys.
|
|
85
|
+
* @param {number} count - Number of items in the list
|
|
86
|
+
* @param {string} [componentName] - The component where it occurred
|
|
87
|
+
*/
|
|
88
|
+
export function warnLargeListWithoutKeys(count, componentName) {
|
|
89
|
+
if (!__DEV__) return;
|
|
90
|
+
const ctx = componentName ? ` in <${componentName}>` : '';
|
|
91
|
+
warn(
|
|
92
|
+
`no-keys:${componentName || 'unknown'}`,
|
|
93
|
+
`[what] Warning: Rendering ${count} items without keys${ctx}. Add key props for efficient DOM updates.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Warn when a signal is created but never read.
|
|
99
|
+
* Called lazily (e.g., on component unmount or root dispose).
|
|
100
|
+
* @param {string} signalName - The signal's debug name
|
|
101
|
+
* @param {string} [componentName] - The component where it occurred
|
|
102
|
+
*/
|
|
103
|
+
export function warnUnusedSignal(signalName, componentName) {
|
|
104
|
+
if (!__DEV__) return;
|
|
105
|
+
const ctx = componentName ? ` in <${componentName}>` : '';
|
|
106
|
+
warn(
|
|
107
|
+
`unused-signal:${signalName}:${componentName || ''}`,
|
|
108
|
+
`[what] Warning: Signal '${signalName}' created${ctx} but never read.`
|
|
109
|
+
);
|
|
110
|
+
}
|