zero-query 0.5.2 → 0.7.5

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 (58) hide show
  1. package/README.md +12 -10
  2. package/cli/commands/build.js +7 -5
  3. package/cli/commands/bundle.js +286 -8
  4. package/cli/commands/dev/index.js +82 -0
  5. package/cli/commands/dev/logger.js +70 -0
  6. package/cli/commands/dev/overlay.js +366 -0
  7. package/cli/commands/dev/server.js +158 -0
  8. package/cli/commands/dev/validator.js +94 -0
  9. package/cli/commands/dev/watcher.js +147 -0
  10. package/cli/scaffold/favicon.ico +0 -0
  11. package/cli/scaffold/index.html +1 -0
  12. package/cli/scaffold/scripts/app.js +15 -22
  13. package/cli/scaffold/scripts/components/about.js +14 -2
  14. package/cli/scaffold/scripts/components/contacts/contacts.css +0 -7
  15. package/cli/scaffold/scripts/components/contacts/contacts.html +8 -7
  16. package/cli/scaffold/scripts/components/contacts/contacts.js +17 -1
  17. package/cli/scaffold/scripts/components/counter.js +30 -10
  18. package/cli/scaffold/scripts/components/home.js +3 -3
  19. package/cli/scaffold/scripts/components/todos.js +6 -5
  20. package/cli/scaffold/styles/styles.css +1 -0
  21. package/cli/utils.js +111 -6
  22. package/dist/zquery.dist.zip +0 -0
  23. package/dist/zquery.js +2005 -216
  24. package/dist/zquery.min.js +3 -13
  25. package/index.d.ts +149 -1080
  26. package/index.js +18 -7
  27. package/package.json +9 -3
  28. package/src/component.js +186 -45
  29. package/src/core.js +327 -35
  30. package/src/diff.js +280 -0
  31. package/src/errors.js +155 -0
  32. package/src/expression.js +806 -0
  33. package/src/http.js +18 -10
  34. package/src/reactive.js +29 -4
  35. package/src/router.js +59 -6
  36. package/src/ssr.js +224 -0
  37. package/src/store.js +24 -8
  38. package/tests/component.test.js +304 -0
  39. package/tests/core.test.js +726 -0
  40. package/tests/diff.test.js +194 -0
  41. package/tests/errors.test.js +162 -0
  42. package/tests/expression.test.js +334 -0
  43. package/tests/http.test.js +181 -0
  44. package/tests/reactive.test.js +191 -0
  45. package/tests/router.test.js +332 -0
  46. package/tests/store.test.js +253 -0
  47. package/tests/utils.test.js +353 -0
  48. package/types/collection.d.ts +368 -0
  49. package/types/component.d.ts +210 -0
  50. package/types/errors.d.ts +103 -0
  51. package/types/http.d.ts +81 -0
  52. package/types/misc.d.ts +166 -0
  53. package/types/reactive.d.ts +76 -0
  54. package/types/router.d.ts +132 -0
  55. package/types/ssr.d.ts +49 -0
  56. package/types/store.d.ts +107 -0
  57. package/types/utils.d.ts +142 -0
  58. /package/cli/commands/{dev.js → dev.old.js} +0 -0
@@ -0,0 +1,304 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { component, mount, mountAll, getInstance, destroy, getRegistry } from '../src/component.js';
3
+ import { ZQueryError } from '../src/errors.js';
4
+
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Setup
8
+ // ---------------------------------------------------------------------------
9
+ beforeEach(() => {
10
+ document.body.innerHTML = '';
11
+ });
12
+
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Component registration
16
+ // ---------------------------------------------------------------------------
17
+
18
+ describe('component() — registration', () => {
19
+ it('registers a component', () => {
20
+ component('test-comp', {
21
+ state: () => ({ count: 0 }),
22
+ render() { return `<p>${this.state.count}</p>`; },
23
+ });
24
+ const registry = getRegistry();
25
+ expect(registry['test-comp']).toBeDefined();
26
+ });
27
+
28
+ it('throws ZQueryError if name has no hyphen', () => {
29
+ expect(() => component('nohyphen', {})).toThrow(ZQueryError);
30
+ });
31
+
32
+ it('throws ZQueryError if name is empty', () => {
33
+ expect(() => component('', {})).toThrow(ZQueryError);
34
+ });
35
+
36
+ it('throws ZQueryError if name is not a string', () => {
37
+ expect(() => component(null, {})).toThrow(ZQueryError);
38
+ });
39
+ });
40
+
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Mount
44
+ // ---------------------------------------------------------------------------
45
+
46
+ describe('mount()', () => {
47
+ beforeEach(() => {
48
+ component('mount-test', {
49
+ state: () => ({ msg: 'Hello' }),
50
+ render() { return `<div class="inner">${this.state.msg}</div>`; },
51
+ });
52
+ document.body.innerHTML = '<mount-test id="target"></mount-test>';
53
+ });
54
+
55
+ it('mounts component and renders HTML', () => {
56
+ const instance = mount('#target', 'mount-test');
57
+ expect(document.querySelector('.inner').textContent).toBe('Hello');
58
+ expect(instance).toBeDefined();
59
+ });
60
+
61
+ it('throws ZQueryError for missing target', () => {
62
+ expect(() => mount('#nonexistent', 'mount-test')).toThrow(ZQueryError);
63
+ });
64
+
65
+ it('throws ZQueryError for unregistered component', () => {
66
+ expect(() => mount('#target', 'unknown-comp')).toThrow(ZQueryError);
67
+ });
68
+
69
+ it('getInstance returns instance after mount', () => {
70
+ mount('#target', 'mount-test');
71
+ const inst = getInstance('#target');
72
+ expect(inst).not.toBeNull();
73
+ expect(inst.state.msg).toBe('Hello');
74
+ });
75
+ });
76
+
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Lifecycle hooks
80
+ // ---------------------------------------------------------------------------
81
+
82
+ describe('component — lifecycle', () => {
83
+ it('calls init on creation', () => {
84
+ const initFn = vi.fn();
85
+ component('life-init', {
86
+ init: initFn,
87
+ render() { return '<div>init</div>'; },
88
+ });
89
+ document.body.innerHTML = '<life-init id="li"></life-init>';
90
+ mount('#li', 'life-init');
91
+ expect(initFn).toHaveBeenCalledOnce();
92
+ });
93
+
94
+ it('calls mounted after first render', () => {
95
+ const mountedFn = vi.fn();
96
+ component('life-mounted', {
97
+ mounted: mountedFn,
98
+ render() { return '<div>mounted</div>'; },
99
+ });
100
+ document.body.innerHTML = '<life-mounted id="lm"></life-mounted>';
101
+ mount('#lm', 'life-mounted');
102
+ expect(mountedFn).toHaveBeenCalledOnce();
103
+ });
104
+
105
+ it('calls destroyed on destroy', () => {
106
+ const destroyedFn = vi.fn();
107
+ component('life-destroy', {
108
+ destroyed: destroyedFn,
109
+ render() { return '<div>destroy</div>'; },
110
+ });
111
+ document.body.innerHTML = '<life-destroy id="ld"></life-destroy>';
112
+ mount('#ld', 'life-destroy');
113
+ destroy('#ld');
114
+ expect(destroyedFn).toHaveBeenCalledOnce();
115
+ });
116
+
117
+ it('does not crash when lifecycle hook throws', () => {
118
+ component('life-throw', {
119
+ init() { throw new Error('init error'); },
120
+ render() { return '<div>throw</div>'; },
121
+ });
122
+ document.body.innerHTML = '<life-throw id="lt"></life-throw>';
123
+ expect(() => mount('#lt', 'life-throw')).not.toThrow();
124
+ });
125
+ });
126
+
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Reactive state
130
+ // ---------------------------------------------------------------------------
131
+
132
+ describe('component — reactive state', () => {
133
+ it('re-renders on state change', async () => {
134
+ component('react-state', {
135
+ state: () => ({ count: 0 }),
136
+ render() { return `<span class="count">${this.state.count}</span>`; },
137
+ });
138
+ document.body.innerHTML = '<react-state id="rs"></react-state>';
139
+ const inst = mount('#rs', 'react-state');
140
+ expect(document.querySelector('.count').textContent).toBe('0');
141
+
142
+ inst.state.count = 5;
143
+ // State update is batched via microtask
144
+ await new Promise(r => queueMicrotask(r));
145
+ expect(document.querySelector('.count').textContent).toBe('5');
146
+ });
147
+ });
148
+
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // Props
152
+ // ---------------------------------------------------------------------------
153
+
154
+ describe('component — props', () => {
155
+ it('receives props', () => {
156
+ component('prop-test', {
157
+ render() { return `<span class="prop">${this.props.label}</span>`; },
158
+ });
159
+ document.body.innerHTML = '<prop-test id="pt"></prop-test>';
160
+ mount('#pt', 'prop-test', { label: 'Hello' });
161
+ expect(document.querySelector('.prop').textContent).toBe('Hello');
162
+ });
163
+
164
+ it('props are frozen', () => {
165
+ component('prop-freeze', {
166
+ render() { return '<div>test</div>'; },
167
+ });
168
+ document.body.innerHTML = '<prop-freeze id="pf"></prop-freeze>';
169
+ const inst = mount('#pf', 'prop-freeze', { x: 1 });
170
+ expect(() => { inst.props.x = 2; }).toThrow();
171
+ });
172
+ });
173
+
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Computed properties
177
+ // ---------------------------------------------------------------------------
178
+
179
+ describe('component — computed', () => {
180
+ it('derives values from state', () => {
181
+ component('comp-computed', {
182
+ state: () => ({ count: 5 }),
183
+ computed: {
184
+ doubled(state) { return state.count * 2; },
185
+ },
186
+ render() { return `<span class="doubled">${this.computed.doubled}</span>`; },
187
+ });
188
+ document.body.innerHTML = '<comp-computed id="cc"></comp-computed>';
189
+ const inst = mount('#cc', 'comp-computed');
190
+ expect(inst.computed.doubled).toBe(10);
191
+ expect(document.querySelector('.doubled').textContent).toBe('10');
192
+ });
193
+ });
194
+
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // User methods
198
+ // ---------------------------------------------------------------------------
199
+
200
+ describe('component — methods', () => {
201
+ it('binds user methods to instance', () => {
202
+ let captured;
203
+ component('method-test', {
204
+ state: () => ({ x: 42 }),
205
+ myMethod() { captured = this.state.x; },
206
+ render() { return '<div>methods</div>'; },
207
+ });
208
+ document.body.innerHTML = '<method-test id="mt"></method-test>';
209
+ const inst = mount('#mt', 'method-test');
210
+ inst.myMethod();
211
+ expect(captured).toBe(42);
212
+ });
213
+ });
214
+
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // setState
218
+ // ---------------------------------------------------------------------------
219
+
220
+ describe('component — setState', () => {
221
+ it('batch updates state', async () => {
222
+ component('set-state', {
223
+ state: () => ({ a: 1, b: 2 }),
224
+ render() { return `<div>${this.state.a}-${this.state.b}</div>`; },
225
+ });
226
+ document.body.innerHTML = '<set-state id="ss"></set-state>';
227
+ const inst = mount('#ss', 'set-state');
228
+ inst.setState({ a: 10, b: 20 });
229
+ await new Promise(r => queueMicrotask(r));
230
+ expect(inst.state.a).toBe(10);
231
+ expect(inst.state.b).toBe(20);
232
+ });
233
+ });
234
+
235
+
236
+ // ---------------------------------------------------------------------------
237
+ // emit
238
+ // ---------------------------------------------------------------------------
239
+
240
+ describe('component — emit', () => {
241
+ it('dispatches custom event', () => {
242
+ component('emit-test', {
243
+ render() { return '<div>emit</div>'; },
244
+ });
245
+ document.body.innerHTML = '<emit-test id="et"></emit-test>';
246
+ const inst = mount('#et', 'emit-test');
247
+
248
+ let received;
249
+ document.querySelector('#et').addEventListener('my-event', (e) => {
250
+ received = e.detail;
251
+ });
252
+ inst.emit('my-event', { data: 42 });
253
+ expect(received).toEqual({ data: 42 });
254
+ });
255
+ });
256
+
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // destroy
260
+ // ---------------------------------------------------------------------------
261
+
262
+ describe('component — destroy', () => {
263
+ it('clears innerHTML and removes from registry', () => {
264
+ component('destroy-test', {
265
+ render() { return '<div class="will-die">alive</div>'; },
266
+ });
267
+ document.body.innerHTML = '<destroy-test id="dt"></destroy-test>';
268
+ mount('#dt', 'destroy-test');
269
+ expect(document.querySelector('.will-die')).not.toBeNull();
270
+ destroy('#dt');
271
+ expect(document.querySelector('.will-die')).toBeNull();
272
+ expect(getInstance('#dt')).toBeNull();
273
+ });
274
+
275
+ it('double destroy does not throw', () => {
276
+ component('destroy-twice', {
277
+ render() { return '<div>twice</div>'; },
278
+ });
279
+ document.body.innerHTML = '<destroy-twice id="d2"></destroy-twice>';
280
+ mount('#d2', 'destroy-twice');
281
+ destroy('#d2');
282
+ expect(() => destroy('#d2')).not.toThrow();
283
+ });
284
+ });
285
+
286
+
287
+ // ---------------------------------------------------------------------------
288
+ // mountAll
289
+ // ---------------------------------------------------------------------------
290
+
291
+ describe('mountAll()', () => {
292
+ it('auto-mounts all registered component tags', () => {
293
+ component('auto-a', {
294
+ render() { return '<span class="auto-a">A</span>'; },
295
+ });
296
+ component('auto-b', {
297
+ render() { return '<span class="auto-b">B</span>'; },
298
+ });
299
+ document.body.innerHTML = '<auto-a></auto-a><auto-b></auto-b>';
300
+ mountAll();
301
+ expect(document.querySelector('.auto-a').textContent).toBe('A');
302
+ expect(document.querySelector('.auto-b').textContent).toBe('B');
303
+ });
304
+ });