zero-query 0.2.9 → 0.4.9

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.
@@ -0,0 +1,516 @@
1
+ /**
2
+ * cli/commands/dev.js — development server with live-reload
3
+ *
4
+ * Starts a zero-http server that serves the project root, injects an
5
+ * SSE live-reload snippet, auto-resolves zquery.min.js, and watches
6
+ * for file changes (CSS hot-swap, everything else full reload).
7
+ *
8
+ * Features:
9
+ * - Pre-validates JS files on save and reports syntax errors
10
+ * - Broadcasts errors to the browser via SSE with code frames
11
+ * - Full-screen error overlay in the browser (runtime + syntax)
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const vm = require('vm');
19
+
20
+ const { args, flag, option } = require('../args');
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Syntax validation helpers
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /**
27
+ * Generate a code frame string for an error at a given line/column.
28
+ * Shows ~4 lines of context around the error with a caret pointer.
29
+ */
30
+ function generateCodeFrame(source, line, column) {
31
+ const lines = source.split('\n');
32
+ const start = Math.max(0, line - 4);
33
+ const end = Math.min(lines.length, line + 3);
34
+ const pad = String(end).length;
35
+ const frame = [];
36
+
37
+ for (let i = start; i < end; i++) {
38
+ const lineNum = String(i + 1).padStart(pad);
39
+ const marker = i === line - 1 ? '>' : ' ';
40
+ frame.push(`${marker} ${lineNum} | ${lines[i]}`);
41
+ if (i === line - 1 && column > 0) {
42
+ frame.push(` ${' '.repeat(pad)} | ${' '.repeat(column - 1)}^`);
43
+ }
44
+ }
45
+ return frame.join('\n');
46
+ }
47
+
48
+ /**
49
+ * Validate a JavaScript file for syntax errors using Node's VM module.
50
+ * Returns null if valid, or an error descriptor object.
51
+ */
52
+ function validateJS(filePath, relPath) {
53
+ let source;
54
+ try { source = fs.readFileSync(filePath, 'utf-8'); } catch { return null; }
55
+
56
+ // Strip import/export so the VM can parse it as a script.
57
+ // Process line-by-line to guarantee line numbers stay accurate.
58
+ const normalized = source.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
59
+ const stripped = normalized.split('\n').map(line => {
60
+ if (/^\s*import\s+.*from\s+['"]/.test(line)) return ' '.repeat(line.length);
61
+ if (/^\s*import\s+['"]/.test(line)) return ' '.repeat(line.length);
62
+ if (/^\s*export\s*\{/.test(line)) return ' '.repeat(line.length);
63
+ line = line.replace(/^(\s*)export\s+default\s+/, '$1');
64
+ line = line.replace(/^(\s*)export\s+(const|let|var|function|class|async\s+function)\s/, '$1$2 ');
65
+ // import.meta is module-only syntax; replace with a harmless expression
66
+ line = line.replace(/import\.meta\.url/g, "'__meta__'");
67
+ line = line.replace(/import\.meta/g, '({})');
68
+ return line;
69
+ }).join('\n');
70
+
71
+ try {
72
+ new vm.Script(stripped, { filename: relPath });
73
+ return null;
74
+ } catch (err) {
75
+ const line = err.stack ? parseInt((err.stack.match(/:(\d+)/) || [])[1]) || 0 : 0;
76
+ const col = err.stack ? parseInt((err.stack.match(/:(\d+):(\d+)/) || [])[2]) || 0 : 0;
77
+ const frame = line > 0 ? generateCodeFrame(source, line, col) : '';
78
+ return {
79
+ type: err.constructor.name || 'SyntaxError',
80
+ message: err.message,
81
+ file: relPath,
82
+ line,
83
+ column: col,
84
+ frame,
85
+ };
86
+ }
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // SSE live-reload + error overlay client script injected into served HTML
91
+ // ---------------------------------------------------------------------------
92
+
93
+ const LIVE_RELOAD_SNIPPET = `<script>
94
+ (function(){
95
+ // -----------------------------------------------------------------------
96
+ // Error Overlay
97
+ // -----------------------------------------------------------------------
98
+ var overlayEl = null;
99
+
100
+ var OVERLAY_STYLE =
101
+ 'position:fixed;top:0;left:0;width:100%;height:100%;' +
102
+ 'background:rgba(0,0,0,0.92);color:#fff;z-index:2147483647;' +
103
+ 'font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;' +
104
+ 'font-size:13px;overflow-y:auto;padding:0;margin:0;box-sizing:border-box;';
105
+
106
+ var HEADER_STYLE =
107
+ 'padding:20px 24px 12px;border-bottom:1px solid rgba(255,255,255,0.1);' +
108
+ 'display:flex;align-items:flex-start;justify-content:space-between;';
109
+
110
+ var TYPE_STYLE =
111
+ 'display:inline-block;padding:3px 8px;border-radius:4px;font-size:11px;' +
112
+ 'font-weight:700;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px;';
113
+
114
+ function createOverlay(data) {
115
+ removeOverlay();
116
+ var wrap = document.createElement('div');
117
+ wrap.id = '__zq_error_overlay';
118
+ wrap.setAttribute('style', OVERLAY_STYLE);
119
+ // keyboard focus for esc
120
+ wrap.setAttribute('tabindex', '-1');
121
+
122
+ var isSyntax = data.type && /syntax|parse/i.test(data.type);
123
+ var badgeColor = isSyntax ? '#e74c3c' : '#e67e22';
124
+
125
+ var html = '';
126
+ // Header row
127
+ html += '<div style="' + HEADER_STYLE + '">';
128
+ html += '<div>';
129
+ html += '<span style="' + TYPE_STYLE + 'background:' + badgeColor + ';">' + esc(data.type || 'Error') + '</span>';
130
+ html += '<div style="font-size:18px;font-weight:600;line-height:1.4;color:#ff6b6b;margin-top:4px;">';
131
+ html += esc(data.message || 'Unknown error');
132
+ html += '</div>';
133
+ html += '</div>';
134
+ // Close button
135
+ html += '<button id="__zq_close" style="' +
136
+ 'background:none;border:1px solid rgba(255,255,255,0.2);color:#999;' +
137
+ 'font-size:20px;cursor:pointer;border-radius:6px;width:32px;height:32px;' +
138
+ 'display:flex;align-items:center;justify-content:center;flex-shrink:0;' +
139
+ 'margin-left:16px;transition:all 0.15s;"' +
140
+ ' onmouseover="this.style.color=\\'#fff\\';this.style.borderColor=\\'rgba(255,255,255,0.5)\\'"' +
141
+ ' onmouseout="this.style.color=\\'#999\\';this.style.borderColor=\\'rgba(255,255,255,0.2)\\'"' +
142
+ '>&times;</button>';
143
+ html += '</div>';
144
+
145
+ // File location
146
+ if (data.file) {
147
+ html += '<div style="padding:10px 24px;color:#8be9fd;font-size:13px;">';
148
+ html += '<span style="color:#888;">File: </span>' + esc(data.file);
149
+ if (data.line) html += '<span style="color:#888;">:</span>' + data.line;
150
+ if (data.column) html += '<span style="color:#888;">:</span>' + data.column;
151
+ html += '</div>';
152
+ }
153
+
154
+ // Code frame
155
+ if (data.frame) {
156
+ html += '<pre style="' +
157
+ 'margin:0;padding:16px 24px;background:rgba(255,255,255,0.04);' +
158
+ 'border-top:1px solid rgba(255,255,255,0.06);' +
159
+ 'border-bottom:1px solid rgba(255,255,255,0.06);' +
160
+ 'overflow-x:auto;line-height:1.6;font-size:13px;' +
161
+ '">';
162
+ var frameLines = data.frame.split('\\n');
163
+ for (var i = 0; i < frameLines.length; i++) {
164
+ var fl = frameLines[i];
165
+ if (fl.charAt(0) === '>') {
166
+ html += '<span style="color:#ff6b6b;font-weight:600;">' + esc(fl) + '</span>\\n';
167
+ } else if (fl.indexOf('^') !== -1 && fl.trim().replace(/[\\s|^]/g, '') === '') {
168
+ html += '<span style="color:#e74c3c;font-weight:700;">' + esc(fl) + '</span>\\n';
169
+ } else {
170
+ html += '<span style="color:#999;">' + esc(fl) + '</span>\\n';
171
+ }
172
+ }
173
+ html += '</pre>';
174
+ }
175
+
176
+ // Stack trace
177
+ if (data.stack) {
178
+ html += '<div style="padding:16px 24px;">';
179
+ html += '<div style="color:#888;font-size:11px;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px;">Stack Trace</div>';
180
+ html += '<pre style="margin:0;color:#bbb;font-size:12px;line-height:1.7;white-space:pre-wrap;word-break:break-word;">';
181
+ html += esc(data.stack);
182
+ html += '</pre></div>';
183
+ }
184
+
185
+ // Tip
186
+ html += '<div style="padding:16px 24px;color:#555;font-size:11px;border-top:1px solid rgba(255,255,255,0.06);">';
187
+ html += 'Fix the error and save — the overlay will clear automatically. Press <kbd style="' +
188
+ 'background:rgba(255,255,255,0.1);padding:1px 6px;border-radius:3px;font-size:11px;' +
189
+ '">Esc</kbd> to dismiss.';
190
+ html += '</div>';
191
+
192
+ wrap.innerHTML = html;
193
+ document.body.appendChild(wrap);
194
+ overlayEl = wrap;
195
+
196
+ // Close button handler
197
+ var closeBtn = document.getElementById('__zq_close');
198
+ if (closeBtn) closeBtn.addEventListener('click', removeOverlay);
199
+ wrap.addEventListener('keydown', function(e) {
200
+ if (e.key === 'Escape') removeOverlay();
201
+ });
202
+ wrap.focus();
203
+ }
204
+
205
+ function removeOverlay() {
206
+ if (overlayEl && overlayEl.parentNode) {
207
+ overlayEl.parentNode.removeChild(overlayEl);
208
+ }
209
+ overlayEl = null;
210
+ }
211
+
212
+ function esc(s) {
213
+ var d = document.createElement('div');
214
+ d.appendChild(document.createTextNode(s));
215
+ return d.innerHTML;
216
+ }
217
+
218
+ // -----------------------------------------------------------------------
219
+ // Runtime error handlers
220
+ // -----------------------------------------------------------------------
221
+ window.addEventListener('error', function(e) {
222
+ if (!e.filename) return;
223
+ var data = {
224
+ type: (e.error && e.error.constructor && e.error.constructor.name) || 'Error',
225
+ message: e.message || String(e.error),
226
+ file: e.filename.replace(location.origin, ''),
227
+ line: e.lineno || 0,
228
+ column: e.colno || 0,
229
+ stack: e.error && e.error.stack ? cleanStack(e.error.stack) : ''
230
+ };
231
+ createOverlay(data);
232
+ logToConsole(data);
233
+ });
234
+
235
+ window.addEventListener('unhandledrejection', function(e) {
236
+ var err = e.reason;
237
+ var data = {
238
+ type: 'Unhandled Promise Rejection',
239
+ message: err && err.message ? err.message : String(err),
240
+ stack: err && err.stack ? cleanStack(err.stack) : ''
241
+ };
242
+ createOverlay(data);
243
+ logToConsole(data);
244
+ });
245
+
246
+ function cleanStack(stack) {
247
+ return stack.split('\\n')
248
+ .filter(function(l) {
249
+ return l.indexOf('__zq_') === -1 && l.indexOf('EventSource') === -1;
250
+ })
251
+ .map(function(l) {
252
+ return l.replace(location.origin, '');
253
+ })
254
+ .join('\\n');
255
+ }
256
+
257
+ function logToConsole(data) {
258
+ var msg = '\\n%c zQuery DevError %c ' + data.type + ': ' + data.message;
259
+ if (data.file) msg += '\\n at ' + data.file + (data.line ? ':' + data.line : '') + (data.column ? ':' + data.column : '');
260
+ console.error(msg, 'background:#e74c3c;color:#fff;padding:2px 6px;border-radius:3px;font-weight:700;', 'color:inherit;');
261
+ if (data.frame) console.error(data.frame);
262
+ }
263
+
264
+ // -----------------------------------------------------------------------
265
+ // SSE connection (live-reload + error events)
266
+ // -----------------------------------------------------------------------
267
+ var es, timer;
268
+ function connect(){
269
+ es = new EventSource('/__zq_reload');
270
+
271
+ es.addEventListener('reload', function(){
272
+ removeOverlay();
273
+ location.reload();
274
+ });
275
+
276
+ es.addEventListener('css', function(e){
277
+ var sheets = document.querySelectorAll('link[rel="stylesheet"]');
278
+ sheets.forEach(function(l){
279
+ var href = l.getAttribute('href');
280
+ if(!href) return;
281
+ var sep = href.indexOf('?') >= 0 ? '&' : '?';
282
+ l.setAttribute('href', href.replace(/[?&]_zqr=\\\\d+/, '') + sep + '_zqr=' + Date.now());
283
+ });
284
+ });
285
+
286
+ es.addEventListener('error:syntax', function(e){
287
+ try {
288
+ var data = JSON.parse(e.data);
289
+ createOverlay(data);
290
+ logToConsole(data);
291
+ } catch(_){}
292
+ });
293
+
294
+ es.addEventListener('error:clear', function(){
295
+ removeOverlay();
296
+ });
297
+
298
+ es.onerror = function(){
299
+ es.close();
300
+ clearTimeout(timer);
301
+ timer = setTimeout(connect, 2000);
302
+ };
303
+ }
304
+ connect();
305
+ })();
306
+ </script>`;
307
+
308
+ // ---------------------------------------------------------------------------
309
+ // devServer
310
+ // ---------------------------------------------------------------------------
311
+
312
+ function devServer() {
313
+ let zeroHttp;
314
+ try {
315
+ zeroHttp = require('zero-http');
316
+ } catch (_) {
317
+ console.error(`\n ✗ zero-http is required for the dev server.`);
318
+ console.error(` Install it: npm install zero-http --save-dev\n`);
319
+ process.exit(1);
320
+ }
321
+
322
+ const { createApp, static: serveStatic } = zeroHttp;
323
+
324
+ // Determine the project root to serve
325
+ let root = null;
326
+ for (let i = 1; i < args.length; i++) {
327
+ if (!args[i].startsWith('-') && args[i - 1] !== '-p' && args[i - 1] !== '--port') {
328
+ root = path.resolve(process.cwd(), args[i]);
329
+ break;
330
+ }
331
+ }
332
+ if (!root) {
333
+ const candidates = [
334
+ process.cwd(),
335
+ path.join(process.cwd(), 'public'),
336
+ path.join(process.cwd(), 'src'),
337
+ ];
338
+ for (const c of candidates) {
339
+ if (fs.existsSync(path.join(c, 'index.html'))) { root = c; break; }
340
+ }
341
+ if (!root) root = process.cwd();
342
+ }
343
+
344
+ const PORT = parseInt(option('port', 'p', '3100'));
345
+
346
+ // SSE clients
347
+ const sseClients = new Set();
348
+
349
+ const app = createApp();
350
+
351
+ // SSE endpoint
352
+ app.get('/__zq_reload', (req, res) => {
353
+ const sse = res.sse({ keepAlive: 30000, keepAliveComment: 'ping' });
354
+ sseClients.add(sse);
355
+ sse.on('close', () => sseClients.delete(sse));
356
+ });
357
+
358
+ // Auto-resolve zquery.min.js
359
+ // __dirname is cli/commands/, package root is two levels up
360
+ const pkgRoot = path.resolve(__dirname, '..', '..');
361
+ const noIntercept = flag('no-intercept');
362
+
363
+ app.use((req, res, next) => {
364
+ if (noIntercept) return next();
365
+ const basename = path.basename(req.url.split('?')[0]).toLowerCase();
366
+ if (basename !== 'zquery.min.js') return next();
367
+
368
+ const candidates = [
369
+ path.join(pkgRoot, 'dist', 'zquery.min.js'),
370
+ path.join(root, 'node_modules', 'zero-query', 'dist', 'zquery.min.js'),
371
+ ];
372
+ for (const p of candidates) {
373
+ if (fs.existsSync(p)) {
374
+ res.set('Content-Type', 'application/javascript; charset=utf-8');
375
+ res.set('Cache-Control', 'no-cache');
376
+ res.send(fs.readFileSync(p, 'utf-8'));
377
+ return;
378
+ }
379
+ }
380
+ next();
381
+ });
382
+
383
+ // Static file serving
384
+ app.use(serveStatic(root, { index: false, dotfiles: 'ignore' }));
385
+
386
+ // SPA fallback — inject live-reload
387
+ app.get('*', (req, res) => {
388
+ if (path.extname(req.url) && path.extname(req.url) !== '.html') {
389
+ res.status(404).send('Not Found');
390
+ return;
391
+ }
392
+ const indexPath = path.join(root, 'index.html');
393
+ if (!fs.existsSync(indexPath)) {
394
+ res.status(404).send('index.html not found');
395
+ return;
396
+ }
397
+ let html = fs.readFileSync(indexPath, 'utf-8');
398
+ if (html.includes('</body>')) {
399
+ html = html.replace('</body>', LIVE_RELOAD_SNIPPET + '\n</body>');
400
+ } else {
401
+ html += LIVE_RELOAD_SNIPPET;
402
+ }
403
+ res.html(html);
404
+ });
405
+
406
+ // Broadcast helper
407
+ function broadcast(eventType, data) {
408
+ for (const sse of sseClients) {
409
+ try { sse.event(eventType, data || ''); } catch (_) { sseClients.delete(sse); }
410
+ }
411
+ }
412
+
413
+ // File watcher
414
+ const IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', '.cache']);
415
+ let debounceTimer;
416
+
417
+ function shouldWatch(filename) {
418
+ if (!filename) return false;
419
+ if (filename.startsWith('.')) return false;
420
+ return true;
421
+ }
422
+
423
+ function isIgnored(filepath) {
424
+ const parts = filepath.split(path.sep);
425
+ return parts.some(p => IGNORE_DIRS.has(p));
426
+ }
427
+
428
+ function collectWatchDirs(dir) {
429
+ const dirs = [dir];
430
+ try {
431
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
432
+ for (const entry of entries) {
433
+ if (!entry.isDirectory()) continue;
434
+ if (IGNORE_DIRS.has(entry.name)) continue;
435
+ const sub = path.join(dir, entry.name);
436
+ dirs.push(...collectWatchDirs(sub));
437
+ }
438
+ } catch (_) {}
439
+ return dirs;
440
+ }
441
+
442
+ const watchDirs = collectWatchDirs(root);
443
+ const watchers = [];
444
+
445
+ // Track current error state to know when to clear
446
+ let currentError = null;
447
+
448
+ for (const dir of watchDirs) {
449
+ try {
450
+ const watcher = fs.watch(dir, (eventType, filename) => {
451
+ if (!shouldWatch(filename)) return;
452
+ const fullPath = path.join(dir, filename || '');
453
+ if (isIgnored(fullPath)) return;
454
+
455
+ clearTimeout(debounceTimer);
456
+ debounceTimer = setTimeout(() => {
457
+ const rel = path.relative(root, fullPath).replace(/\\/g, '/');
458
+ const ext = path.extname(filename).toLowerCase();
459
+ const now = new Date().toLocaleTimeString();
460
+
461
+ if (ext === '.css') {
462
+ console.log(` ${now} \x1b[35m css \x1b[0m ${rel}`);
463
+ broadcast('css', rel);
464
+ return;
465
+ }
466
+
467
+ // Validate JS files for syntax errors before triggering reload
468
+ if (ext === '.js') {
469
+ const err = validateJS(fullPath, rel);
470
+ if (err) {
471
+ currentError = rel;
472
+ console.log(` ${now} \x1b[31m error \x1b[0m ${rel}`);
473
+ console.log(` \x1b[31m${err.type}: ${err.message}\x1b[0m`);
474
+ if (err.line) console.log(` \x1b[2mat line ${err.line}${err.column ? ':' + err.column : ''}\x1b[0m`);
475
+ broadcast('error:syntax', JSON.stringify(err));
476
+ return;
477
+ }
478
+ // File was fixed — clear previous error if it was in this file
479
+ if (currentError === rel) {
480
+ currentError = null;
481
+ broadcast('error:clear', '');
482
+ }
483
+ }
484
+
485
+ console.log(` ${now} \x1b[36m reload \x1b[0m ${rel}`);
486
+ broadcast('reload', rel);
487
+ }, 100);
488
+ });
489
+ watchers.push(watcher);
490
+ } catch (_) {}
491
+ }
492
+
493
+ app.listen(PORT, () => {
494
+ console.log(`\n \x1b[1mzQuery Dev Server\x1b[0m`);
495
+ console.log(` \x1b[2m${'-'.repeat(40)}\x1b[0m`);
496
+ console.log(` Local: \x1b[36mhttp://localhost:${PORT}/\x1b[0m`);
497
+ console.log(` Root: ${path.relative(process.cwd(), root) || '.'}`);
498
+ console.log(` Live Reload: \x1b[32menabled\x1b[0m (SSE)`);
499
+ console.log(` Overlay: \x1b[32menabled\x1b[0m (syntax + runtime errors)`);
500
+ if (noIntercept) console.log(` Intercept: \x1b[33mdisabled\x1b[0m (--no-intercept)`);
501
+ console.log(` Watching: all files in ${watchDirs.length} director${watchDirs.length === 1 ? 'y' : 'ies'}`);
502
+ console.log(` \x1b[2m${'-'.repeat(40)}\x1b[0m`);
503
+ console.log(` Press Ctrl+C to stop\n`);
504
+ });
505
+
506
+ // Graceful shutdown
507
+ process.on('SIGINT', () => {
508
+ console.log('\n Shutting down...');
509
+ watchers.forEach(w => w.close());
510
+ for (const sse of sseClients) { try { sse.close(); } catch (_) {} }
511
+ app.close(() => process.exit(0));
512
+ setTimeout(() => process.exit(0), 1000);
513
+ });
514
+ }
515
+
516
+ module.exports = devServer;
package/cli/help.js ADDED
@@ -0,0 +1,92 @@
1
+ // cli/help.js — show CLI usage information
2
+
3
+ function showHelp() {
4
+ console.log(`
5
+ zQuery CLI \u2014 create, dev, bundle & build
6
+
7
+ COMMANDS
8
+
9
+ create [dir] Scaffold a new zQuery project
10
+ Creates index.html, scripts/, styles/ in the target directory
11
+ (defaults to the current directory)
12
+
13
+ dev [root] Start a dev server with live-reload
14
+ --port, -p <number> Port number (default: 3100)
15
+ --no-intercept Disable auto-resolution of zquery.min.js
16
+ (serve the on-disk vendor copy instead)
17
+
18
+ Includes error overlay: syntax errors are
19
+ caught on save and shown as a full-screen
20
+ overlay in the browser. Runtime errors and
21
+ unhandled rejections are also captured.
22
+
23
+ bundle [dir] Bundle app ES modules into a single file
24
+ --out, -o <path> Output directory (default: dist/ next to index.html)
25
+ --html <file> Use a specific HTML file (default: auto-detected)
26
+
27
+ build Build the zQuery library \u2192 dist/ --watch, -w Watch src/ and rebuild on changes (must be run from the project root where src/ lives)
28
+
29
+ SMART DEFAULTS
30
+
31
+ The bundler works with zero flags for typical projects:
32
+ \u2022 Entry is auto-detected with strict precedence:
33
+ 1. index.html first, then other .html files
34
+ 2. Within HTML: module script pointing to app.js, else first module script
35
+ 3. JS scan: $.router( first (entry point), then $.mount( / $.store(
36
+ 4. Convention fallbacks (scripts/app.js, app.js, etc.)
37
+ \u2022 zquery.min.js is always embedded (auto-built from source if not found)
38
+ \u2022 index.html is rewritten for both server and local (file://) use
39
+ \u2022 Output goes to dist/server/ and dist/local/ next to the detected index.html
40
+
41
+ OUTPUT
42
+
43
+ The bundler produces two self-contained sub-directories:
44
+
45
+ dist/server/ deploy to your web server
46
+ index.html has <base href="/"> for SPA deep routes
47
+ z-<entry>.<hash>.js readable bundle
48
+ z-<entry>.<hash>.min.js minified bundle
49
+
50
+ dist/local/ open from disk (file://)
51
+ index.html relative paths, no <base> tag
52
+ z-<entry>.<hash>.js same bundle
53
+
54
+ Previous hashed builds are automatically cleaned on each rebuild.
55
+
56
+ DEVELOPMENT
57
+
58
+ zquery dev start a dev server with live-reload (port 3100)
59
+ zquery dev --port 8080 custom port
60
+
61
+ The dev server includes a full-screen error overlay:
62
+ • JS files are syntax-checked on save — errors block reload
63
+ and show an overlay with exact file, line:column, and code frame
64
+ • Runtime errors and unhandled rejections are also captured
65
+ • The overlay auto-clears when the file is fixed and saved
66
+ • Press Esc or click × to dismiss manually
67
+
68
+ EXAMPLES
69
+
70
+ # Scaffold a new project and start developing
71
+ zquery create my-app && zquery dev my-app
72
+
73
+ # Start dev server with live-reload
74
+ zquery dev my-app
75
+
76
+ # Build the library only
77
+ zquery build
78
+
79
+ # Bundle an app from the project root
80
+ zquery bundle my-app/
81
+
82
+ # Custom output directory
83
+ zquery bundle my-app/ -o build/
84
+
85
+ The bundler walks the ES module import graph starting from the entry
86
+ file, topologically sorts dependencies, strips import/export syntax,
87
+ and concatenates everything into a single IIFE with content-hashed
88
+ filenames for cache-busting. No dependencies needed \u2014 just Node.js.
89
+ `);
90
+ }
91
+
92
+ module.exports = showHelp;
package/cli/index.js ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * zQuery CLI — entry point
5
+ *
6
+ * Dispatches to the appropriate command module under cli/commands/.
7
+ * Run `zquery --help` for full usage information.
8
+ */
9
+
10
+ const { join } = require('path');
11
+ const { watch } = require('fs');
12
+
13
+ const { args, flag } = require('./args');
14
+ const buildLibrary = require('./commands/build');
15
+ const bundleApp = require('./commands/bundle');
16
+ const devServer = require('./commands/dev');
17
+ const createProject = require('./commands/create');
18
+ const showHelp = require('./help');
19
+
20
+ const command = args[0];
21
+
22
+ if (!command || command === '--help' || command === '-h' || command === 'help') {
23
+ showHelp();
24
+ } else if (command === 'create') {
25
+ createProject(args);
26
+ } else if (command === 'build') {
27
+ console.log('\n zQuery Library Build\n');
28
+ buildLibrary();
29
+
30
+ if (flag('watch', 'w')) {
31
+ console.log(' Watching src/ for changes...\n');
32
+ const srcDir = join(process.cwd(), 'src');
33
+ let debounce;
34
+
35
+ function rebuild() {
36
+ clearTimeout(debounce);
37
+ debounce = setTimeout(() => {
38
+ console.log(' Rebuilding...\n');
39
+ try { buildLibrary(); } catch (e) { console.error(e.message); }
40
+ }, 200);
41
+ }
42
+
43
+ watch(srcDir, { recursive: true }, rebuild);
44
+ watch(join(process.cwd(), 'index.js'), rebuild);
45
+ }
46
+ } else if (command === 'bundle') {
47
+ bundleApp();
48
+ } else if (command === 'dev') {
49
+ devServer();
50
+ } else {
51
+ console.error(`\n Unknown command: ${command}\n Run "zquery --help" for usage.\n`);
52
+ process.exit(1);
53
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tony Wiedman "molex"
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.