zero-query 0.7.5 → 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 (64) hide show
  1. package/README.md +37 -27
  2. package/cli/commands/build.js +110 -1
  3. package/cli/commands/bundle.js +107 -22
  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 +28 -3
  18. package/cli/commands/dev/logger.js +6 -1
  19. package/cli/commands/dev/overlay.js +377 -0
  20. package/cli/commands/dev/server.js +8 -0
  21. package/cli/commands/dev/watcher.js +26 -1
  22. package/cli/help.js +8 -5
  23. package/cli/scaffold/{scripts → app}/app.js +1 -1
  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/app/components/home.js +137 -0
  27. package/cli/scaffold/{scripts → app}/routes.js +1 -1
  28. package/cli/scaffold/{scripts → app}/store.js +6 -6
  29. package/cli/scaffold/assets/.gitkeep +0 -0
  30. package/cli/scaffold/{styles/styles.css → global.css} +3 -2
  31. package/cli/scaffold/index.html +11 -11
  32. package/dist/zquery.dist.zip +0 -0
  33. package/dist/zquery.js +746 -134
  34. package/dist/zquery.min.js +2 -2
  35. package/index.d.ts +11 -9
  36. package/index.js +15 -10
  37. package/package.json +3 -2
  38. package/src/component.js +161 -48
  39. package/src/core.js +57 -11
  40. package/src/diff.js +256 -58
  41. package/src/expression.js +33 -3
  42. package/src/reactive.js +37 -5
  43. package/src/router.js +195 -6
  44. package/tests/component.test.js +582 -0
  45. package/tests/core.test.js +251 -0
  46. package/tests/diff.test.js +333 -2
  47. package/tests/expression.test.js +148 -0
  48. package/tests/http.test.js +108 -0
  49. package/tests/reactive.test.js +148 -0
  50. package/tests/router.test.js +317 -0
  51. package/tests/store.test.js +126 -0
  52. package/tests/utils.test.js +161 -2
  53. package/types/collection.d.ts +17 -2
  54. package/types/component.d.ts +7 -0
  55. package/types/misc.d.ts +13 -0
  56. package/types/router.d.ts +30 -1
  57. package/cli/commands/dev.old.js +0 -520
  58. package/cli/scaffold/scripts/components/home.js +0 -137
  59. /package/cli/scaffold/{scripts → app}/components/contacts/contacts.css +0 -0
  60. /package/cli/scaffold/{scripts → app}/components/contacts/contacts.html +0 -0
  61. /package/cli/scaffold/{scripts → app}/components/contacts/contacts.js +0 -0
  62. /package/cli/scaffold/{scripts → app}/components/counter.js +0 -0
  63. /package/cli/scaffold/{scripts → app}/components/not-found.js +0 -0
  64. /package/cli/scaffold/{scripts → app}/components/todos.js +0 -0
@@ -332,3 +332,151 @@ describe('expression parser — multi-scope', () => {
332
332
  expect(safeEval('y', [{ x: 1 }, { y: 2 }])).toBe(2);
333
333
  });
334
334
  });
335
+
336
+
337
+ // ---------------------------------------------------------------------------
338
+ // in operator
339
+ // ---------------------------------------------------------------------------
340
+
341
+ describe('expression parser — in operator', () => {
342
+ it('checks property existence', () => {
343
+ expect(eval_("'x' in obj", { obj: { x: 1 } })).toBe(true);
344
+ expect(eval_("'y' in obj", { obj: { x: 1 } })).toBe(false);
345
+ });
346
+ });
347
+
348
+
349
+ // ---------------------------------------------------------------------------
350
+ // instanceof operator
351
+ // ---------------------------------------------------------------------------
352
+
353
+ describe('expression parser — instanceof', () => {
354
+ it('checks instanceOf', () => {
355
+ expect(eval_('arr instanceof Array', { arr: [1, 2] })).toBe(true);
356
+ expect(eval_('obj instanceof Array', { obj: {} })).toBe(false);
357
+ });
358
+ });
359
+
360
+
361
+ // ---------------------------------------------------------------------------
362
+ // Nested ternary
363
+ // ---------------------------------------------------------------------------
364
+
365
+ describe('expression parser — nested ternary', () => {
366
+ it('evaluates simple ternary correctly', () => {
367
+ expect(eval_("x > 10 ? 'big' : 'small'", { x: 12 })).toBe('big');
368
+ expect(eval_("x > 10 ? 'big' : 'small'", { x: 2 })).toBe('small');
369
+ });
370
+ });
371
+
372
+
373
+ // ---------------------------------------------------------------------------
374
+ // Chained method calls
375
+ // ---------------------------------------------------------------------------
376
+
377
+ describe('expression parser — chained calls', () => {
378
+ it('chains array methods', () => {
379
+ expect(eval_('items.filter(x => x > 1).map(x => x * 2)', { items: [1, 2, 3] })).toEqual([4, 6]);
380
+ });
381
+
382
+ it('chains string methods', () => {
383
+ expect(eval_("name.trim().toUpperCase()", { name: ' hello ' })).toBe('HELLO');
384
+ });
385
+ });
386
+
387
+
388
+ // ---------------------------------------------------------------------------
389
+ // Spread operator
390
+ // ---------------------------------------------------------------------------
391
+
392
+ describe('expression parser — spread / rest', () => {
393
+ it('spread is not supported — returns gracefully', () => {
394
+ // The parser does not support spread syntax; verify it doesn't throw
395
+ const result = eval_('[...items, 4]', { items: [1, 2, 3] });
396
+ expect(result).toBeDefined();
397
+ });
398
+ });
399
+
400
+
401
+ // ---------------------------------------------------------------------------
402
+ // Destructuring assignment in arrow body
403
+ // ---------------------------------------------------------------------------
404
+
405
+ describe('expression parser — complex arrow', () => {
406
+ it('arrow as callback in array method', () => {
407
+ const items = [{ n: 'a' }, { n: 'b' }];
408
+ expect(eval_('items.map(x => x.n)', { items })).toEqual(['a', 'b']);
409
+ });
410
+
411
+ it('arrow with ternary body', () => {
412
+ const fn = eval_('x => x > 0 ? "pos" : "neg"');
413
+ expect(fn(1)).toBe('pos');
414
+ expect(fn(-1)).toBe('neg');
415
+ });
416
+ });
417
+
418
+
419
+ // ---------------------------------------------------------------------------
420
+ // Bitwise operators
421
+ // ---------------------------------------------------------------------------
422
+
423
+ describe('expression parser — bitwise', () => {
424
+ it('bitwise operators are not supported — does not throw', () => {
425
+ // The expression parser does not implement bitwise operators
426
+ // Verify graceful fallback rather than crashes
427
+ expect(() => eval_('5 | 3')).not.toThrow();
428
+ expect(() => eval_('5 & 3')).not.toThrow();
429
+ expect(() => eval_('5 ^ 3')).not.toThrow();
430
+ });
431
+ });
432
+
433
+
434
+ // ---------------------------------------------------------------------------
435
+ // Comma expressions
436
+ // ---------------------------------------------------------------------------
437
+
438
+ describe('expression parser — comma', () => {
439
+ it('comma expressions are not supported — does not throw', () => {
440
+ // The parser does not support comma expressions
441
+ expect(() => eval_('(1, 2, 3)')).not.toThrow();
442
+ });
443
+ });
444
+
445
+
446
+ // ---------------------------------------------------------------------------
447
+ // Edge cases
448
+ // ---------------------------------------------------------------------------
449
+
450
+ describe('expression parser — edge cases', () => {
451
+ it('handles very long dot chains', () => {
452
+ const data = { a: { b: { c: { d: { e: 42 } } } } };
453
+ expect(eval_('a.b.c.d.e', data)).toBe(42);
454
+ });
455
+
456
+ it('handles numeric string keys in bracket access', () => {
457
+ expect(eval_("items['0']", { items: ['a', 'b'] })).toBe('a');
458
+ });
459
+
460
+ it('handles conditional access chains', () => {
461
+ expect(eval_('a?.b?.c', { a: null })).toBe(undefined);
462
+ expect(eval_('a?.b?.c', { a: { b: { c: 1 } } })).toBe(1);
463
+ });
464
+
465
+ it('handles string with special characters', () => {
466
+ expect(eval_("'hello\\nworld'")).toContain('hello');
467
+ });
468
+
469
+ it('handles negative numbers in expressions', () => {
470
+ expect(eval_('-1 + -2')).toBe(-3);
471
+ });
472
+
473
+ it('exponentiation ** is not supported — does not throw', () => {
474
+ // The parser does not implement ** operator
475
+ expect(() => eval_('2 ** 3')).not.toThrow();
476
+ });
477
+
478
+ it('handles empty array/object', () => {
479
+ expect(eval_('[]')).toEqual([]);
480
+ expect(eval_('{}')).toEqual({});
481
+ });
482
+ });
@@ -179,3 +179,111 @@ describe('http — text response', () => {
179
179
  expect(result.data).toBe('Hello World');
180
180
  });
181
181
  });
182
+
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // Interceptors
186
+ // ---------------------------------------------------------------------------
187
+
188
+ describe('http — interceptors', () => {
189
+ it('request interceptor via onRequest', async () => {
190
+ http.configure({ baseURL: '' });
191
+ http.onRequest((fetchOpts, url) => {
192
+ fetchOpts.headers['X-Custom'] = 'test';
193
+ });
194
+ mockFetch({});
195
+ await http.get('https://api.test.com/data');
196
+ const opts = fetchSpy.mock.calls[0][1];
197
+ expect(opts.headers['X-Custom']).toBe('test');
198
+ });
199
+
200
+ it('response interceptor via onResponse', async () => {
201
+ http.onResponse((result) => {
202
+ result.intercepted = true;
203
+ });
204
+ mockFetch({ x: 1 });
205
+ const result = await http.get('https://api.test.com/data');
206
+ expect(result.intercepted).toBe(true);
207
+ });
208
+ });
209
+
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // Timeout / abort
213
+ // ---------------------------------------------------------------------------
214
+
215
+ describe('http — abort signal', () => {
216
+ it('passes signal through options', async () => {
217
+ const controller = new AbortController();
218
+ mockFetch({});
219
+ await http.get('https://api.test.com/data', null, { signal: controller.signal });
220
+ const opts = fetchSpy.mock.calls[0][1];
221
+ expect(opts.signal).toBe(controller.signal);
222
+ });
223
+ });
224
+
225
+
226
+ // ---------------------------------------------------------------------------
227
+ // Blob response
228
+ // ---------------------------------------------------------------------------
229
+
230
+ describe('http — blob response', () => {
231
+ it('can request blob responses', async () => {
232
+ mockFetch('binary data');
233
+ const result = await http.get('https://api.test.com/file', null, { responseType: 'blob' });
234
+ // Should have a data field (blob or fallback text)
235
+ expect(result.data).toBeDefined();
236
+ });
237
+ });
238
+
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // HEAD and OPTIONS methods
242
+ // ---------------------------------------------------------------------------
243
+
244
+ describe('http — raw fetch pass-through', () => {
245
+ it('raw() delegates to native fetch', async () => {
246
+ mockFetch({ ok: true });
247
+ await http.raw('https://api.test.com/ping', { method: 'HEAD' });
248
+ expect(fetchSpy).toHaveBeenCalledWith('https://api.test.com/ping', { method: 'HEAD' });
249
+ });
250
+ });
251
+
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // Request with custom headers
255
+ // ---------------------------------------------------------------------------
256
+
257
+ describe('http — custom per-request headers', () => {
258
+ it('merges per-request headers', async () => {
259
+ mockFetch({});
260
+ await http.get('https://api.test.com/data', null, {
261
+ headers: { 'X-Request-Id': '123' },
262
+ });
263
+ const opts = fetchSpy.mock.calls[0][1];
264
+ expect(opts.headers['X-Request-Id']).toBe('123');
265
+ });
266
+ });
267
+
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // Response metadata
271
+ // ---------------------------------------------------------------------------
272
+
273
+ describe('http — response metadata', () => {
274
+ it('includes status and ok in result', async () => {
275
+ mockFetch({ data: 'yes' }, true, 200);
276
+ const result = await http.get('https://api.test.com/data');
277
+ expect(result.ok).toBe(true);
278
+ expect(result.status).toBe(200);
279
+ });
280
+
281
+ it('includes statusText in error', async () => {
282
+ mockFetch({ error: 'bad' }, false, 500);
283
+ try {
284
+ await http.get('https://api.test.com/fail');
285
+ } catch (err) {
286
+ expect(err.message).toContain('500');
287
+ }
288
+ });
289
+ });
@@ -188,4 +188,152 @@ describe('effect()', () => {
188
188
  effect(() => { throw new Error('fail'); });
189
189
  }).not.toThrow();
190
190
  });
191
+
192
+ it('dispose stops re-running on signal change', () => {
193
+ const s = signal(0);
194
+ const log = vi.fn();
195
+ const dispose = effect(() => { log(s.value); });
196
+ expect(log).toHaveBeenCalledTimes(1);
197
+ dispose();
198
+ s.value = 1;
199
+ expect(log).toHaveBeenCalledTimes(1); // no additional call
200
+ });
201
+
202
+ it('dispose removes effect from signal subscribers', () => {
203
+ const s = signal(0);
204
+ const log = vi.fn();
205
+ const dispose = effect(() => { log(s.value); });
206
+ dispose();
207
+ // After disposing, the signal should not hold a reference to the effect
208
+ s.value = 99;
209
+ expect(log).toHaveBeenCalledTimes(1); // only the initial run
210
+ });
211
+
212
+ it('tracks multiple signals', () => {
213
+ const a = signal(1);
214
+ const b = signal(2);
215
+ const log = vi.fn();
216
+ effect(() => { log(a.value + b.value); });
217
+ expect(log).toHaveBeenCalledWith(3);
218
+ a.value = 10;
219
+ expect(log).toHaveBeenCalledWith(12);
220
+ b.value = 20;
221
+ expect(log).toHaveBeenCalledWith(30);
222
+ expect(log).toHaveBeenCalledTimes(3);
223
+ });
224
+
225
+ it('handles conditional dependency tracking', () => {
226
+ const toggle = signal(true);
227
+ const a = signal('A');
228
+ const b = signal('B');
229
+ const log = vi.fn();
230
+ effect(() => {
231
+ log(toggle.value ? a.value : b.value);
232
+ });
233
+ expect(log).toHaveBeenCalledWith('A');
234
+ // Change b — should NOT re-run because b is not tracked when toggle=true
235
+ b.value = 'B2';
236
+ // After toggle switches, b becomes tracked
237
+ toggle.value = false;
238
+ expect(log).toHaveBeenCalledWith('B2');
239
+ });
240
+ });
241
+
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // reactive — array mutations
245
+ // ---------------------------------------------------------------------------
246
+
247
+ describe('reactive — arrays', () => {
248
+ it('detects push on a reactive array', () => {
249
+ const fn = vi.fn();
250
+ const obj = reactive({ items: [1, 2, 3] }, fn);
251
+ obj.items.push(4);
252
+ expect(fn).toHaveBeenCalled();
253
+ });
254
+
255
+ it('detects index assignment', () => {
256
+ const fn = vi.fn();
257
+ const obj = reactive({ items: ['a', 'b'] }, fn);
258
+ obj.items[0] = 'z';
259
+ expect(fn).toHaveBeenCalledWith('0', 'z', 'a');
260
+ });
261
+ });
262
+
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // computed — advanced
266
+ // ---------------------------------------------------------------------------
267
+
268
+ describe('computed — advanced', () => {
269
+ it('chains computed signals', () => {
270
+ const count = signal(2);
271
+ const doubled = computed(() => count.value * 2);
272
+ const quadrupled = computed(() => doubled.value * 2);
273
+ expect(quadrupled.value).toBe(8);
274
+ count.value = 3;
275
+ expect(quadrupled.value).toBe(12);
276
+ });
277
+
278
+ it('does not recompute when dependencies unchanged (diamond)', () => {
279
+ const s = signal(1);
280
+ const a = computed(() => s.value + 1);
281
+ const b = computed(() => s.value + 2);
282
+ const spy = vi.fn(() => a.value + b.value);
283
+ const c = computed(spy);
284
+ expect(c.value).toBe(5); // (1+1)+(1+2)
285
+ spy.mockClear();
286
+ s.value = 10;
287
+ expect(c.value).toBe(23); // (10+1)+(10+2)
288
+ });
289
+
290
+ it('peek does not create dependency', () => {
291
+ const s = signal(0);
292
+ const log = vi.fn();
293
+ effect(() => {
294
+ log(s.peek());
295
+ });
296
+ expect(log).toHaveBeenCalledWith(0);
297
+ s.value = 1;
298
+ // peek doesn't track, so effect should NOT re-run
299
+ expect(log).toHaveBeenCalledTimes(1);
300
+ });
301
+ });
302
+
303
+
304
+ // ---------------------------------------------------------------------------
305
+ // Signal — batch behavior
306
+ // ---------------------------------------------------------------------------
307
+
308
+ describe('Signal — multiple subscribers', () => {
309
+ it('notifies all subscribers', () => {
310
+ const s = signal(0);
311
+ const fn1 = vi.fn();
312
+ const fn2 = vi.fn();
313
+ s.subscribe(fn1);
314
+ s.subscribe(fn2);
315
+ s.value = 1;
316
+ expect(fn1).toHaveBeenCalledOnce();
317
+ expect(fn2).toHaveBeenCalledOnce();
318
+ });
319
+
320
+ it('unsubscribing one does not affect others', () => {
321
+ const s = signal(0);
322
+ const fn1 = vi.fn();
323
+ const fn2 = vi.fn();
324
+ const unsub1 = s.subscribe(fn1);
325
+ s.subscribe(fn2);
326
+ unsub1();
327
+ s.value = 1;
328
+ expect(fn1).not.toHaveBeenCalled();
329
+ expect(fn2).toHaveBeenCalledOnce();
330
+ });
331
+
332
+ it('handles rapid sequential updates', () => {
333
+ const s = signal(0);
334
+ const log = vi.fn();
335
+ s.subscribe(log);
336
+ for (let i = 1; i <= 10; i++) s.value = i;
337
+ expect(log).toHaveBeenCalledTimes(10);
338
+ });
191
339
  });