zero-query 0.6.3 → 0.8.6

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.
Files changed (72) hide show
  1. package/README.md +39 -29
  2. package/cli/commands/build.js +113 -4
  3. package/cli/commands/bundle.js +392 -29
  4. package/cli/commands/create.js +1 -1
  5. package/cli/commands/dev/devtools/index.js +56 -0
  6. package/cli/commands/dev/devtools/js/components.js +49 -0
  7. package/cli/commands/dev/devtools/js/core.js +409 -0
  8. package/cli/commands/dev/devtools/js/elements.js +413 -0
  9. package/cli/commands/dev/devtools/js/network.js +166 -0
  10. package/cli/commands/dev/devtools/js/performance.js +73 -0
  11. package/cli/commands/dev/devtools/js/router.js +105 -0
  12. package/cli/commands/dev/devtools/js/source.js +132 -0
  13. package/cli/commands/dev/devtools/js/stats.js +35 -0
  14. package/cli/commands/dev/devtools/js/tabs.js +79 -0
  15. package/cli/commands/dev/devtools/panel.html +95 -0
  16. package/cli/commands/dev/devtools/styles.css +244 -0
  17. package/cli/commands/dev/index.js +29 -4
  18. package/cli/commands/dev/logger.js +6 -1
  19. package/cli/commands/dev/overlay.js +428 -2
  20. package/cli/commands/dev/server.js +42 -5
  21. package/cli/commands/dev/watcher.js +59 -1
  22. package/cli/help.js +8 -5
  23. package/cli/scaffold/{scripts → app}/app.js +16 -23
  24. package/cli/scaffold/{scripts → app}/components/about.js +4 -4
  25. package/cli/scaffold/{scripts → app}/components/api-demo.js +1 -1
  26. package/cli/scaffold/{scripts → app}/components/contacts/contacts.css +0 -7
  27. package/cli/scaffold/{scripts → app}/components/contacts/contacts.html +3 -3
  28. package/cli/scaffold/app/components/home.js +137 -0
  29. package/cli/scaffold/{scripts → app}/routes.js +1 -1
  30. package/cli/scaffold/{scripts → app}/store.js +6 -6
  31. package/cli/scaffold/assets/.gitkeep +0 -0
  32. package/cli/scaffold/{styles/styles.css → global.css} +4 -2
  33. package/cli/scaffold/index.html +12 -11
  34. package/cli/utils.js +111 -6
  35. package/dist/zquery.dist.zip +0 -0
  36. package/dist/zquery.js +1122 -158
  37. package/dist/zquery.min.js +3 -16
  38. package/index.d.ts +129 -1290
  39. package/index.js +15 -10
  40. package/package.json +7 -6
  41. package/src/component.js +172 -49
  42. package/src/core.js +359 -18
  43. package/src/diff.js +256 -58
  44. package/src/expression.js +33 -3
  45. package/src/reactive.js +37 -5
  46. package/src/router.js +243 -7
  47. package/tests/component.test.js +886 -0
  48. package/tests/core.test.js +977 -0
  49. package/tests/diff.test.js +525 -0
  50. package/tests/errors.test.js +162 -0
  51. package/tests/expression.test.js +482 -0
  52. package/tests/http.test.js +289 -0
  53. package/tests/reactive.test.js +339 -0
  54. package/tests/router.test.js +649 -0
  55. package/tests/store.test.js +379 -0
  56. package/tests/utils.test.js +512 -0
  57. package/types/collection.d.ts +383 -0
  58. package/types/component.d.ts +217 -0
  59. package/types/errors.d.ts +103 -0
  60. package/types/http.d.ts +81 -0
  61. package/types/misc.d.ts +179 -0
  62. package/types/reactive.d.ts +76 -0
  63. package/types/router.d.ts +161 -0
  64. package/types/ssr.d.ts +49 -0
  65. package/types/store.d.ts +107 -0
  66. package/types/utils.d.ts +142 -0
  67. package/cli/commands/dev.old.js +0 -520
  68. package/cli/scaffold/scripts/components/home.js +0 -137
  69. /package/cli/scaffold/{scripts → app}/components/contacts/contacts.js +0 -0
  70. /package/cli/scaffold/{scripts → app}/components/counter.js +0 -0
  71. /package/cli/scaffold/{scripts → app}/components/not-found.js +0 -0
  72. /package/cli/scaffold/{scripts → app}/components/todos.js +0 -0
@@ -0,0 +1,512 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ debounce, throttle, pipe, once, sleep,
4
+ escapeHtml, html, trust, uuid, camelCase, kebabCase,
5
+ deepClone, deepMerge, isEqual, param, parseQuery,
6
+ storage, session, bus,
7
+ } from '../src/utils.js';
8
+
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Function utilities
12
+ // ---------------------------------------------------------------------------
13
+
14
+ describe('debounce', () => {
15
+ beforeEach(() => { vi.useFakeTimers(); });
16
+
17
+ it('delays execution until after ms of inactivity', () => {
18
+ const fn = vi.fn();
19
+ const debounced = debounce(fn, 100);
20
+ debounced('a');
21
+ debounced('b');
22
+ expect(fn).not.toHaveBeenCalled();
23
+ vi.advanceTimersByTime(100);
24
+ expect(fn).toHaveBeenCalledOnce();
25
+ expect(fn).toHaveBeenCalledWith('b');
26
+ });
27
+
28
+ it('uses default 250ms delay', () => {
29
+ const fn = vi.fn();
30
+ const debounced = debounce(fn);
31
+ debounced();
32
+ vi.advanceTimersByTime(249);
33
+ expect(fn).not.toHaveBeenCalled();
34
+ vi.advanceTimersByTime(1);
35
+ expect(fn).toHaveBeenCalledOnce();
36
+ });
37
+
38
+ it('cancel() stops pending execution', () => {
39
+ const fn = vi.fn();
40
+ const debounced = debounce(fn, 100);
41
+ debounced();
42
+ debounced.cancel();
43
+ vi.advanceTimersByTime(200);
44
+ expect(fn).not.toHaveBeenCalled();
45
+ });
46
+
47
+ it('resets timer on subsequent calls', () => {
48
+ const fn = vi.fn();
49
+ const debounced = debounce(fn, 100);
50
+ debounced();
51
+ vi.advanceTimersByTime(80);
52
+ debounced();
53
+ vi.advanceTimersByTime(80);
54
+ expect(fn).not.toHaveBeenCalled();
55
+ vi.advanceTimersByTime(20);
56
+ expect(fn).toHaveBeenCalledOnce();
57
+ });
58
+ });
59
+
60
+
61
+ describe('throttle', () => {
62
+ beforeEach(() => { vi.useFakeTimers(); });
63
+
64
+ it('fires immediately on first call', () => {
65
+ const fn = vi.fn();
66
+ const throttled = throttle(fn, 100);
67
+ throttled('a');
68
+ expect(fn).toHaveBeenCalledWith('a');
69
+ });
70
+
71
+ it('delays subsequent calls within the window', () => {
72
+ const fn = vi.fn();
73
+ const throttled = throttle(fn, 100);
74
+ throttled('a');
75
+ throttled('b');
76
+ expect(fn).toHaveBeenCalledTimes(1);
77
+ vi.advanceTimersByTime(100);
78
+ expect(fn).toHaveBeenCalledTimes(2);
79
+ expect(fn).toHaveBeenLastCalledWith('b');
80
+ });
81
+ });
82
+
83
+
84
+ describe('pipe', () => {
85
+ it('composes functions left-to-right', () => {
86
+ const add1 = x => x + 1;
87
+ const double = x => x * 2;
88
+ expect(pipe(add1, double)(3)).toBe(8);
89
+ });
90
+
91
+ it('handles a single function', () => {
92
+ const identity = x => x;
93
+ expect(pipe(identity)(42)).toBe(42);
94
+ });
95
+
96
+ it('handles no functions', () => {
97
+ expect(pipe()(10)).toBe(10);
98
+ });
99
+ });
100
+
101
+
102
+ describe('once', () => {
103
+ it('only calls function once', () => {
104
+ const fn = vi.fn(() => 42);
105
+ const onceFn = once(fn);
106
+ expect(onceFn()).toBe(42);
107
+ expect(onceFn()).toBe(42);
108
+ expect(fn).toHaveBeenCalledOnce();
109
+ });
110
+
111
+ it('passes arguments to the first call', () => {
112
+ const fn = vi.fn((a, b) => a + b);
113
+ const onceFn = once(fn);
114
+ expect(onceFn(1, 2)).toBe(3);
115
+ expect(onceFn(10, 20)).toBe(3);
116
+ });
117
+ });
118
+
119
+
120
+ describe('sleep', () => {
121
+ it('returns a promise that resolves after ms', async () => {
122
+ vi.useFakeTimers();
123
+ const p = sleep(100);
124
+ vi.advanceTimersByTime(100);
125
+ await expect(p).resolves.toBeUndefined();
126
+ });
127
+ });
128
+
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // String utilities
132
+ // ---------------------------------------------------------------------------
133
+
134
+ describe('escapeHtml', () => {
135
+ it('escapes &, <, >, ", \'', () => {
136
+ expect(escapeHtml('&<>"\'')).toBe('&amp;&lt;&gt;&quot;&#39;');
137
+ });
138
+
139
+ it('converts non-strings to string', () => {
140
+ expect(escapeHtml(42)).toBe('42');
141
+ expect(escapeHtml(null)).toBe('null');
142
+ });
143
+
144
+ it('handles empty string', () => {
145
+ expect(escapeHtml('')).toBe('');
146
+ });
147
+ });
148
+
149
+
150
+ describe('trust', () => {
151
+ it('returns a TrustedHTML instance', () => {
152
+ const t = trust('<b>bold</b>');
153
+ expect(t.toString()).toBe('<b>bold</b>');
154
+ });
155
+ });
156
+
157
+
158
+ describe('uuid', () => {
159
+ it('returns a string', () => {
160
+ expect(typeof uuid()).toBe('string');
161
+ });
162
+
163
+ it('returns different values on successive calls', () => {
164
+ const a = uuid();
165
+ const b = uuid();
166
+ expect(a).not.toBe(b);
167
+ });
168
+
169
+ it('has valid UUID v4 format', () => {
170
+ expect(uuid()).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
171
+ });
172
+ });
173
+
174
+
175
+ describe('camelCase', () => {
176
+ it('converts kebab-case to camelCase', () => {
177
+ expect(camelCase('my-component')).toBe('myComponent');
178
+ expect(camelCase('a-b-c')).toBe('aBC');
179
+ });
180
+
181
+ it('handles no hyphens', () => {
182
+ expect(camelCase('hello')).toBe('hello');
183
+ });
184
+ });
185
+
186
+
187
+ describe('kebabCase', () => {
188
+ it('converts camelCase to kebab-case', () => {
189
+ expect(kebabCase('myComponent')).toBe('my-component');
190
+ expect(kebabCase('fooBarBaz')).toBe('foo-bar-baz');
191
+ });
192
+
193
+ it('handles already kebab-case', () => {
194
+ expect(kebabCase('hello')).toBe('hello');
195
+ });
196
+ });
197
+
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // Object utilities
201
+ // ---------------------------------------------------------------------------
202
+
203
+ describe('deepClone', () => {
204
+ it('creates an independent copy', () => {
205
+ const obj = { a: 1, b: { c: 2 } };
206
+ const clone = deepClone(obj);
207
+ clone.b.c = 99;
208
+ expect(obj.b.c).toBe(2);
209
+ });
210
+
211
+ it('handles arrays', () => {
212
+ const arr = [1, [2, 3]];
213
+ const clone = deepClone(arr);
214
+ clone[1][0] = 99;
215
+ expect(arr[1][0]).toBe(2);
216
+ });
217
+ });
218
+
219
+
220
+ describe('deepMerge', () => {
221
+ it('deeply merges objects', () => {
222
+ const a = { x: 1, nested: { y: 2 } };
223
+ const b = { nested: { z: 3 }, w: 4 };
224
+ const result = deepMerge(a, b);
225
+ expect(result).toEqual({ x: 1, nested: { y: 2, z: 3 }, w: 4 });
226
+ });
227
+
228
+ it('overwrites non-object values', () => {
229
+ const result = deepMerge({ a: 1 }, { a: 2 });
230
+ expect(result.a).toBe(2);
231
+ });
232
+
233
+ it('handles arrays by replacing them', () => {
234
+ const result = deepMerge({ a: [1, 2] }, { a: [3, 4] });
235
+ expect(result.a).toEqual([3, 4]);
236
+ });
237
+ });
238
+
239
+
240
+ describe('isEqual', () => {
241
+ it('returns true for equal primitives', () => {
242
+ expect(isEqual(1, 1)).toBe(true);
243
+ expect(isEqual('a', 'a')).toBe(true);
244
+ expect(isEqual(null, null)).toBe(true);
245
+ });
246
+
247
+ it('returns false for different primitives', () => {
248
+ expect(isEqual(1, 2)).toBe(false);
249
+ expect(isEqual('a', 'b')).toBe(false);
250
+ });
251
+
252
+ it('returns true for deeply equal objects', () => {
253
+ expect(isEqual({ a: { b: 1 } }, { a: { b: 1 } })).toBe(true);
254
+ });
255
+
256
+ it('returns false for objects with different keys', () => {
257
+ expect(isEqual({ a: 1 }, { b: 1 })).toBe(false);
258
+ });
259
+
260
+ it('returns false for objects with different lengths', () => {
261
+ expect(isEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false);
262
+ });
263
+
264
+ it('handles null comparisons', () => {
265
+ expect(isEqual(null, {})).toBe(false);
266
+ expect(isEqual({}, null)).toBe(false);
267
+ });
268
+ });
269
+
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // URL utilities
273
+ // ---------------------------------------------------------------------------
274
+
275
+ describe('param', () => {
276
+ it('serializes object to query string', () => {
277
+ expect(param({ a: '1', b: '2' })).toBe('a=1&b=2');
278
+ });
279
+
280
+ it('handles empty object', () => {
281
+ expect(param({})).toBe('');
282
+ });
283
+ });
284
+
285
+
286
+ describe('parseQuery', () => {
287
+ it('parses query string to object', () => {
288
+ expect(parseQuery('a=1&b=2')).toEqual({ a: '1', b: '2' });
289
+ });
290
+
291
+ it('handles leading ?', () => {
292
+ expect(parseQuery('?foo=bar')).toEqual({ foo: 'bar' });
293
+ });
294
+
295
+ it('handles empty string', () => {
296
+ expect(parseQuery('')).toEqual({});
297
+ });
298
+ });
299
+
300
+
301
+ // ---------------------------------------------------------------------------
302
+ // html template tag
303
+ // ---------------------------------------------------------------------------
304
+
305
+ describe('html template tag', () => {
306
+ it('auto-escapes interpolated values', () => {
307
+ const userInput = '<script>alert("xss")</script>';
308
+ const result = html`<div>${userInput}</div>`;
309
+ expect(result).toContain('&lt;script&gt;');
310
+ expect(result).not.toContain('<script>');
311
+ });
312
+
313
+ it('does not escape trusted HTML', () => {
314
+ const safe = trust('<b>bold</b>');
315
+ const result = html`<div>${safe}</div>`;
316
+ expect(result).toContain('<b>bold</b>');
317
+ });
318
+
319
+ it('handles null/undefined values', () => {
320
+ const result = html`<span>${null}</span>`;
321
+ expect(result).toBe('<span></span>');
322
+ });
323
+ });
324
+
325
+
326
+ // ---------------------------------------------------------------------------
327
+ // storage helpers
328
+ // ---------------------------------------------------------------------------
329
+
330
+ describe('storage (localStorage wrapper)', () => {
331
+ beforeEach(() => { localStorage.clear(); });
332
+
333
+ it('set and get a value', () => {
334
+ storage.set('key', { a: 1 });
335
+ expect(storage.get('key')).toEqual({ a: 1 });
336
+ });
337
+
338
+ it('returns fallback for missing key', () => {
339
+ expect(storage.get('missing', 'default')).toBe('default');
340
+ });
341
+
342
+ it('returns null as default fallback', () => {
343
+ expect(storage.get('missing')).toBeNull();
344
+ });
345
+
346
+ it('remove deletes a key', () => {
347
+ storage.set('key', 42);
348
+ storage.remove('key');
349
+ expect(storage.get('key')).toBeNull();
350
+ });
351
+
352
+ it('clear removes all keys', () => {
353
+ storage.set('a', 1);
354
+ storage.set('b', 2);
355
+ storage.clear();
356
+ expect(storage.get('a')).toBeNull();
357
+ expect(storage.get('b')).toBeNull();
358
+ });
359
+
360
+ it('handles non-JSON values gracefully', () => {
361
+ localStorage.setItem('bad', '{not json}');
362
+ expect(storage.get('bad', 'fallback')).toBe('fallback');
363
+ });
364
+ });
365
+
366
+
367
+ describe('session (sessionStorage wrapper)', () => {
368
+ beforeEach(() => { sessionStorage.clear(); });
369
+
370
+ it('set and get a value', () => {
371
+ session.set('key', [1, 2, 3]);
372
+ expect(session.get('key')).toEqual([1, 2, 3]);
373
+ });
374
+
375
+ it('returns fallback for missing key', () => {
376
+ expect(session.get('missing', 'default')).toBe('default');
377
+ });
378
+
379
+ it('remove deletes a key', () => {
380
+ session.set('key', 'val');
381
+ session.remove('key');
382
+ expect(session.get('key')).toBeNull();
383
+ });
384
+
385
+ it('clear removes all keys', () => {
386
+ session.set('a', 1);
387
+ session.clear();
388
+ expect(session.get('a')).toBeNull();
389
+ });
390
+ });
391
+
392
+
393
+ // ---------------------------------------------------------------------------
394
+ // Event bus
395
+ // ---------------------------------------------------------------------------
396
+
397
+ describe('bus (event bus)', () => {
398
+ beforeEach(() => { bus.clear(); });
399
+
400
+ it('on() and emit()', () => {
401
+ const fn = vi.fn();
402
+ bus.on('test', fn);
403
+ bus.emit('test', 'data');
404
+ expect(fn).toHaveBeenCalledWith('data');
405
+ });
406
+
407
+ it('off() removes handler', () => {
408
+ const fn = vi.fn();
409
+ bus.on('test', fn);
410
+ bus.off('test', fn);
411
+ bus.emit('test');
412
+ expect(fn).not.toHaveBeenCalled();
413
+ });
414
+
415
+ it('on() returns unsubscribe function', () => {
416
+ const fn = vi.fn();
417
+ const unsub = bus.on('test', fn);
418
+ unsub();
419
+ bus.emit('test');
420
+ expect(fn).not.toHaveBeenCalled();
421
+ });
422
+
423
+ it('once() fires handler only once', () => {
424
+ const fn = vi.fn();
425
+ bus.once('test', fn);
426
+ bus.emit('test', 'first');
427
+ bus.emit('test', 'second');
428
+ expect(fn).toHaveBeenCalledOnce();
429
+ expect(fn).toHaveBeenCalledWith('first');
430
+ });
431
+
432
+ it('emit with multiple args', () => {
433
+ const fn = vi.fn();
434
+ bus.on('test', fn);
435
+ bus.emit('test', 1, 2, 3);
436
+ expect(fn).toHaveBeenCalledWith(1, 2, 3);
437
+ });
438
+
439
+ it('multiple handlers on same event', () => {
440
+ const fn1 = vi.fn();
441
+ const fn2 = vi.fn();
442
+ bus.on('test', fn1);
443
+ bus.on('test', fn2);
444
+ bus.emit('test');
445
+ expect(fn1).toHaveBeenCalledOnce();
446
+ expect(fn2).toHaveBeenCalledOnce();
447
+ });
448
+
449
+ it('clear() removes all handlers', () => {
450
+ const fn = vi.fn();
451
+ bus.on('a', fn);
452
+ bus.on('b', fn);
453
+ bus.clear();
454
+ bus.emit('a');
455
+ bus.emit('b');
456
+ expect(fn).not.toHaveBeenCalled();
457
+ });
458
+ });
459
+
460
+ // ---------------------------------------------------------------------------
461
+ // Event bus
462
+ // ---------------------------------------------------------------------------
463
+
464
+ describe('bus (EventBus)', () => {
465
+ beforeEach(() => { bus.clear(); });
466
+
467
+ it('on/emit — fires handler for matching events', () => {
468
+ const fn = vi.fn();
469
+ bus.on('test', fn);
470
+ bus.emit('test', 42);
471
+ expect(fn).toHaveBeenCalledWith(42);
472
+ });
473
+
474
+ it('off — removes handler', () => {
475
+ const fn = vi.fn();
476
+ bus.on('test', fn);
477
+ bus.off('test', fn);
478
+ bus.emit('test');
479
+ expect(fn).not.toHaveBeenCalled();
480
+ });
481
+
482
+ it('on() returns unsubscribe function', () => {
483
+ const fn = vi.fn();
484
+ const unsub = bus.on('test', fn);
485
+ unsub();
486
+ bus.emit('test');
487
+ expect(fn).not.toHaveBeenCalled();
488
+ });
489
+
490
+ it('once — fires handler only once', () => {
491
+ const fn = vi.fn();
492
+ bus.once('test', fn);
493
+ bus.emit('test', 'a');
494
+ bus.emit('test', 'b');
495
+ expect(fn).toHaveBeenCalledOnce();
496
+ expect(fn).toHaveBeenCalledWith('a');
497
+ });
498
+
499
+ it('clear — removes all handlers', () => {
500
+ const fn = vi.fn();
501
+ bus.on('a', fn);
502
+ bus.on('b', fn);
503
+ bus.clear();
504
+ bus.emit('a');
505
+ bus.emit('b');
506
+ expect(fn).not.toHaveBeenCalled();
507
+ });
508
+
509
+ it('emit with no handlers does not throw', () => {
510
+ expect(() => bus.emit('nonexistent')).not.toThrow();
511
+ });
512
+ });