zero-query 0.2.4 → 0.2.7

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/cli.js CHANGED
@@ -3,8 +3,9 @@
3
3
  /**
4
4
  * zQuery CLI
5
5
  *
6
- * Zero-dependency command-line tool for building the zQuery library
7
- * and bundling zQuery-based applications into a single file.
6
+ * Zero-dependency command-line tool for building the zQuery library,
7
+ * bundling zQuery-based applications, and running a dev server with
8
+ * live-reload.
8
9
  *
9
10
  * Usage:
10
11
  * zquery build Build the zQuery library (dist/)
@@ -13,7 +14,9 @@
13
14
  * zquery bundle scripts/app.js Specify entry explicitly
14
15
  * zquery bundle -o build/ Custom output directory
15
16
  * zquery bundle --html other.html Use a specific HTML file instead of auto-detected
16
- * zquery bundle --watch Watch & rebuild on changes
17
+ *
18
+ * zquery dev [root] Start dev server with live-reload
19
+ * zquery dev --port 8080 Custom port (default: 3100)
17
20
  *
18
21
  * Smart defaults (no flags needed for typical projects):
19
22
  * - Entry is auto-detected from index.html's <script type="module" src="...">
@@ -24,6 +27,7 @@
24
27
  * Examples:
25
28
  * cd my-app && npx zero-query bundle # just works!
26
29
  * npx zero-query bundle path/to/scripts/app.js # works from anywhere
30
+ * cd my-app && npx zquery dev # dev server with live-reload
27
31
  */
28
32
 
29
33
  const fs = require('fs');
@@ -412,7 +416,6 @@ function bundleApp() {
412
416
  }
413
417
 
414
418
  const outPath = option('out', 'o', null);
415
- const watchMode = flag('watch', 'w');
416
419
 
417
420
  // Auto-detect index.html by walking up from the entry file, then check cwd
418
421
  let htmlFile = option('html', null, null);
@@ -595,26 +598,6 @@ function bundleApp() {
595
598
  }
596
599
 
597
600
  doBuild();
598
-
599
- // Watch mode
600
- if (watchMode) {
601
- const watchDirs = new Set();
602
- const files = walkImportGraph(entry);
603
- files.forEach(f => watchDirs.add(path.dirname(f)));
604
-
605
- console.log(' Watching for changes...\n');
606
- let debounceTimer;
607
- for (const dir of watchDirs) {
608
- fs.watch(dir, { recursive: true }, (_, filename) => {
609
- if (!filename || !filename.endsWith('.js')) return;
610
- clearTimeout(debounceTimer);
611
- debounceTimer = setTimeout(() => {
612
- console.log(` Changed: ${filename} — rebuilding...`);
613
- try { doBuild(); } catch (e) { console.error(` ✗ ${e.message}`); }
614
- }, 200);
615
- });
616
- }
617
- }
618
601
  }
619
602
 
620
603
  /**
@@ -745,13 +728,232 @@ function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFi
745
728
  }
746
729
 
747
730
 
731
+ // ---------------------------------------------------------------------------
732
+ // "dev" command — development server with live-reload
733
+ // ---------------------------------------------------------------------------
734
+
735
+ /**
736
+ * SSE live-reload client script injected into served HTML.
737
+ * Connects to /__zq_reload, reloads on 'reload' events,
738
+ * and hot-swaps CSS on 'css' events without a full reload.
739
+ */
740
+ const LIVE_RELOAD_SNIPPET = `<script>
741
+ (function(){
742
+ var es, timer;
743
+ function connect(){
744
+ es = new EventSource('/__zq_reload');
745
+ es.addEventListener('reload', function(){ location.reload(); });
746
+ es.addEventListener('css', function(e){
747
+ var sheets = document.querySelectorAll('link[rel="stylesheet"]');
748
+ sheets.forEach(function(l){
749
+ var href = l.getAttribute('href');
750
+ if(!href) return;
751
+ var sep = href.indexOf('?') >= 0 ? '&' : '?';
752
+ l.setAttribute('href', href.replace(/[?&]_zqr=\\d+/, '') + sep + '_zqr=' + Date.now());
753
+ });
754
+ });
755
+ es.onerror = function(){
756
+ es.close();
757
+ clearTimeout(timer);
758
+ timer = setTimeout(connect, 2000);
759
+ };
760
+ }
761
+ connect();
762
+ })();
763
+ </script>`;
764
+
765
+ function devServer() {
766
+ let zeroHttp;
767
+ try {
768
+ zeroHttp = require('zero-http');
769
+ } catch (_) {
770
+ console.error(`\n ✗ zero-http is required for the dev server.`);
771
+ console.error(` Install it: npm install zero-http --save-dev\n`);
772
+ process.exit(1);
773
+ }
774
+
775
+ const { createApp, static: serveStatic } = zeroHttp;
776
+
777
+ // Determine the project root to serve
778
+ let root = null;
779
+ for (let i = 1; i < args.length; i++) {
780
+ if (!args[i].startsWith('-') && args[i - 1] !== '-p' && args[i - 1] !== '--port') {
781
+ root = path.resolve(process.cwd(), args[i]);
782
+ break;
783
+ }
784
+ }
785
+ if (!root) {
786
+ // Auto-detect: look for index.html in cwd or common sub-dirs
787
+ const candidates = [
788
+ process.cwd(),
789
+ path.join(process.cwd(), 'public'),
790
+ path.join(process.cwd(), 'src'),
791
+ ];
792
+ for (const c of candidates) {
793
+ if (fs.existsSync(path.join(c, 'index.html'))) { root = c; break; }
794
+ }
795
+ if (!root) root = process.cwd();
796
+ }
797
+
798
+ const PORT = parseInt(option('port', 'p', '3100'));
799
+
800
+ // SSE clients for live-reload
801
+ const sseClients = new Set();
802
+
803
+ const app = createApp();
804
+
805
+ // SSE endpoint — clients connect here for reload notifications
806
+ app.get('/__zq_reload', (req, res) => {
807
+ const sse = res.sse({ keepAlive: 30000, keepAliveComment: 'ping' });
808
+ sseClients.add(sse);
809
+ sse.on('close', () => sseClients.delete(sse));
810
+ });
811
+
812
+ // Auto-resolve zquery.min.js — serve the freshest version regardless of
813
+ // what's on disk in the project. Priority:
814
+ // 1. Package dist/ (when running from the repo after `npm run build`)
815
+ // 2. node_modules/zero-query/dist/ (when installed as a dependency)
816
+ // 3. Fall through to static serving (vendor copy on disk)
817
+ // Registered as middleware so it runs BEFORE serveStatic.
818
+ app.use((req, res, next) => {
819
+ const basename = path.basename(req.url.split('?')[0]).toLowerCase();
820
+ if (basename !== 'zquery.min.js') return next();
821
+
822
+ const candidates = [
823
+ path.join(__dirname, 'dist', 'zquery.min.js'), // package repo
824
+ path.join(root, 'node_modules', 'zero-query', 'dist', 'zquery.min.js'), // npm dep
825
+ ];
826
+ for (const p of candidates) {
827
+ if (fs.existsSync(p)) {
828
+ res.set('Content-Type', 'application/javascript; charset=utf-8');
829
+ res.set('Cache-Control', 'no-cache');
830
+ res.send(fs.readFileSync(p, 'utf-8'));
831
+ return;
832
+ }
833
+ }
834
+ next(); // fall through to static / 404
835
+ });
836
+
837
+ // Static file serving
838
+ app.use(serveStatic(root, { index: false, dotfiles: 'ignore' }));
839
+
840
+ // SPA fallback — inject live-reload snippet into HTML
841
+ app.get('*', (req, res) => {
842
+ if (path.extname(req.url) && path.extname(req.url) !== '.html') {
843
+ res.status(404).send('Not Found');
844
+ return;
845
+ }
846
+ const indexPath = path.join(root, 'index.html');
847
+ if (!fs.existsSync(indexPath)) {
848
+ res.status(404).send('index.html not found');
849
+ return;
850
+ }
851
+ let html = fs.readFileSync(indexPath, 'utf-8');
852
+ // Inject live-reload snippet before </body> or at end
853
+ if (html.includes('</body>')) {
854
+ html = html.replace('</body>', LIVE_RELOAD_SNIPPET + '\n</body>');
855
+ } else {
856
+ html += LIVE_RELOAD_SNIPPET;
857
+ }
858
+ res.html(html);
859
+ });
860
+
861
+ // Broadcast a reload event to all connected SSE clients
862
+ function broadcast(eventType, data) {
863
+ for (const sse of sseClients) {
864
+ try { sse.event(eventType, data || ''); } catch (_) { sseClients.delete(sse); }
865
+ }
866
+ }
867
+
868
+ // File watcher — watch the project root for changes
869
+ const WATCH_EXTS = new Set(['.js', '.css', '.html', '.htm', '.json', '.svg']);
870
+ const IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', '.cache']);
871
+ let debounceTimer;
872
+
873
+ function shouldWatch(filename) {
874
+ if (!filename) return false;
875
+ const ext = path.extname(filename).toLowerCase();
876
+ return WATCH_EXTS.has(ext);
877
+ }
878
+
879
+ function isIgnored(filepath) {
880
+ const parts = filepath.split(path.sep);
881
+ return parts.some(p => IGNORE_DIRS.has(p));
882
+ }
883
+
884
+ // Collect directories to watch (walk root, skip ignored)
885
+ function collectWatchDirs(dir) {
886
+ const dirs = [dir];
887
+ try {
888
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
889
+ for (const entry of entries) {
890
+ if (!entry.isDirectory()) continue;
891
+ if (IGNORE_DIRS.has(entry.name)) continue;
892
+ const sub = path.join(dir, entry.name);
893
+ dirs.push(...collectWatchDirs(sub));
894
+ }
895
+ } catch (_) {}
896
+ return dirs;
897
+ }
898
+
899
+ const watchDirs = collectWatchDirs(root);
900
+ const watchers = [];
901
+
902
+ for (const dir of watchDirs) {
903
+ try {
904
+ const watcher = fs.watch(dir, (eventType, filename) => {
905
+ if (!shouldWatch(filename)) return;
906
+ const fullPath = path.join(dir, filename || '');
907
+ if (isIgnored(fullPath)) return;
908
+
909
+ clearTimeout(debounceTimer);
910
+ debounceTimer = setTimeout(() => {
911
+ const rel = path.relative(root, fullPath).replace(/\\/g, '/');
912
+ const ext = path.extname(filename).toLowerCase();
913
+ const now = new Date().toLocaleTimeString();
914
+
915
+ if (ext === '.css') {
916
+ console.log(` ${now} \x1b[35m css \x1b[0m ${rel}`);
917
+ broadcast('css', rel);
918
+ } else {
919
+ console.log(` ${now} \x1b[36m reload \x1b[0m ${rel}`);
920
+ broadcast('reload', rel);
921
+ }
922
+ }, 100);
923
+ });
924
+ watchers.push(watcher);
925
+ } catch (_) {}
926
+ }
927
+
928
+ app.listen(PORT, () => {
929
+ console.log(`\n \x1b[1mzQuery Dev Server\x1b[0m`);
930
+ console.log(` \x1b[2m${'─'.repeat(40)}\x1b[0m`);
931
+ console.log(` Local: \x1b[36mhttp://localhost:${PORT}/\x1b[0m`);
932
+ console.log(` Root: ${path.relative(process.cwd(), root) || '.'}`);
933
+ console.log(` Live Reload: \x1b[32menabled\x1b[0m (SSE)`);
934
+ console.log(` Watching: ${WATCH_EXTS.size} file types in ${watchDirs.length} director${watchDirs.length === 1 ? 'y' : 'ies'}`);
935
+ console.log(` \x1b[2m${'─'.repeat(40)}\x1b[0m`);
936
+ console.log(` Press Ctrl+C to stop\n`);
937
+ });
938
+
939
+ // Graceful shutdown
940
+ process.on('SIGINT', () => {
941
+ console.log('\n Shutting down...');
942
+ watchers.forEach(w => w.close());
943
+ for (const sse of sseClients) { try { sse.close(); } catch (_) {} }
944
+ app.close(() => process.exit(0));
945
+ setTimeout(() => process.exit(0), 1000);
946
+ });
947
+ }
948
+
949
+
748
950
  // ---------------------------------------------------------------------------
749
951
  // Help
750
952
  // ---------------------------------------------------------------------------
751
953
 
752
954
  function showHelp() {
753
955
  console.log(`
754
- zQuery CLI — build & bundle tool
956
+ zQuery CLI — build, bundle & dev tool
755
957
 
756
958
  COMMANDS
757
959
 
@@ -761,7 +963,9 @@ function showHelp() {
761
963
  bundle [entry] Bundle app ES modules into a single file
762
964
  --out, -o <path> Output directory (default: dist/ next to index.html)
763
965
  --html <file> Use a specific HTML file (default: auto-detected)
764
- --watch, -w Watch source files and rebuild on changes
966
+
967
+ dev [root] Start a dev server with live-reload
968
+ --port, -p <number> Port number (default: 3100)
765
969
 
766
970
  SMART DEFAULTS
767
971
 
@@ -788,11 +992,14 @@ function showHelp() {
788
992
 
789
993
  DEVELOPMENT
790
994
 
791
- npm run serve start a local dev server (zero-http, SPA routing)
792
- npm run dev watch mode auto-rebuild bundle on source changes
995
+ zquery dev start a dev server with live-reload (port 3100)
996
+ zquery dev --port 8080 custom port
793
997
 
794
998
  EXAMPLES
795
999
 
1000
+ # Start dev server with live-reload
1001
+ cd my-app && zquery dev
1002
+
796
1003
  # Build the library only
797
1004
  zquery build
798
1005
 
@@ -805,9 +1012,6 @@ function showHelp() {
805
1012
  # Custom output directory
806
1013
  zquery bundle -o build/
807
1014
 
808
- # Watch mode
809
- zquery bundle --watch
810
-
811
1015
  The bundler walks the ES module import graph starting from the entry
812
1016
  file, topologically sorts dependencies, strips import/export syntax,
813
1017
  and concatenates everything into a single IIFE with content-hashed
@@ -827,6 +1031,8 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
827
1031
  buildLibrary();
828
1032
  } else if (command === 'bundle') {
829
1033
  bundleApp();
1034
+ } else if (command === 'dev') {
1035
+ devServer();
830
1036
  } else {
831
1037
  console.error(`\n Unknown command: ${command}\n Run "zquery --help" for usage.\n`);
832
1038
  process.exit(1);
Binary file
package/dist/zquery.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * zQuery (zeroQuery) v0.2.4
2
+ * zQuery (zeroQuery) v0.2.7
3
3
  * Lightweight Frontend Library
4
4
  * https://github.com/tonywied17/zero-query
5
5
  * (c) 2026 Anthony Wiedman — MIT License
@@ -2576,7 +2576,7 @@ $.session = session;
2576
2576
  $.bus = bus;
2577
2577
 
2578
2578
  // --- Meta ------------------------------------------------------------------
2579
- $.version = '0.2.4';
2579
+ $.version = '0.2.7';
2580
2580
  $.meta = {}; // populated at build time by CLI bundler
2581
2581
 
2582
2582
  $.noConflict = () => {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * zQuery (zeroQuery) v0.2.4
2
+ * zQuery (zeroQuery) v0.2.7
3
3
  * Lightweight Frontend Library
4
4
  * https://github.com/tonywied17/zero-query
5
5
  * (c) 2026 Anthony Wiedman — MIT License
@@ -13,5 +13,5 @@ class Router { constructor(config = {}) { this._el = null; const isFile = typeof
13
13
  class Store { constructor(config = {}) { this._subscribers = new Map(); this._wildcards = new Set(); this._actions = config.actions || {}; this._getters = config.getters || {}; this._middleware = []; this._history = []; this._debug = config.debug || false; const initial = typeof config.state === 'function' ? config.state() : { ...(config.state || {}) }; this.state = reactive(initial, (key, value, old) => { const subs = this._subscribers.get(key); if (subs) subs.forEach(fn => fn(value, old, key)); this._wildcards.forEach(fn => fn(key, value, old)); }); this.getters = {}; for (const [name, fn] of Object.entries(this._getters)) { Object.defineProperty(this.getters, name, { get: () => fn(this.state.__raw || this.state), enumerable: true }); } } dispatch(name, ...args) { const action = this._actions[name]; if (!action) { console.warn(`zQuery Store: Unknown action "${name}"`); return; } for (const mw of this._middleware) { const result = mw(name, args, this.state); if (result === false) return; } if (this._debug) { console.log(`%c[Store] ${name}`, 'color: #4CAF50; font-weight: bold;', ...args); } const result = action(this.state, ...args); this._history.push({ action: name, args, timestamp: Date.now() }); return result; } subscribe(keyOrFn, fn) { if (typeof keyOrFn === 'function') { this._wildcards.add(keyOrFn); return () => this._wildcards.delete(keyOrFn); } if (!this._subscribers.has(keyOrFn)) { this._subscribers.set(keyOrFn, new Set()); } this._subscribers.get(keyOrFn).add(fn); return () => this._subscribers.get(keyOrFn)?.delete(fn); } snapshot() { return JSON.parse(JSON.stringify(this.state.__raw || this.state)); } replaceState(newState) { const raw = this.state.__raw || this.state; for (const key of Object.keys(raw)) { delete this.state[key]; } Object.assign(this.state, newState); } use(fn) { this._middleware.push(fn); return this; } get history() { return [...this._history]; } reset(initialState) { this.replaceState(initialState); this._history = []; }
14
14
  const _config = { baseURL: '', headers: { 'Content-Type': 'application/json' }, timeout: 30000,
15
15
  function debounce(fn, ms = 250) { let timer; const debounced = (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); }; debounced.cancel = () => clearTimeout(timer); return debounced;
16
- function $(selector, context) { if (typeof selector === 'function') { query.ready(selector); return; } return query(selector, context);
16
+ function $(selector, context) { if (typeof selector === 'function') { query.ready(selector); return; } return query(selector, context);
17
17
  })(typeof window !== 'undefined' ? window : globalThis);
package/index.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Lightweight modern frontend library — jQuery-like selectors, reactive
5
5
  * components, SPA router, state management, HTTP client & utilities.
6
6
  *
7
- * @version 0.2.4
7
+ * @version 0.2.5
8
8
  * @license MIT
9
9
  * @see https://z-query.com/docs
10
10
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zero-query",
3
- "version": "0.2.4",
3
+ "version": "0.2.7",
4
4
  "description": "Lightweight modern frontend library — jQuery-like selectors, reactive components, SPA router, and state management with zero dependencies.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -18,8 +18,7 @@
18
18
  ],
19
19
  "scripts": {
20
20
  "build": "node build.js",
21
- "dev": "npx zquery bundle examples/starter-app/scripts/app.js --watch",
22
- "serve": "node examples/starter-app/local-server.js",
21
+ "dev": "node cli.js dev examples/starter-app",
23
22
  "dev-lib": "node build.js --watch",
24
23
  "bundle": "node cli.js bundle",
25
24
  "bundle:app": "node cli.js bundle examples/starter-app/scripts/app.js"