zero-query 0.6.3 → 0.8.6
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 +39 -29
- package/cli/commands/build.js +113 -4
- package/cli/commands/bundle.js +392 -29
- package/cli/commands/create.js +1 -1
- package/cli/commands/dev/devtools/index.js +56 -0
- package/cli/commands/dev/devtools/js/components.js +49 -0
- package/cli/commands/dev/devtools/js/core.js +409 -0
- package/cli/commands/dev/devtools/js/elements.js +413 -0
- package/cli/commands/dev/devtools/js/network.js +166 -0
- package/cli/commands/dev/devtools/js/performance.js +73 -0
- package/cli/commands/dev/devtools/js/router.js +105 -0
- package/cli/commands/dev/devtools/js/source.js +132 -0
- package/cli/commands/dev/devtools/js/stats.js +35 -0
- package/cli/commands/dev/devtools/js/tabs.js +79 -0
- package/cli/commands/dev/devtools/panel.html +95 -0
- package/cli/commands/dev/devtools/styles.css +244 -0
- package/cli/commands/dev/index.js +29 -4
- package/cli/commands/dev/logger.js +6 -1
- package/cli/commands/dev/overlay.js +428 -2
- package/cli/commands/dev/server.js +42 -5
- package/cli/commands/dev/watcher.js +59 -1
- package/cli/help.js +8 -5
- package/cli/scaffold/{scripts → app}/app.js +16 -23
- package/cli/scaffold/{scripts → app}/components/about.js +4 -4
- package/cli/scaffold/{scripts → app}/components/api-demo.js +1 -1
- package/cli/scaffold/{scripts → app}/components/contacts/contacts.css +0 -7
- package/cli/scaffold/{scripts → app}/components/contacts/contacts.html +3 -3
- package/cli/scaffold/app/components/home.js +137 -0
- package/cli/scaffold/{scripts → app}/routes.js +1 -1
- package/cli/scaffold/{scripts → app}/store.js +6 -6
- package/cli/scaffold/assets/.gitkeep +0 -0
- package/cli/scaffold/{styles/styles.css → global.css} +4 -2
- package/cli/scaffold/index.html +12 -11
- package/cli/utils.js +111 -6
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +1122 -158
- package/dist/zquery.min.js +3 -16
- package/index.d.ts +129 -1290
- package/index.js +15 -10
- package/package.json +7 -6
- package/src/component.js +172 -49
- package/src/core.js +359 -18
- package/src/diff.js +256 -58
- package/src/expression.js +33 -3
- package/src/reactive.js +37 -5
- package/src/router.js +243 -7
- package/tests/component.test.js +886 -0
- package/tests/core.test.js +977 -0
- package/tests/diff.test.js +525 -0
- package/tests/errors.test.js +162 -0
- package/tests/expression.test.js +482 -0
- package/tests/http.test.js +289 -0
- package/tests/reactive.test.js +339 -0
- package/tests/router.test.js +649 -0
- package/tests/store.test.js +379 -0
- package/tests/utils.test.js +512 -0
- package/types/collection.d.ts +383 -0
- package/types/component.d.ts +217 -0
- package/types/errors.d.ts +103 -0
- package/types/http.d.ts +81 -0
- package/types/misc.d.ts +179 -0
- package/types/reactive.d.ts +76 -0
- package/types/router.d.ts +161 -0
- package/types/ssr.d.ts +49 -0
- package/types/store.d.ts +107 -0
- package/types/utils.d.ts +142 -0
- package/cli/commands/dev.old.js +0 -520
- package/cli/scaffold/scripts/components/home.js +0 -137
- /package/cli/scaffold/{scripts → app}/components/contacts/contacts.js +0 -0
- /package/cli/scaffold/{scripts → app}/components/counter.js +0 -0
- /package/cli/scaffold/{scripts → app}/components/not-found.js +0 -0
- /package/cli/scaffold/{scripts → app}/components/todos.js +0 -0
|
@@ -281,14 +281,63 @@ const OVERLAY_SCRIPT = `<script>
|
|
|
281
281
|
location.reload();
|
|
282
282
|
});
|
|
283
283
|
|
|
284
|
-
es.addEventListener('css', function() {
|
|
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
|
|
285
289
|
var sheets = document.querySelectorAll('link[rel="stylesheet"]');
|
|
286
290
|
sheets.forEach(function(l) {
|
|
287
291
|
var href = l.getAttribute('href');
|
|
288
292
|
if (!href) return;
|
|
293
|
+
var clean = href.replace(/[?&]_zqr=\\d+/, '').replace(/^\\/+/, '');
|
|
294
|
+
if (changedPath && clean.indexOf(changedPath) === -1) return;
|
|
295
|
+
matched = true;
|
|
289
296
|
var sep = href.indexOf('?') >= 0 ? '&' : '?';
|
|
290
|
-
l.setAttribute('href', href.replace(/[?&]_zqr
|
|
297
|
+
l.setAttribute('href', href.replace(/[?&]_zqr=\\d+/, '') + sep + '_zqr=' + Date.now());
|
|
291
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(); }
|
|
292
341
|
});
|
|
293
342
|
|
|
294
343
|
es.addEventListener('error:syntax', function(e) {
|
|
@@ -311,6 +360,383 @@ const OVERLAY_SCRIPT = `<script>
|
|
|
311
360
|
}
|
|
312
361
|
|
|
313
362
|
connect();
|
|
363
|
+
|
|
364
|
+
// =====================================================================
|
|
365
|
+
// Fetch / $.http Interceptor — pretty console logging
|
|
366
|
+
// =====================================================================
|
|
367
|
+
var __zqChannel;
|
|
368
|
+
try { __zqChannel = new BroadcastChannel('__zq_devtools'); } catch(e) {}
|
|
369
|
+
|
|
370
|
+
var __zqRequests = [];
|
|
371
|
+
var __zqMorphEvents = [];
|
|
372
|
+
var __zqMorphCount = 0;
|
|
373
|
+
var __zqRenderCount = 0;
|
|
374
|
+
var __zqReqId = 0;
|
|
375
|
+
var _origFetch = window.fetch;
|
|
376
|
+
|
|
377
|
+
window.fetch = function(input, init) {
|
|
378
|
+
var url = typeof input === 'string' ? input : (input && input.url ? input.url : String(input));
|
|
379
|
+
var method = ((init && init.method) || (input && input.method) || 'GET').toUpperCase();
|
|
380
|
+
var id = ++__zqReqId;
|
|
381
|
+
var start = performance.now();
|
|
382
|
+
|
|
383
|
+
// Skip internal dev-server requests
|
|
384
|
+
if (url.indexOf('__zq_') !== -1 || url.indexOf('/_devtools') !== -1) {
|
|
385
|
+
return _origFetch.apply(this, arguments);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return _origFetch.apply(this, arguments).then(function(response) {
|
|
389
|
+
var elapsed = Math.round(performance.now() - start);
|
|
390
|
+
var status = response.status;
|
|
391
|
+
var cloned = response.clone();
|
|
392
|
+
|
|
393
|
+
cloned.text().then(function(bodyText) {
|
|
394
|
+
var entry = {
|
|
395
|
+
id: id, method: method, url: url, status: status,
|
|
396
|
+
elapsed: elapsed, bodyPreview: bodyText.slice(0, 5000),
|
|
397
|
+
timestamp: Date.now()
|
|
398
|
+
};
|
|
399
|
+
__zqRequests.push(entry);
|
|
400
|
+
if (__zqRequests.length > 500) __zqRequests.shift();
|
|
401
|
+
updateDevBar();
|
|
402
|
+
|
|
403
|
+
// Pretty console log
|
|
404
|
+
var isOk = status >= 200 && status < 300;
|
|
405
|
+
var color = isOk ? '#2ecc71' : status < 400 ? '#f39c12' : '#e74c3c';
|
|
406
|
+
|
|
407
|
+
console.groupCollapsed(
|
|
408
|
+
'%c ' + method + ' %c' + status + '%c ' + url + ' %c' + elapsed + 'ms',
|
|
409
|
+
'background:' + color + ';color:#fff;padding:2px 6px;border-radius:3px;font-weight:700;font-size:11px',
|
|
410
|
+
'color:' + color + ';font-weight:700;margin-left:8px',
|
|
411
|
+
'color:inherit;margin-left:4px',
|
|
412
|
+
'color:#888;margin-left:8px;font-size:11px'
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
// Response body
|
|
416
|
+
try {
|
|
417
|
+
var parsed = JSON.parse(bodyText);
|
|
418
|
+
console.log('%c Response ', 'background:#1e1e2e;color:#8be9fd;padding:2px 6px;border-radius:2px;font-weight:600', parsed);
|
|
419
|
+
} catch(pe) {
|
|
420
|
+
if (bodyText.length > 0) {
|
|
421
|
+
console.log('%c Response ', 'background:#1e1e2e;color:#8be9fd;padding:2px 6px;border-radius:2px;font-weight:600',
|
|
422
|
+
bodyText.length > 500 ? bodyText.slice(0, 500) + '... (' + bodyText.length + ' chars)' : bodyText);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Headers
|
|
427
|
+
try {
|
|
428
|
+
console.log('%c Headers ', 'background:#1e1e2e;color:#bd93f9;padding:2px 6px;border-radius:2px;font-weight:600',
|
|
429
|
+
Object.fromEntries(response.headers.entries()));
|
|
430
|
+
} catch(he) {}
|
|
431
|
+
|
|
432
|
+
// Request body (if sent)
|
|
433
|
+
if (init && init.body) {
|
|
434
|
+
try {
|
|
435
|
+
console.log('%c Request ', 'background:#1e1e2e;color:#f1fa8c;padding:2px 6px;border-radius:2px;font-weight:600',
|
|
436
|
+
JSON.parse(init.body));
|
|
437
|
+
} catch(re) {
|
|
438
|
+
console.log('%c Request ', 'background:#1e1e2e;color:#f1fa8c;padding:2px 6px;border-radius:2px;font-weight:600',
|
|
439
|
+
String(init.body).slice(0, 500));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
console.groupEnd();
|
|
444
|
+
|
|
445
|
+
// Broadcast to devtools
|
|
446
|
+
if (__zqChannel) {
|
|
447
|
+
try { __zqChannel.postMessage({ type: 'http', data: entry }); } catch(ce) {}
|
|
448
|
+
}
|
|
449
|
+
}).catch(function() {});
|
|
450
|
+
|
|
451
|
+
return response;
|
|
452
|
+
}, function(err) {
|
|
453
|
+
var elapsed = Math.round(performance.now() - start);
|
|
454
|
+
console.groupCollapsed(
|
|
455
|
+
'%c ' + method + ' %cERR%c ' + url + ' %c' + elapsed + 'ms',
|
|
456
|
+
'background:#e74c3c;color:#fff;padding:2px 6px;border-radius:3px;font-weight:700;font-size:11px',
|
|
457
|
+
'color:#e74c3c;font-weight:700;margin-left:8px',
|
|
458
|
+
'color:inherit;margin-left:4px',
|
|
459
|
+
'color:#888;margin-left:8px;font-size:11px'
|
|
460
|
+
);
|
|
461
|
+
console.error(err);
|
|
462
|
+
console.groupEnd();
|
|
463
|
+
|
|
464
|
+
var entry = { id: id, method: method, url: url, status: 0, elapsed: elapsed, bodyPreview: err.message, timestamp: Date.now() };
|
|
465
|
+
__zqRequests.push(entry);
|
|
466
|
+
updateDevBar();
|
|
467
|
+
if (__zqChannel) {
|
|
468
|
+
try { __zqChannel.postMessage({ type: 'http', data: entry }); } catch(ce) {}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
throw err;
|
|
472
|
+
});
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// =====================================================================
|
|
476
|
+
// Morph instrumentation — hook via window.__zqMorphHook (set by diff.js)
|
|
477
|
+
// =====================================================================
|
|
478
|
+
window.__zqMorphHook = function(el, elapsed) {
|
|
479
|
+
__zqMorphCount++;
|
|
480
|
+
updateDevBar();
|
|
481
|
+
|
|
482
|
+
var evt = { target: el.id || el.tagName.toLowerCase(), elapsed: elapsed, kind: 'morph', timestamp: Date.now() };
|
|
483
|
+
__zqMorphEvents.push(evt);
|
|
484
|
+
if (__zqMorphEvents.length > 200) __zqMorphEvents.shift();
|
|
485
|
+
|
|
486
|
+
// Console timing for slow morphs (> 4ms)
|
|
487
|
+
if (elapsed > 4) {
|
|
488
|
+
console.log(
|
|
489
|
+
'%c morph %c' + elapsed.toFixed(2) + 'ms%c ' + (el.id || el.tagName.toLowerCase()),
|
|
490
|
+
'background:#9b59b6;color:#fff;padding:2px 6px;border-radius:3px;font-weight:700;font-size:11px',
|
|
491
|
+
'color:' + (elapsed > 16 ? '#e74c3c' : '#f39c12') + ';font-weight:700;margin-left:8px',
|
|
492
|
+
'color:#888;margin-left:4px'
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Broadcast to devtools
|
|
497
|
+
if (__zqChannel) {
|
|
498
|
+
try {
|
|
499
|
+
__zqChannel.postMessage({
|
|
500
|
+
type: 'morph-detail',
|
|
501
|
+
data: evt
|
|
502
|
+
});
|
|
503
|
+
} catch(ce) {}
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// =====================================================================
|
|
508
|
+
// Render instrumentation — hook for first-renders & route swaps
|
|
509
|
+
// =====================================================================
|
|
510
|
+
window.__zqRenderHook = function(el, elapsed, kind, name) {
|
|
511
|
+
__zqRenderCount++;
|
|
512
|
+
__zqMorphCount++; // count renders in the morph total for the toolbar
|
|
513
|
+
updateDevBar();
|
|
514
|
+
|
|
515
|
+
var evt = { target: name || el.id || el.tagName.toLowerCase(), elapsed: elapsed, kind: kind, timestamp: Date.now() };
|
|
516
|
+
__zqMorphEvents.push(evt);
|
|
517
|
+
if (__zqMorphEvents.length > 200) __zqMorphEvents.shift();
|
|
518
|
+
|
|
519
|
+
// Console log for route/mount renders
|
|
520
|
+
var label = kind === 'route' ? ' route ' : ' mount ';
|
|
521
|
+
var bg = kind === 'route' ? '#d29922' : '#3fb950';
|
|
522
|
+
console.log(
|
|
523
|
+
'%c' + label + '%c' + elapsed.toFixed(2) + 'ms%c ' + (name || el.id || el.tagName.toLowerCase()),
|
|
524
|
+
'background:' + bg + ';color:#fff;padding:2px 6px;border-radius:3px;font-weight:700;font-size:11px',
|
|
525
|
+
'color:' + (elapsed > 16 ? '#e74c3c' : '#888') + ';font-weight:700;margin-left:8px',
|
|
526
|
+
'color:#888;margin-left:4px'
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
// Broadcast to devtools
|
|
530
|
+
if (__zqChannel) {
|
|
531
|
+
try {
|
|
532
|
+
__zqChannel.postMessage({
|
|
533
|
+
type: 'render-detail',
|
|
534
|
+
data: evt
|
|
535
|
+
});
|
|
536
|
+
} catch(ce) {}
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
// =====================================================================
|
|
541
|
+
// Router instrumentation — history state tracking for devtools
|
|
542
|
+
// =====================================================================
|
|
543
|
+
var __zqRouterEvents = [];
|
|
544
|
+
|
|
545
|
+
var _origPushState = history.pushState;
|
|
546
|
+
history.pushState = function(state, title, url) {
|
|
547
|
+
_origPushState.apply(this, arguments);
|
|
548
|
+
var isSubstate = state && state.__zq === 'substate';
|
|
549
|
+
var evt = {
|
|
550
|
+
action: isSubstate ? 'substate' : 'navigate',
|
|
551
|
+
url: String(url || location.href).replace(location.origin, ''),
|
|
552
|
+
key: isSubstate ? state.key : null,
|
|
553
|
+
data: isSubstate ? state.data : null,
|
|
554
|
+
timestamp: Date.now()
|
|
555
|
+
};
|
|
556
|
+
__zqRouterEvents.push(evt);
|
|
557
|
+
if (__zqRouterEvents.length > 200) __zqRouterEvents.shift();
|
|
558
|
+
if (__zqChannel) {
|
|
559
|
+
try { __zqChannel.postMessage({ type: 'router', data: evt }); } catch(e) {}
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
var _origReplaceState = history.replaceState;
|
|
564
|
+
history.replaceState = function(state, title, url) {
|
|
565
|
+
_origReplaceState.apply(this, arguments);
|
|
566
|
+
var isSubstate = state && state.__zq === 'substate';
|
|
567
|
+
var evt = {
|
|
568
|
+
action: 'replace',
|
|
569
|
+
url: String(url || location.href).replace(location.origin, ''),
|
|
570
|
+
key: isSubstate ? state.key : null,
|
|
571
|
+
data: isSubstate ? state.data : null,
|
|
572
|
+
timestamp: Date.now()
|
|
573
|
+
};
|
|
574
|
+
__zqRouterEvents.push(evt);
|
|
575
|
+
if (__zqRouterEvents.length > 200) __zqRouterEvents.shift();
|
|
576
|
+
if (__zqChannel) {
|
|
577
|
+
try { __zqChannel.postMessage({ type: 'router', data: evt }); } catch(e) {}
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
window.addEventListener('popstate', function(e) {
|
|
582
|
+
var state = e.state;
|
|
583
|
+
var isSubstate = state && state.__zq === 'substate';
|
|
584
|
+
var evt = {
|
|
585
|
+
action: isSubstate ? 'pop-substate' : 'pop',
|
|
586
|
+
url: location.pathname + location.hash,
|
|
587
|
+
key: isSubstate ? state.key : null,
|
|
588
|
+
data: isSubstate ? state.data : null,
|
|
589
|
+
timestamp: Date.now()
|
|
590
|
+
};
|
|
591
|
+
__zqRouterEvents.push(evt);
|
|
592
|
+
if (__zqRouterEvents.length > 200) __zqRouterEvents.shift();
|
|
593
|
+
if (__zqChannel) {
|
|
594
|
+
try { __zqChannel.postMessage({ type: 'router', data: evt }); } catch(e) {}
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
window.addEventListener('hashchange', function() {
|
|
599
|
+
var evt = {
|
|
600
|
+
action: 'hashchange',
|
|
601
|
+
url: location.hash,
|
|
602
|
+
timestamp: Date.now()
|
|
603
|
+
};
|
|
604
|
+
__zqRouterEvents.push(evt);
|
|
605
|
+
if (__zqRouterEvents.length > 200) __zqRouterEvents.shift();
|
|
606
|
+
if (__zqChannel) {
|
|
607
|
+
try { __zqChannel.postMessage({ type: 'router', data: evt }); } catch(e) {}
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// =====================================================================
|
|
612
|
+
// Dev Toolbar — floating bar with DOM viewer button & request counter
|
|
613
|
+
// =====================================================================
|
|
614
|
+
var devBar;
|
|
615
|
+
|
|
616
|
+
function createDevBar() {
|
|
617
|
+
devBar = document.createElement('div');
|
|
618
|
+
devBar.id = '__zq_devbar';
|
|
619
|
+
devBar.setAttribute('style',
|
|
620
|
+
'position:fixed;bottom:12px;right:12px;z-index:2147483646;' +
|
|
621
|
+
'display:flex;align-items:center;gap:6px;' +
|
|
622
|
+
'background:rgba(22,27,34,0.92);border:1px solid rgba(48,54,61,0.8);' +
|
|
623
|
+
'border-radius:8px;padding:4px 6px;backdrop-filter:blur(8px);' +
|
|
624
|
+
'font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;' +
|
|
625
|
+
'font-size:11px;color:#8b949e;user-select:none;cursor:default;' +
|
|
626
|
+
'box-shadow:0 4px 12px rgba(0,0,0,0.4);'
|
|
627
|
+
);
|
|
628
|
+
devBar.innerHTML =
|
|
629
|
+
'<span style="color:#58a6ff;font-weight:700;padding:0 4px;font-size:10px;letter-spacing:.5px">zQ</span>' +
|
|
630
|
+
'<span id="__zq_bar_reqs" title="Network requests" style="padding:2px 6px;border-radius:4px;' +
|
|
631
|
+
'background:rgba(88,166,255,0.1);color:#58a6ff;cursor:pointer;font-size:10px;font-weight:600;">0 req</span>' +
|
|
632
|
+
'<span id="__zq_bar_morphs" title="Render operations" style="padding:2px 6px;border-radius:4px;' +
|
|
633
|
+
'background:rgba(188,140,255,0.1);color:#bc8cff;cursor:pointer;font-size:10px;font-weight:600;">0 render</span>' +
|
|
634
|
+
'<button id="__zq_bar_dom" title="Open DevTools (/_devtools)" style="' +
|
|
635
|
+
'padding:3px 8px;border-radius:4px;font-size:10px;font-weight:700;' +
|
|
636
|
+
'background:rgba(63,185,80,0.15);color:#3fb950;border:1px solid rgba(63,185,80,0.3);' +
|
|
637
|
+
'cursor:pointer;font-family:inherit;transition:all .15s;' +
|
|
638
|
+
'">DOM</button>' +
|
|
639
|
+
'<button id="__zq_bar_close" title="Close toolbar" style="' +
|
|
640
|
+
'padding:0 4px;color:#484f58;cursor:pointer;font-size:14px;border:none;' +
|
|
641
|
+
'background:none;font-family:inherit;line-height:1;' +
|
|
642
|
+
'">×</button>';
|
|
643
|
+
|
|
644
|
+
document.body.appendChild(devBar);
|
|
645
|
+
updateDevBar();
|
|
646
|
+
|
|
647
|
+
// Check if we're inside a devtools split-view iframe
|
|
648
|
+
function isInSplitFrame() {
|
|
649
|
+
try { return window.parent !== window && window.parent.document.getElementById('app-frame'); }
|
|
650
|
+
catch(e) { return false; }
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Switch tab in devtools (works for both split iframe and popup)
|
|
654
|
+
function switchDevTab(tab) {
|
|
655
|
+
if (__zqChannel) {
|
|
656
|
+
__zqChannel.postMessage({ type: 'switch-tab', tab: tab });
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Req counter → Network tab
|
|
661
|
+
document.getElementById('__zq_bar_reqs').addEventListener('click', function() {
|
|
662
|
+
if (isInSplitFrame()) {
|
|
663
|
+
switchDevTab('network');
|
|
664
|
+
} else {
|
|
665
|
+
openDevToolsPopup('network');
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// Render counter → Performance tab
|
|
670
|
+
document.getElementById('__zq_bar_morphs').addEventListener('click', function() {
|
|
671
|
+
if (isInSplitFrame()) {
|
|
672
|
+
switchDevTab('perf');
|
|
673
|
+
} else {
|
|
674
|
+
openDevToolsPopup('perf');
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// DOM button → Elements tab (in split) or open popup
|
|
679
|
+
var __zqPopup = null;
|
|
680
|
+
function openDevToolsPopup(tab) {
|
|
681
|
+
// If popup is already open, just switch the tab via BroadcastChannel
|
|
682
|
+
if (__zqPopup && !__zqPopup.closed) {
|
|
683
|
+
switchDevTab(tab);
|
|
684
|
+
__zqPopup.focus();
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
var w = 1080, h = 800;
|
|
688
|
+
var left = window.screenX + window.outerWidth - w - 20;
|
|
689
|
+
var top = window.screenY + 60;
|
|
690
|
+
var url = '/_devtools' + (tab ? '#' + tab : '');
|
|
691
|
+
__zqPopup = window.open(url, '__zq_devtools',
|
|
692
|
+
'width=' + w + ',height=' + h + ',left=' + left + ',top=' + top +
|
|
693
|
+
',resizable=yes,scrollbars=yes');
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
document.getElementById('__zq_bar_dom').addEventListener('click', function() {
|
|
697
|
+
if (isInSplitFrame()) {
|
|
698
|
+
switchDevTab('dom');
|
|
699
|
+
} else {
|
|
700
|
+
openDevToolsPopup('dom');
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// Close button
|
|
705
|
+
document.getElementById('__zq_bar_close').addEventListener('click', function() {
|
|
706
|
+
devBar.style.display = 'none';
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
// Hover effects
|
|
710
|
+
document.getElementById('__zq_bar_dom').addEventListener('mouseover', function() {
|
|
711
|
+
this.style.background = 'rgba(63,185,80,0.3)';
|
|
712
|
+
});
|
|
713
|
+
document.getElementById('__zq_bar_dom').addEventListener('mouseout', function() {
|
|
714
|
+
this.style.background = 'rgba(63,185,80,0.15)';
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function updateDevBar() {
|
|
719
|
+
if (!devBar) return;
|
|
720
|
+
var reqEl = document.getElementById('__zq_bar_reqs');
|
|
721
|
+
var morphEl = document.getElementById('__zq_bar_morphs');
|
|
722
|
+
if (reqEl) reqEl.textContent = __zqRequests.length + ' req';
|
|
723
|
+
if (morphEl) morphEl.textContent = __zqMorphCount + ' render';
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Expose for devtools popup
|
|
727
|
+
window.__zqDevTools = {
|
|
728
|
+
get requests() { return __zqRequests; },
|
|
729
|
+
get morphEvents() { return __zqMorphEvents; },
|
|
730
|
+
get morphCount() { return __zqMorphCount; },
|
|
731
|
+
get renderCount() { return __zqRenderCount; },
|
|
732
|
+
get routerEvents() { return __zqRouterEvents; }
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
if (document.readyState === 'loading') {
|
|
736
|
+
document.addEventListener('DOMContentLoaded', createDevBar);
|
|
737
|
+
} else {
|
|
738
|
+
createDevBar();
|
|
739
|
+
}
|
|
314
740
|
})();
|
|
315
741
|
</script>`;
|
|
316
742
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const OVERLAY_SCRIPT = require('./overlay');
|
|
14
|
+
const DEVTOOLS_HTML = require('./devtools');
|
|
14
15
|
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
16
17
|
// SSE client pool
|
|
@@ -44,22 +45,51 @@ class SSEPool {
|
|
|
44
45
|
// Server factory
|
|
45
46
|
// ---------------------------------------------------------------------------
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Prompt the user to auto-install zero-http when it isn't found.
|
|
50
|
+
* Resolves `true` if the user accepts, `false` otherwise.
|
|
51
|
+
*/
|
|
52
|
+
function promptInstall() {
|
|
53
|
+
const rl = require('readline').createInterface({
|
|
54
|
+
input: process.stdin,
|
|
55
|
+
output: process.stdout,
|
|
56
|
+
});
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
rl.question(
|
|
59
|
+
'\n The local dev server requires zero-http, which is not installed.\n' +
|
|
60
|
+
' This package is only used during development and is not needed\n' +
|
|
61
|
+
' for building, bundling, or production.\n' +
|
|
62
|
+
' Install it now? (y/n): ',
|
|
63
|
+
(answer) => {
|
|
64
|
+
rl.close();
|
|
65
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
47
71
|
/**
|
|
48
72
|
* @param {object} opts
|
|
49
73
|
* @param {string} opts.root — absolute path to project root
|
|
50
74
|
* @param {string} opts.htmlEntry — e.g. 'index.html'
|
|
51
75
|
* @param {number} opts.port
|
|
52
76
|
* @param {boolean} opts.noIntercept — skip zquery.min.js auto-resolve
|
|
53
|
-
* @returns {{ app, pool: SSEPool, listen: Function }}
|
|
77
|
+
* @returns {Promise<{ app, pool: SSEPool, listen: Function }>}
|
|
54
78
|
*/
|
|
55
|
-
function createServer({ root, htmlEntry, port, noIntercept }) {
|
|
79
|
+
async function createServer({ root, htmlEntry, port, noIntercept }) {
|
|
56
80
|
let zeroHttp;
|
|
57
81
|
try {
|
|
58
82
|
zeroHttp = require('zero-http');
|
|
59
83
|
} catch {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
84
|
+
const ok = await promptInstall();
|
|
85
|
+
if (!ok) {
|
|
86
|
+
console.error('\n ✖ Cannot start dev server without zero-http.\n');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
const { execSync } = require('child_process');
|
|
90
|
+
console.log('\n Installing zero-http...\n');
|
|
91
|
+
execSync('npm install zero-http --save-dev', { stdio: 'inherit' });
|
|
92
|
+
zeroHttp = require('zero-http');
|
|
63
93
|
}
|
|
64
94
|
|
|
65
95
|
const { createApp, static: serveStatic } = zeroHttp;
|
|
@@ -73,6 +103,13 @@ function createServer({ root, htmlEntry, port, noIntercept }) {
|
|
|
73
103
|
pool.add(sse);
|
|
74
104
|
});
|
|
75
105
|
|
|
106
|
+
// ---- DevTools panel ----
|
|
107
|
+
app.get('/_devtools', (req, res) => {
|
|
108
|
+
res.set('Content-Type', 'text/html; charset=utf-8');
|
|
109
|
+
res.set('Cache-Control', 'no-cache');
|
|
110
|
+
res.send(DEVTOOLS_HTML);
|
|
111
|
+
});
|
|
112
|
+
|
|
76
113
|
// ---- Auto-resolve zquery.min.js ----
|
|
77
114
|
const pkgRoot = path.resolve(__dirname, '..', '..', '..');
|
|
78
115
|
|
|
@@ -28,6 +28,14 @@ function isIgnored(filepath) {
|
|
|
28
28
|
return filepath.split(path.sep).some(p => IGNORE_DIRS.has(p));
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Return the file's mtime as a millisecond timestamp, or 0 if unreadable.
|
|
33
|
+
* Used to ignore spurious fs.watch events (Windows fires on reads too).
|
|
34
|
+
*/
|
|
35
|
+
function mtime(filepath) {
|
|
36
|
+
try { return fs.statSync(filepath).mtimeMs; } catch { return 0; }
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
/** Recursively collect every directory under `dir` (excluding ignored). */
|
|
32
40
|
function collectWatchDirs(dir) {
|
|
33
41
|
const dirs = [dir];
|
|
@@ -52,12 +60,31 @@ function collectWatchDirs(dir) {
|
|
|
52
60
|
* @param {SSEPool} opts.pool — SSE broadcast pool
|
|
53
61
|
* @returns {{ dirs: string[], destroy: Function }}
|
|
54
62
|
*/
|
|
55
|
-
function startWatcher({ root, pool }) {
|
|
63
|
+
function startWatcher({ root, pool, bundleMode, serveRoot }) {
|
|
56
64
|
const watchDirs = collectWatchDirs(root);
|
|
57
65
|
const watchers = [];
|
|
58
66
|
|
|
59
67
|
let debounceTimer;
|
|
60
68
|
let currentError = null; // track which file has an active error
|
|
69
|
+
let bundleTimer = null; // debounce for bundle rebuilds
|
|
70
|
+
|
|
71
|
+
// Track file mtimes so we only react to genuine writes.
|
|
72
|
+
// On Windows, fs.watch fires on reads/access too, which causes
|
|
73
|
+
// spurious reloads the first time the server serves a file.
|
|
74
|
+
// We seed the cache with current mtimes so the first real save
|
|
75
|
+
// (which changes the mtime) is always detected.
|
|
76
|
+
const mtimeCache = new Map();
|
|
77
|
+
for (const d of watchDirs) {
|
|
78
|
+
try {
|
|
79
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
80
|
+
if (entry.isFile()) {
|
|
81
|
+
const fp = path.join(d, entry.name);
|
|
82
|
+
const mt = mtime(fp);
|
|
83
|
+
if (mt) mtimeCache.set(fp, mt);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch { /* skip */ }
|
|
87
|
+
}
|
|
61
88
|
|
|
62
89
|
for (const dir of watchDirs) {
|
|
63
90
|
try {
|
|
@@ -68,6 +95,13 @@ function startWatcher({ root, pool }) {
|
|
|
68
95
|
|
|
69
96
|
clearTimeout(debounceTimer);
|
|
70
97
|
debounceTimer = setTimeout(() => {
|
|
98
|
+
// Skip if the file hasn't actually been modified
|
|
99
|
+
const mt = mtime(fullPath);
|
|
100
|
+
if (mt === 0) return; // deleted or unreadable
|
|
101
|
+
const prev = mtimeCache.get(fullPath);
|
|
102
|
+
mtimeCache.set(fullPath, mt);
|
|
103
|
+
if (prev !== undefined && mt === prev) return; // unchanged
|
|
104
|
+
|
|
71
105
|
const rel = path.relative(root, fullPath).replace(/\\/g, '/');
|
|
72
106
|
const ext = path.extname(filename).toLowerCase();
|
|
73
107
|
|
|
@@ -95,6 +129,30 @@ function startWatcher({ root, pool }) {
|
|
|
95
129
|
}
|
|
96
130
|
|
|
97
131
|
// ---- Full reload ----
|
|
132
|
+
if (bundleMode) {
|
|
133
|
+
// Debounce bundle rebuilds (500ms) so rapid saves don't spam
|
|
134
|
+
clearTimeout(bundleTimer);
|
|
135
|
+
bundleTimer = setTimeout(() => {
|
|
136
|
+
try {
|
|
137
|
+
const bundleFn = require('../bundle');
|
|
138
|
+
const { args: cliArgs } = require('../../args');
|
|
139
|
+
const savedArgs = cliArgs.slice();
|
|
140
|
+
cliArgs.length = 0;
|
|
141
|
+
cliArgs.push('bundle');
|
|
142
|
+
const prevCwd = process.cwd();
|
|
143
|
+
process.chdir(root);
|
|
144
|
+
bundleFn();
|
|
145
|
+
process.chdir(prevCwd);
|
|
146
|
+
cliArgs.length = 0;
|
|
147
|
+
savedArgs.forEach(a => cliArgs.push(a));
|
|
148
|
+
logReload(rel + ' (rebuilt)');
|
|
149
|
+
pool.broadcast('reload', rel);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error(' Bundle rebuild failed:', err.message);
|
|
152
|
+
}
|
|
153
|
+
}, 500);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
98
156
|
logReload(rel);
|
|
99
157
|
pool.broadcast('reload', rel);
|
|
100
158
|
}, 100);
|
package/cli/help.js
CHANGED
|
@@ -7,7 +7,7 @@ function showHelp() {
|
|
|
7
7
|
COMMANDS
|
|
8
8
|
|
|
9
9
|
create [dir] Scaffold a new zQuery project
|
|
10
|
-
Creates index.html,
|
|
10
|
+
Creates index.html, global.css, app/, assets/ in the target directory
|
|
11
11
|
(defaults to the current directory)
|
|
12
12
|
|
|
13
13
|
dev [root] Start a dev server with live-reload
|
|
@@ -15,6 +15,8 @@ function showHelp() {
|
|
|
15
15
|
--index, -i <file> Index HTML file (default: index.html)
|
|
16
16
|
--no-intercept Disable auto-resolution of zquery.min.js
|
|
17
17
|
(serve the on-disk vendor copy instead)
|
|
18
|
+
--bundle, -b Serve the bundled build (runs bundler first,
|
|
19
|
+
serves from dist/server/, auto-rebuilds on save)
|
|
18
20
|
|
|
19
21
|
Includes error overlay: syntax errors are
|
|
20
22
|
caught on save and shown as a full-screen
|
|
@@ -24,7 +26,8 @@ function showHelp() {
|
|
|
24
26
|
bundle [dir|file] Bundle app ES modules into a single file
|
|
25
27
|
--out, -o <path> Output directory (default: dist/ next to HTML file)
|
|
26
28
|
--index, -i <file> Index HTML file (default: auto-detected)
|
|
27
|
-
--minimal, -m Only output HTML
|
|
29
|
+
--minimal, -m Only output HTML, bundled JS, and global CSS (skip static assets)
|
|
30
|
+
--global-css <path> Override global CSS input (default: first <link> in HTML)
|
|
28
31
|
|
|
29
32
|
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)
|
|
30
33
|
|
|
@@ -35,7 +38,7 @@ function showHelp() {
|
|
|
35
38
|
1. index.html first, then other .html files
|
|
36
39
|
2. Within HTML: module script pointing to app.js, else first module script
|
|
37
40
|
3. JS scan: $.router( first (entry point), then $.mount( / $.store(
|
|
38
|
-
4. Convention fallbacks (scripts/app.js, app.js, etc.)
|
|
41
|
+
4. Convention fallbacks (app/app.js, scripts/app.js, app.js, etc.)
|
|
39
42
|
• Passing a directory auto-detects the entry; passing a file uses it directly
|
|
40
43
|
• zquery.min.js is always embedded (auto-built from source if not found)
|
|
41
44
|
• HTML file is auto-detected (any .html, not just index.html)
|
|
@@ -83,12 +86,12 @@ function showHelp() {
|
|
|
83
86
|
zquery bundle my-app/
|
|
84
87
|
|
|
85
88
|
# Pass a direct entry file (skip auto-detection)
|
|
86
|
-
zquery bundle my-app/
|
|
89
|
+
zquery bundle my-app/app/main.js
|
|
87
90
|
|
|
88
91
|
# Custom output directory
|
|
89
92
|
zquery bundle my-app/ -o build/
|
|
90
93
|
|
|
91
|
-
# Minimal build (
|
|
94
|
+
# Minimal build (HTML + JS + global CSS, no static asset copying)
|
|
92
95
|
zquery bundle my-app/ --minimal
|
|
93
96
|
|
|
94
97
|
# Dev server with a custom index page
|