zero-query 0.5.2 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +12 -10
  2. package/cli/commands/build.js +7 -5
  3. package/cli/commands/bundle.js +286 -8
  4. package/cli/commands/dev/index.js +82 -0
  5. package/cli/commands/dev/logger.js +70 -0
  6. package/cli/commands/dev/overlay.js +366 -0
  7. package/cli/commands/dev/server.js +158 -0
  8. package/cli/commands/dev/validator.js +94 -0
  9. package/cli/commands/dev/watcher.js +147 -0
  10. package/cli/scaffold/favicon.ico +0 -0
  11. package/cli/scaffold/index.html +1 -0
  12. package/cli/scaffold/scripts/app.js +15 -22
  13. package/cli/scaffold/scripts/components/about.js +14 -2
  14. package/cli/scaffold/scripts/components/contacts/contacts.css +0 -7
  15. package/cli/scaffold/scripts/components/contacts/contacts.html +8 -7
  16. package/cli/scaffold/scripts/components/contacts/contacts.js +17 -1
  17. package/cli/scaffold/scripts/components/counter.js +30 -10
  18. package/cli/scaffold/scripts/components/home.js +3 -3
  19. package/cli/scaffold/scripts/components/todos.js +6 -5
  20. package/cli/scaffold/styles/styles.css +1 -0
  21. package/cli/utils.js +111 -6
  22. package/dist/zquery.dist.zip +0 -0
  23. package/dist/zquery.js +2005 -216
  24. package/dist/zquery.min.js +3 -13
  25. package/index.d.ts +149 -1080
  26. package/index.js +18 -7
  27. package/package.json +9 -3
  28. package/src/component.js +186 -45
  29. package/src/core.js +327 -35
  30. package/src/diff.js +280 -0
  31. package/src/errors.js +155 -0
  32. package/src/expression.js +806 -0
  33. package/src/http.js +18 -10
  34. package/src/reactive.js +29 -4
  35. package/src/router.js +59 -6
  36. package/src/ssr.js +224 -0
  37. package/src/store.js +24 -8
  38. package/tests/component.test.js +304 -0
  39. package/tests/core.test.js +726 -0
  40. package/tests/diff.test.js +194 -0
  41. package/tests/errors.test.js +162 -0
  42. package/tests/expression.test.js +334 -0
  43. package/tests/http.test.js +181 -0
  44. package/tests/reactive.test.js +191 -0
  45. package/tests/router.test.js +332 -0
  46. package/tests/store.test.js +253 -0
  47. package/tests/utils.test.js +353 -0
  48. package/types/collection.d.ts +368 -0
  49. package/types/component.d.ts +210 -0
  50. package/types/errors.d.ts +103 -0
  51. package/types/http.d.ts +81 -0
  52. package/types/misc.d.ts +166 -0
  53. package/types/reactive.d.ts +76 -0
  54. package/types/router.d.ts +132 -0
  55. package/types/ssr.d.ts +49 -0
  56. package/types/store.d.ts +107 -0
  57. package/types/utils.d.ts +142 -0
  58. /package/cli/commands/{dev.js → dev.old.js} +0 -0
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src=".github/images/logo.svg" alt="zQuery logo" width="300" height="300">
2
+ <img src=".github/images/logo-animated.svg" alt="zQuery logo" width="300" height="300">
3
3
  </p>
4
4
 
5
5
  <h1 align="center">zQuery</h1>
@@ -15,18 +15,18 @@
15
15
 
16
16
  </p>
17
17
 
18
- > **Lightweight, zero-dependency frontend library that combines jQuery-style DOM manipulation with a modern reactive component system, SPA router, global state management, HTTP client, and utility toolkit — all in a single ~54 KB minified browser bundle. Works out of the box with ES modules. An optional CLI bundler is available for single-file production builds.**
18
+ > **Lightweight, zero-dependency frontend library that combines jQuery-style DOM manipulation with a modern reactive component system, SPA router, global state management, HTTP client, and utility toolkit — all in a single ~80 KB minified browser bundle. Works out of the box with ES modules. An optional CLI bundler is available for single-file production builds.**
19
19
 
20
20
  ## Features
21
21
 
22
22
  | Module | Highlights |
23
23
  | --- | --- |
24
- | **Core `$()`** | jQuery-like chainable selectors, traversal, DOM manipulation, events, animation |
25
- | **Components** | Reactive state, template literals, `@event` delegation (8 modifiers), `z-model` two-way binding, directives (`z-if`/`z-else-if`/`z-else`, `z-for`, `z-show`, `z-bind`/`:attr`, `z-class`, `z-style`, `z-text`, `z-html`, `z-ref`, `z-cloak`, `z-pre`), scoped styles, external templates (`templateUrl` / `styleUrl`), lifecycle hooks, auto-injected base styles (`z-cloak` hiding, mobile tap-highlight suppression) |
26
- | **Router** | History & hash mode, route params (`:id`), guards, lazy loading, `z-link` navigation |
24
+ | **Router** | History & hash mode, route params (`:id`), guards, lazy loading, `z-link` navigation, `z-to-top` scroll modifier (`instant`/`smooth`) |
25
+ | **Components** | Reactive state, template literals, `@event` delegation (8 modifiers), `z-model` two-way binding, computed properties, watch callbacks, slot-based content projection, directives (`z-if`/`z-else-if`/`z-else`, `z-for`, `z-show`, `z-bind`/`:attr`, `z-class`, `z-style`, `z-text`, `z-html`, `z-ref`, `z-cloak`, `z-pre`, `z-key`), DOM morphing engine (no innerHTML rebuild), CSP-safe expression evaluation, scoped styles, external templates (`templateUrl` / `styleUrl`), lifecycle hooks, auto-injected base styles |
27
26
  | **Store** | Reactive global state, named actions, computed getters, middleware, subscriptions |
28
27
  | **HTTP** | Fetch wrapper with auto-JSON, interceptors, timeout/abort, base URL |
29
28
  | **Reactive** | Deep proxy reactivity, Signals, computed values, effects |
29
+ | **Selectors & DOM** | jQuery-like chainable selectors, traversal, DOM manipulation, events, animation |
30
30
  | **Utils** | debounce, throttle, pipe, once, sleep, escapeHtml, uuid, deepClone, deepMerge, storage/session wrappers, event bus |
31
31
 
32
32
  ---
@@ -69,7 +69,7 @@ If you prefer **zero tooling**, download `dist/zQuery.min.js` from the [GitHub r
69
69
  git clone https://github.com/tonywied17/zero-query.git
70
70
  cd zero-query
71
71
  npx zquery build
72
- # → dist/zQuery.min.js (~54 KB)
72
+ # → dist/zQuery.min.js (~80 KB)
73
73
  ```
74
74
 
75
75
  ### Include in HTML
@@ -240,13 +240,15 @@ location / {
240
240
 
241
241
  | Namespace | Methods |
242
242
  | --- | --- |
243
- | `$()` | Single-element selector → `Element \| null` |
244
- | `$.all()` | Collection selector `ZQueryCollection` |
245
- | `$.id` `$.class` `$.classes` `$.tag` `$.children` | Quick DOM refs |
243
+ | `$()` | Chainable selector → `ZQueryCollection` (CSS selectors, elements, NodeLists, HTML strings) |
244
+ | `$.all()` | Alias for `$()` — identical behavior |
245
+ | `$.id` `$.class` `$.classes` `$.tag` `$.name` `$.children` | Quick DOM refs |
246
246
  | `$.create` | Element factory |
247
247
  | `$.ready` `$.on` `$.off` | DOM ready, global event delegation & direct listeners |
248
248
  | `$.fn` | Collection prototype (extend it) |
249
249
  | `$.component` `$.mount` `$.mountAll` `$.getInstance` `$.destroy` `$.components` | Component system |
250
+ | `$.morph` | DOM morphing engine — patch existing DOM to match new HTML without destroying unchanged nodes |
251
+ | `$.safeEval` | CSP-safe expression evaluator (replaces `eval` / `new Function`) |
250
252
  | `$.style` | Dynamically load global stylesheet file(s) at runtime |
251
253
  | `$.router` `$.getRouter` | SPA router |
252
254
  | `$.store` `$.getStore` | State management |
@@ -276,7 +278,7 @@ For full method signatures, options, and examples, see **[API.md](API.md)**.
276
278
 
277
279
  ## Editor Support
278
280
 
279
- The official **[zQuery for VS Code](https://marketplace.visualstudio.com/items?itemName=zQuery.zquery-vs-code)** extension provides autocomplete, hover docs, HTML directive support, and 140+ code snippets for every API method and directive. Install it from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=zQuery.zquery-vs-code) or search **"zQuery for VS Code"** in Extensions.
281
+ The official **[zQuery for VS Code](https://marketplace.visualstudio.com/items?itemName=zQuery.zquery-vs-code)** extension provides autocomplete, hover docs, HTML directive support, and 185+ code snippets for every API method and directive. Install it from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=zQuery.zquery-vs-code) or search **"zQuery for VS Code"** in Extensions.
280
282
 
281
283
  ---
282
284
 
@@ -16,8 +16,10 @@ function buildLibrary() {
16
16
  const VERSION = pkg.version;
17
17
 
18
18
  const modules = [
19
- 'src/reactive.js', 'src/core.js', 'src/component.js',
20
- 'src/router.js', 'src/store.js', 'src/http.js', 'src/utils.js',
19
+ 'src/errors.js',
20
+ 'src/reactive.js', 'src/core.js', 'src/expression.js', 'src/diff.js',
21
+ 'src/component.js', 'src/router.js', 'src/store.js', 'src/http.js',
22
+ 'src/utils.js',
21
23
  ];
22
24
 
23
25
  const DIST = path.join(process.cwd(), 'dist');
@@ -32,7 +34,7 @@ function buildLibrary() {
32
34
  code = code.replace(/^import\s+[\s\S]*?from\s+['"].*?['"];?\s*$/gm, '');
33
35
  code = code.replace(/^export\s+(default\s+)?/gm, '');
34
36
  code = code.replace(/^export\s*\{[\s\S]*?\};\s*$/gm, '');
35
- return `// --- ${file} ${''.repeat(60 - file.length)}\n${code.trim()}`;
37
+ return `// --- ${file} ${'-'.repeat(60 - file.length)}\n${code.trim()}`;
36
38
  });
37
39
 
38
40
  let indexCode = fs.readFileSync(path.join(process.cwd(), 'index.js'), 'utf-8');
@@ -40,9 +42,9 @@ function buildLibrary() {
40
42
  indexCode = indexCode.replace(/^export\s*\{[\s\S]*?\};\s*$/gm, '');
41
43
  indexCode = indexCode.replace(/^export\s+(default\s+)?/gm, '');
42
44
 
43
- const banner = `/**\n * zQuery (zeroQuery) v${VERSION}\n * Lightweight Frontend Library\n * https://github.com/tonywied17/zero-query\n * (c) ${new Date().getFullYear()} Anthony Wiedman MIT License\n */`;
45
+ const banner = `/**\n * zQuery (zeroQuery) v${VERSION}\n * Lightweight Frontend Library\n * https://github.com/tonywied17/zero-query\n * (c) ${new Date().getFullYear()} Anthony Wiedman - MIT License\n */`;
44
46
 
45
- const bundle = `${banner}\n(function(global) {\n 'use strict';\n\n${parts.join('\n\n')}\n\n// --- index.js (assembly) ${''.repeat(42)}\n${indexCode.trim().replace("'__VERSION__'", `'${VERSION}'`)}\n\n})(typeof window !== 'undefined' ? window : globalThis);\n`;
47
+ const bundle = `${banner}\n(function(global) {\n 'use strict';\n\n${parts.join('\n\n')}\n\n// --- index.js (assembly) ${'-'.repeat(42)}\n${indexCode.trim().replace("'__VERSION__'", `'${VERSION}'`)}\n\n})(typeof window !== 'undefined' ? window : globalThis);\n`;
46
48
 
47
49
  fs.writeFileSync(OUT_FILE, bundle, 'utf-8');
48
50
  fs.writeFileSync(MIN_FILE, minify(bundle, banner), 'utf-8');
@@ -24,6 +24,8 @@ const buildLibrary = require('./build');
24
24
  /** Resolve an import specifier relative to the importing file. */
25
25
  function resolveImport(specifier, fromFile) {
26
26
  if (!specifier.startsWith('.') && !specifier.startsWith('/')) return null;
27
+ // Skip non-JS assets (CSS, images, etc.) referenced in code examples
28
+ if (/\.(?:css|json|svg|png|jpg|gif|woff2?|ttf|eot)$/i.test(specifier)) return null;
27
29
  let resolved = path.resolve(path.dirname(fromFile), specifier);
28
30
  if (!path.extname(resolved) && fs.existsSync(resolved + '.js')) {
29
31
  resolved += '.js';
@@ -112,6 +114,243 @@ function rewriteResourceUrls(code, filePath, projectRoot) {
112
114
  );
113
115
  }
114
116
 
117
+ /**
118
+ * Minify HTML for inlining — strips indentation and collapses whitespace
119
+ * between tags. Preserves content inside <pre>, <code>, and <textarea>
120
+ * blocks verbatim so syntax-highlighted code samples survive.
121
+ */
122
+ function minifyHTML(html) {
123
+ const preserved = [];
124
+ // Protect <pre>…</pre> and <textarea>…</textarea> (multiline code blocks)
125
+ html = html.replace(/<(pre|textarea)(\b[^>]*)>[\s\S]*?<\/\1>/gi, m => {
126
+ preserved.push(m);
127
+ return `\x00P${preserved.length - 1}\x00`;
128
+ });
129
+ // Strip HTML comments
130
+ html = html.replace(/<!--[\s\S]*?-->/g, '');
131
+ // Collapse runs of whitespace (newlines + indentation) to a single space
132
+ html = html.replace(/\s{2,}/g, ' ');
133
+ // Remove space between tags: "> <" → "><"
134
+ // but preserve a space when an inline element is involved (a, span, strong, em, b, i, code, small, sub, sup, abbr, label)
135
+ html = html.replace(/>\s+</g, (m, offset) => {
136
+ const before = html.slice(Math.max(0, offset - 80), offset + 1);
137
+ const after = html.slice(offset + m.length - 1, offset + m.length + 40);
138
+ const inlineTags = /\b(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)\b/i;
139
+ const closingInline = /<\/\s*(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)\s*>$/i.test(before);
140
+ const openingInline = /^<(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)[\s>]/i.test(after);
141
+ return (closingInline || openingInline) ? '> <' : '><';
142
+ });
143
+ // Remove spaces inside opening tags: <tag attr = "val" > → <tag attr="val">
144
+ html = html.replace(/ *\/ *>/g, '/>');
145
+ html = html.replace(/ *= */g, '=');
146
+ // Trim
147
+ html = html.trim();
148
+ // Restore preserved blocks
149
+ html = html.replace(/\x00P(\d+)\x00/g, (_, i) => preserved[+i]);
150
+ return html;
151
+ }
152
+
153
+ /**
154
+ * Minify CSS for inlining — strips comments, collapses whitespace,
155
+ * removes unnecessary spaces around punctuation.
156
+ */
157
+ function minifyCSS(css) {
158
+ // Strip block comments
159
+ css = css.replace(/\/\*[\s\S]*?\*\//g, '');
160
+ // Collapse whitespace
161
+ css = css.replace(/\s{2,}/g, ' ');
162
+ // Remove spaces around { } : ; ,
163
+ css = css.replace(/\s*([{};:,])\s*/g, '$1');
164
+ // Remove trailing semicolons before }
165
+ css = css.replace(/;}/g, '}');
166
+ return css.trim();
167
+ }
168
+
169
+ /**
170
+ * Walk JS source and minify the HTML/CSS inside template literals.
171
+ * Handles ${…} interpolations (with nesting) and preserves <pre> blocks.
172
+ * Only trims whitespace sequences that appear between/around HTML tags.
173
+ */
174
+ function minifyTemplateLiterals(code) {
175
+ let out = '';
176
+ let i = 0;
177
+ while (i < code.length) {
178
+ const ch = code[i];
179
+
180
+ // Regular string: copy verbatim
181
+ if (ch === '"' || ch === "'") {
182
+ const q = ch; out += ch; i++;
183
+ while (i < code.length) {
184
+ if (code[i] === '\\') { out += code[i] + (code[i + 1] || ''); i += 2; continue; }
185
+ out += code[i];
186
+ if (code[i] === q) { i++; break; }
187
+ i++;
188
+ }
189
+ continue;
190
+ }
191
+
192
+ // Line comment: copy verbatim
193
+ if (ch === '/' && code[i + 1] === '/') {
194
+ while (i < code.length && code[i] !== '\n') out += code[i++];
195
+ continue;
196
+ }
197
+ // Block comment: copy verbatim
198
+ if (ch === '/' && code[i + 1] === '*') {
199
+ out += '/*'; i += 2;
200
+ while (i < code.length && !(code[i] === '*' && code[i + 1] === '/')) out += code[i++];
201
+ out += '*/'; i += 2;
202
+ continue;
203
+ }
204
+
205
+ // Template literal: extract, minify HTML, and emit
206
+ if (ch === '`') {
207
+ out += _minifyTemplate(code, i);
208
+ // Advance past the template
209
+ i = _skipTemplateLiteral(code, i);
210
+ continue;
211
+ }
212
+
213
+ out += ch; i++;
214
+ }
215
+ return out;
216
+ }
217
+
218
+ /** Extract a full template literal (handling nested ${…}) and return it minified. */
219
+ function _minifyTemplate(code, start) {
220
+ const end = _skipTemplateLiteral(code, start);
221
+ const raw = code.substring(start, end);
222
+ // Only minify templates that contain HTML tags or CSS rules
223
+ if (/<\w/.test(raw)) return _collapseTemplateWS(raw);
224
+ if (/[{};]\s/.test(raw) && /[.#\w-]+\s*\{/.test(raw)) return _collapseTemplateCSS(raw);
225
+ return raw;
226
+ }
227
+
228
+ /** Return index past the closing backtick of a template starting at `start`. */
229
+ function _skipTemplateLiteral(code, start) {
230
+ let i = start + 1; // skip opening backtick
231
+ let depth = 0;
232
+ while (i < code.length) {
233
+ if (code[i] === '\\') { i += 2; continue; }
234
+ if (code[i] === '$' && code[i + 1] === '{') { depth++; i += 2; continue; }
235
+ if (depth > 0) {
236
+ if (code[i] === '{') { depth++; i++; continue; }
237
+ if (code[i] === '}') { depth--; i++; continue; }
238
+ if (code[i] === '`') { i = _skipTemplateLiteral(code, i); continue; }
239
+ if (code[i] === '"' || code[i] === "'") {
240
+ const q = code[i]; i++;
241
+ while (i < code.length) {
242
+ if (code[i] === '\\') { i += 2; continue; }
243
+ if (code[i] === q) { i++; break; }
244
+ i++;
245
+ }
246
+ continue;
247
+ }
248
+ i++; continue;
249
+ }
250
+ if (code[i] === '`') { i++; return i; } // closing backtick
251
+ i++;
252
+ }
253
+ return i;
254
+ }
255
+
256
+ /**
257
+ * Collapse whitespace in the text portions of an HTML template literal,
258
+ * preserving ${…} expressions, <pre> blocks, and inline text spacing.
259
+ */
260
+ function _collapseTemplateWS(tpl) {
261
+ // Build array of segments: text portions vs ${…} expressions
262
+ const segments = [];
263
+ let i = 1; // skip opening backtick
264
+ let text = '';
265
+ while (i < tpl.length - 1) { // stop before closing backtick
266
+ if (tpl[i] === '\\') { text += tpl[i] + (tpl[i + 1] || ''); i += 2; continue; }
267
+ if (tpl[i] === '$' && tpl[i + 1] === '{') {
268
+ if (text) { segments.push({ type: 'text', val: text }); text = ''; }
269
+ // Collect the full expression
270
+ let depth = 1; let expr = '${'; i += 2;
271
+ while (i < tpl.length - 1 && depth > 0) {
272
+ if (tpl[i] === '\\') { expr += tpl[i] + (tpl[i + 1] || ''); i += 2; continue; }
273
+ if (tpl[i] === '{') depth++;
274
+ if (tpl[i] === '}') depth--;
275
+ if (depth > 0) { expr += tpl[i]; i++; } else { expr += '}'; i++; }
276
+ }
277
+ segments.push({ type: 'expr', val: expr });
278
+ continue;
279
+ }
280
+ text += tpl[i]; i++;
281
+ }
282
+ if (text) segments.push({ type: 'text', val: text });
283
+
284
+ // Minify text segments (collapse whitespace between/around tags)
285
+ for (let s = 0; s < segments.length; s++) {
286
+ if (segments[s].type !== 'text') continue;
287
+ let t = segments[s].val;
288
+ // Protect <pre>…</pre> regions
289
+ const preserved = [];
290
+ t = t.replace(/<pre(\b[^>]*)>[\s\S]*?<\/pre>/gi, m => {
291
+ preserved.push(m);
292
+ return `\x00P${preserved.length - 1}\x00`;
293
+ });
294
+ // Collapse whitespace runs that touch a < or > but preserve a space
295
+ // when an inline element boundary is involved
296
+ t = t.replace(/>\s{2,}/g, (m, offset) => {
297
+ const before = t.slice(Math.max(0, offset - 80), offset + 1);
298
+ if (/<\/\s*(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)\s*>$/i.test(before)) return '> ';
299
+ return '>';
300
+ });
301
+ t = t.replace(/\s{2,}</g, (m, offset) => {
302
+ const after = t.slice(offset + m.length - 1, offset + m.length + 40);
303
+ if (/^<(a|span|strong|em|b|i|code|small|sub|sup|abbr|label)[\s>]/i.test(after)) return ' <';
304
+ return '<';
305
+ });
306
+ // Collapse other multi-whitespace runs to a single space
307
+ t = t.replace(/\s{2,}/g, ' ');
308
+ // Restore <pre> blocks
309
+ t = t.replace(/\x00P(\d+)\x00/g, (_, idx) => preserved[+idx]);
310
+ segments[s].val = t;
311
+ }
312
+
313
+ return '`' + segments.map(s => s.val).join('') + '`';
314
+ }
315
+
316
+ /**
317
+ * Collapse CSS whitespace inside a template literal (styles: `…`).
318
+ * Uses the same segment-splitting approach as _collapseTemplateWS so
319
+ * ${…} expressions are preserved untouched.
320
+ */
321
+ function _collapseTemplateCSS(tpl) {
322
+ const segments = [];
323
+ let i = 1; let text = '';
324
+ while (i < tpl.length - 1) {
325
+ if (tpl[i] === '\\') { text += tpl[i] + (tpl[i + 1] || ''); i += 2; continue; }
326
+ if (tpl[i] === '$' && tpl[i + 1] === '{') {
327
+ if (text) { segments.push({ type: 'text', val: text }); text = ''; }
328
+ let depth = 1; let expr = '${'; i += 2;
329
+ while (i < tpl.length - 1 && depth > 0) {
330
+ if (tpl[i] === '\\') { expr += tpl[i] + (tpl[i + 1] || ''); i += 2; continue; }
331
+ if (tpl[i] === '{') depth++;
332
+ if (tpl[i] === '}') depth--;
333
+ if (depth > 0) { expr += tpl[i]; i++; } else { expr += '}'; i++; }
334
+ }
335
+ segments.push({ type: 'expr', val: expr });
336
+ continue;
337
+ }
338
+ text += tpl[i]; i++;
339
+ }
340
+ if (text) segments.push({ type: 'text', val: text });
341
+
342
+ for (let s = 0; s < segments.length; s++) {
343
+ if (segments[s].type !== 'text') continue;
344
+ let t = segments[s].val;
345
+ t = t.replace(/\/\*[\s\S]*?\*\//g, '');
346
+ t = t.replace(/\s{2,}/g, ' ');
347
+ t = t.replace(/\s*([{};:,])\s*/g, '$1');
348
+ t = t.replace(/;}/g, '}');
349
+ segments[s].val = t;
350
+ }
351
+ return '`' + segments.map(s => s.val).join('') + '`';
352
+ }
353
+
115
354
  /**
116
355
  * Scan bundled source files for external resource references
117
356
  * (pages config, templateUrl, styleUrl) and return a map of
@@ -152,7 +391,8 @@ function collectInlineResources(files, projectRoot) {
152
391
  }
153
392
 
154
393
  // styleUrl:
155
- const styleMatch = code.match(/styleUrl\s*:\s*['"]([^'"]+)['"]/);
394
+ const styleUrlRe = /styleUrl\s*:\s*['"]([^'"]+)['"]/g;
395
+ const styleMatch = styleUrlRe.exec(code);
156
396
  if (styleMatch) {
157
397
  const stylePath = path.join(fileDir, styleMatch[1]);
158
398
  if (fs.existsSync(stylePath)) {
@@ -172,6 +412,12 @@ function collectInlineResources(files, projectRoot) {
172
412
  }
173
413
  }
174
414
 
415
+ // Minify inlined resources by type
416
+ for (const key of Object.keys(inlineMap)) {
417
+ if (key.endsWith('.html')) inlineMap[key] = minifyHTML(inlineMap[key]);
418
+ else if (key.endsWith('.css')) inlineMap[key] = minifyCSS(inlineMap[key]);
419
+ }
420
+
175
421
  return inlineMap;
176
422
  }
177
423
 
@@ -316,6 +562,37 @@ function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFi
316
562
  assets.add(ref);
317
563
  }
318
564
 
565
+ // Also scan the bundled JS for src/href references to local assets
566
+ const bundleContent = fs.existsSync(bundleFile) ? fs.readFileSync(bundleFile, 'utf-8') : '';
567
+ const jsAssetRe = /\b(?:src|href)\s*=\s*["\\]*["']([^"']+\.(?:svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf|eot|mp4|webm))["']/gi;
568
+ while ((m = jsAssetRe.exec(bundleContent)) !== null) {
569
+ const ref = m[1];
570
+ if (ref.startsWith('http') || ref.startsWith('//') || ref.startsWith('data:')) continue;
571
+ if (!assets.has(ref)) {
572
+ const refAbs = path.resolve(htmlDir, ref);
573
+ if (fs.existsSync(refAbs)) assets.add(ref);
574
+ }
575
+ }
576
+
577
+ // For any referenced asset directories, copy all sibling files too
578
+ const assetDirs = new Set();
579
+ for (const asset of assets) {
580
+ const dir = path.dirname(asset);
581
+ if (dir && dir !== '.') assetDirs.add(dir);
582
+ }
583
+ for (const dir of assetDirs) {
584
+ const absDirPath = path.resolve(htmlDir, dir);
585
+ if (fs.existsSync(absDirPath) && fs.statSync(absDirPath).isDirectory()) {
586
+ for (const child of fs.readdirSync(absDirPath)) {
587
+ const childRel = path.join(dir, child).replace(/\\/g, '/');
588
+ const childAbs = path.join(absDirPath, child);
589
+ if (fs.statSync(childAbs).isFile() && !assets.has(childRel)) {
590
+ assets.add(childRel);
591
+ }
592
+ }
593
+ }
594
+ }
595
+
319
596
  // Copy assets into both dist dirs
320
597
  let copiedCount = 0;
321
598
  for (const asset of assets) {
@@ -574,8 +851,9 @@ function bundleApp() {
574
851
  code = stripModuleSyntax(code);
575
852
  code = replaceImportMeta(code, file, projectRoot);
576
853
  code = rewriteResourceUrls(code, file, projectRoot);
854
+ code = minifyTemplateLiterals(code);
577
855
  const rel = path.relative(projectRoot, file);
578
- return `// --- ${rel} ${''.repeat(Math.max(1, 60 - rel.length))}\n${code.trim()}`;
856
+ return `// --- ${rel} ${'-'.repeat(Math.max(1, 60 - rel.length))}\n${code.trim()}`;
579
857
  });
580
858
 
581
859
  // Embed zquery.min.js
@@ -612,8 +890,8 @@ function bundleApp() {
612
890
 
613
891
  if (libPath) {
614
892
  const libBytes = fs.statSync(libPath).size;
615
- libSection = `// --- zquery.min.js (library) ${''.repeat(34)}\n${fs.readFileSync(libPath, 'utf-8').trim()}\n\n`
616
- + `// --- Build-time metadata ————————————————————————————\nif(typeof $!=="undefined"){$.meta=Object.assign($.meta||{},{libSize:${libBytes}});}\n\n`;
893
+ libSection = `// --- zquery.min.js (library) ${'-'.repeat(34)}\n${fs.readFileSync(libPath, 'utf-8').trim()}\n\n`
894
+ + `// --- Build-time metadata ${'-'.repeat(50)}\nif(typeof $!=="undefined"){$.meta=Object.assign($.meta||{},{libSize:${libBytes}});}\n\n`;
617
895
  console.log(` Embedded library from ${path.relative(projectRoot, libPath)} (${(libBytes / 1024).toFixed(1)} KB)`);
618
896
  } else {
619
897
  console.warn(`\n ⚠ Could not find zquery.min.js anywhere`);
@@ -621,7 +899,7 @@ function bundleApp() {
621
899
  }
622
900
  }
623
901
 
624
- const banner = `/**\n * App bundle built by zQuery CLI\n * Entry: ${entryRel}\n * ${new Date().toISOString()}\n */`;
902
+ const banner = `/**\n * App bundle - built by zQuery CLI\n * Entry: ${entryRel}\n * ${new Date().toISOString()}\n */`;
625
903
 
626
904
  // Inline resources
627
905
  const inlineMap = collectInlineResources(files, projectRoot);
@@ -635,7 +913,7 @@ function bundleApp() {
635
913
  .replace(/\r/g, '');
636
914
  return ` '${key}': '${escaped}'`;
637
915
  });
638
- inlineSection = `// --- Inlined resources (file:// support) ${''.repeat(20)}\nwindow.__zqInline = {\n${entries.join(',\n')}\n};\n\n`;
916
+ inlineSection = `// --- Inlined resources (file:// support) ${'-'.repeat(20)}\nwindow.__zqInline = {\n${entries.join(',\n')}\n};\n\n`;
639
917
  console.log(`\n Inlined ${Object.keys(inlineMap).length} external resource(s)`);
640
918
  }
641
919
 
@@ -668,10 +946,10 @@ function bundleApp() {
668
946
  console.log(`\n ✓ ${bundleBase} (${sizeKB(fs.readFileSync(bundleFile))} KB)`);
669
947
  console.log(` ✓ ${minBase} (${sizeKB(fs.readFileSync(minFile))} KB)`);
670
948
 
671
- // Rewrite HTML (use full bundle minified version mangles template literal whitespace)
949
+ // Rewrite HTML to reference the minified bundle
672
950
  const bundledFileSet = new Set(files);
673
951
  if (htmlFile) {
674
- rewriteHtml(projectRoot, htmlFile, bundleFile, true, bundledFileSet, serverDir, localDir);
952
+ rewriteHtml(projectRoot, htmlFile, minFile, true, bundledFileSet, serverDir, localDir);
675
953
  }
676
954
 
677
955
  // Copy static asset directories (icons/, images/, fonts/, etc.)
@@ -0,0 +1,82 @@
1
+ /**
2
+ * cli/commands/dev/index.js — Dev server orchestrator
3
+ *
4
+ * Ties together the HTTP server, file watcher, logger, and overlay
5
+ * to provide a complete development environment with live-reload,
6
+ * syntax validation, and a full-screen error overlay that surfaces
7
+ * both build-time and runtime ZQueryErrors.
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const { args, flag, option } = require('../../args');
16
+ const { createServer } = require('./server');
17
+ const { startWatcher } = require('./watcher');
18
+ const { printBanner } = require('./logger');
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Resolve project root
22
+ // ---------------------------------------------------------------------------
23
+
24
+ function resolveRoot(htmlEntry) {
25
+ // Explicit positional argument → zquery dev <dir>
26
+ for (let i = 1; i < args.length; i++) {
27
+ const prev = args[i - 1];
28
+ if (!args[i].startsWith('-') && prev !== '-p' && prev !== '--port' && prev !== '--index' && prev !== '-i') {
29
+ return path.resolve(process.cwd(), args[i]);
30
+ }
31
+ }
32
+
33
+ // Auto-detect: first candidate that contains the HTML entry file
34
+ const candidates = [
35
+ process.cwd(),
36
+ path.join(process.cwd(), 'public'),
37
+ path.join(process.cwd(), 'src'),
38
+ ];
39
+ for (const c of candidates) {
40
+ if (fs.existsSync(path.join(c, htmlEntry))) return c;
41
+ }
42
+ return process.cwd();
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // devServer — main entry point (called from cli/index.js)
47
+ // ---------------------------------------------------------------------------
48
+
49
+ async function devServer() {
50
+ const htmlEntry = option('index', 'i', 'index.html');
51
+ const port = parseInt(option('port', 'p', '3100'), 10);
52
+ const noIntercept = flag('no-intercept');
53
+ const root = resolveRoot(htmlEntry);
54
+
55
+ // Start HTTP server + SSE pool
56
+ const { app, pool, listen } = await createServer({ root, htmlEntry, port, noIntercept });
57
+
58
+ // Start file watcher
59
+ const watcher = startWatcher({ root, pool });
60
+
61
+ // Boot
62
+ listen(() => {
63
+ printBanner({
64
+ port,
65
+ root: path.relative(process.cwd(), root) || '.',
66
+ htmlEntry,
67
+ noIntercept,
68
+ watchDirCount: watcher.dirs.length,
69
+ });
70
+ });
71
+
72
+ // Graceful shutdown
73
+ process.on('SIGINT', () => {
74
+ console.log('\n Shutting down...');
75
+ watcher.destroy();
76
+ pool.closeAll();
77
+ app.close(() => process.exit(0));
78
+ setTimeout(() => process.exit(0), 1000);
79
+ });
80
+ }
81
+
82
+ module.exports = devServer;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * cli/commands/dev/logger.js — Terminal output helpers
3
+ *
4
+ * Provides styled console output for the dev server: startup banner,
5
+ * timestamped file-change messages, and error formatting.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ // ANSI colour helpers (works on all modern terminals)
11
+ const c = {
12
+ reset: '\x1b[0m',
13
+ bold: '\x1b[1m',
14
+ dim: '\x1b[2m',
15
+ red: '\x1b[31m',
16
+ green: '\x1b[32m',
17
+ yellow: '\x1b[33m',
18
+ cyan: '\x1b[36m',
19
+ magenta: '\x1b[35m',
20
+ };
21
+
22
+ function timestamp() {
23
+ return new Date().toLocaleTimeString();
24
+ }
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Event-level log helpers
28
+ // ---------------------------------------------------------------------------
29
+
30
+ function logCSS(relPath) {
31
+ console.log(` ${timestamp()} ${c.magenta} css ${c.reset} ${relPath}`);
32
+ }
33
+
34
+ function logReload(relPath) {
35
+ console.log(` ${timestamp()} ${c.cyan} reload ${c.reset} ${relPath}`);
36
+ }
37
+
38
+ function logError(descriptor) {
39
+ const t = timestamp();
40
+ console.log(` ${t} ${c.red} error ${c.reset} ${descriptor.file}`);
41
+ console.log(` ${c.red}${descriptor.type}: ${descriptor.message}${c.reset}`);
42
+ if (descriptor.line) {
43
+ console.log(` ${c.dim}at line ${descriptor.line}${descriptor.column ? ':' + descriptor.column : ''}${c.reset}`);
44
+ }
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Startup banner
49
+ // ---------------------------------------------------------------------------
50
+
51
+ function printBanner({ port, root, htmlEntry, noIntercept, watchDirCount }) {
52
+ const rule = c.dim + '-'.repeat(40) + c.reset;
53
+ console.log(`\n ${c.bold}zQuery Dev Server${c.reset}`);
54
+ console.log(` ${rule}`);
55
+ console.log(` Local: ${c.cyan}http://localhost:${port}/${c.reset}`);
56
+ console.log(` Root: ${root}`);
57
+ if (htmlEntry !== 'index.html') {
58
+ console.log(` HTML: ${c.cyan}${htmlEntry}${c.reset}`);
59
+ }
60
+ console.log(` Live Reload: ${c.green}enabled${c.reset} (SSE)`);
61
+ console.log(` Overlay: ${c.green}enabled${c.reset} (syntax + runtime + ZQueryError)`);
62
+ if (noIntercept) {
63
+ console.log(` Intercept: ${c.yellow}disabled${c.reset} (--no-intercept)`);
64
+ }
65
+ console.log(` Watching: all files in ${watchDirCount} director${watchDirCount === 1 ? 'y' : 'ies'}`);
66
+ console.log(` ${rule}`);
67
+ console.log(` Press Ctrl+C to stop\n`);
68
+ }
69
+
70
+ module.exports = { logCSS, logReload, logError, printBanner };