zero-query 1.1.1 → 1.2.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 (154) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +2 -0
  3. package/cli/args.js +33 -33
  4. package/cli/commands/build-api.js +443 -442
  5. package/cli/commands/build.js +254 -247
  6. package/cli/commands/bundle.js +1228 -1224
  7. package/cli/commands/create.js +137 -121
  8. package/cli/commands/dev/devtools/index.js +56 -56
  9. package/cli/commands/dev/devtools/js/components.js +49 -49
  10. package/cli/commands/dev/devtools/js/core.js +423 -423
  11. package/cli/commands/dev/devtools/js/elements.js +421 -421
  12. package/cli/commands/dev/devtools/js/network.js +166 -166
  13. package/cli/commands/dev/devtools/js/performance.js +73 -73
  14. package/cli/commands/dev/devtools/js/router.js +105 -105
  15. package/cli/commands/dev/devtools/js/source.js +132 -132
  16. package/cli/commands/dev/devtools/js/stats.js +35 -35
  17. package/cli/commands/dev/devtools/js/tabs.js +79 -79
  18. package/cli/commands/dev/devtools/panel.html +95 -95
  19. package/cli/commands/dev/devtools/styles.css +244 -244
  20. package/cli/commands/dev/index.js +107 -107
  21. package/cli/commands/dev/logger.js +75 -75
  22. package/cli/commands/dev/overlay.js +858 -858
  23. package/cli/commands/dev/server.js +220 -220
  24. package/cli/commands/dev/validator.js +94 -94
  25. package/cli/commands/dev/watcher.js +172 -172
  26. package/cli/help.js +114 -112
  27. package/cli/index.js +52 -52
  28. package/cli/scaffold/default/LICENSE +21 -21
  29. package/cli/scaffold/default/app/app.js +207 -207
  30. package/cli/scaffold/default/app/components/about.js +201 -201
  31. package/cli/scaffold/default/app/components/api-demo.js +143 -143
  32. package/cli/scaffold/default/app/components/contact-card.js +231 -231
  33. package/cli/scaffold/default/app/components/contacts/contacts.css +706 -706
  34. package/cli/scaffold/default/app/components/contacts/contacts.html +200 -200
  35. package/cli/scaffold/default/app/components/contacts/contacts.js +196 -196
  36. package/cli/scaffold/default/app/components/counter.js +127 -127
  37. package/cli/scaffold/default/app/components/home.js +249 -249
  38. package/cli/scaffold/default/app/components/not-found.js +16 -16
  39. package/cli/scaffold/default/app/components/playground/playground.css +115 -115
  40. package/cli/scaffold/default/app/components/playground/playground.html +161 -161
  41. package/cli/scaffold/default/app/components/playground/playground.js +116 -116
  42. package/cli/scaffold/default/app/components/todos.js +225 -225
  43. package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -97
  44. package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -146
  45. package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -280
  46. package/cli/scaffold/default/app/routes.js +15 -15
  47. package/cli/scaffold/default/app/store.js +101 -101
  48. package/cli/scaffold/default/global.css +552 -552
  49. package/cli/scaffold/default/index.html +99 -99
  50. package/cli/scaffold/minimal/app/app.js +85 -85
  51. package/cli/scaffold/minimal/app/components/about.js +68 -68
  52. package/cli/scaffold/minimal/app/components/counter.js +122 -122
  53. package/cli/scaffold/minimal/app/components/home.js +68 -68
  54. package/cli/scaffold/minimal/app/components/not-found.js +16 -16
  55. package/cli/scaffold/minimal/app/routes.js +9 -9
  56. package/cli/scaffold/minimal/app/store.js +36 -36
  57. package/cli/scaffold/minimal/global.css +300 -300
  58. package/cli/scaffold/minimal/index.html +44 -44
  59. package/cli/scaffold/ssr/app/app.js +41 -41
  60. package/cli/scaffold/ssr/app/components/about.js +55 -55
  61. package/cli/scaffold/ssr/app/components/blog/index.js +65 -65
  62. package/cli/scaffold/ssr/app/components/blog/post.js +86 -86
  63. package/cli/scaffold/ssr/app/components/home.js +37 -37
  64. package/cli/scaffold/ssr/app/components/not-found.js +15 -15
  65. package/cli/scaffold/ssr/app/routes.js +8 -8
  66. package/cli/scaffold/ssr/global.css +228 -228
  67. package/cli/scaffold/ssr/index.html +37 -37
  68. package/cli/scaffold/ssr/package.json +8 -8
  69. package/cli/scaffold/ssr/server/data/posts.js +144 -144
  70. package/cli/scaffold/ssr/server/index.js +213 -213
  71. package/cli/scaffold/webrtc/app/app.js +11 -0
  72. package/cli/scaffold/webrtc/app/components/video-room.js +295 -0
  73. package/cli/scaffold/webrtc/app/lib/room.js +252 -0
  74. package/cli/scaffold/webrtc/assets/.gitkeep +0 -0
  75. package/cli/scaffold/webrtc/global.css +250 -0
  76. package/cli/scaffold/webrtc/index.html +21 -0
  77. package/cli/utils.js +305 -287
  78. package/dist/API.md +661 -0
  79. package/dist/zquery.dist.zip +0 -0
  80. package/dist/zquery.js +10313 -6614
  81. package/dist/zquery.min.js +8 -631
  82. package/index.d.ts +570 -371
  83. package/index.js +311 -240
  84. package/package.json +76 -70
  85. package/src/component.js +1709 -1691
  86. package/src/core.js +921 -921
  87. package/src/diff.js +497 -497
  88. package/src/errors.js +209 -209
  89. package/src/expression.js +922 -922
  90. package/src/http.js +242 -242
  91. package/src/package.json +1 -1
  92. package/src/reactive.js +255 -255
  93. package/src/router.js +843 -843
  94. package/src/ssr.js +418 -418
  95. package/src/store.js +318 -318
  96. package/src/utils.js +515 -515
  97. package/src/webrtc/e2ee.js +351 -0
  98. package/src/webrtc/errors.js +116 -0
  99. package/src/webrtc/ice.js +301 -0
  100. package/src/webrtc/index.js +131 -0
  101. package/src/webrtc/joinToken.js +119 -0
  102. package/src/webrtc/observe.js +172 -0
  103. package/src/webrtc/peer.js +351 -0
  104. package/src/webrtc/reactive.js +268 -0
  105. package/src/webrtc/room.js +625 -0
  106. package/src/webrtc/sdp.js +302 -0
  107. package/src/webrtc/sfu/index.js +43 -0
  108. package/src/webrtc/sfu/livekit.js +131 -0
  109. package/src/webrtc/sfu/mediasoup.js +150 -0
  110. package/src/webrtc/signaling.js +373 -0
  111. package/src/webrtc/turn.js +237 -0
  112. package/tests/_helpers/webrtcFakes.js +289 -0
  113. package/tests/audit.test.js +4158 -4158
  114. package/tests/cli.test.js +1136 -1103
  115. package/tests/compare.test.js +497 -486
  116. package/tests/component.test.js +3969 -3938
  117. package/tests/core.test.js +1910 -1910
  118. package/tests/dev-server.test.js +489 -489
  119. package/tests/diff.test.js +1416 -1416
  120. package/tests/docs.test.js +1664 -1650
  121. package/tests/electron-features.test.js +864 -864
  122. package/tests/errors.test.js +619 -619
  123. package/tests/expression.test.js +1056 -1056
  124. package/tests/http.test.js +648 -648
  125. package/tests/reactive.test.js +819 -819
  126. package/tests/router.test.js +2327 -2327
  127. package/tests/ssr.test.js +870 -870
  128. package/tests/store.test.js +830 -830
  129. package/tests/test-minifier.js +153 -153
  130. package/tests/test-ssr.js +27 -27
  131. package/tests/utils.test.js +1377 -1377
  132. package/tests/webrtc/e2ee.test.js +283 -0
  133. package/tests/webrtc/ice.test.js +202 -0
  134. package/tests/webrtc/joinToken.test.js +89 -0
  135. package/tests/webrtc/observe.test.js +111 -0
  136. package/tests/webrtc/peer.test.js +373 -0
  137. package/tests/webrtc/reactive.test.js +235 -0
  138. package/tests/webrtc/room.test.js +406 -0
  139. package/tests/webrtc/sdp.test.js +151 -0
  140. package/tests/webrtc/sfu-livekit.test.js +119 -0
  141. package/tests/webrtc/sfu.test.js +160 -0
  142. package/tests/webrtc/signaling.test.js +251 -0
  143. package/tests/webrtc/turn.test.js +256 -0
  144. package/types/collection.d.ts +383 -383
  145. package/types/component.d.ts +186 -186
  146. package/types/errors.d.ts +135 -135
  147. package/types/http.d.ts +92 -92
  148. package/types/misc.d.ts +201 -201
  149. package/types/reactive.d.ts +98 -98
  150. package/types/router.d.ts +190 -190
  151. package/types/ssr.d.ts +102 -102
  152. package/types/store.d.ts +146 -146
  153. package/types/utils.d.ts +245 -245
  154. package/types/webrtc.d.ts +653 -0
package/tests/cli.test.js CHANGED
@@ -1,1103 +1,1136 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
-
3
-
4
- // ---------------------------------------------------------------------------
5
- // CLI bundle - stripModuleSyntax
6
- // ---------------------------------------------------------------------------
7
-
8
- describe('CLI - stripModuleSyntax', () => {
9
- let stripModuleSyntax;
10
-
11
- beforeEach(async () => {
12
- vi.resetModules();
13
- const mod = await import('../cli/commands/bundle.js');
14
- stripModuleSyntax = mod.stripModuleSyntax;
15
- });
16
-
17
- // -- Import stripping ---------------------------------------------------
18
-
19
- it('strips named import from module', () => {
20
- const result = stripModuleSyntax("import { foo } from './mod.js';\nconst x = 1;");
21
- expect(result.code.trim()).toBe('const x = 1;');
22
- });
23
-
24
- it('strips default import from module', () => {
25
- const result = stripModuleSyntax("import foo from './mod.js';\nconst x = 1;");
26
- expect(result.code.trim()).toBe('const x = 1;');
27
- });
28
-
29
- it('strips side-effect import', () => {
30
- const result = stripModuleSyntax("import './mod.js';\nconst x = 1;");
31
- expect(result.code.trim()).toBe('const x = 1;');
32
- });
33
-
34
- it('strips multi-line import', () => {
35
- const input = "import {\n a,\n b,\n c\n} from './mod.js';\nconst x = 1;";
36
- const result = stripModuleSyntax(input);
37
- expect(result.code.trim()).toBe('const x = 1;');
38
- });
39
-
40
- // -- export default -----------------------------------------------------
41
-
42
- it('strips export default keyword', () => {
43
- const result = stripModuleSyntax('export default function foo() {}');
44
- expect(result.code.trim()).toBe('function foo() {}');
45
- });
46
-
47
- // -- export const/let/var → var -----------------------------------------
48
-
49
- it('converts export const to var', () => {
50
- const result = stripModuleSyntax('export const x = 1;');
51
- expect(result.code.trim()).toBe('var x = 1;');
52
- });
53
-
54
- it('converts export let to var', () => {
55
- const result = stripModuleSyntax('export let y = 2;');
56
- expect(result.code.trim()).toBe('var y = 2;');
57
- });
58
-
59
- it('converts export var (keeps var)', () => {
60
- const result = stripModuleSyntax('export var z = 3;');
61
- expect(result.code.trim()).toBe('var z = 3;');
62
- });
63
-
64
- // -- export function → var assignment -----------------------------------
65
-
66
- it('converts export function to var assignment', () => {
67
- const result = stripModuleSyntax('export function greet(name) { return name; }');
68
- expect(result.code.trim()).toBe('var greet = function greet(name) { return name; }');
69
- });
70
-
71
- it('converts export async function to var assignment', () => {
72
- const result = stripModuleSyntax('export async function fetchData() {}');
73
- expect(result.code.trim()).toBe('var fetchData = async function fetchData() {}');
74
- });
75
-
76
- // -- export class → var assignment --------------------------------------
77
-
78
- it('converts export class to var assignment', () => {
79
- const result = stripModuleSyntax('export class MyComponent {}');
80
- expect(result.code.trim()).toBe('var MyComponent = class MyComponent {}');
81
- });
82
-
83
- // -- bare export block: export { a, b } ---------------------------------
84
-
85
- it('converts bare exported function declarations to var', () => {
86
- const input = 'function buildIndex() {}\nexport { buildIndex };';
87
- const result = stripModuleSyntax(input);
88
- expect(result.code).toContain('var buildIndex = function buildIndex()');
89
- expect(result.code).not.toContain('export');
90
- expect(result.bareExportNames).toEqual([{ local: 'buildIndex', exported: 'buildIndex' }]);
91
- });
92
-
93
- it('converts bare exported async function to var', () => {
94
- const input = 'async function load() {}\nexport { load };';
95
- const result = stripModuleSyntax(input);
96
- expect(result.code).toContain('var load = async function load()');
97
- });
98
-
99
- it('converts bare exported const/let declarations to var', () => {
100
- const input = 'const MAX = 10;\nlet count = 0;\nexport { MAX, count };';
101
- const result = stripModuleSyntax(input);
102
- expect(result.code).toContain('var MAX');
103
- expect(result.code).toContain('var count');
104
- expect(result.code).not.toContain('const MAX');
105
- expect(result.code).not.toContain('let count');
106
- });
107
-
108
- it('handles multi-line bare export block', () => {
109
- const input = 'function a() {}\nfunction b() {}\nexport {\n a,\n b\n};';
110
- const result = stripModuleSyntax(input);
111
- expect(result.code).toContain('var a = function a()');
112
- expect(result.code).toContain('var b = function b()');
113
- expect(result.bareExportNames).toHaveLength(2);
114
- });
115
-
116
- // -- export { local as exported } aliasing ------------------------------
117
-
118
- it('creates alias for export { local as exported }', () => {
119
- const input = 'function foo() {}\nexport { foo as bar };';
120
- const result = stripModuleSyntax(input);
121
- expect(result.code).toContain('var foo = function foo()');
122
- expect(result.code).toContain('var bar = foo;');
123
- expect(result.bareExportNames).toEqual([{ local: 'foo', exported: 'bar' }]);
124
- });
125
-
126
- it('creates multiple aliases when needed', () => {
127
- const input = 'function a() {}\nfunction b() {}\nexport { a as x, b as y };';
128
- const result = stripModuleSyntax(input);
129
- expect(result.code).toContain('var x = a;');
130
- expect(result.code).toContain('var y = b;');
131
- });
132
-
133
- it('does not create alias when local equals exported', () => {
134
- const input = 'function foo() {}\nexport { foo };';
135
- const result = stripModuleSyntax(input);
136
- expect(result.code).not.toContain('var foo = foo;');
137
- });
138
-
139
- it('handles mix of aliased and non-aliased exports', () => {
140
- const input = 'function a() {}\nfunction b() {}\nexport { a, b as c };';
141
- const result = stripModuleSyntax(input);
142
- expect(result.code).toContain('var a = function a()');
143
- expect(result.code).toContain('var b = function b()');
144
- expect(result.code).not.toContain('var a = a;');
145
- expect(result.code).toContain('var c = b;');
146
- });
147
-
148
- // -- Template literal preservation --------------------------------------
149
-
150
- it('preserves export keyword inside template literal', () => {
151
- const input = "const example = `export const x = 1;`;\nexport const y = 2;";
152
- const result = stripModuleSyntax(input);
153
- expect(result.code).toContain('`export const x = 1;`');
154
- expect(result.code).toContain('var y = 2;');
155
- });
156
-
157
- it('preserves export class inside template literal', () => {
158
- const input = "const code = `export class Counter {}`;\nexport class Real {}";
159
- const result = stripModuleSyntax(input);
160
- expect(result.code).toContain('`export class Counter {}`');
161
- expect(result.code).toContain('var Real = class Real');
162
- });
163
-
164
- it('preserves nested template literal content', () => {
165
- const input = "const x = `outer ${`inner export const a = 1;`} end`;\nexport const b = 2;";
166
- const result = stripModuleSyntax(input);
167
- expect(result.code).toContain('inner export const a = 1;');
168
- expect(result.code).toContain('var b = 2;');
169
- });
170
-
171
- it('handles deeply nested template literals', () => {
172
- const input = "const x = `a ${y ? `b ${`c`}` : ''} d`;\nexport const z = 1;";
173
- const result = stripModuleSyntax(input);
174
- expect(result.code).toContain('var z = 1;');
175
- // Nested templates should be preserved without corruption
176
- expect(result.code).toContain('`a ${');
177
- });
178
-
179
- // -- Edge cases ---------------------------------------------------------
180
-
181
- it('returns empty bareExportNames when no bare exports', () => {
182
- const result = stripModuleSyntax('export const x = 1;');
183
- expect(result.bareExportNames).toEqual([]);
184
- });
185
-
186
- it('handles code with no imports or exports', () => {
187
- const input = 'const x = 1;\nfunction foo() {}';
188
- const result = stripModuleSyntax(input);
189
- expect(result.code.trim()).toBe(input);
190
- expect(result.bareExportNames).toEqual([]);
191
- });
192
-
193
- it('preserves indentation', () => {
194
- const result = stripModuleSyntax(' export const x = 1;');
195
- expect(result.code).toContain(' var x = 1;');
196
- });
197
-
198
- it('handles export inside string (not template)', () => {
199
- const input = 'const s = "export const x = 1;";\nexport const y = 2;';
200
- const result = stripModuleSyntax(input);
201
- expect(result.code).toContain('"export const x = 1;"');
202
- expect(result.code).toContain('var y = 2;');
203
- });
204
- });
205
-
206
-
207
- // ---------------------------------------------------------------------------
208
- // CLI - minify ASI preservation
209
- // ---------------------------------------------------------------------------
210
-
211
- describe('CLI - minify ASI preservation', () => {
212
- let minify;
213
-
214
- beforeEach(async () => {
215
- vi.resetModules();
216
- const mod = await import('../cli/utils.js');
217
- minify = mod.minify;
218
- });
219
-
220
- it('preserves newline between } and var', () => {
221
- const input = 'var x = function() {}\nvar y = 1;';
222
- const result = minify(input, '');
223
- expect(result).toContain('}\nvar');
224
- expect(result).not.toContain('}var');
225
- });
226
-
227
- it('preserves newline between } and identifier', () => {
228
- const input = 'function a() {}\nfunction b() {}';
229
- const result = minify(input, '');
230
- expect(result).toContain('}\nfunction');
231
- expect(result).not.toContain('}function');
232
- });
233
-
234
- it('preserves newline between } and const', () => {
235
- const input = 'if (true) {}\nconst x = 1;';
236
- const result = minify(input, '');
237
- expect(result).toContain('}\nconst');
238
- });
239
-
240
- it('preserves newline between } and let', () => {
241
- const input = 'if (true) {}\nlet x = 1;';
242
- const result = minify(input, '');
243
- expect(result).toContain('}\nlet');
244
- });
245
-
246
- it('does not add newline where none existed', () => {
247
- const input = 'if(x){a()} else{b()}';
248
- const result = minify(input, '');
249
- // } followed by else on same line — no newline needed
250
- expect(result).not.toContain('}\n');
251
- });
252
-
253
- it('handles }\\n followed by underscore-prefixed identifier', () => {
254
- const input = 'var x = function() {}\n_init();';
255
- const result = minify(input, '');
256
- expect(result).toContain('}\n_init');
257
- });
258
-
259
- it('handles }\\n followed by $-prefixed identifier', () => {
260
- const input = 'var x = function() {}\n$el.show();';
261
- const result = minify(input, '');
262
- expect(result).toContain('}\n$el');
263
- });
264
- });
265
-
266
-
267
- // ---------------------------------------------------------------------------
268
- // CLI utils - stripComments
269
- // ---------------------------------------------------------------------------
270
-
271
- describe('CLI - stripComments', () => {
272
- let stripComments;
273
-
274
- beforeEach(async () => {
275
- // Dynamic import to handle CommonJS module
276
- const mod = await import('../cli/utils.js');
277
- stripComments = mod.stripComments || mod.default?.stripComments;
278
- });
279
-
280
- it('strips single-line comments', () => {
281
- expect(stripComments('let x = 1; // comment\nlet y = 2;')).toBe('let x = 1; \nlet y = 2;');
282
- });
283
-
284
- it('strips block comments', () => {
285
- expect(stripComments('let x = /* inline comment */ 1;')).toBe('let x = 1;');
286
- });
287
-
288
- it('strips multi-line block comments', () => {
289
- const input = 'a;\n/* line1\n line2\n*/\nb;';
290
- const result = stripComments(input);
291
- expect(result).toBe('a;\n\nb;');
292
- });
293
-
294
- it('preserves single-quoted strings containing //', () => {
295
- expect(stripComments("let x = 'http://example.com';")).toBe("let x = 'http://example.com';");
296
- });
297
-
298
- it('preserves double-quoted strings containing //', () => {
299
- expect(stripComments('let x = "http://example.com";')).toBe('let x = "http://example.com";');
300
- });
301
-
302
- it('preserves template literals containing //', () => {
303
- expect(stripComments('let x = `http://example.com`;')).toBe('let x = `http://example.com`;');
304
- });
305
-
306
- it('preserves template literals with ${} containing //', () => {
307
- const input = 'let x = `${a}//not-a-comment`;';
308
- const result = stripComments(input);
309
- expect(result).toContain('//not-a-comment');
310
- });
311
-
312
- it('strips comment after string', () => {
313
- expect(stripComments('let x = "hello"; // comment')).toBe('let x = "hello"; ');
314
- });
315
-
316
- it('handles escaped quotes in strings', () => {
317
- expect(stripComments("let x = 'it\\'s a test'; // comment")).toBe("let x = 'it\\'s a test'; ");
318
- });
319
-
320
- it('preserves regex containing //', () => {
321
- const input = 'let re = /http:\\/\\//; // comment';
322
- const result = stripComments(input);
323
- expect(result).toContain('/http:\\/\\//');
324
- expect(result).not.toContain('// comment');
325
- });
326
-
327
- it('handles empty string', () => {
328
- expect(stripComments('')).toBe('');
329
- });
330
-
331
- it('handles string with no comments', () => {
332
- expect(stripComments('let x = 1;')).toBe('let x = 1;');
333
- });
334
-
335
- it('handles consecutive comments', () => {
336
- const input = '// comment1\n// comment2\ncode;';
337
- const result = stripComments(input);
338
- expect(result).toBe('\n\ncode;');
339
- });
340
-
341
- it('handles block comment at start of file', () => {
342
- expect(stripComments('/* header */\ncode;')).toBe('\ncode;');
343
- });
344
-
345
- it('handles nested template literal in ${} block', () => {
346
- const input = 'let x = `outer ${`inner`} end`;';
347
- const result = stripComments(input);
348
- expect(result).toBe(input);
349
- });
350
- });
351
-
352
-
353
- // ---------------------------------------------------------------------------
354
- // CLI utils - minify
355
- // ---------------------------------------------------------------------------
356
-
357
- describe('CLI - minify', () => {
358
- let minify;
359
-
360
- beforeEach(async () => {
361
- const mod = await import('../cli/utils.js');
362
- minify = mod.minify || mod.default?.minify;
363
- });
364
-
365
- it('collapses whitespace', () => {
366
- const result = minify('let x = 1;', '/* banner */');
367
- expect(result).toContain('let x=1;');
368
- });
369
-
370
- it('preserves banner', () => {
371
- const result = minify('let x = 1;', '/* v1.0 */');
372
- expect(result.startsWith('/* v1.0 */')).toBe(true);
373
- });
374
-
375
- it('strips comments during minification', () => {
376
- const result = minify('let x = 1; // comment\nlet y = 2;', '');
377
- expect(result).not.toContain('// comment');
378
- });
379
-
380
- it('preserves string content', () => {
381
- const result = minify('let x = "hello world";', '');
382
- expect(result).toContain('"hello world"');
383
- });
384
-
385
- it('preserves template literal content', () => {
386
- const result = minify('let x = `hello world`;', '');
387
- expect(result).toContain('`hello world`');
388
- });
389
-
390
- it('does not merge ++ into + +', () => {
391
- const result = minify('a + +b', '');
392
- expect(result).toContain('+ +');
393
- });
394
-
395
- it('does not merge -- into - -', () => {
396
- const result = minify('a - -b', '');
397
- expect(result).toContain('- -');
398
- });
399
-
400
- it('preserves space between keywords and identifiers', () => {
401
- const result = minify('return value;', '');
402
- expect(result).toContain('return value');
403
- });
404
-
405
- it('removes block comments', () => {
406
- const result = minify('a /* comment */ = 1;', '');
407
- expect(result).not.toContain('comment');
408
- });
409
-
410
- it('handles empty input', () => {
411
- const result = minify('', '');
412
- expect(result).toBe('\n');
413
- });
414
- });
415
-
416
-
417
- // ---------------------------------------------------------------------------
418
- // CLI utils - sizeKB
419
- // ---------------------------------------------------------------------------
420
-
421
- describe('CLI - sizeKB', () => {
422
- let sizeKB;
423
-
424
- beforeEach(async () => {
425
- const mod = await import('../cli/utils.js');
426
- sizeKB = mod.sizeKB || mod.default?.sizeKB;
427
- });
428
-
429
- it('formats 1024 bytes as 1.0', () => {
430
- expect(sizeKB({ length: 1024 })).toBe('1.0');
431
- });
432
-
433
- it('formats 512 bytes as 0.5', () => {
434
- expect(sizeKB({ length: 512 })).toBe('0.5');
435
- });
436
-
437
- it('formats 0 bytes as 0.0', () => {
438
- expect(sizeKB({ length: 0 })).toBe('0.0');
439
- });
440
-
441
- it('formats 2560 bytes as 2.5', () => {
442
- expect(sizeKB({ length: 2560 })).toBe('2.5');
443
- });
444
-
445
- it('formats large size correctly', () => {
446
- expect(sizeKB({ length: 102400 })).toBe('100.0');
447
- });
448
- });
449
-
450
-
451
- // ---------------------------------------------------------------------------
452
- // CLI args - flag and option
453
- // ---------------------------------------------------------------------------
454
-
455
- describe('CLI - args module', () => {
456
- let originalArgv;
457
-
458
- beforeEach(() => {
459
- originalArgv = [...process.argv];
460
- });
461
-
462
- afterEach(() => {
463
- process.argv = originalArgv;
464
- // Clear the require cache so the module re-reads argv
465
- vi.resetModules();
466
- });
467
-
468
- it('flag returns true when --verbose is present', async () => {
469
- process.argv = ['node', 'script', '--verbose'];
470
- const mod = await import('../cli/args.js');
471
- const { flag } = mod;
472
- expect(flag('verbose')).toBe(true);
473
- });
474
-
475
- it('flag returns false when flag is absent', async () => {
476
- process.argv = ['node', 'script'];
477
- const mod = await import('../cli/args.js');
478
- const { flag } = mod;
479
- expect(flag('verbose')).toBe(false);
480
- });
481
-
482
- it('flag detects short flag -v', async () => {
483
- process.argv = ['node', 'script', '-v'];
484
- const mod = await import('../cli/args.js');
485
- const { flag } = mod;
486
- expect(flag('verbose', 'v')).toBe(true);
487
- });
488
-
489
- it('option reads value after --port', async () => {
490
- process.argv = ['node', 'script', '--port', '8080'];
491
- const mod = await import('../cli/args.js');
492
- const { option } = mod;
493
- expect(option('port')).toBe('8080');
494
- });
495
-
496
- it('option reads value after short -p', async () => {
497
- process.argv = ['node', 'script', '-p', '3000'];
498
- const mod = await import('../cli/args.js');
499
- const { option } = mod;
500
- expect(option('port', 'p')).toBe('3000');
501
- });
502
-
503
- it('option returns fallback when missing', async () => {
504
- process.argv = ['node', 'script'];
505
- const mod = await import('../cli/args.js');
506
- const { option } = mod;
507
- expect(option('port', 'p', '3100')).toBe('3100');
508
- });
509
-
510
- it('option returns fallback when flag has no value', async () => {
511
- process.argv = ['node', 'script', '--port'];
512
- const mod = await import('../cli/args.js');
513
- const { option } = mod;
514
- expect(option('port', 'p', '3100')).toBe('3100');
515
- });
516
- });
517
-
518
-
519
- // ===========================================================================
520
- // showHelp
521
- // ===========================================================================
522
-
523
- describe('CLI - showHelp', () => {
524
- it('outputs help text to console', async () => {
525
- const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
526
- const showHelp = (await import('../cli/help.js')).default;
527
- showHelp();
528
- expect(spy).toHaveBeenCalled();
529
- const text = spy.mock.calls.map(c => c.join(' ')).join('\n');
530
- expect(text).toContain('create');
531
- expect(text).toContain('dev');
532
- expect(text).toContain('bundle');
533
- expect(text).toContain('build');
534
- spy.mockRestore();
535
- });
536
-
537
- it('help text mentions port flag', async () => {
538
- const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
539
- const showHelp = (await import('../cli/help.js')).default;
540
- showHelp();
541
- const text = spy.mock.calls.map(c => c.join(' ')).join('\n');
542
- expect(text).toContain('--port');
543
- spy.mockRestore();
544
- });
545
- });
546
-
547
-
548
- // ===========================================================================
549
- // stripComments - additional edge cases
550
- // ===========================================================================
551
-
552
- describe('CLI - stripComments extra', () => {
553
- let stripComments;
554
- beforeEach(async () => {
555
- const mod = await import('../cli/utils.js');
556
- stripComments = mod.stripComments;
557
- });
558
-
559
- it('handles consecutive single-line comments', () => {
560
- const input = '// one\n// two\nconst x = 1;';
561
- const result = stripComments(input);
562
- expect(result).not.toContain('one');
563
- expect(result).not.toContain('two');
564
- expect(result).toContain('const x = 1');
565
- });
566
-
567
- it('preserves code after block comment', () => {
568
- const input = 'a /* comment */ + b';
569
- const result = stripComments(input);
570
- expect(result).toContain('a');
571
- expect(result).toContain('+ b');
572
- expect(result).not.toContain('comment');
573
- });
574
-
575
- it('handles multiline block comment', () => {
576
- const input = '/*\n line1\n line2\n*/\ncode();';
577
- const result = stripComments(input);
578
- expect(result).toContain('code()');
579
- expect(result).not.toContain('line1');
580
- });
581
-
582
- it('does not strip // inside template expression', () => {
583
- const input = '`${a // b}`';
584
- const result = stripComments(input);
585
- // The template should be preserved as-is
586
- expect(result).toContain('`');
587
- });
588
-
589
- it('handles code with no comments', () => {
590
- const input = 'const x = 1;\nconst y = 2;';
591
- expect(stripComments(input)).toBe(input);
592
- });
593
- });
594
-
595
-
596
- // ===========================================================================
597
- // minify - additional edge cases
598
- // ===========================================================================
599
-
600
- describe('CLI - minify extra', () => {
601
- let minify;
602
- beforeEach(async () => {
603
- const mod = await import('../cli/utils.js');
604
- minify = mod.minify;
605
- });
606
-
607
- it('preserves string content with spaces', () => {
608
- const result = minify('const msg = "hello world";', '');
609
- expect(result).toContain('"hello world"');
610
- });
611
-
612
- it('empty input returns banner only', () => {
613
- const result = minify('', '/* banner */');
614
- expect(result.trim()).toBe('/* banner */');
615
- });
616
-
617
- it('collapses multiple newlines', () => {
618
- const result = minify('a\n\n\n\nb', '');
619
- // Should not have multiple consecutive newlines in output
620
- expect(result).not.toMatch(/\n\n\n/);
621
- });
622
-
623
- it('handles template literals with spaces', () => {
624
- const result = minify('const x = `hello world`;', '');
625
- expect(result).toContain('`hello world`');
626
- });
627
- });
628
-
629
-
630
- // ===========================================================================
631
- // sizeKB - edge cases
632
- // ===========================================================================
633
-
634
- describe('CLI - sizeKB extra', () => {
635
- let sizeKB;
636
- beforeEach(async () => {
637
- const mod = await import('../cli/utils.js');
638
- sizeKB = mod.sizeKB;
639
- });
640
-
641
- it('zero-length buffer', () => {
642
- expect(sizeKB({ length: 0 })).toBe('0.0');
643
- });
644
-
645
- it('exactly 1KB', () => {
646
- expect(sizeKB({ length: 1024 })).toBe('1.0');
647
- });
648
-
649
- it('small buffer', () => {
650
- expect(sizeKB({ length: 100 })).toBe('0.1');
651
- });
652
- });
653
-
654
-
655
- // ===========================================================================
656
- // copyDirSync
657
- // ===========================================================================
658
-
659
- describe('CLI - copyDirSync', () => {
660
- let copyDirSync;
661
- const fs = require('fs');
662
- const path = require('path');
663
- const os = require('os');
664
-
665
- beforeEach(async () => {
666
- const mod = await import('../cli/utils.js');
667
- copyDirSync = mod.copyDirSync;
668
- });
669
-
670
- it('copies directory recursively', () => {
671
- const src = path.join(os.tmpdir(), 'zq-test-src-' + Date.now());
672
- const dest = path.join(os.tmpdir(), 'zq-test-dest-' + Date.now());
673
-
674
- // Create source structure
675
- fs.mkdirSync(path.join(src, 'sub'), { recursive: true });
676
- fs.writeFileSync(path.join(src, 'a.txt'), 'hello');
677
- fs.writeFileSync(path.join(src, 'sub', 'b.txt'), 'world');
678
-
679
- copyDirSync(src, dest);
680
-
681
- expect(fs.existsSync(path.join(dest, 'a.txt'))).toBe(true);
682
- expect(fs.readFileSync(path.join(dest, 'a.txt'), 'utf8')).toBe('hello');
683
- expect(fs.existsSync(path.join(dest, 'sub', 'b.txt'))).toBe(true);
684
- expect(fs.readFileSync(path.join(dest, 'sub', 'b.txt'), 'utf8')).toBe('world');
685
-
686
- // Cleanup
687
- fs.rmSync(src, { recursive: true, force: true });
688
- fs.rmSync(dest, { recursive: true, force: true });
689
- });
690
- });
691
-
692
-
693
- // ===========================================================================
694
- // flag/option - additional cases
695
- // ===========================================================================
696
-
697
- describe('CLI - flag/option extra', () => {
698
- it('flag returns false when absent', async () => {
699
- process.argv = ['node', 'script'];
700
- vi.resetModules();
701
- const { flag } = await import('../cli/args.js');
702
- expect(flag('missing', 'm')).toBe(false);
703
- });
704
-
705
- it('option with short form', async () => {
706
- process.argv = ['node', 'script', '-o', '/dist'];
707
- vi.resetModules();
708
- const { option } = await import('../cli/args.js');
709
- expect(option('output', 'o', 'default')).toBe('/dist');
710
- });
711
-
712
- it('multiple flags', async () => {
713
- process.argv = ['node', 'script', '--verbose', '--watch'];
714
- vi.resetModules();
715
- const { flag } = await import('../cli/args.js');
716
- expect(flag('verbose', 'v')).toBe(true);
717
- expect(flag('watch', 'w')).toBe(true);
718
- });
719
- });
720
-
721
-
722
- // ===========================================================================
723
- // createProject - scaffold command
724
- // ===========================================================================
725
-
726
- describe('CLI - createProject', () => {
727
- const fs = require('fs');
728
- const path = require('path');
729
- const os = require('os');
730
-
731
- let createProject;
732
-
733
- beforeEach(async () => {
734
- vi.resetModules();
735
- const mod = await import('../cli/commands/create.js');
736
- createProject = mod.default || mod;
737
- });
738
-
739
- function tmpDir() {
740
- const dir = path.join(os.tmpdir(), 'zq-create-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8));
741
- fs.mkdirSync(dir, { recursive: true });
742
- return dir;
743
- }
744
-
745
- function cleanup(dir) {
746
- if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
747
- }
748
-
749
- // -- scaffold variant directories exist --
750
-
751
- it('default scaffold directory exists', () => {
752
- const dir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default');
753
- expect(fs.existsSync(dir)).toBe(true);
754
- });
755
-
756
- it('minimal scaffold directory exists', () => {
757
- const dir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal');
758
- expect(fs.existsSync(dir)).toBe(true);
759
- });
760
-
761
- // -- default scaffold contains expected files --
762
-
763
- it('default scaffold has index.html', () => {
764
- const f = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default', 'index.html');
765
- expect(fs.existsSync(f)).toBe(true);
766
- });
767
-
768
- it('default scaffold has app/app.js', () => {
769
- const f = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default', 'app', 'app.js');
770
- expect(fs.existsSync(f)).toBe(true);
771
- });
772
-
773
- it('default scaffold has app/routes.js', () => {
774
- const f = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default', 'app', 'routes.js');
775
- expect(fs.existsSync(f)).toBe(true);
776
- });
777
-
778
- it('default scaffold has app/store.js', () => {
779
- const f = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default', 'app', 'store.js');
780
- expect(fs.existsSync(f)).toBe(true);
781
- });
782
-
783
- // -- minimal scaffold contains expected files --
784
-
785
- it('minimal scaffold has index.html', () => {
786
- const f = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal', 'index.html');
787
- expect(fs.existsSync(f)).toBe(true);
788
- });
789
-
790
- it('minimal scaffold has app/app.js', () => {
791
- const f = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal', 'app', 'app.js');
792
- expect(fs.existsSync(f)).toBe(true);
793
- });
794
-
795
- it('minimal scaffold has 4 components', () => {
796
- const dir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal', 'app', 'components');
797
- const files = fs.readdirSync(dir).sort();
798
- expect(files).toEqual(['about.js', 'counter.js', 'home.js', 'not-found.js']);
799
- });
800
-
801
- // -- minimal scaffold is a subset of default --
802
-
803
- it('minimal scaffold has fewer files than default', () => {
804
- function countFiles(dir) {
805
- let count = 0;
806
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
807
- if (entry.isDirectory()) count += countFiles(path.join(dir, entry.name));
808
- else count++;
809
- }
810
- return count;
811
- }
812
- const defDir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default');
813
- const minDir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal');
814
- expect(countFiles(minDir)).toBeLessThan(countFiles(defDir));
815
- });
816
-
817
- // -- {{NAME}} replacement --
818
-
819
- it('scaffold templates contain {{NAME}} placeholder', () => {
820
- const html = fs.readFileSync(
821
- path.resolve(__dirname, '..', 'cli', 'scaffold', 'default', 'index.html'), 'utf-8'
822
- );
823
- expect(html).toContain('{{NAME}}');
824
- });
825
-
826
- it('minimal scaffold templates contain {{NAME}} placeholder', () => {
827
- const html = fs.readFileSync(
828
- path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal', 'index.html'), 'utf-8'
829
- );
830
- expect(html).toContain('{{NAME}}');
831
- });
832
-
833
- // -- walkDir functionality (tested via module internals) --
834
-
835
- it('scaffolds default project into a target directory', () => {
836
- const target = tmpDir();
837
- const projDir = path.join(target, 'test-app');
838
-
839
- // Simulate: process.argv for default (no --minimal)
840
- process.argv = ['node', 'zquery', 'create', 'test-app'];
841
- vi.resetModules();
842
-
843
- // We can't easily call createProject because it uses process.exit.
844
- // Instead, test the walkDir + copy logic directly.
845
- const scaffoldDir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default');
846
-
847
- function walkDir(dir, prefix = '') {
848
- const entries = [];
849
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
850
- const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
851
- if (entry.isDirectory()) entries.push(...walkDir(path.join(dir, entry.name), rel));
852
- else entries.push(rel);
853
- }
854
- return entries;
855
- }
856
-
857
- const files = walkDir(scaffoldDir);
858
- expect(files.length).toBeGreaterThan(5);
859
- expect(files).toContain('index.html');
860
- expect(files).toContain('global.css');
861
- expect(files).toContain('app/app.js');
862
- expect(files).toContain('app/routes.js');
863
- expect(files).toContain('app/store.js');
864
-
865
- cleanup(target);
866
- });
867
-
868
- it('walkDir lists minimal scaffold files correctly', () => {
869
- const scaffoldDir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal');
870
-
871
- function walkDir(dir, prefix = '') {
872
- const entries = [];
873
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
874
- const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
875
- if (entry.isDirectory()) entries.push(...walkDir(path.join(dir, entry.name), rel));
876
- else entries.push(rel);
877
- }
878
- return entries;
879
- }
880
-
881
- const files = walkDir(scaffoldDir);
882
- expect(files).toContain('index.html');
883
- expect(files).toContain('global.css');
884
- expect(files).toContain('app/app.js');
885
- expect(files).toContain('app/routes.js');
886
- expect(files).toContain('app/store.js');
887
- expect(files).toContain('app/components/home.js');
888
- expect(files).toContain('app/components/counter.js');
889
- expect(files).toContain('app/components/about.js');
890
- expect(files).toContain('app/components/not-found.js');
891
- expect(files).toContain('assets/.gitkeep');
892
- });
893
-
894
- // -- {{NAME}} replacement in copied files --
895
-
896
- it('replaces {{NAME}} in copied scaffold files', () => {
897
- const target = tmpDir();
898
- const scaffoldDir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal');
899
- const projectName = 'my-cool-app';
900
-
901
- function walkDir(dir, prefix = '') {
902
- const entries = [];
903
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
904
- const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
905
- if (entry.isDirectory()) entries.push(...walkDir(path.join(dir, entry.name), rel));
906
- else entries.push(rel);
907
- }
908
- return entries;
909
- }
910
-
911
- const files = walkDir(scaffoldDir);
912
-
913
- for (const rel of files) {
914
- let content = fs.readFileSync(path.join(scaffoldDir, rel), 'utf-8');
915
- content = content.replace(/\{\{NAME\}\}/g, projectName);
916
- const dest = path.join(target, rel);
917
- fs.mkdirSync(path.dirname(dest), { recursive: true });
918
- fs.writeFileSync(dest, content, 'utf-8');
919
- }
920
-
921
- const html = fs.readFileSync(path.join(target, 'index.html'), 'utf-8');
922
- expect(html).toContain(projectName);
923
- expect(html).not.toContain('{{NAME}}');
924
-
925
- const appJs = fs.readFileSync(path.join(target, 'app', 'app.js'), 'utf-8');
926
- expect(appJs).toContain(projectName);
927
- expect(appJs).not.toContain('{{NAME}}');
928
-
929
- cleanup(target);
930
- });
931
-
932
- // -- conflict detection --
933
-
934
- it('conflicts array detects existing files', () => {
935
- const target = tmpDir();
936
- fs.writeFileSync(path.join(target, 'index.html'), 'existing');
937
- fs.mkdirSync(path.join(target, 'app'), { recursive: true });
938
-
939
- const conflicts = ['index.html', 'global.css', 'app', 'assets'].filter(f =>
940
- fs.existsSync(path.join(target, f))
941
- );
942
-
943
- expect(conflicts).toContain('index.html');
944
- expect(conflicts).toContain('app');
945
- expect(conflicts).not.toContain('global.css');
946
- expect(conflicts).not.toContain('assets');
947
-
948
- cleanup(target);
949
- });
950
-
951
- it('no conflicts in empty directory', () => {
952
- const target = tmpDir();
953
-
954
- const conflicts = ['index.html', 'global.css', 'app', 'assets'].filter(f =>
955
- fs.existsSync(path.join(target, f))
956
- );
957
-
958
- expect(conflicts).toHaveLength(0);
959
-
960
- cleanup(target);
961
- });
962
-
963
- // -- flag parsing for --minimal --
964
-
965
- it('--minimal flag resolves to minimal variant', async () => {
966
- process.argv = ['node', 'zquery', 'create', '--minimal', 'my-app'];
967
- vi.resetModules();
968
- const { flag } = await import('../cli/args.js');
969
- expect(flag('minimal', 'm')).toBe(true);
970
- });
971
-
972
- it('-m short flag resolves to minimal variant', async () => {
973
- process.argv = ['node', 'zquery', 'create', '-m', 'my-app'];
974
- vi.resetModules();
975
- const { flag } = await import('../cli/args.js');
976
- expect(flag('minimal', 'm')).toBe(true);
977
- });
978
-
979
- it('no flag defaults to default variant', async () => {
980
- process.argv = ['node', 'zquery', 'create', 'my-app'];
981
- vi.resetModules();
982
- const { flag } = await import('../cli/args.js');
983
- expect(flag('minimal', 'm')).toBe(false);
984
- });
985
-
986
- // -- dirArg parsing skips flags --
987
-
988
- it('dirArg parsing skips flag args', () => {
989
- const args = ['create', '--minimal', 'my-app'];
990
- const dirArg = args.slice(1).find(a => !a.startsWith('-'));
991
- expect(dirArg).toBe('my-app');
992
- });
993
-
994
- it('dirArg parsing returns first positional', () => {
995
- const args = ['create', 'my-app'];
996
- const dirArg = args.slice(1).find(a => !a.startsWith('-'));
997
- expect(dirArg).toBe('my-app');
998
- });
999
-
1000
- it('dirArg is undefined when no positional given', () => {
1001
- const args = ['create', '--minimal'];
1002
- const dirArg = args.slice(1).find(a => !a.startsWith('-'));
1003
- expect(dirArg).toBeUndefined();
1004
- });
1005
-
1006
- it('dirArg with flag after dir name', () => {
1007
- const args = ['create', 'my-app', '--minimal'];
1008
- const dirArg = args.slice(1).find(a => !a.startsWith('-'));
1009
- expect(dirArg).toBe('my-app');
1010
- });
1011
-
1012
- // -- help text mentions --minimal --
1013
-
1014
- it('help text includes --minimal flag', async () => {
1015
- vi.resetModules();
1016
- const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
1017
- const showHelp = (await import('../cli/help.js')).default;
1018
- showHelp();
1019
- const text = spy.mock.calls.map(c => c.join(' ')).join('\n');
1020
- expect(text).toContain('--minimal');
1021
- spy.mockRestore();
1022
- });
1023
- });
1024
-
1025
-
1026
- // ===========================================================================
1027
- // minifyTemplateLiterals - regex literal awareness
1028
- // ===========================================================================
1029
-
1030
- describe('CLI - minifyTemplateLiterals regex handling', () => {
1031
- let minifyTemplateLiterals;
1032
-
1033
- beforeEach(async () => {
1034
- const mod = await import('../cli/commands/bundle.js');
1035
- minifyTemplateLiterals = mod.minifyTemplateLiterals;
1036
- });
1037
-
1038
- it('preserves code after regex containing backtick characters', () => {
1039
- // Simulates: h = h.replace(/`([^`]+)`/g, '<code>$1</code>');
1040
- // The backtick inside the regex must not be mistaken for a template literal.
1041
- const input = "h = h.replace(/\x60([^\x60]+)\x60/g, '<code>$1</code>');\nreturn h;";
1042
- const result = minifyTemplateLiterals(input);
1043
- expect(result).toContain('return h');
1044
- expect(result).toContain('<code>$1</code>');
1045
- });
1046
-
1047
- it('does not eat closing braces after backtick-in-regex', () => {
1048
- const input = [
1049
- "var fmt = {",
1050
- " md(raw) {",
1051
- " let h = raw;",
1052
- " h = h.replace(/\x60([^\x60]+)\x60/g, '<code>$1</code>');",
1053
- " h = h.replace(/^[\\-\\*] (.+)$/gm, '<li>$1</li>');",
1054
- " return h;",
1055
- " }",
1056
- "};",
1057
- "var canvas = document.getElementById('bg-canvas');",
1058
- ].join('\n');
1059
- const result = minifyTemplateLiterals(input);
1060
- expect(result).toContain('return h;');
1061
- expect(result).toContain('};');
1062
- expect(result).toContain("getElementById('bg-canvas')");
1063
- });
1064
-
1065
- it('handles regex with single backtick', () => {
1066
- const input = "x.replace(/\x60/g, \"'\");\nvar y = 1;";
1067
- const result = minifyTemplateLiterals(input);
1068
- expect(result).toContain('var y = 1');
1069
- });
1070
-
1071
- it('handles regex backtick in character class', () => {
1072
- const input = "x.match(/[\x60'\"]/g);\nvar z = 2;";
1073
- const result = minifyTemplateLiterals(input);
1074
- expect(result).toContain('var z = 2');
1075
- });
1076
-
1077
- it('does not confuse division operator with regex', () => {
1078
- // After a number or identifier, / is division, not regex start
1079
- const input = "var x = a / b;\nvar t = \x60hello\x60;";
1080
- const result = minifyTemplateLiterals(input);
1081
- expect(result).toContain('a / b');
1082
- expect(result).toContain('\x60hello\x60');
1083
- });
1084
-
1085
- it('still minifies real template literals correctly', () => {
1086
- const input = "var html = \x60<div> <span> text </span> </div>\x60;";
1087
- const result = minifyTemplateLiterals(input);
1088
- // Template whitespace should be collapsed
1089
- expect(result).not.toContain(' <span> ');
1090
- });
1091
-
1092
- it('handles regex after return keyword', () => {
1093
- const input = "return /\x60test\x60/g;\nvar after = 1;";
1094
- const result = minifyTemplateLiterals(input);
1095
- expect(result).toContain('var after = 1');
1096
- });
1097
-
1098
- it('handles regex after assignment operator', () => {
1099
- const input = "var re = /\x60([^\x60]+)\x60/gi;\nvar next = true;";
1100
- const result = minifyTemplateLiterals(input);
1101
- expect(result).toContain('var next = true');
1102
- });
1103
- });
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // CLI bundle - stripModuleSyntax
6
+ // ---------------------------------------------------------------------------
7
+
8
+ describe('CLI - stripModuleSyntax', () => {
9
+ let stripModuleSyntax;
10
+
11
+ beforeEach(async () => {
12
+ vi.resetModules();
13
+ const mod = await import('../cli/commands/bundle.js');
14
+ stripModuleSyntax = mod.stripModuleSyntax;
15
+ });
16
+
17
+ // -- Import stripping ---------------------------------------------------
18
+
19
+ it('strips named import from module', () => {
20
+ const result = stripModuleSyntax("import { foo } from './mod.js';\nconst x = 1;");
21
+ expect(result.code.trim()).toBe('const x = 1;');
22
+ });
23
+
24
+ it('strips default import from module', () => {
25
+ const result = stripModuleSyntax("import foo from './mod.js';\nconst x = 1;");
26
+ expect(result.code.trim()).toBe('const x = 1;');
27
+ });
28
+
29
+ it('strips side-effect import', () => {
30
+ const result = stripModuleSyntax("import './mod.js';\nconst x = 1;");
31
+ expect(result.code.trim()).toBe('const x = 1;');
32
+ });
33
+
34
+ it('strips multi-line import', () => {
35
+ const input = "import {\n a,\n b,\n c\n} from './mod.js';\nconst x = 1;";
36
+ const result = stripModuleSyntax(input);
37
+ expect(result.code.trim()).toBe('const x = 1;');
38
+ });
39
+
40
+ // -- export default -----------------------------------------------------
41
+
42
+ it('strips export default keyword', () => {
43
+ const result = stripModuleSyntax('export default function foo() {}');
44
+ expect(result.code.trim()).toBe('function foo() {}');
45
+ });
46
+
47
+ // -- export const/let/var → var -----------------------------------------
48
+
49
+ it('converts export const to var', () => {
50
+ const result = stripModuleSyntax('export const x = 1;');
51
+ expect(result.code.trim()).toBe('var x = 1;');
52
+ });
53
+
54
+ it('converts export let to var', () => {
55
+ const result = stripModuleSyntax('export let y = 2;');
56
+ expect(result.code.trim()).toBe('var y = 2;');
57
+ });
58
+
59
+ it('converts export var (keeps var)', () => {
60
+ const result = stripModuleSyntax('export var z = 3;');
61
+ expect(result.code.trim()).toBe('var z = 3;');
62
+ });
63
+
64
+ // -- export function → var assignment -----------------------------------
65
+
66
+ it('converts export function to var assignment', () => {
67
+ const result = stripModuleSyntax('export function greet(name) { return name; }');
68
+ expect(result.code.trim()).toBe('var greet = function greet(name) { return name; }');
69
+ });
70
+
71
+ it('converts export async function to var assignment', () => {
72
+ const result = stripModuleSyntax('export async function fetchData() {}');
73
+ expect(result.code.trim()).toBe('var fetchData = async function fetchData() {}');
74
+ });
75
+
76
+ // -- export class → var assignment --------------------------------------
77
+
78
+ it('converts export class to var assignment', () => {
79
+ const result = stripModuleSyntax('export class MyComponent {}');
80
+ expect(result.code.trim()).toBe('var MyComponent = class MyComponent {}');
81
+ });
82
+
83
+ // -- bare export block: export { a, b } ---------------------------------
84
+
85
+ it('converts bare exported function declarations to var', () => {
86
+ const input = 'function buildIndex() {}\nexport { buildIndex };';
87
+ const result = stripModuleSyntax(input);
88
+ expect(result.code).toContain('var buildIndex = function buildIndex()');
89
+ expect(result.code).not.toContain('export');
90
+ expect(result.bareExportNames).toEqual([{ local: 'buildIndex', exported: 'buildIndex' }]);
91
+ });
92
+
93
+ it('converts bare exported async function to var', () => {
94
+ const input = 'async function load() {}\nexport { load };';
95
+ const result = stripModuleSyntax(input);
96
+ expect(result.code).toContain('var load = async function load()');
97
+ });
98
+
99
+ it('converts bare exported const/let declarations to var', () => {
100
+ const input = 'const MAX = 10;\nlet count = 0;\nexport { MAX, count };';
101
+ const result = stripModuleSyntax(input);
102
+ expect(result.code).toContain('var MAX');
103
+ expect(result.code).toContain('var count');
104
+ expect(result.code).not.toContain('const MAX');
105
+ expect(result.code).not.toContain('let count');
106
+ });
107
+
108
+ it('handles multi-line bare export block', () => {
109
+ const input = 'function a() {}\nfunction b() {}\nexport {\n a,\n b\n};';
110
+ const result = stripModuleSyntax(input);
111
+ expect(result.code).toContain('var a = function a()');
112
+ expect(result.code).toContain('var b = function b()');
113
+ expect(result.bareExportNames).toHaveLength(2);
114
+ });
115
+
116
+ // -- export { local as exported } aliasing ------------------------------
117
+
118
+ it('creates alias for export { local as exported }', () => {
119
+ const input = 'function foo() {}\nexport { foo as bar };';
120
+ const result = stripModuleSyntax(input);
121
+ expect(result.code).toContain('var foo = function foo()');
122
+ expect(result.code).toContain('var bar = foo;');
123
+ expect(result.bareExportNames).toEqual([{ local: 'foo', exported: 'bar' }]);
124
+ });
125
+
126
+ it('creates multiple aliases when needed', () => {
127
+ const input = 'function a() {}\nfunction b() {}\nexport { a as x, b as y };';
128
+ const result = stripModuleSyntax(input);
129
+ expect(result.code).toContain('var x = a;');
130
+ expect(result.code).toContain('var y = b;');
131
+ });
132
+
133
+ it('does not create alias when local equals exported', () => {
134
+ const input = 'function foo() {}\nexport { foo };';
135
+ const result = stripModuleSyntax(input);
136
+ expect(result.code).not.toContain('var foo = foo;');
137
+ });
138
+
139
+ it('handles mix of aliased and non-aliased exports', () => {
140
+ const input = 'function a() {}\nfunction b() {}\nexport { a, b as c };';
141
+ const result = stripModuleSyntax(input);
142
+ expect(result.code).toContain('var a = function a()');
143
+ expect(result.code).toContain('var b = function b()');
144
+ expect(result.code).not.toContain('var a = a;');
145
+ expect(result.code).toContain('var c = b;');
146
+ });
147
+
148
+ // -- Template literal preservation --------------------------------------
149
+
150
+ it('preserves export keyword inside template literal', () => {
151
+ const input = "const example = `export const x = 1;`;\nexport const y = 2;";
152
+ const result = stripModuleSyntax(input);
153
+ expect(result.code).toContain('`export const x = 1;`');
154
+ expect(result.code).toContain('var y = 2;');
155
+ });
156
+
157
+ it('preserves export class inside template literal', () => {
158
+ const input = "const code = `export class Counter {}`;\nexport class Real {}";
159
+ const result = stripModuleSyntax(input);
160
+ expect(result.code).toContain('`export class Counter {}`');
161
+ expect(result.code).toContain('var Real = class Real');
162
+ });
163
+
164
+ it('preserves nested template literal content', () => {
165
+ const input = "const x = `outer ${`inner export const a = 1;`} end`;\nexport const b = 2;";
166
+ const result = stripModuleSyntax(input);
167
+ expect(result.code).toContain('inner export const a = 1;');
168
+ expect(result.code).toContain('var b = 2;');
169
+ });
170
+
171
+ it('handles deeply nested template literals', () => {
172
+ const input = "const x = `a ${y ? `b ${`c`}` : ''} d`;\nexport const z = 1;";
173
+ const result = stripModuleSyntax(input);
174
+ expect(result.code).toContain('var z = 1;');
175
+ // Nested templates should be preserved without corruption
176
+ expect(result.code).toContain('`a ${');
177
+ });
178
+
179
+ // -- Edge cases ---------------------------------------------------------
180
+
181
+ it('returns empty bareExportNames when no bare exports', () => {
182
+ const result = stripModuleSyntax('export const x = 1;');
183
+ expect(result.bareExportNames).toEqual([]);
184
+ });
185
+
186
+ it('handles code with no imports or exports', () => {
187
+ const input = 'const x = 1;\nfunction foo() {}';
188
+ const result = stripModuleSyntax(input);
189
+ expect(result.code.trim()).toBe(input);
190
+ expect(result.bareExportNames).toEqual([]);
191
+ });
192
+
193
+ it('preserves indentation', () => {
194
+ const result = stripModuleSyntax(' export const x = 1;');
195
+ expect(result.code).toContain(' var x = 1;');
196
+ });
197
+
198
+ it('handles export inside string (not template)', () => {
199
+ const input = 'const s = "export const x = 1;";\nexport const y = 2;';
200
+ const result = stripModuleSyntax(input);
201
+ expect(result.code).toContain('"export const x = 1;"');
202
+ expect(result.code).toContain('var y = 2;');
203
+ });
204
+ });
205
+
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // CLI - minify ASI preservation
209
+ // ---------------------------------------------------------------------------
210
+
211
+ describe('CLI - minify ASI preservation', () => {
212
+ let minify;
213
+
214
+ beforeEach(async () => {
215
+ vi.resetModules();
216
+ const mod = await import('../cli/utils.js');
217
+ minify = mod.minify;
218
+ });
219
+
220
+ it('preserves newline between } and var', () => {
221
+ const input = 'var x = function() {}\nvar y = 1;';
222
+ const result = minify(input, '');
223
+ expect(result).toContain('}\nvar');
224
+ expect(result).not.toContain('}var');
225
+ });
226
+
227
+ it('preserves newline between } and identifier', () => {
228
+ const input = 'function a() {}\nfunction b() {}';
229
+ const result = minify(input, '');
230
+ expect(result).toContain('}\nfunction');
231
+ expect(result).not.toContain('}function');
232
+ });
233
+
234
+ it('preserves newline between } and const', () => {
235
+ const input = 'if (true) {}\nconst x = 1;';
236
+ const result = minify(input, '');
237
+ expect(result).toContain('}\nconst');
238
+ });
239
+
240
+ it('preserves newline between } and let', () => {
241
+ const input = 'if (true) {}\nlet x = 1;';
242
+ const result = minify(input, '');
243
+ expect(result).toContain('}\nlet');
244
+ });
245
+
246
+ it('does not add newline where none existed', () => {
247
+ const input = 'if(x){a()} else{b()}';
248
+ const result = minify(input, '');
249
+ // } followed by else on same line — no newline needed
250
+ expect(result).not.toContain('}\n');
251
+ });
252
+
253
+ it('handles }\\n followed by underscore-prefixed identifier', () => {
254
+ const input = 'var x = function() {}\n_init();';
255
+ const result = minify(input, '');
256
+ expect(result).toContain('}\n_init');
257
+ });
258
+
259
+ it('handles }\\n followed by $-prefixed identifier', () => {
260
+ const input = 'var x = function() {}\n$el.show();';
261
+ const result = minify(input, '');
262
+ expect(result).toContain('}\n$el');
263
+ });
264
+ });
265
+
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // CLI utils - stripComments
269
+ // ---------------------------------------------------------------------------
270
+
271
+ describe('CLI - stripComments', () => {
272
+ let stripComments;
273
+
274
+ beforeEach(async () => {
275
+ // Dynamic import to handle CommonJS module
276
+ const mod = await import('../cli/utils.js');
277
+ stripComments = mod.stripComments || mod.default?.stripComments;
278
+ });
279
+
280
+ it('strips single-line comments', () => {
281
+ expect(stripComments('let x = 1; // comment\nlet y = 2;')).toBe('let x = 1; \nlet y = 2;');
282
+ });
283
+
284
+ it('strips block comments', () => {
285
+ expect(stripComments('let x = /* inline comment */ 1;')).toBe('let x = 1;');
286
+ });
287
+
288
+ it('strips multi-line block comments', () => {
289
+ const input = 'a;\n/* line1\n line2\n*/\nb;';
290
+ const result = stripComments(input);
291
+ expect(result).toBe('a;\n\nb;');
292
+ });
293
+
294
+ it('preserves single-quoted strings containing //', () => {
295
+ expect(stripComments("let x = 'http://example.com';")).toBe("let x = 'http://example.com';");
296
+ });
297
+
298
+ it('preserves double-quoted strings containing //', () => {
299
+ expect(stripComments('let x = "http://example.com";')).toBe('let x = "http://example.com";');
300
+ });
301
+
302
+ it('preserves template literals containing //', () => {
303
+ expect(stripComments('let x = `http://example.com`;')).toBe('let x = `http://example.com`;');
304
+ });
305
+
306
+ it('preserves template literals with ${} containing //', () => {
307
+ const input = 'let x = `${a}//not-a-comment`;';
308
+ const result = stripComments(input);
309
+ expect(result).toContain('//not-a-comment');
310
+ });
311
+
312
+ it('strips comment after string', () => {
313
+ expect(stripComments('let x = "hello"; // comment')).toBe('let x = "hello"; ');
314
+ });
315
+
316
+ it('handles escaped quotes in strings', () => {
317
+ expect(stripComments("let x = 'it\\'s a test'; // comment")).toBe("let x = 'it\\'s a test'; ");
318
+ });
319
+
320
+ it('preserves regex containing //', () => {
321
+ const input = 'let re = /http:\\/\\//; // comment';
322
+ const result = stripComments(input);
323
+ expect(result).toContain('/http:\\/\\//');
324
+ expect(result).not.toContain('// comment');
325
+ });
326
+
327
+ it('handles empty string', () => {
328
+ expect(stripComments('')).toBe('');
329
+ });
330
+
331
+ it('handles string with no comments', () => {
332
+ expect(stripComments('let x = 1;')).toBe('let x = 1;');
333
+ });
334
+
335
+ it('handles consecutive comments', () => {
336
+ const input = '// comment1\n// comment2\ncode;';
337
+ const result = stripComments(input);
338
+ expect(result).toBe('\n\ncode;');
339
+ });
340
+
341
+ it('handles block comment at start of file', () => {
342
+ expect(stripComments('/* header */\ncode;')).toBe('\ncode;');
343
+ });
344
+
345
+ it('handles nested template literal in ${} block', () => {
346
+ const input = 'let x = `outer ${`inner`} end`;';
347
+ const result = stripComments(input);
348
+ expect(result).toBe(input);
349
+ });
350
+ });
351
+
352
+
353
+ // ---------------------------------------------------------------------------
354
+ // CLI utils - minify
355
+ // ---------------------------------------------------------------------------
356
+
357
+ describe('CLI - minify', () => {
358
+ let minify;
359
+
360
+ beforeEach(async () => {
361
+ const mod = await import('../cli/utils.js');
362
+ minify = mod.minify || mod.default?.minify;
363
+ });
364
+
365
+ it('collapses whitespace', () => {
366
+ const result = minify('let x = 1;', '/* banner */');
367
+ expect(result).toContain('let x=1;');
368
+ });
369
+
370
+ it('preserves banner', () => {
371
+ const result = minify('let x = 1;', '/* v1.0 */');
372
+ expect(result.startsWith('/* v1.0 */')).toBe(true);
373
+ });
374
+
375
+ it('strips comments during minification', () => {
376
+ const result = minify('let x = 1; // comment\nlet y = 2;', '');
377
+ expect(result).not.toContain('// comment');
378
+ });
379
+
380
+ it('preserves string content', () => {
381
+ const result = minify('let x = "hello world";', '');
382
+ expect(result).toContain('"hello world"');
383
+ });
384
+
385
+ it('preserves template literal content', () => {
386
+ const result = minify('let x = `hello world`;', '');
387
+ expect(result).toContain('`hello world`');
388
+ });
389
+
390
+ it('does not merge ++ into + +', () => {
391
+ const result = minify('a + +b', '');
392
+ expect(result).toContain('+ +');
393
+ });
394
+
395
+ it('does not merge -- into - -', () => {
396
+ const result = minify('a - -b', '');
397
+ expect(result).toContain('- -');
398
+ });
399
+
400
+ it('preserves space between keywords and identifiers', () => {
401
+ const result = minify('return value;', '');
402
+ expect(result).toContain('return value');
403
+ });
404
+
405
+ it('removes block comments', () => {
406
+ const result = minify('a /* comment */ = 1;', '');
407
+ expect(result).not.toContain('comment');
408
+ });
409
+
410
+ it('handles empty input', () => {
411
+ const result = minify('', '');
412
+ expect(result).toBe('\n');
413
+ });
414
+ });
415
+
416
+
417
+ // ---------------------------------------------------------------------------
418
+ // CLI utils - sizeKB
419
+ // ---------------------------------------------------------------------------
420
+
421
+ describe('CLI - sizeKB', () => {
422
+ let sizeKB;
423
+
424
+ beforeEach(async () => {
425
+ const mod = await import('../cli/utils.js');
426
+ sizeKB = mod.sizeKB || mod.default?.sizeKB;
427
+ });
428
+
429
+ it('formats 1024 bytes as 1.0', () => {
430
+ expect(sizeKB({ length: 1024 })).toBe('1.0');
431
+ });
432
+
433
+ it('formats 512 bytes as 0.5', () => {
434
+ expect(sizeKB({ length: 512 })).toBe('0.5');
435
+ });
436
+
437
+ it('formats 0 bytes as 0.0', () => {
438
+ expect(sizeKB({ length: 0 })).toBe('0.0');
439
+ });
440
+
441
+ it('formats 2560 bytes as 2.5', () => {
442
+ expect(sizeKB({ length: 2560 })).toBe('2.5');
443
+ });
444
+
445
+ it('formats large size correctly', () => {
446
+ expect(sizeKB({ length: 102400 })).toBe('100.0');
447
+ });
448
+ });
449
+
450
+
451
+ // ---------------------------------------------------------------------------
452
+ // CLI args - flag and option
453
+ // ---------------------------------------------------------------------------
454
+
455
+ describe('CLI - args module', () => {
456
+ let originalArgv;
457
+
458
+ beforeEach(() => {
459
+ originalArgv = [...process.argv];
460
+ });
461
+
462
+ afterEach(() => {
463
+ process.argv = originalArgv;
464
+ // Clear the require cache so the module re-reads argv
465
+ vi.resetModules();
466
+ });
467
+
468
+ it('flag returns true when --verbose is present', async () => {
469
+ process.argv = ['node', 'script', '--verbose'];
470
+ const mod = await import('../cli/args.js');
471
+ const { flag } = mod;
472
+ expect(flag('verbose')).toBe(true);
473
+ });
474
+
475
+ it('flag returns false when flag is absent', async () => {
476
+ process.argv = ['node', 'script'];
477
+ const mod = await import('../cli/args.js');
478
+ const { flag } = mod;
479
+ expect(flag('verbose')).toBe(false);
480
+ });
481
+
482
+ it('flag detects short flag -v', async () => {
483
+ process.argv = ['node', 'script', '-v'];
484
+ const mod = await import('../cli/args.js');
485
+ const { flag } = mod;
486
+ expect(flag('verbose', 'v')).toBe(true);
487
+ });
488
+
489
+ it('option reads value after --port', async () => {
490
+ process.argv = ['node', 'script', '--port', '8080'];
491
+ const mod = await import('../cli/args.js');
492
+ const { option } = mod;
493
+ expect(option('port')).toBe('8080');
494
+ });
495
+
496
+ it('option reads value after short -p', async () => {
497
+ process.argv = ['node', 'script', '-p', '3000'];
498
+ const mod = await import('../cli/args.js');
499
+ const { option } = mod;
500
+ expect(option('port', 'p')).toBe('3000');
501
+ });
502
+
503
+ it('option returns fallback when missing', async () => {
504
+ process.argv = ['node', 'script'];
505
+ const mod = await import('../cli/args.js');
506
+ const { option } = mod;
507
+ expect(option('port', 'p', '3100')).toBe('3100');
508
+ });
509
+
510
+ it('option returns fallback when flag has no value', async () => {
511
+ process.argv = ['node', 'script', '--port'];
512
+ const mod = await import('../cli/args.js');
513
+ const { option } = mod;
514
+ expect(option('port', 'p', '3100')).toBe('3100');
515
+ });
516
+ });
517
+
518
+
519
+ // ===========================================================================
520
+ // showHelp
521
+ // ===========================================================================
522
+
523
+ describe('CLI - showHelp', () => {
524
+ it('outputs help text to console', async () => {
525
+ const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
526
+ const showHelp = (await import('../cli/help.js')).default;
527
+ showHelp();
528
+ expect(spy).toHaveBeenCalled();
529
+ const text = spy.mock.calls.map(c => c.join(' ')).join('\n');
530
+ expect(text).toContain('create');
531
+ expect(text).toContain('dev');
532
+ expect(text).toContain('bundle');
533
+ expect(text).toContain('build');
534
+ spy.mockRestore();
535
+ });
536
+
537
+ it('help text mentions port flag', async () => {
538
+ const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
539
+ const showHelp = (await import('../cli/help.js')).default;
540
+ showHelp();
541
+ const text = spy.mock.calls.map(c => c.join(' ')).join('\n');
542
+ expect(text).toContain('--port');
543
+ spy.mockRestore();
544
+ });
545
+ });
546
+
547
+
548
+ // ===========================================================================
549
+ // stripComments - additional edge cases
550
+ // ===========================================================================
551
+
552
+ describe('CLI - stripComments extra', () => {
553
+ let stripComments;
554
+ beforeEach(async () => {
555
+ const mod = await import('../cli/utils.js');
556
+ stripComments = mod.stripComments;
557
+ });
558
+
559
+ it('handles consecutive single-line comments', () => {
560
+ const input = '// one\n// two\nconst x = 1;';
561
+ const result = stripComments(input);
562
+ expect(result).not.toContain('one');
563
+ expect(result).not.toContain('two');
564
+ expect(result).toContain('const x = 1');
565
+ });
566
+
567
+ it('preserves code after block comment', () => {
568
+ const input = 'a /* comment */ + b';
569
+ const result = stripComments(input);
570
+ expect(result).toContain('a');
571
+ expect(result).toContain('+ b');
572
+ expect(result).not.toContain('comment');
573
+ });
574
+
575
+ it('handles multiline block comment', () => {
576
+ const input = '/*\n line1\n line2\n*/\ncode();';
577
+ const result = stripComments(input);
578
+ expect(result).toContain('code()');
579
+ expect(result).not.toContain('line1');
580
+ });
581
+
582
+ it('does not strip // inside template expression', () => {
583
+ const input = '`${a // b}`';
584
+ const result = stripComments(input);
585
+ // The template should be preserved as-is
586
+ expect(result).toContain('`');
587
+ });
588
+
589
+ it('handles code with no comments', () => {
590
+ const input = 'const x = 1;\nconst y = 2;';
591
+ expect(stripComments(input)).toBe(input);
592
+ });
593
+ });
594
+
595
+
596
+ // ===========================================================================
597
+ // minify - additional edge cases
598
+ // ===========================================================================
599
+
600
+ describe('CLI - minify extra', () => {
601
+ let minify;
602
+ beforeEach(async () => {
603
+ const mod = await import('../cli/utils.js');
604
+ minify = mod.minify;
605
+ });
606
+
607
+ it('preserves string content with spaces', () => {
608
+ const result = minify('const msg = "hello world";', '');
609
+ expect(result).toContain('"hello world"');
610
+ });
611
+
612
+ it('empty input returns banner only', () => {
613
+ const result = minify('', '/* banner */');
614
+ expect(result.trim()).toBe('/* banner */');
615
+ });
616
+
617
+ it('collapses multiple newlines', () => {
618
+ const result = minify('a\n\n\n\nb', '');
619
+ // Should not have multiple consecutive newlines in output
620
+ expect(result).not.toMatch(/\n\n\n/);
621
+ });
622
+
623
+ it('handles template literals with spaces', () => {
624
+ const result = minify('const x = `hello world`;', '');
625
+ expect(result).toContain('`hello world`');
626
+ });
627
+ });
628
+
629
+
630
+ // ===========================================================================
631
+ // sizeKB - edge cases
632
+ // ===========================================================================
633
+
634
+ describe('CLI - sizeKB extra', () => {
635
+ let sizeKB;
636
+ beforeEach(async () => {
637
+ const mod = await import('../cli/utils.js');
638
+ sizeKB = mod.sizeKB;
639
+ });
640
+
641
+ it('zero-length buffer', () => {
642
+ expect(sizeKB({ length: 0 })).toBe('0.0');
643
+ });
644
+
645
+ it('exactly 1KB', () => {
646
+ expect(sizeKB({ length: 1024 })).toBe('1.0');
647
+ });
648
+
649
+ it('small buffer', () => {
650
+ expect(sizeKB({ length: 100 })).toBe('0.1');
651
+ });
652
+ });
653
+
654
+
655
+ // ===========================================================================
656
+ // copyDirSync
657
+ // ===========================================================================
658
+
659
+ describe('CLI - copyDirSync', () => {
660
+ let copyDirSync;
661
+ const fs = require('fs');
662
+ const path = require('path');
663
+ const os = require('os');
664
+
665
+ beforeEach(async () => {
666
+ const mod = await import('../cli/utils.js');
667
+ copyDirSync = mod.copyDirSync;
668
+ });
669
+
670
+ it('copies directory recursively', () => {
671
+ const src = path.join(os.tmpdir(), 'zq-test-src-' + Date.now());
672
+ const dest = path.join(os.tmpdir(), 'zq-test-dest-' + Date.now());
673
+
674
+ // Create source structure
675
+ fs.mkdirSync(path.join(src, 'sub'), { recursive: true });
676
+ fs.writeFileSync(path.join(src, 'a.txt'), 'hello');
677
+ fs.writeFileSync(path.join(src, 'sub', 'b.txt'), 'world');
678
+
679
+ copyDirSync(src, dest);
680
+
681
+ expect(fs.existsSync(path.join(dest, 'a.txt'))).toBe(true);
682
+ expect(fs.readFileSync(path.join(dest, 'a.txt'), 'utf8')).toBe('hello');
683
+ expect(fs.existsSync(path.join(dest, 'sub', 'b.txt'))).toBe(true);
684
+ expect(fs.readFileSync(path.join(dest, 'sub', 'b.txt'), 'utf8')).toBe('world');
685
+
686
+ // Cleanup
687
+ fs.rmSync(src, { recursive: true, force: true });
688
+ fs.rmSync(dest, { recursive: true, force: true });
689
+ });
690
+ });
691
+
692
+
693
+ // ===========================================================================
694
+ // flag/option - additional cases
695
+ // ===========================================================================
696
+
697
+ describe('CLI - flag/option extra', () => {
698
+ it('flag returns false when absent', async () => {
699
+ process.argv = ['node', 'script'];
700
+ vi.resetModules();
701
+ const { flag } = await import('../cli/args.js');
702
+ expect(flag('missing', 'm')).toBe(false);
703
+ });
704
+
705
+ it('option with short form', async () => {
706
+ process.argv = ['node', 'script', '-o', '/dist'];
707
+ vi.resetModules();
708
+ const { option } = await import('../cli/args.js');
709
+ expect(option('output', 'o', 'default')).toBe('/dist');
710
+ });
711
+
712
+ it('multiple flags', async () => {
713
+ process.argv = ['node', 'script', '--verbose', '--watch'];
714
+ vi.resetModules();
715
+ const { flag } = await import('../cli/args.js');
716
+ expect(flag('verbose', 'v')).toBe(true);
717
+ expect(flag('watch', 'w')).toBe(true);
718
+ });
719
+ });
720
+
721
+
722
+ // ===========================================================================
723
+ // createProject - scaffold command
724
+ // ===========================================================================
725
+
726
+ describe('CLI - createProject', () => {
727
+ const fs = require('fs');
728
+ const path = require('path');
729
+ const os = require('os');
730
+
731
+ let createProject;
732
+
733
+ beforeEach(async () => {
734
+ vi.resetModules();
735
+ const mod = await import('../cli/commands/create.js');
736
+ createProject = mod.default || mod;
737
+ });
738
+
739
+ function tmpDir() {
740
+ const dir = path.join(os.tmpdir(), 'zq-create-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8));
741
+ fs.mkdirSync(dir, { recursive: true });
742
+ return dir;
743
+ }
744
+
745
+ function cleanup(dir) {
746
+ if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
747
+ }
748
+
749
+ // -- scaffold variant directories exist --
750
+
751
+ it('default scaffold directory exists', () => {
752
+ const dir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default');
753
+ expect(fs.existsSync(dir)).toBe(true);
754
+ });
755
+
756
+ it('minimal scaffold directory exists', () => {
757
+ const dir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal');
758
+ expect(fs.existsSync(dir)).toBe(true);
759
+ });
760
+
761
+ it('webrtc scaffold directory exists', () => {
762
+ const dir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'webrtc');
763
+ expect(fs.existsSync(dir)).toBe(true);
764
+ });
765
+
766
+ it('webrtc scaffold has index.html with {{NAME}} placeholder', () => {
767
+ const html = fs.readFileSync(
768
+ path.resolve(__dirname, '..', 'cli', 'scaffold', 'webrtc', 'index.html'), 'utf-8'
769
+ );
770
+ expect(html).toContain('{{NAME}}');
771
+ expect(html).toContain('video-room');
772
+ });
773
+
774
+ it('webrtc scaffold has app/components/video-room.js wired to LocalRoom', () => {
775
+ const src = fs.readFileSync(
776
+ path.resolve(__dirname, '..', 'cli', 'scaffold', 'webrtc', 'app', 'components', 'video-room.js'),
777
+ 'utf-8'
778
+ );
779
+ expect(src).toContain('LocalRoom');
780
+ expect(src).toContain('z-stream');
781
+ expect(src).toContain('video-room');
782
+ });
783
+
784
+ it('webrtc scaffold ships a BroadcastChannel-based LocalRoom helper', () => {
785
+ const src = fs.readFileSync(
786
+ path.resolve(__dirname, '..', 'cli', 'scaffold', 'webrtc', 'app', 'lib', 'room.js'),
787
+ 'utf-8'
788
+ );
789
+ expect(src).toContain('BroadcastChannel');
790
+ expect(src).toContain('class LocalRoom');
791
+ expect(src).toContain('$.Peer');
792
+ });
793
+
794
+ // -- default scaffold contains expected files --
795
+
796
+ it('default scaffold has index.html', () => {
797
+ const f = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default', 'index.html');
798
+ expect(fs.existsSync(f)).toBe(true);
799
+ });
800
+
801
+ it('default scaffold has app/app.js', () => {
802
+ const f = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default', 'app', 'app.js');
803
+ expect(fs.existsSync(f)).toBe(true);
804
+ });
805
+
806
+ it('default scaffold has app/routes.js', () => {
807
+ const f = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default', 'app', 'routes.js');
808
+ expect(fs.existsSync(f)).toBe(true);
809
+ });
810
+
811
+ it('default scaffold has app/store.js', () => {
812
+ const f = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default', 'app', 'store.js');
813
+ expect(fs.existsSync(f)).toBe(true);
814
+ });
815
+
816
+ // -- minimal scaffold contains expected files --
817
+
818
+ it('minimal scaffold has index.html', () => {
819
+ const f = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal', 'index.html');
820
+ expect(fs.existsSync(f)).toBe(true);
821
+ });
822
+
823
+ it('minimal scaffold has app/app.js', () => {
824
+ const f = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal', 'app', 'app.js');
825
+ expect(fs.existsSync(f)).toBe(true);
826
+ });
827
+
828
+ it('minimal scaffold has 4 components', () => {
829
+ const dir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal', 'app', 'components');
830
+ const files = fs.readdirSync(dir).sort();
831
+ expect(files).toEqual(['about.js', 'counter.js', 'home.js', 'not-found.js']);
832
+ });
833
+
834
+ // -- minimal scaffold is a subset of default --
835
+
836
+ it('minimal scaffold has fewer files than default', () => {
837
+ function countFiles(dir) {
838
+ let count = 0;
839
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
840
+ if (entry.isDirectory()) count += countFiles(path.join(dir, entry.name));
841
+ else count++;
842
+ }
843
+ return count;
844
+ }
845
+ const defDir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default');
846
+ const minDir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal');
847
+ expect(countFiles(minDir)).toBeLessThan(countFiles(defDir));
848
+ });
849
+
850
+ // -- {{NAME}} replacement --
851
+
852
+ it('scaffold templates contain {{NAME}} placeholder', () => {
853
+ const html = fs.readFileSync(
854
+ path.resolve(__dirname, '..', 'cli', 'scaffold', 'default', 'index.html'), 'utf-8'
855
+ );
856
+ expect(html).toContain('{{NAME}}');
857
+ });
858
+
859
+ it('minimal scaffold templates contain {{NAME}} placeholder', () => {
860
+ const html = fs.readFileSync(
861
+ path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal', 'index.html'), 'utf-8'
862
+ );
863
+ expect(html).toContain('{{NAME}}');
864
+ });
865
+
866
+ // -- walkDir functionality (tested via module internals) --
867
+
868
+ it('scaffolds default project into a target directory', () => {
869
+ const target = tmpDir();
870
+ const projDir = path.join(target, 'test-app');
871
+
872
+ // Simulate: process.argv for default (no --minimal)
873
+ process.argv = ['node', 'zquery', 'create', 'test-app'];
874
+ vi.resetModules();
875
+
876
+ // We can't easily call createProject because it uses process.exit.
877
+ // Instead, test the walkDir + copy logic directly.
878
+ const scaffoldDir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default');
879
+
880
+ function walkDir(dir, prefix = '') {
881
+ const entries = [];
882
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
883
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
884
+ if (entry.isDirectory()) entries.push(...walkDir(path.join(dir, entry.name), rel));
885
+ else entries.push(rel);
886
+ }
887
+ return entries;
888
+ }
889
+
890
+ const files = walkDir(scaffoldDir);
891
+ expect(files.length).toBeGreaterThan(5);
892
+ expect(files).toContain('index.html');
893
+ expect(files).toContain('global.css');
894
+ expect(files).toContain('app/app.js');
895
+ expect(files).toContain('app/routes.js');
896
+ expect(files).toContain('app/store.js');
897
+
898
+ cleanup(target);
899
+ });
900
+
901
+ it('walkDir lists minimal scaffold files correctly', () => {
902
+ const scaffoldDir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal');
903
+
904
+ function walkDir(dir, prefix = '') {
905
+ const entries = [];
906
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
907
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
908
+ if (entry.isDirectory()) entries.push(...walkDir(path.join(dir, entry.name), rel));
909
+ else entries.push(rel);
910
+ }
911
+ return entries;
912
+ }
913
+
914
+ const files = walkDir(scaffoldDir);
915
+ expect(files).toContain('index.html');
916
+ expect(files).toContain('global.css');
917
+ expect(files).toContain('app/app.js');
918
+ expect(files).toContain('app/routes.js');
919
+ expect(files).toContain('app/store.js');
920
+ expect(files).toContain('app/components/home.js');
921
+ expect(files).toContain('app/components/counter.js');
922
+ expect(files).toContain('app/components/about.js');
923
+ expect(files).toContain('app/components/not-found.js');
924
+ expect(files).toContain('assets/.gitkeep');
925
+ });
926
+
927
+ // -- {{NAME}} replacement in copied files --
928
+
929
+ it('replaces {{NAME}} in copied scaffold files', () => {
930
+ const target = tmpDir();
931
+ const scaffoldDir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal');
932
+ const projectName = 'my-cool-app';
933
+
934
+ function walkDir(dir, prefix = '') {
935
+ const entries = [];
936
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
937
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
938
+ if (entry.isDirectory()) entries.push(...walkDir(path.join(dir, entry.name), rel));
939
+ else entries.push(rel);
940
+ }
941
+ return entries;
942
+ }
943
+
944
+ const files = walkDir(scaffoldDir);
945
+
946
+ for (const rel of files) {
947
+ let content = fs.readFileSync(path.join(scaffoldDir, rel), 'utf-8');
948
+ content = content.replace(/\{\{NAME\}\}/g, projectName);
949
+ const dest = path.join(target, rel);
950
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
951
+ fs.writeFileSync(dest, content, 'utf-8');
952
+ }
953
+
954
+ const html = fs.readFileSync(path.join(target, 'index.html'), 'utf-8');
955
+ expect(html).toContain(projectName);
956
+ expect(html).not.toContain('{{NAME}}');
957
+
958
+ const appJs = fs.readFileSync(path.join(target, 'app', 'app.js'), 'utf-8');
959
+ expect(appJs).toContain(projectName);
960
+ expect(appJs).not.toContain('{{NAME}}');
961
+
962
+ cleanup(target);
963
+ });
964
+
965
+ // -- conflict detection --
966
+
967
+ it('conflicts array detects existing files', () => {
968
+ const target = tmpDir();
969
+ fs.writeFileSync(path.join(target, 'index.html'), 'existing');
970
+ fs.mkdirSync(path.join(target, 'app'), { recursive: true });
971
+
972
+ const conflicts = ['index.html', 'global.css', 'app', 'assets'].filter(f =>
973
+ fs.existsSync(path.join(target, f))
974
+ );
975
+
976
+ expect(conflicts).toContain('index.html');
977
+ expect(conflicts).toContain('app');
978
+ expect(conflicts).not.toContain('global.css');
979
+ expect(conflicts).not.toContain('assets');
980
+
981
+ cleanup(target);
982
+ });
983
+
984
+ it('no conflicts in empty directory', () => {
985
+ const target = tmpDir();
986
+
987
+ const conflicts = ['index.html', 'global.css', 'app', 'assets'].filter(f =>
988
+ fs.existsSync(path.join(target, f))
989
+ );
990
+
991
+ expect(conflicts).toHaveLength(0);
992
+
993
+ cleanup(target);
994
+ });
995
+
996
+ // -- flag parsing for --minimal --
997
+
998
+ it('--minimal flag resolves to minimal variant', async () => {
999
+ process.argv = ['node', 'zquery', 'create', '--minimal', 'my-app'];
1000
+ vi.resetModules();
1001
+ const { flag } = await import('../cli/args.js');
1002
+ expect(flag('minimal', 'm')).toBe(true);
1003
+ });
1004
+
1005
+ it('-m short flag resolves to minimal variant', async () => {
1006
+ process.argv = ['node', 'zquery', 'create', '-m', 'my-app'];
1007
+ vi.resetModules();
1008
+ const { flag } = await import('../cli/args.js');
1009
+ expect(flag('minimal', 'm')).toBe(true);
1010
+ });
1011
+
1012
+ it('no flag defaults to default variant', async () => {
1013
+ process.argv = ['node', 'zquery', 'create', 'my-app'];
1014
+ vi.resetModules();
1015
+ const { flag } = await import('../cli/args.js');
1016
+ expect(flag('minimal', 'm')).toBe(false);
1017
+ });
1018
+
1019
+ // -- dirArg parsing skips flags --
1020
+
1021
+ it('dirArg parsing skips flag args', () => {
1022
+ const args = ['create', '--minimal', 'my-app'];
1023
+ const dirArg = args.slice(1).find(a => !a.startsWith('-'));
1024
+ expect(dirArg).toBe('my-app');
1025
+ });
1026
+
1027
+ it('dirArg parsing returns first positional', () => {
1028
+ const args = ['create', 'my-app'];
1029
+ const dirArg = args.slice(1).find(a => !a.startsWith('-'));
1030
+ expect(dirArg).toBe('my-app');
1031
+ });
1032
+
1033
+ it('dirArg is undefined when no positional given', () => {
1034
+ const args = ['create', '--minimal'];
1035
+ const dirArg = args.slice(1).find(a => !a.startsWith('-'));
1036
+ expect(dirArg).toBeUndefined();
1037
+ });
1038
+
1039
+ it('dirArg with flag after dir name', () => {
1040
+ const args = ['create', 'my-app', '--minimal'];
1041
+ const dirArg = args.slice(1).find(a => !a.startsWith('-'));
1042
+ expect(dirArg).toBe('my-app');
1043
+ });
1044
+
1045
+ // -- help text mentions --minimal --
1046
+
1047
+ it('help text includes --minimal flag', async () => {
1048
+ vi.resetModules();
1049
+ const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
1050
+ const showHelp = (await import('../cli/help.js')).default;
1051
+ showHelp();
1052
+ const text = spy.mock.calls.map(c => c.join(' ')).join('\n');
1053
+ expect(text).toContain('--minimal');
1054
+ spy.mockRestore();
1055
+ });
1056
+ });
1057
+
1058
+
1059
+ // ===========================================================================
1060
+ // minifyTemplateLiterals - regex literal awareness
1061
+ // ===========================================================================
1062
+
1063
+ describe('CLI - minifyTemplateLiterals regex handling', () => {
1064
+ let minifyTemplateLiterals;
1065
+
1066
+ beforeEach(async () => {
1067
+ const mod = await import('../cli/commands/bundle.js');
1068
+ minifyTemplateLiterals = mod.minifyTemplateLiterals;
1069
+ });
1070
+
1071
+ it('preserves code after regex containing backtick characters', () => {
1072
+ // Simulates: h = h.replace(/`([^`]+)`/g, '<code>$1</code>');
1073
+ // The backtick inside the regex must not be mistaken for a template literal.
1074
+ const input = "h = h.replace(/\x60([^\x60]+)\x60/g, '<code>$1</code>');\nreturn h;";
1075
+ const result = minifyTemplateLiterals(input);
1076
+ expect(result).toContain('return h');
1077
+ expect(result).toContain('<code>$1</code>');
1078
+ });
1079
+
1080
+ it('does not eat closing braces after backtick-in-regex', () => {
1081
+ const input = [
1082
+ "var fmt = {",
1083
+ " md(raw) {",
1084
+ " let h = raw;",
1085
+ " h = h.replace(/\x60([^\x60]+)\x60/g, '<code>$1</code>');",
1086
+ " h = h.replace(/^[\\-\\*] (.+)$/gm, '<li>$1</li>');",
1087
+ " return h;",
1088
+ " }",
1089
+ "};",
1090
+ "var canvas = document.getElementById('bg-canvas');",
1091
+ ].join('\n');
1092
+ const result = minifyTemplateLiterals(input);
1093
+ expect(result).toContain('return h;');
1094
+ expect(result).toContain('};');
1095
+ expect(result).toContain("getElementById('bg-canvas')");
1096
+ });
1097
+
1098
+ it('handles regex with single backtick', () => {
1099
+ const input = "x.replace(/\x60/g, \"'\");\nvar y = 1;";
1100
+ const result = minifyTemplateLiterals(input);
1101
+ expect(result).toContain('var y = 1');
1102
+ });
1103
+
1104
+ it('handles regex backtick in character class', () => {
1105
+ const input = "x.match(/[\x60'\"]/g);\nvar z = 2;";
1106
+ const result = minifyTemplateLiterals(input);
1107
+ expect(result).toContain('var z = 2');
1108
+ });
1109
+
1110
+ it('does not confuse division operator with regex', () => {
1111
+ // After a number or identifier, / is division, not regex start
1112
+ const input = "var x = a / b;\nvar t = \x60hello\x60;";
1113
+ const result = minifyTemplateLiterals(input);
1114
+ expect(result).toContain('a / b');
1115
+ expect(result).toContain('\x60hello\x60');
1116
+ });
1117
+
1118
+ it('still minifies real template literals correctly', () => {
1119
+ const input = "var html = \x60<div> <span> text </span> </div>\x60;";
1120
+ const result = minifyTemplateLiterals(input);
1121
+ // Template whitespace should be collapsed
1122
+ expect(result).not.toContain(' <span> ');
1123
+ });
1124
+
1125
+ it('handles regex after return keyword', () => {
1126
+ const input = "return /\x60test\x60/g;\nvar after = 1;";
1127
+ const result = minifyTemplateLiterals(input);
1128
+ expect(result).toContain('var after = 1');
1129
+ });
1130
+
1131
+ it('handles regex after assignment operator', () => {
1132
+ const input = "var re = /\x60([^\x60]+)\x60/gi;\nvar next = true;";
1133
+ const result = minifyTemplateLiterals(input);
1134
+ expect(result).toContain('var next = true');
1135
+ });
1136
+ });