zero-query 0.9.8 → 1.0.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.
Files changed (99) hide show
  1. package/README.md +55 -31
  2. package/cli/args.js +1 -1
  3. package/cli/commands/build.js +2 -2
  4. package/cli/commands/bundle.js +15 -15
  5. package/cli/commands/create.js +41 -7
  6. package/cli/commands/dev/devtools/index.js +1 -1
  7. package/cli/commands/dev/devtools/js/core.js +14 -14
  8. package/cli/commands/dev/devtools/js/elements.js +4 -4
  9. package/cli/commands/dev/devtools/js/stats.js +1 -1
  10. package/cli/commands/dev/devtools/styles.css +2 -2
  11. package/cli/commands/dev/index.js +2 -2
  12. package/cli/commands/dev/logger.js +1 -1
  13. package/cli/commands/dev/overlay.js +21 -14
  14. package/cli/commands/dev/server.js +5 -5
  15. package/cli/commands/dev/validator.js +7 -7
  16. package/cli/commands/dev/watcher.js +6 -6
  17. package/cli/help.js +4 -2
  18. package/cli/index.js +2 -2
  19. package/cli/scaffold/default/app/app.js +17 -18
  20. package/cli/scaffold/default/app/components/about.js +9 -9
  21. package/cli/scaffold/default/app/components/api-demo.js +6 -6
  22. package/cli/scaffold/default/app/components/contact-card.js +4 -4
  23. package/cli/scaffold/default/app/components/contacts/contacts.css +2 -2
  24. package/cli/scaffold/default/app/components/contacts/contacts.html +3 -3
  25. package/cli/scaffold/default/app/components/contacts/contacts.js +11 -11
  26. package/cli/scaffold/default/app/components/counter.js +8 -8
  27. package/cli/scaffold/default/app/components/home.js +13 -13
  28. package/cli/scaffold/default/app/components/not-found.js +1 -1
  29. package/cli/scaffold/default/app/components/playground/playground.css +1 -1
  30. package/cli/scaffold/default/app/components/playground/playground.html +11 -11
  31. package/cli/scaffold/default/app/components/playground/playground.js +11 -11
  32. package/cli/scaffold/default/app/components/todos.js +8 -8
  33. package/cli/scaffold/default/app/components/toolkit/toolkit.css +1 -1
  34. package/cli/scaffold/default/app/components/toolkit/toolkit.html +4 -4
  35. package/cli/scaffold/default/app/components/toolkit/toolkit.js +7 -7
  36. package/cli/scaffold/default/app/routes.js +1 -1
  37. package/cli/scaffold/default/app/store.js +1 -1
  38. package/cli/scaffold/default/global.css +2 -2
  39. package/cli/scaffold/default/index.html +2 -2
  40. package/cli/scaffold/minimal/app/app.js +6 -7
  41. package/cli/scaffold/minimal/app/components/about.js +5 -5
  42. package/cli/scaffold/minimal/app/components/counter.js +6 -6
  43. package/cli/scaffold/minimal/app/components/home.js +8 -8
  44. package/cli/scaffold/minimal/app/components/not-found.js +1 -1
  45. package/cli/scaffold/minimal/app/routes.js +1 -1
  46. package/cli/scaffold/minimal/app/store.js +1 -1
  47. package/cli/scaffold/minimal/global.css +2 -2
  48. package/cli/scaffold/minimal/index.html +1 -1
  49. package/cli/scaffold/ssr/app/app.js +29 -0
  50. package/cli/scaffold/ssr/app/components/about.js +28 -0
  51. package/cli/scaffold/ssr/app/components/home.js +37 -0
  52. package/cli/scaffold/ssr/app/components/not-found.js +15 -0
  53. package/cli/scaffold/ssr/app/routes.js +6 -0
  54. package/cli/scaffold/ssr/global.css +113 -0
  55. package/cli/scaffold/ssr/index.html +31 -0
  56. package/cli/scaffold/ssr/package.json +8 -0
  57. package/cli/scaffold/ssr/server/index.js +118 -0
  58. package/cli/utils.js +6 -6
  59. package/dist/zquery.dist.zip +0 -0
  60. package/dist/zquery.js +565 -228
  61. package/dist/zquery.min.js +2 -2
  62. package/index.d.ts +25 -12
  63. package/index.js +11 -7
  64. package/package.json +9 -3
  65. package/src/component.js +64 -63
  66. package/src/core.js +15 -15
  67. package/src/diff.js +38 -38
  68. package/src/errors.js +72 -18
  69. package/src/expression.js +15 -17
  70. package/src/http.js +4 -4
  71. package/src/package.json +1 -0
  72. package/src/reactive.js +75 -9
  73. package/src/router.js +104 -24
  74. package/src/ssr.js +133 -39
  75. package/src/store.js +103 -21
  76. package/src/utils.js +64 -12
  77. package/tests/audit.test.js +143 -15
  78. package/tests/cli.test.js +20 -20
  79. package/tests/component.test.js +121 -121
  80. package/tests/core.test.js +56 -56
  81. package/tests/diff.test.js +42 -42
  82. package/tests/errors.test.js +425 -147
  83. package/tests/expression.test.js +58 -53
  84. package/tests/http.test.js +20 -20
  85. package/tests/reactive.test.js +185 -24
  86. package/tests/router.test.js +501 -74
  87. package/tests/ssr.test.js +444 -10
  88. package/tests/store.test.js +264 -23
  89. package/tests/utils.test.js +163 -26
  90. package/types/collection.d.ts +2 -2
  91. package/types/component.d.ts +5 -5
  92. package/types/errors.d.ts +36 -4
  93. package/types/http.d.ts +3 -3
  94. package/types/misc.d.ts +9 -9
  95. package/types/reactive.d.ts +25 -3
  96. package/types/router.d.ts +10 -6
  97. package/types/ssr.d.ts +22 -2
  98. package/types/store.d.ts +40 -5
  99. package/types/utils.d.ts +1 -1
@@ -12,6 +12,7 @@ import {
12
12
  escapeHtml, html, trust, TrustedHTML, uuid, camelCase, kebabCase,
13
13
  deepClone, deepMerge, isEqual, param, parseQuery,
14
14
  storage, session, EventBus, bus,
15
+ setPath,
15
16
  } from '../src/utils.js';
16
17
  import { createRouter, getRouter } from '../src/router.js';
17
18
  import { component, mount, mountAll, destroy, prefetch, getInstance } from '../src/component.js';
@@ -2861,7 +2862,7 @@ describe('Store', () => {
2861
2862
  actions: { inc(state) { state.count++; } }
2862
2863
  });
2863
2864
  const received = [];
2864
- store.subscribe('count', (val, old) => received.push({ val, old }));
2865
+ store.subscribe('count', (key, val, old) => received.push({ val, old }));
2865
2866
  store.dispatch('inc');
2866
2867
  store.dispatch('inc');
2867
2868
  expect(received).toEqual([
@@ -2876,7 +2877,7 @@ describe('Store', () => {
2876
2877
  actions: { bump(state) { state.x++; } }
2877
2878
  });
2878
2879
  const received = [];
2879
- const unsub = store.subscribe('x', (val) => received.push(val));
2880
+ const unsub = store.subscribe('x', (key, val) => received.push(val));
2880
2881
  store.dispatch('bump');
2881
2882
  unsub();
2882
2883
  store.dispatch('bump');
@@ -3062,7 +3063,7 @@ describe('Store', () => {
3062
3063
  });
3063
3064
  const received = [];
3064
3065
  store.subscribe('x', () => { throw new Error('sub error'); });
3065
- store.subscribe('x', (val) => received.push(val));
3066
+ store.subscribe('x', (key, val) => received.push(val));
3066
3067
  store.dispatch('bump');
3067
3068
  // The second subscriber still gets called because reportError is used (not re-throw)
3068
3069
  expect(received).toEqual([1]);
@@ -3093,7 +3094,7 @@ describe('Store', () => {
3093
3094
  state: { x: 0 }
3094
3095
  });
3095
3096
  const received = [];
3096
- store.subscribe('x', (val) => received.push(val));
3097
+ store.subscribe('x', (key, val) => received.push(val));
3097
3098
  store.state.x = 99;
3098
3099
  expect(received).toEqual([99]);
3099
3100
  });
@@ -3895,7 +3896,7 @@ describe('TrustedHTML and html template tag', () => {
3895
3896
 
3896
3897
 
3897
3898
  // ===========================================================================
3898
- // 24. Bug 9 `new` constructor globals reachable
3899
+ // 24. Bug 9 - `new` constructor globals reachable
3899
3900
  // ===========================================================================
3900
3901
  describe('new constructor globals (Bug 9)', () => {
3901
3902
  const eval_ = (expr, ctx = {}) => safeEval(expr, [ctx]);
@@ -3912,10 +3913,9 @@ describe('new constructor globals (Bug 9)', () => {
3912
3913
  expect(result.has(2)).toBe(true);
3913
3914
  });
3914
3915
 
3915
- it('new RegExp creates a RegExp', () => {
3916
+ it('new RegExp is blocked (ReDoS prevention)', () => {
3916
3917
  const result = eval_('new RegExp(pat, flags)', { pat: '^hello', flags: 'i' });
3917
- expect(result).toBeInstanceOf(RegExp);
3918
- expect(result.test('Hello world')).toBe(true);
3918
+ expect(result).toBeUndefined();
3919
3919
  });
3920
3920
 
3921
3921
  it('new URL creates a URL', () => {
@@ -3931,10 +3931,9 @@ describe('new constructor globals (Bug 9)', () => {
3931
3931
  expect(result.get('b')).toBe('2');
3932
3932
  });
3933
3933
 
3934
- it('new Error creates an Error', () => {
3934
+ it('new Error is blocked (info disclosure prevention)', () => {
3935
3935
  const result = eval_('new Error(msg)', { msg: 'test error' });
3936
- expect(result).toBeInstanceOf(Error);
3937
- expect(result.message).toBe('test error');
3936
+ expect(result).toBeUndefined();
3938
3937
  });
3939
3938
 
3940
3939
  it('Map and Set are accessible as identifiers for instanceof', () => {
@@ -3945,7 +3944,7 @@ describe('new constructor globals (Bug 9)', () => {
3945
3944
 
3946
3945
 
3947
3946
  // ===========================================================================
3948
- // 25. Bug 10 optional_call preserves `this` binding
3947
+ // 25. Bug 10 - optional_call preserves `this` binding
3949
3948
  // ===========================================================================
3950
3949
  describe('optional_call this binding (Bug 10)', () => {
3951
3950
  const eval_ = (expr, ctx = {}) => safeEval(expr, [ctx]);
@@ -3974,7 +3973,7 @@ describe('optional_call this binding (Bug 10)', () => {
3974
3973
 
3975
3974
 
3976
3975
  // ===========================================================================
3977
- // 26. Bug 11 HTTP abort vs timeout distinction
3976
+ // 26. Bug 11 - HTTP abort vs timeout distinction
3978
3977
  // ===========================================================================
3979
3978
  describe('HTTP abort vs timeout message (Bug 11)', () => {
3980
3979
  it('user abort says "aborted" not "timeout"', async () => {
@@ -4000,7 +3999,7 @@ describe('HTTP abort vs timeout message (Bug 11)', () => {
4000
3999
 
4001
4000
 
4002
4001
  // ===========================================================================
4003
- // 27. Bug 12 isEqual circular reference protection
4002
+ // 27. Bug 12 - isEqual circular reference protection
4004
4003
  // ===========================================================================
4005
4004
  describe('isEqual circular reference protection (Bug 12)', () => {
4006
4005
  it('does not stack overflow on circular objects', () => {
@@ -4008,7 +4007,7 @@ describe('isEqual circular reference protection (Bug 12)', () => {
4008
4007
  a.self = a;
4009
4008
  const b = { x: 1 };
4010
4009
  b.self = b;
4011
- // Should not throw just return true (both are circular in the same shape)
4010
+ // Should not throw - just return true (both are circular in the same shape)
4012
4011
  expect(() => isEqual(a, b)).not.toThrow();
4013
4012
  expect(isEqual(a, b)).toBe(true);
4014
4013
  });
@@ -4028,3 +4027,132 @@ describe('isEqual circular reference protection (Bug 12)', () => {
4028
4027
  expect(isEqual([1, 2], [1, 3])).toBe(false);
4029
4028
  });
4030
4029
  });
4030
+
4031
+
4032
+ // ===========================================================================
4033
+ // SECURITY AUDIT v2 - Prototype Pollution, ReDoS, Expression Safety
4034
+ // ===========================================================================
4035
+
4036
+ describe('Security: deepMerge prototype pollution prevention', () => {
4037
+ it('blocks __proto__ key from being merged', () => {
4038
+ const target = {};
4039
+ const malicious = JSON.parse('{"__proto__": {"polluted": true}}');
4040
+ deepMerge(target, malicious);
4041
+ expect(target.polluted).toBeUndefined();
4042
+ expect(({}).polluted).toBeUndefined();
4043
+ });
4044
+
4045
+ it('blocks constructor key from being merged', () => {
4046
+ const target = {};
4047
+ deepMerge(target, { constructor: { prototype: { polluted: true } } });
4048
+ expect(({}).polluted).toBeUndefined();
4049
+ });
4050
+
4051
+ it('blocks prototype key from being merged', () => {
4052
+ const target = {};
4053
+ deepMerge(target, { prototype: { polluted: true } });
4054
+ expect(target.prototype).toBeUndefined();
4055
+ });
4056
+
4057
+ it('still merges safe keys normally', () => {
4058
+ const result = deepMerge({}, { a: 1, b: { c: 2 } });
4059
+ expect(result).toEqual({ a: 1, b: { c: 2 } });
4060
+ });
4061
+
4062
+ it('blocks nested __proto__ pollution attempt', () => {
4063
+ const target = { nested: {} };
4064
+ const malicious = JSON.parse('{"nested": {"__proto__": {"deep": true}}}');
4065
+ deepMerge(target, malicious);
4066
+ expect(({}).deep).toBeUndefined();
4067
+ expect(target.nested.deep).toBeUndefined();
4068
+ });
4069
+ });
4070
+
4071
+ describe('Security: setPath prototype pollution prevention', () => {
4072
+ it('blocks __proto__ in path segments', () => {
4073
+ const obj = {};
4074
+ setPath(obj, '__proto__.polluted', true);
4075
+ expect(({}).polluted).toBeUndefined();
4076
+ });
4077
+
4078
+ it('blocks constructor in path segments', () => {
4079
+ const obj = {};
4080
+ setPath(obj, 'constructor.prototype.polluted', true);
4081
+ expect(({}).polluted).toBeUndefined();
4082
+ });
4083
+
4084
+ it('blocks prototype as final key', () => {
4085
+ const obj = {};
4086
+ setPath(obj, 'prototype', { evil: true });
4087
+ expect(obj.prototype).toBeUndefined();
4088
+ });
4089
+
4090
+ it('still sets safe paths normally', () => {
4091
+ const obj = {};
4092
+ setPath(obj, 'a.b.c', 42);
4093
+ expect(obj.a.b.c).toBe(42);
4094
+ });
4095
+ });
4096
+
4097
+ describe('Security: expression evaluator - blocked constructors', () => {
4098
+ it('blocks RegExp constructor (ReDoS prevention)', () => {
4099
+ expect(eval_('new RegExp(".*")')).toBeUndefined();
4100
+ expect(eval_('RegExp')).toBeUndefined();
4101
+ });
4102
+
4103
+ it('blocks Error constructor (info leak prevention)', () => {
4104
+ expect(eval_('new Error("test")')).toBeUndefined();
4105
+ });
4106
+
4107
+ it('blocks Function constructor', () => {
4108
+ expect(eval_('new Function("return 1")')).toBeUndefined();
4109
+ });
4110
+
4111
+ it('still allows safe constructors', () => {
4112
+ expect(eval_('new Date(2024, 0, 1)')).toBeInstanceOf(Date);
4113
+ expect(eval_('new Map')).toBeInstanceOf(Map);
4114
+ expect(eval_('new Set')).toBeInstanceOf(Set);
4115
+ expect(eval_('new Array(3)')).toBeInstanceOf(Array);
4116
+ expect(eval_('new URL("https://example.com")')).toBeInstanceOf(URL);
4117
+ });
4118
+ });
4119
+
4120
+ describe('Security: expression evaluator - blocked property access', () => {
4121
+ it('blocks __proto__ access', () => {
4122
+ expect(eval_('obj.__proto__', { obj: {} })).toBeUndefined();
4123
+ });
4124
+
4125
+ it('blocks constructor access', () => {
4126
+ expect(eval_('obj.constructor', { obj: {} })).toBeUndefined();
4127
+ });
4128
+
4129
+ it('blocks prototype access', () => {
4130
+ expect(eval_('obj.prototype', { obj: function(){} })).toBeUndefined();
4131
+ });
4132
+
4133
+ it('blocks __defineGetter__ access', () => {
4134
+ expect(eval_('obj.__defineGetter__', { obj: {} })).toBeUndefined();
4135
+ });
4136
+
4137
+ it('blocks call/apply/bind', () => {
4138
+ expect(eval_('fn.call', { fn: () => {} })).toBeUndefined();
4139
+ expect(eval_('fn.apply', { fn: () => {} })).toBeUndefined();
4140
+ expect(eval_('fn.bind', { fn: () => {} })).toBeUndefined();
4141
+ });
4142
+ });
4143
+
4144
+ describe('Security: template expression HTML escaping', () => {
4145
+ it('escapeHtml escapes script tags', () => {
4146
+ expect(escapeHtml('<script>alert(1)</script>')).toBe('&lt;script&gt;alert(1)&lt;/script&gt;');
4147
+ });
4148
+
4149
+ it('escapeHtml escapes quotes and ampersands', () => {
4150
+ expect(escapeHtml('"&\'')).toBe('&quot;&amp;&#39;');
4151
+ });
4152
+
4153
+ it('escapeHtml handles non-string input', () => {
4154
+ expect(escapeHtml(42)).toBe('42');
4155
+ expect(escapeHtml(null)).toBe('null');
4156
+ expect(escapeHtml(undefined)).toBe('undefined');
4157
+ });
4158
+ });
package/tests/cli.test.js CHANGED
@@ -2,10 +2,10 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
 
3
3
 
4
4
  // ---------------------------------------------------------------------------
5
- // CLI utils stripComments
5
+ // CLI utils - stripComments
6
6
  // ---------------------------------------------------------------------------
7
7
 
8
- describe('CLI stripComments', () => {
8
+ describe('CLI - stripComments', () => {
9
9
  let stripComments;
10
10
 
11
11
  beforeEach(async () => {
@@ -88,10 +88,10 @@ describe('CLI — stripComments', () => {
88
88
 
89
89
 
90
90
  // ---------------------------------------------------------------------------
91
- // CLI utils minify
91
+ // CLI utils - minify
92
92
  // ---------------------------------------------------------------------------
93
93
 
94
- describe('CLI minify', () => {
94
+ describe('CLI - minify', () => {
95
95
  let minify;
96
96
 
97
97
  beforeEach(async () => {
@@ -152,10 +152,10 @@ describe('CLI — minify', () => {
152
152
 
153
153
 
154
154
  // ---------------------------------------------------------------------------
155
- // CLI utils sizeKB
155
+ // CLI utils - sizeKB
156
156
  // ---------------------------------------------------------------------------
157
157
 
158
- describe('CLI sizeKB', () => {
158
+ describe('CLI - sizeKB', () => {
159
159
  let sizeKB;
160
160
 
161
161
  beforeEach(async () => {
@@ -186,10 +186,10 @@ describe('CLI — sizeKB', () => {
186
186
 
187
187
 
188
188
  // ---------------------------------------------------------------------------
189
- // CLI args flag and option
189
+ // CLI args - flag and option
190
190
  // ---------------------------------------------------------------------------
191
191
 
192
- describe('CLI args module', () => {
192
+ describe('CLI - args module', () => {
193
193
  let originalArgv;
194
194
 
195
195
  beforeEach(() => {
@@ -257,7 +257,7 @@ describe('CLI — args module', () => {
257
257
  // showHelp
258
258
  // ===========================================================================
259
259
 
260
- describe('CLI showHelp', () => {
260
+ describe('CLI - showHelp', () => {
261
261
  it('outputs help text to console', async () => {
262
262
  const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
263
263
  const showHelp = (await import('../cli/help.js')).default;
@@ -283,10 +283,10 @@ describe('CLI — showHelp', () => {
283
283
 
284
284
 
285
285
  // ===========================================================================
286
- // stripComments additional edge cases
286
+ // stripComments - additional edge cases
287
287
  // ===========================================================================
288
288
 
289
- describe('CLI stripComments extra', () => {
289
+ describe('CLI - stripComments extra', () => {
290
290
  let stripComments;
291
291
  beforeEach(async () => {
292
292
  const mod = await import('../cli/utils.js');
@@ -331,10 +331,10 @@ describe('CLI — stripComments extra', () => {
331
331
 
332
332
 
333
333
  // ===========================================================================
334
- // minify additional edge cases
334
+ // minify - additional edge cases
335
335
  // ===========================================================================
336
336
 
337
- describe('CLI minify extra', () => {
337
+ describe('CLI - minify extra', () => {
338
338
  let minify;
339
339
  beforeEach(async () => {
340
340
  const mod = await import('../cli/utils.js');
@@ -365,10 +365,10 @@ describe('CLI — minify extra', () => {
365
365
 
366
366
 
367
367
  // ===========================================================================
368
- // sizeKB edge cases
368
+ // sizeKB - edge cases
369
369
  // ===========================================================================
370
370
 
371
- describe('CLI sizeKB extra', () => {
371
+ describe('CLI - sizeKB extra', () => {
372
372
  let sizeKB;
373
373
  beforeEach(async () => {
374
374
  const mod = await import('../cli/utils.js');
@@ -393,7 +393,7 @@ describe('CLI — sizeKB extra', () => {
393
393
  // copyDirSync
394
394
  // ===========================================================================
395
395
 
396
- describe('CLI copyDirSync', () => {
396
+ describe('CLI - copyDirSync', () => {
397
397
  let copyDirSync;
398
398
  const fs = require('fs');
399
399
  const path = require('path');
@@ -428,10 +428,10 @@ describe('CLI — copyDirSync', () => {
428
428
 
429
429
 
430
430
  // ===========================================================================
431
- // flag/option additional cases
431
+ // flag/option - additional cases
432
432
  // ===========================================================================
433
433
 
434
- describe('CLI flag/option extra', () => {
434
+ describe('CLI - flag/option extra', () => {
435
435
  it('flag returns false when absent', async () => {
436
436
  process.argv = ['node', 'script'];
437
437
  vi.resetModules();
@@ -457,10 +457,10 @@ describe('CLI — flag/option extra', () => {
457
457
 
458
458
 
459
459
  // ===========================================================================
460
- // createProject scaffold command
460
+ // createProject - scaffold command
461
461
  // ===========================================================================
462
462
 
463
- describe('CLI createProject', () => {
463
+ describe('CLI - createProject', () => {
464
464
  const fs = require('fs');
465
465
  const path = require('path');
466
466
  const os = require('os');