zero-query 0.4.9 → 0.6.3

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.
package/README.md CHANGED
@@ -15,14 +15,14 @@
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 ~84 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
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) |
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 |
26
26
  | **Router** | History & hash mode, route params (`:id`), guards, lazy loading, `z-link` navigation |
27
27
  | **Store** | Reactive global state, named actions, computed getters, middleware, subscriptions |
28
28
  | **HTTP** | Fetch wrapper with auto-JSON, interceptors, timeout/abort, base URL |
@@ -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 (~84 KB)
73
73
  ```
74
74
 
75
75
  ### Include in HTML
@@ -162,7 +162,7 @@ my-app/
162
162
 
163
163
  ## CLI Bundler
164
164
 
165
- The CLI can compile your entire app — ES modules, the library, external templates, and assets — into a **single bundled file**.
165
+ The CLI compiles your entire app — ES modules, the library, external templates, and assets — into a **single production-ready bundle**. It outputs two builds in one step: a `server/` build for deploying to any web server, and a `local/` build that works straight from disk. No config, no flags — just point it at your app.
166
166
 
167
167
  ```bash
168
168
  # Auto-detect entry from any .html with a module script
@@ -170,6 +170,9 @@ npx zquery bundle
170
170
 
171
171
  # Or point to an app directory from anywhere
172
172
  npx zquery bundle my-app/
173
+
174
+ # Or pass a direct entry file (skips auto-detection)
175
+ npx zquery bundle my-app/scripts/main.js
173
176
  ```
174
177
 
175
178
  Output goes to `dist/` next to your `index.html`:
@@ -192,7 +195,8 @@ dist/
192
195
  | Flag | Short | Description |
193
196
  | --- | --- | --- |
194
197
  | `--out <path>` | `-o` | Custom output directory |
195
- | `--html <file>` | | Use a specific HTML file |
198
+ | `--index <file>` | `-i` | Index HTML file (default: auto-detected) |
199
+ | `--minimal` | `-m` | Only output HTML + bundled JS (skip static assets) |
196
200
 
197
201
  ### What the Bundler Does
198
202
 
@@ -236,13 +240,15 @@ location / {
236
240
 
237
241
  | Namespace | Methods |
238
242
  | --- | --- |
239
- | `$()` | Single-element selector → `Element \| null` |
240
- | `$.all()` | Collection selector `ZQueryCollection` |
241
- | `$.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 |
242
246
  | `$.create` | Element factory |
243
247
  | `$.ready` `$.on` `$.off` | DOM ready, global event delegation & direct listeners |
244
248
  | `$.fn` | Collection prototype (extend it) |
245
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`) |
246
252
  | `$.style` | Dynamically load global stylesheet file(s) at runtime |
247
253
  | `$.router` `$.getRouter` | SPA router |
248
254
  | `$.store` `$.getStore` | State management |
@@ -261,8 +267,8 @@ location / {
261
267
  | CLI Command | Description |
262
268
  | --- | --- |
263
269
  | `zquery create [dir]` | Scaffold a new project (index.html, components, store, styles) |
264
- | `zquery dev [root]` | Dev server with live-reload &amp; error overlay (port 3100) |
265
- | `zquery bundle [dir]` | Bundle app into a single IIFE file |
270
+ | `zquery dev [root]` | Dev server with live-reload &amp; error overlay (port 3100). `--index` for custom HTML. |
271
+ | `zquery bundle [dir\|file]` | Bundle app into a single IIFE file. Accepts dir or direct entry file. |
266
272
  | `zquery build` | Build the zQuery library (`dist/zQuery.min.js`) |
267
273
  | `zquery --help` | Show CLI usage |
268
274
 
@@ -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');
@@ -13,7 +13,7 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const crypto = require('crypto');
15
15
 
16
- const { args, option } = require('../args');
16
+ const { args, flag, option } = require('../args');
17
17
  const { minify, sizeKB } = require('../utils');
18
18
  const buildLibrary = require('./build');
19
19
 
@@ -104,6 +104,8 @@ function rewriteResourceUrls(code, filePath, projectRoot) {
104
104
  (match, prefix, quote, url) => {
105
105
  if (url.startsWith('/') || url.includes('://')) return match;
106
106
  const abs = path.resolve(fileDir, url);
107
+ // Only rewrite if the file actually exists — avoids mangling code examples
108
+ if (!fs.existsSync(abs)) return match;
107
109
  const rel = path.relative(projectRoot, abs).replace(/\\/g, '/');
108
110
  return `${prefix}${quote}${rel}${quote}`;
109
111
  }
@@ -379,18 +381,68 @@ function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFi
379
381
  console.log(` ✓ Copied ${copiedCount} asset(s) into both dist dirs`);
380
382
  }
381
383
 
384
+ // ---------------------------------------------------------------------------
385
+ // Static asset copying
386
+ // ---------------------------------------------------------------------------
387
+
388
+ /**
389
+ * Copy the entire app directory into both dist/server and dist/local,
390
+ * skipping only build outputs and tooling dirs. This ensures all static
391
+ * assets (icons/, images/, fonts/, styles/, scripts/, manifests, etc.)
392
+ * are available in the built output without maintaining a fragile whitelist.
393
+ */
394
+ function copyStaticAssets(appRoot, serverDir, localDir, bundledFiles) {
395
+ const SKIP_DIRS = new Set(['dist', 'node_modules', '.git', '.vscode', 'scripts']);
396
+
397
+ let copiedCount = 0;
398
+
399
+ function copyEntry(srcPath, relPath) {
400
+ const stat = fs.statSync(srcPath);
401
+ if (stat.isDirectory()) {
402
+ if (SKIP_DIRS.has(path.basename(srcPath))) return;
403
+ for (const child of fs.readdirSync(srcPath)) {
404
+ copyEntry(path.join(srcPath, child), path.join(relPath, child));
405
+ }
406
+ } else {
407
+ if (bundledFiles && bundledFiles.has(path.resolve(srcPath))) return;
408
+ for (const distDir of [serverDir, localDir]) {
409
+ const dest = path.join(distDir, relPath);
410
+ if (fs.existsSync(dest)) continue; // already copied by rewriteHtml
411
+ const dir = path.dirname(dest);
412
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
413
+ fs.copyFileSync(srcPath, dest);
414
+ }
415
+ copiedCount++;
416
+ }
417
+ }
418
+
419
+ for (const entry of fs.readdirSync(appRoot, { withFileTypes: true })) {
420
+ if (entry.isDirectory()) {
421
+ if (SKIP_DIRS.has(entry.name)) continue;
422
+ copyEntry(path.join(appRoot, entry.name), entry.name);
423
+ } else {
424
+ copyEntry(path.join(appRoot, entry.name), entry.name);
425
+ }
426
+ }
427
+
428
+ if (copiedCount > 0) {
429
+ console.log(` \u2713 Copied ${copiedCount} additional static asset(s) into both dist dirs`);
430
+ }
431
+ }
432
+
382
433
  // ---------------------------------------------------------------------------
383
434
  // Main bundleApp function
384
435
  // ---------------------------------------------------------------------------
385
436
 
386
437
  function bundleApp() {
387
438
  const projectRoot = process.cwd();
439
+ const minimal = flag('minimal', 'm');
388
440
 
389
- // Entry point — accepts a directory (auto-detects entry inside it) or a file
441
+ // Entry point — positional arg (directory or file) or auto-detection
390
442
  let entry = null;
391
443
  let targetDir = null;
392
444
  for (let i = 1; i < args.length; i++) {
393
- if (!args[i].startsWith('-') && args[i - 1] !== '-o' && args[i - 1] !== '--out' && args[i - 1] !== '--html') {
445
+ if (!args[i].startsWith('-') && args[i - 1] !== '-o' && args[i - 1] !== '--out' && args[i - 1] !== '--index') {
394
446
  const resolved = path.resolve(projectRoot, args[i]);
395
447
  if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
396
448
  targetDir = resolved;
@@ -405,16 +457,19 @@ function bundleApp() {
405
457
 
406
458
  if (!entry || !fs.existsSync(entry)) {
407
459
  console.error(`\n \u2717 Could not find entry file.`);
408
- console.error(` Provide an app directory: zquery bundle my-app/\n`);
460
+ console.error(` Provide an app directory: zquery bundle my-app/`);
461
+ console.error(` Or pass a direct entry file: zquery bundle my-app/scripts/main.js\n`);
409
462
  process.exit(1);
410
463
  }
411
464
 
412
465
  const outPath = option('out', 'o', null);
413
466
 
414
- // Auto-detect index.html
415
- let htmlFile = option('html', null, null);
467
+ // Auto-detect HTML file
468
+ let htmlFile = option('index', 'i', null);
416
469
  let htmlAbs = htmlFile ? path.resolve(projectRoot, htmlFile) : null;
417
470
  if (!htmlFile) {
471
+ // Strategy: first look for index.html walking up from entry, then
472
+ // scan for any .html that references the entry via a module script tag.
418
473
  const htmlCandidates = [];
419
474
  let entryDir = path.dirname(entry);
420
475
  while (entryDir.length >= projectRoot.length) {
@@ -432,6 +487,47 @@ function bundleApp() {
432
487
  break;
433
488
  }
434
489
  }
490
+
491
+ // If no index.html found, scan for any .html file that references
492
+ // the entry point (supports home.html, app.html, etc.)
493
+ if (!htmlAbs) {
494
+ const searchRoot = targetDir || projectRoot;
495
+ const htmlScan = [];
496
+ for (const e of fs.readdirSync(searchRoot, { withFileTypes: true })) {
497
+ if (e.isFile() && e.name.endsWith('.html')) {
498
+ htmlScan.push(path.join(searchRoot, e.name));
499
+ } else if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules' && e.name !== 'dist') {
500
+ try {
501
+ for (const child of fs.readdirSync(path.join(searchRoot, e.name), { withFileTypes: true })) {
502
+ if (child.isFile() && child.name.endsWith('.html')) {
503
+ htmlScan.push(path.join(searchRoot, e.name, child.name));
504
+ }
505
+ }
506
+ } catch { /* skip */ }
507
+ }
508
+ }
509
+ // Prefer the HTML file that references our entry via a module script
510
+ const moduleScriptRe = /<script[^>]+type\s*=\s*["']module["'][^>]+src\s*=\s*["']([^"']+)["']/g;
511
+ for (const hp of htmlScan) {
512
+ const content = fs.readFileSync(hp, 'utf-8');
513
+ let m;
514
+ moduleScriptRe.lastIndex = 0;
515
+ while ((m = moduleScriptRe.exec(content)) !== null) {
516
+ const resolved = path.resolve(path.dirname(hp), m[1]);
517
+ if (resolved === path.resolve(entry)) {
518
+ htmlAbs = hp;
519
+ htmlFile = path.relative(projectRoot, hp);
520
+ break;
521
+ }
522
+ }
523
+ if (htmlAbs) break;
524
+ }
525
+ // Last resort: use the first .html found
526
+ if (!htmlAbs && htmlScan.length > 0) {
527
+ htmlAbs = htmlScan[0];
528
+ htmlFile = path.relative(projectRoot, htmlScan[0]);
529
+ }
530
+ }
435
531
  }
436
532
 
437
533
  // Output directory
@@ -459,7 +555,8 @@ function bundleApp() {
459
555
  console.log(` Entry: ${entryRel}`);
460
556
  console.log(` Output: ${path.relative(projectRoot, baseDistDir)}/server/ & local/`);
461
557
  console.log(` Library: embedded`);
462
- console.log(` HTML: ${htmlFile || 'not found (no index.html detected)'}`);
558
+ console.log(` HTML: ${htmlFile || 'not found (no HTML detected)'}`);
559
+ if (minimal) console.log(` Mode: minimal (HTML + bundle only)`);
463
560
  console.log('');
464
561
 
465
562
  // ------ doBuild (inlined) ------
@@ -571,10 +668,16 @@ function bundleApp() {
571
668
  console.log(`\n ✓ ${bundleBase} (${sizeKB(fs.readFileSync(bundleFile))} KB)`);
572
669
  console.log(` ✓ ${minBase} (${sizeKB(fs.readFileSync(minFile))} KB)`);
573
670
 
574
- // Rewrite HTML
671
+ // Rewrite HTML (use full bundle — minified version mangles template literal whitespace)
672
+ const bundledFileSet = new Set(files);
575
673
  if (htmlFile) {
576
- const bundledFileSet = new Set(files);
577
- rewriteHtml(projectRoot, htmlFile, minFile, true, bundledFileSet, serverDir, localDir);
674
+ rewriteHtml(projectRoot, htmlFile, bundleFile, true, bundledFileSet, serverDir, localDir);
675
+ }
676
+
677
+ // Copy static asset directories (icons/, images/, fonts/, etc.)
678
+ if (!minimal) {
679
+ const appRoot = htmlAbs ? path.dirname(htmlAbs) : (targetDir || projectRoot);
680
+ copyStaticAssets(appRoot, serverDir, localDir, bundledFileSet);
578
681
  }
579
682
 
580
683
  const elapsed = Date.now() - start;
@@ -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
+ 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 } = 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 };