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
@@ -0,0 +1,366 @@
1
+ /**
2
+ * cli/commands/dev/overlay.js — Client-side error overlay + SSE live-reload
3
+ *
4
+ * Returns an HTML <script> snippet that is injected before </body> in
5
+ * every HTML response served by the dev server. Responsibilities:
6
+ *
7
+ * 1. Error Overlay — full-screen dark overlay with code frames, stack
8
+ * traces, and ZQueryError metadata. Dismissable via Esc or ×.
9
+ * 2. Runtime error hooks — window.onerror, unhandledrejection, AND
10
+ * the zQuery $.onError() hook so framework-level errors are
11
+ * surfaced in the overlay automatically.
12
+ * 3. SSE connection — listens for reload / css / error:syntax /
13
+ * error:clear events from the dev server watcher.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ // The snippet is a self-contained IIFE — no external dependencies.
19
+ // It must work in all browsers that support EventSource (IE11 excluded).
20
+
21
+ const OVERLAY_SCRIPT = `<script>
22
+ (function(){
23
+ // =====================================================================
24
+ // Error overlay
25
+ // =====================================================================
26
+ var overlayEl = null;
27
+
28
+ var OVERLAY_CSS =
29
+ 'position:fixed;top:0;left:0;width:100%;height:100%;' +
30
+ 'background:rgba(0,0,0,0.92);color:#fff;z-index:2147483647;' +
31
+ 'font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;' +
32
+ 'font-size:13px;overflow-y:auto;padding:0;margin:0;box-sizing:border-box;';
33
+
34
+ var HEADER_CSS =
35
+ 'padding:20px 24px 12px;border-bottom:1px solid rgba(255,255,255,0.1);' +
36
+ 'display:flex;align-items:flex-start;justify-content:space-between;';
37
+
38
+ var BADGE_CSS =
39
+ 'display:inline-block;padding:3px 8px;border-radius:4px;font-size:11px;' +
40
+ 'font-weight:700;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px;';
41
+
42
+ // Map ZQueryError code prefixes to colours so devs can see at a glance
43
+ // which subsystem produced the error.
44
+ var CODE_COLORS = {
45
+ 'ZQ_REACTIVE': '#9b59b6',
46
+ 'ZQ_SIGNAL': '#9b59b6',
47
+ 'ZQ_EFFECT': '#9b59b6',
48
+ 'ZQ_EXPR': '#2980b9',
49
+ 'ZQ_COMP': '#16a085',
50
+ 'ZQ_ROUTER': '#d35400',
51
+ 'ZQ_STORE': '#8e44ad',
52
+ 'ZQ_HTTP': '#2c3e50',
53
+ 'ZQ_DEV': '#e74c3c',
54
+ 'ZQ_INVALID': '#7f8c8d',
55
+ };
56
+
57
+ function badgeColor(data) {
58
+ if (data.code) {
59
+ var keys = Object.keys(CODE_COLORS);
60
+ for (var i = 0; i < keys.length; i++) {
61
+ if (data.code.indexOf(keys[i]) === 0) return CODE_COLORS[keys[i]];
62
+ }
63
+ }
64
+ if (data.type && /syntax|parse/i.test(data.type)) return '#e74c3c';
65
+ return '#e67e22';
66
+ }
67
+
68
+ function esc(s) {
69
+ var d = document.createElement('div');
70
+ d.appendChild(document.createTextNode(s));
71
+ return d.innerHTML;
72
+ }
73
+
74
+ function createOverlay(data) {
75
+ removeOverlay();
76
+ var wrap = document.createElement('div');
77
+ wrap.id = '__zq_error_overlay';
78
+ wrap.setAttribute('style', OVERLAY_CSS);
79
+ wrap.setAttribute('tabindex', '-1');
80
+
81
+ var color = badgeColor(data);
82
+ var html = '';
83
+
84
+ // ----- header row -----
85
+ html += '<div style="' + HEADER_CSS + '">';
86
+ html += '<div>';
87
+
88
+ // Error code badge (if present)
89
+ if (data.code) {
90
+ html += '<span style="' + BADGE_CSS + 'background:' + color + ';margin-right:6px;">' + esc(data.code) + '</span>';
91
+ }
92
+ // Type badge
93
+ html += '<span style="' + BADGE_CSS + 'background:' + (data.code ? 'rgba(255,255,255,0.1)' : color) + ';">' + esc(data.type || 'Error') + '</span>';
94
+
95
+ // Message
96
+ html += '<div style="font-size:18px;font-weight:600;line-height:1.4;color:#ff6b6b;margin-top:4px;">';
97
+ html += esc(data.message || 'Unknown error');
98
+ html += '</div></div>';
99
+
100
+ // Close button
101
+ html += '<button id="__zq_close" style="' +
102
+ 'background:none;border:1px solid rgba(255,255,255,0.2);color:#999;' +
103
+ 'font-size:20px;cursor:pointer;border-radius:6px;width:32px;height:32px;' +
104
+ 'display:flex;align-items:center;justify-content:center;flex-shrink:0;' +
105
+ 'margin-left:16px;transition:all 0.15s;"' +
106
+ ' onmouseover="this.style.color=\\'#fff\\';this.style.borderColor=\\'rgba(255,255,255,0.5)\\'"' +
107
+ ' onmouseout="this.style.color=\\'#999\\';this.style.borderColor=\\'rgba(255,255,255,0.2)\\'"' +
108
+ '>&times;</button>';
109
+ html += '</div>';
110
+
111
+ // ----- file location -----
112
+ if (data.file) {
113
+ html += '<div style="padding:10px 24px;color:#8be9fd;font-size:13px;">';
114
+ html += '<span style="color:#888;">File: </span>' + esc(data.file);
115
+ if (data.line) html += '<span style="color:#888;">:</span>' + data.line;
116
+ if (data.column) html += '<span style="color:#888;">:</span>' + data.column;
117
+ html += '</div>';
118
+ }
119
+
120
+ // ----- ZQueryError context (key/value pairs) -----
121
+ if (data.context && typeof data.context === 'object' && Object.keys(data.context).length) {
122
+ html += '<div style="padding:8px 24px;display:flex;flex-wrap:wrap;gap:8px;">';
123
+ var ctxKeys = Object.keys(data.context);
124
+ for (var ci = 0; ci < ctxKeys.length; ci++) {
125
+ var k = ctxKeys[ci], v = data.context[k];
126
+ html += '<span style="background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);' +
127
+ 'padding:3px 10px;border-radius:4px;font-size:12px;">' +
128
+ '<span style="color:#888;">' + esc(k) + ': </span>' +
129
+ '<span style="color:#f1fa8c;">' + esc(typeof v === 'object' ? JSON.stringify(v) : String(v)) + '</span>' +
130
+ '</span>';
131
+ }
132
+ html += '</div>';
133
+ }
134
+
135
+ // ----- code frame -----
136
+ if (data.frame) {
137
+ html += '<pre style="' +
138
+ 'margin:0;padding:16px 24px;background:rgba(255,255,255,0.04);' +
139
+ 'border-top:1px solid rgba(255,255,255,0.06);' +
140
+ 'border-bottom:1px solid rgba(255,255,255,0.06);' +
141
+ 'overflow-x:auto;line-height:1.6;font-size:13px;">';
142
+ var lines = data.frame.split('\\n');
143
+ for (var fi = 0; fi < lines.length; fi++) {
144
+ var fl = lines[fi];
145
+ if (fl.charAt(0) === '>') {
146
+ html += '<span style="color:#ff6b6b;font-weight:600;">' + esc(fl) + '</span>\\n';
147
+ } else if (fl.indexOf('^') !== -1 && fl.trim().replace(/[\\s|^]/g, '') === '') {
148
+ html += '<span style="color:#e74c3c;font-weight:700;">' + esc(fl) + '</span>\\n';
149
+ } else {
150
+ html += '<span style="color:#999;">' + esc(fl) + '</span>\\n';
151
+ }
152
+ }
153
+ html += '</pre>';
154
+ }
155
+
156
+ // ----- stack trace -----
157
+ if (data.stack) {
158
+ html += '<div style="padding:16px 24px;">';
159
+ html += '<div style="color:#888;font-size:11px;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px;">Stack Trace</div>';
160
+ html += '<pre style="margin:0;color:#bbb;font-size:12px;line-height:1.7;white-space:pre-wrap;word-break:break-word;">';
161
+ html += esc(data.stack);
162
+ html += '</pre></div>';
163
+ }
164
+
165
+ // ----- tip -----
166
+ html += '<div style="padding:16px 24px;color:#555;font-size:11px;border-top:1px solid rgba(255,255,255,0.06);">';
167
+ html += 'Fix the error and save \\u2014 the overlay will clear automatically. Press <kbd style="' +
168
+ 'background:rgba(255,255,255,0.1);padding:1px 6px;border-radius:3px;font-size:11px;' +
169
+ '">Esc</kbd> to dismiss.';
170
+ html += '</div>';
171
+
172
+ wrap.innerHTML = html;
173
+ document.body.appendChild(wrap);
174
+ overlayEl = wrap;
175
+
176
+ var closeBtn = document.getElementById('__zq_close');
177
+ if (closeBtn) closeBtn.addEventListener('click', removeOverlay);
178
+ wrap.addEventListener('keydown', function(e) { if (e.key === 'Escape') removeOverlay(); });
179
+ wrap.focus();
180
+ }
181
+
182
+ function removeOverlay() {
183
+ if (overlayEl && overlayEl.parentNode) overlayEl.parentNode.removeChild(overlayEl);
184
+ overlayEl = null;
185
+ }
186
+
187
+ // =====================================================================
188
+ // Console helper
189
+ // =====================================================================
190
+ function logToConsole(data) {
191
+ var label = data.code ? data.code + ' ' : '';
192
+ var msg = '\\n%c zQuery DevError %c ' + label + data.type + ': ' + data.message;
193
+ if (data.file) msg += '\\n at ' + data.file + (data.line ? ':' + data.line : '') + (data.column ? ':' + data.column : '');
194
+ console.error(msg, 'background:#e74c3c;color:#fff;padding:2px 6px;border-radius:3px;font-weight:700;', 'color:inherit;');
195
+ if (data.frame) console.error(data.frame);
196
+ }
197
+
198
+ function cleanStack(stack) {
199
+ return stack.split('\\n')
200
+ .filter(function(l) { return l.indexOf('__zq_') === -1 && l.indexOf('EventSource') === -1; })
201
+ .map(function(l) { return l.replace(location.origin, ''); })
202
+ .join('\\n');
203
+ }
204
+
205
+ // =====================================================================
206
+ // Runtime error hooks
207
+ // =====================================================================
208
+ window.addEventListener('error', function(e) {
209
+ if (!e.filename) return;
210
+ var err = e.error || {};
211
+ var data = {
212
+ code: err.code || '',
213
+ type: (err.constructor && err.constructor.name) || 'Error',
214
+ message: e.message || String(err),
215
+ file: e.filename.replace(location.origin, ''),
216
+ line: e.lineno || 0,
217
+ column: e.colno || 0,
218
+ context: err.context || null,
219
+ stack: err.stack ? cleanStack(err.stack) : ''
220
+ };
221
+ createOverlay(data);
222
+ logToConsole(data);
223
+ });
224
+
225
+ window.addEventListener('unhandledrejection', function(e) {
226
+ var err = e.reason || {};
227
+ var data = {
228
+ code: err.code || '',
229
+ type: err.name === 'ZQueryError' ? 'ZQueryError' : 'Unhandled Promise Rejection',
230
+ message: err.message || String(err),
231
+ context: err.context || null,
232
+ stack: err.stack ? cleanStack(err.stack) : ''
233
+ };
234
+ createOverlay(data);
235
+ logToConsole(data);
236
+ });
237
+
238
+ // =====================================================================
239
+ // Hook into zQuery's $.onError() when the library is loaded
240
+ // =====================================================================
241
+ function hookZQueryErrors() {
242
+ // $.onError is set by the framework — wait for it
243
+ if (typeof $ !== 'undefined' && typeof $.onError === 'function') {
244
+ $.onError(function(zqErr) {
245
+ var data = {
246
+ code: zqErr.code || '',
247
+ type: 'ZQueryError',
248
+ message: zqErr.message,
249
+ context: zqErr.context || null,
250
+ stack: zqErr.stack ? cleanStack(zqErr.stack) : ''
251
+ };
252
+ createOverlay(data);
253
+ logToConsole(data);
254
+ });
255
+ return;
256
+ }
257
+ // Retry until the library has loaded (max ~5s)
258
+ if (hookZQueryErrors._tries < 50) {
259
+ hookZQueryErrors._tries++;
260
+ setTimeout(hookZQueryErrors, 100);
261
+ }
262
+ }
263
+ hookZQueryErrors._tries = 0;
264
+ // Defer so the page's own scripts load first
265
+ if (document.readyState === 'loading') {
266
+ document.addEventListener('DOMContentLoaded', hookZQueryErrors);
267
+ } else {
268
+ setTimeout(hookZQueryErrors, 0);
269
+ }
270
+
271
+ // =====================================================================
272
+ // SSE connection (live-reload + server-pushed errors)
273
+ // =====================================================================
274
+ var es, reconnectTimer;
275
+
276
+ function connect() {
277
+ es = new EventSource('/__zq_reload');
278
+
279
+ es.addEventListener('reload', function() {
280
+ removeOverlay();
281
+ location.reload();
282
+ });
283
+
284
+ es.addEventListener('css', function(e) {
285
+ var changedPath = (e.data || '').replace(/^\\/+/, '');
286
+ var matched = false;
287
+
288
+ // 1) Try cache-busting matching <link rel="stylesheet"> tags
289
+ var sheets = document.querySelectorAll('link[rel="stylesheet"]');
290
+ sheets.forEach(function(l) {
291
+ var href = l.getAttribute('href');
292
+ if (!href) return;
293
+ var clean = href.replace(/[?&]_zqr=\\d+/, '').replace(/^\\/+/, '');
294
+ if (changedPath && clean.indexOf(changedPath) === -1) return;
295
+ matched = true;
296
+ var sep = href.indexOf('?') >= 0 ? '&' : '?';
297
+ l.setAttribute('href', href.replace(/[?&]_zqr=\\d+/, '') + sep + '_zqr=' + Date.now());
298
+ });
299
+
300
+ // 2) Try hot-swapping scoped <style data-zq-style-urls> elements
301
+ // These come from component styleUrl — the CSS was fetched, scoped,
302
+ // and injected as an inline <style>. We re-fetch and re-scope it.
303
+ if (!matched) {
304
+ var scopedEls = document.querySelectorAll('style[data-zq-style-urls]');
305
+ scopedEls.forEach(function(el) {
306
+ var urls = el.getAttribute('data-zq-style-urls') || '';
307
+ var hit = urls.split(' ').some(function(u) {
308
+ return u && u.replace(/^\\/+/, '').indexOf(changedPath) !== -1;
309
+ });
310
+ if (!hit) return;
311
+ matched = true;
312
+
313
+ var scopeAttr = el.getAttribute('data-zq-scope') || '';
314
+ var inlineStyles = el.getAttribute('data-zq-inline') || '';
315
+
316
+ // Re-fetch all style URLs (cache-busted)
317
+ var urlList = urls.split(' ').filter(Boolean);
318
+ Promise.all(urlList.map(function(u) {
319
+ return fetch(u + (u.indexOf('?') >= 0 ? '&' : '?') + '_zqr=' + Date.now())
320
+ .then(function(r) { return r.text(); });
321
+ })).then(function(results) {
322
+ var raw = (inlineStyles ? inlineStyles + '\\n' : '') + results.join('\\n');
323
+ // Re-scope CSS with the same scope attribute
324
+ if (scopeAttr) {
325
+ var inAt = 0;
326
+ raw = raw.replace(/([^{}]+)\\{|\\}/g, function(m, sel) {
327
+ if (m === '}') { if (inAt > 0) inAt--; return m; }
328
+ var t = sel.trim();
329
+ if (t.charAt(0) === '@') { inAt++; return m; }
330
+ if (inAt > 0 && /^[\\d%\\s,fromto]+$/.test(t.replace(/\\s/g, ''))) return m;
331
+ return sel.split(',').map(function(s) { return '[' + scopeAttr + '] ' + s.trim(); }).join(', ') + ' {';
332
+ });
333
+ }
334
+ el.textContent = raw;
335
+ });
336
+ });
337
+ }
338
+
339
+ // 3) Nothing matched — fall back to full reload
340
+ if (!matched) { location.reload(); }
341
+ });
342
+
343
+ es.addEventListener('error:syntax', function(e) {
344
+ try { var data = JSON.parse(e.data); createOverlay(data); logToConsole(data); } catch(_){}
345
+ });
346
+
347
+ es.addEventListener('error:runtime', function(e) {
348
+ try { var data = JSON.parse(e.data); createOverlay(data); logToConsole(data); } catch(_){}
349
+ });
350
+
351
+ es.addEventListener('error:clear', function() {
352
+ removeOverlay();
353
+ });
354
+
355
+ es.onerror = function() {
356
+ es.close();
357
+ clearTimeout(reconnectTimer);
358
+ reconnectTimer = setTimeout(connect, 2000);
359
+ };
360
+ }
361
+
362
+ connect();
363
+ })();
364
+ </script>`;
365
+
366
+ module.exports = OVERLAY_SCRIPT;
@@ -0,0 +1,158 @@
1
+ /**
2
+ * cli/commands/dev/server.js — HTTP server & SSE broadcasting
3
+ *
4
+ * Creates the zero-http app, serves static files, injects the
5
+ * error-overlay snippet into HTML responses, and manages the
6
+ * SSE connection pool for live-reload events.
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const OVERLAY_SCRIPT = require('./overlay');
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // SSE client pool
17
+ // ---------------------------------------------------------------------------
18
+
19
+ class SSEPool {
20
+ constructor() {
21
+ this._clients = new Set();
22
+ }
23
+
24
+ add(sse) {
25
+ this._clients.add(sse);
26
+ sse.on('close', () => this._clients.delete(sse));
27
+ }
28
+
29
+ broadcast(eventType, data) {
30
+ for (const sse of this._clients) {
31
+ try { sse.event(eventType, data || ''); } catch { this._clients.delete(sse); }
32
+ }
33
+ }
34
+
35
+ closeAll() {
36
+ for (const sse of this._clients) {
37
+ try { sse.close(); } catch { /* ignore */ }
38
+ }
39
+ this._clients.clear();
40
+ }
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Server factory
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /**
48
+ * Prompt the user to auto-install zero-http when it isn't found.
49
+ * Resolves `true` if the user accepts, `false` otherwise.
50
+ */
51
+ function promptInstall() {
52
+ const rl = require('readline').createInterface({
53
+ input: process.stdin,
54
+ output: process.stdout,
55
+ });
56
+ return new Promise((resolve) => {
57
+ rl.question(
58
+ '\n The local dev server requires zero-http, which is not installed.\n' +
59
+ ' This package is only used during development and is not needed\n' +
60
+ ' for building, bundling, or production.\n' +
61
+ ' Install it now? (y/n): ',
62
+ (answer) => {
63
+ rl.close();
64
+ resolve(answer.trim().toLowerCase() === 'y');
65
+ }
66
+ );
67
+ });
68
+ }
69
+
70
+ /**
71
+ * @param {object} opts
72
+ * @param {string} opts.root — absolute path to project root
73
+ * @param {string} opts.htmlEntry — e.g. 'index.html'
74
+ * @param {number} opts.port
75
+ * @param {boolean} opts.noIntercept — skip zquery.min.js auto-resolve
76
+ * @returns {Promise<{ app, pool: SSEPool, listen: Function }>}
77
+ */
78
+ async function createServer({ root, htmlEntry, port, noIntercept }) {
79
+ let zeroHttp;
80
+ try {
81
+ zeroHttp = require('zero-http');
82
+ } catch {
83
+ const ok = await promptInstall();
84
+ if (!ok) {
85
+ console.error('\n ✖ Cannot start dev server without zero-http.\n');
86
+ process.exit(1);
87
+ }
88
+ const { execSync } = require('child_process');
89
+ console.log('\n Installing zero-http...\n');
90
+ execSync('npm install zero-http --save-dev', { stdio: 'inherit' });
91
+ zeroHttp = require('zero-http');
92
+ }
93
+
94
+ const { createApp, static: serveStatic } = zeroHttp;
95
+
96
+ const app = createApp();
97
+ const pool = new SSEPool();
98
+
99
+ // ---- SSE endpoint ----
100
+ app.get('/__zq_reload', (req, res) => {
101
+ const sse = res.sse({ keepAlive: 30000, keepAliveComment: 'ping' });
102
+ pool.add(sse);
103
+ });
104
+
105
+ // ---- Auto-resolve zquery.min.js ----
106
+ const pkgRoot = path.resolve(__dirname, '..', '..', '..');
107
+
108
+ app.use((req, res, next) => {
109
+ if (noIntercept) return next();
110
+ const basename = path.basename(req.url.split('?')[0]).toLowerCase();
111
+ if (basename !== 'zquery.min.js') return next();
112
+
113
+ const candidates = [
114
+ path.join(pkgRoot, 'dist', 'zquery.min.js'),
115
+ path.join(root, 'node_modules', 'zero-query', 'dist', 'zquery.min.js'),
116
+ ];
117
+ for (const p of candidates) {
118
+ if (fs.existsSync(p)) {
119
+ res.set('Content-Type', 'application/javascript; charset=utf-8');
120
+ res.set('Cache-Control', 'no-cache');
121
+ res.send(fs.readFileSync(p, 'utf-8'));
122
+ return;
123
+ }
124
+ }
125
+ next();
126
+ });
127
+
128
+ // ---- Static files ----
129
+ app.use(serveStatic(root, { index: false, dotfiles: 'ignore' }));
130
+
131
+ // ---- SPA fallback — inject overlay/SSE snippet ----
132
+ app.get('*', (req, res) => {
133
+ if (path.extname(req.url) && path.extname(req.url) !== '.html') {
134
+ res.status(404).send('Not Found');
135
+ return;
136
+ }
137
+ const indexPath = path.join(root, htmlEntry);
138
+ if (!fs.existsSync(indexPath)) {
139
+ res.status(404).send(`${htmlEntry} not found`);
140
+ return;
141
+ }
142
+ let html = fs.readFileSync(indexPath, 'utf-8');
143
+ if (html.includes('</body>')) {
144
+ html = html.replace('</body>', OVERLAY_SCRIPT + '\n</body>');
145
+ } else {
146
+ html += OVERLAY_SCRIPT;
147
+ }
148
+ res.html(html);
149
+ });
150
+
151
+ function listen(cb) {
152
+ app.listen(port, cb);
153
+ }
154
+
155
+ return { app, pool, listen };
156
+ }
157
+
158
+ module.exports = { createServer, SSEPool };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * cli/commands/dev/validator.js — JS syntax validation
3
+ *
4
+ * Pre-validates JavaScript files on save using Node's VM module.
5
+ * Returns structured error descriptors with code frames compatible
6
+ * with the browser error overlay.
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const vm = require('vm');
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Code frame generator
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /**
19
+ * Build a code frame showing ~4 lines of context around the error
20
+ * with a caret pointer at the offending column.
21
+ *
22
+ * @param {string} source — full file contents
23
+ * @param {number} line — 1-based line number
24
+ * @param {number} column — 1-based column number
25
+ * @returns {string}
26
+ */
27
+ function generateCodeFrame(source, line, column) {
28
+ const lines = source.split('\n');
29
+ const start = Math.max(0, line - 4);
30
+ const end = Math.min(lines.length, line + 3);
31
+ const pad = String(end).length;
32
+ const frame = [];
33
+
34
+ for (let i = start; i < end; i++) {
35
+ const num = String(i + 1).padStart(pad);
36
+ const marker = i === line - 1 ? '>' : ' ';
37
+ frame.push(`${marker} ${num} | ${lines[i]}`);
38
+ if (i === line - 1 && column > 0) {
39
+ frame.push(` ${' '.repeat(pad)} | ${' '.repeat(column - 1)}^`);
40
+ }
41
+ }
42
+ return frame.join('\n');
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // JS validation
47
+ // ---------------------------------------------------------------------------
48
+
49
+ /**
50
+ * Validate a JavaScript file for syntax errors.
51
+ *
52
+ * Strips ESM import/export statements (preserving line numbers) so the
53
+ * VM can parse module-style code, then compiles via vm.Script.
54
+ *
55
+ * @param {string} filePath — absolute path to the file
56
+ * @param {string} relPath — display-friendly relative path
57
+ * @returns {object|null} — error descriptor, or null if valid
58
+ */
59
+ function validateJS(filePath, relPath) {
60
+ let source;
61
+ try { source = fs.readFileSync(filePath, 'utf-8'); } catch { return null; }
62
+
63
+ const normalized = source.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
64
+ const stripped = normalized.split('\n').map(line => {
65
+ if (/^\s*import\s+.*from\s+['"]/.test(line)) return ' '.repeat(line.length);
66
+ if (/^\s*import\s+['"]/.test(line)) return ' '.repeat(line.length);
67
+ if (/^\s*export\s*\{/.test(line)) return ' '.repeat(line.length);
68
+ line = line.replace(/^(\s*)export\s+default\s+/, '$1');
69
+ line = line.replace(/^(\s*)export\s+(const|let|var|function|class|async\s+function)\s/, '$1$2 ');
70
+ line = line.replace(/import\.meta\.url/g, "'__meta__'");
71
+ line = line.replace(/import\.meta/g, '({})');
72
+ return line;
73
+ }).join('\n');
74
+
75
+ try {
76
+ new vm.Script(stripped, { filename: relPath });
77
+ return null;
78
+ } catch (err) {
79
+ const line = err.stack ? parseInt((err.stack.match(/:(\d+)/) || [])[1]) || 0 : 0;
80
+ const col = err.stack ? parseInt((err.stack.match(/:(\d+):(\d+)/) || [])[2]) || 0 : 0;
81
+ const frame = line > 0 ? generateCodeFrame(source, line, col) : '';
82
+ return {
83
+ code: 'ZQ_DEV_SYNTAX',
84
+ type: err.constructor.name || 'SyntaxError',
85
+ message: err.message,
86
+ file: relPath,
87
+ line,
88
+ column: col,
89
+ frame,
90
+ };
91
+ }
92
+ }
93
+
94
+ module.exports = { generateCodeFrame, validateJS };