zero-query 0.5.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/cli/commands/build.js +4 -2
- package/cli/commands/dev/index.js +82 -0
- package/cli/commands/dev/logger.js +70 -0
- package/cli/commands/dev/overlay.js +317 -0
- package/cli/commands/dev/server.js +129 -0
- package/cli/commands/dev/validator.js +94 -0
- package/cli/commands/dev/watcher.js +114 -0
- package/cli/scaffold/favicon.ico +0 -0
- package/cli/scaffold/scripts/components/about.js +14 -2
- package/cli/scaffold/scripts/components/contacts/contacts.html +5 -4
- package/cli/scaffold/scripts/components/contacts/contacts.js +17 -1
- package/cli/scaffold/scripts/components/counter.js +30 -10
- package/cli/scaffold/scripts/components/home.js +3 -3
- package/cli/scaffold/scripts/components/todos.js +6 -5
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +1542 -105
- package/dist/zquery.min.js +11 -8
- package/index.d.ts +252 -20
- package/index.js +18 -7
- package/package.json +8 -2
- package/src/component.js +175 -44
- package/src/core.js +22 -25
- package/src/diff.js +280 -0
- package/src/errors.js +155 -0
- package/src/expression.js +806 -0
- package/src/http.js +18 -10
- package/src/reactive.js +29 -4
- package/src/router.js +11 -5
- package/src/ssr.js +224 -0
- package/src/store.js +24 -8
- /package/cli/commands/{dev.js → dev.old.js} +0 -0
package/README.md
CHANGED
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
|
-
> **Lightweight, zero-dependency frontend library that combines jQuery-style DOM manipulation with a modern reactive component system, SPA router, global state management, HTTP client, and utility toolkit — all in a single ~
|
|
18
|
+
> **Lightweight, zero-dependency frontend library that combines jQuery-style DOM manipulation with a modern reactive component system, SPA router, global state management, HTTP client, and utility toolkit — all in a single ~84 KB minified browser bundle. Works out of the box with ES modules. An optional CLI bundler is available for single-file production builds.**
|
|
19
19
|
|
|
20
20
|
## Features
|
|
21
21
|
|
|
22
22
|
| Module | Highlights |
|
|
23
23
|
| --- | --- |
|
|
24
24
|
| **Core `$()`** | jQuery-like chainable selectors, traversal, DOM manipulation, events, animation |
|
|
25
|
-
| **Components** | Reactive state, template literals, `@event` delegation (8 modifiers), `z-model` two-way binding, directives (`z-if`/`z-else-if`/`z-else`, `z-for`, `z-show`, `z-bind`/`:attr`, `z-class`, `z-style`, `z-text`, `z-html`, `z-ref`, `z-cloak`, `z-pre`), scoped styles, external templates (`templateUrl` / `styleUrl`), lifecycle hooks, auto-injected base styles
|
|
25
|
+
| **Components** | Reactive state, template literals, `@event` delegation (8 modifiers), `z-model` two-way binding, computed properties, watch callbacks, slot-based content projection, directives (`z-if`/`z-else-if`/`z-else`, `z-for`, `z-show`, `z-bind`/`:attr`, `z-class`, `z-style`, `z-text`, `z-html`, `z-ref`, `z-cloak`, `z-pre`, `z-key`), DOM morphing engine (no innerHTML rebuild), CSP-safe expression evaluation, scoped styles, external templates (`templateUrl` / `styleUrl`), lifecycle hooks, auto-injected base styles |
|
|
26
26
|
| **Router** | History & hash mode, route params (`:id`), guards, lazy loading, `z-link` navigation |
|
|
27
27
|
| **Store** | Reactive global state, named actions, computed getters, middleware, subscriptions |
|
|
28
28
|
| **HTTP** | Fetch wrapper with auto-JSON, interceptors, timeout/abort, base URL |
|
|
@@ -69,7 +69,7 @@ If you prefer **zero tooling**, download `dist/zQuery.min.js` from the [GitHub r
|
|
|
69
69
|
git clone https://github.com/tonywied17/zero-query.git
|
|
70
70
|
cd zero-query
|
|
71
71
|
npx zquery build
|
|
72
|
-
# → dist/zQuery.min.js (~
|
|
72
|
+
# → dist/zQuery.min.js (~84 KB)
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
### Include in HTML
|
|
@@ -240,13 +240,15 @@ location / {
|
|
|
240
240
|
|
|
241
241
|
| Namespace | Methods |
|
|
242
242
|
| --- | --- |
|
|
243
|
-
| `$()` |
|
|
244
|
-
| `$.all()` |
|
|
245
|
-
| `$.id` `$.class` `$.classes` `$.tag` `$.children` | Quick DOM refs |
|
|
243
|
+
| `$()` | Chainable selector → `ZQueryCollection` (CSS selectors, elements, NodeLists, HTML strings) |
|
|
244
|
+
| `$.all()` | Alias for `$()` — identical behavior |
|
|
245
|
+
| `$.id` `$.class` `$.classes` `$.tag` `$.name` `$.children` | Quick DOM refs |
|
|
246
246
|
| `$.create` | Element factory |
|
|
247
247
|
| `$.ready` `$.on` `$.off` | DOM ready, global event delegation & direct listeners |
|
|
248
248
|
| `$.fn` | Collection prototype (extend it) |
|
|
249
249
|
| `$.component` `$.mount` `$.mountAll` `$.getInstance` `$.destroy` `$.components` | Component system |
|
|
250
|
+
| `$.morph` | DOM morphing engine — patch existing DOM to match new HTML without destroying unchanged nodes |
|
|
251
|
+
| `$.safeEval` | CSP-safe expression evaluator (replaces `eval` / `new Function`) |
|
|
250
252
|
| `$.style` | Dynamically load global stylesheet file(s) at runtime |
|
|
251
253
|
| `$.router` `$.getRouter` | SPA router |
|
|
252
254
|
| `$.store` `$.getStore` | State management |
|
package/cli/commands/build.js
CHANGED
|
@@ -16,8 +16,10 @@ function buildLibrary() {
|
|
|
16
16
|
const VERSION = pkg.version;
|
|
17
17
|
|
|
18
18
|
const modules = [
|
|
19
|
-
'src/
|
|
20
|
-
'src/
|
|
19
|
+
'src/errors.js',
|
|
20
|
+
'src/reactive.js', 'src/core.js', 'src/expression.js', 'src/diff.js',
|
|
21
|
+
'src/component.js', 'src/router.js', 'src/store.js', 'src/http.js',
|
|
22
|
+
'src/utils.js',
|
|
21
23
|
];
|
|
22
24
|
|
|
23
25
|
const DIST = path.join(process.cwd(), 'dist');
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/dev/index.js — Dev server orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Ties together the HTTP server, file watcher, logger, and overlay
|
|
5
|
+
* to provide a complete development environment with live-reload,
|
|
6
|
+
* syntax validation, and a full-screen error overlay that surfaces
|
|
7
|
+
* both build-time and runtime ZQueryErrors.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const { args, flag, option } = require('../../args');
|
|
16
|
+
const { createServer } = require('./server');
|
|
17
|
+
const { startWatcher } = require('./watcher');
|
|
18
|
+
const { printBanner } = require('./logger');
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Resolve project root
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
function resolveRoot(htmlEntry) {
|
|
25
|
+
// Explicit positional argument → zquery dev <dir>
|
|
26
|
+
for (let i = 1; i < args.length; i++) {
|
|
27
|
+
const prev = args[i - 1];
|
|
28
|
+
if (!args[i].startsWith('-') && prev !== '-p' && prev !== '--port' && prev !== '--index' && prev !== '-i') {
|
|
29
|
+
return path.resolve(process.cwd(), args[i]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Auto-detect: first candidate that contains the HTML entry file
|
|
34
|
+
const candidates = [
|
|
35
|
+
process.cwd(),
|
|
36
|
+
path.join(process.cwd(), 'public'),
|
|
37
|
+
path.join(process.cwd(), 'src'),
|
|
38
|
+
];
|
|
39
|
+
for (const c of candidates) {
|
|
40
|
+
if (fs.existsSync(path.join(c, htmlEntry))) return c;
|
|
41
|
+
}
|
|
42
|
+
return process.cwd();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// devServer — main entry point (called from cli/index.js)
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
function devServer() {
|
|
50
|
+
const htmlEntry = option('index', 'i', 'index.html');
|
|
51
|
+
const port = parseInt(option('port', 'p', '3100'), 10);
|
|
52
|
+
const noIntercept = flag('no-intercept');
|
|
53
|
+
const root = resolveRoot(htmlEntry);
|
|
54
|
+
|
|
55
|
+
// Start HTTP server + SSE pool
|
|
56
|
+
const { app, pool, listen } = createServer({ root, htmlEntry, port, noIntercept });
|
|
57
|
+
|
|
58
|
+
// Start file watcher
|
|
59
|
+
const watcher = startWatcher({ root, pool });
|
|
60
|
+
|
|
61
|
+
// Boot
|
|
62
|
+
listen(() => {
|
|
63
|
+
printBanner({
|
|
64
|
+
port,
|
|
65
|
+
root: path.relative(process.cwd(), root) || '.',
|
|
66
|
+
htmlEntry,
|
|
67
|
+
noIntercept,
|
|
68
|
+
watchDirCount: watcher.dirs.length,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Graceful shutdown
|
|
73
|
+
process.on('SIGINT', () => {
|
|
74
|
+
console.log('\n Shutting down...');
|
|
75
|
+
watcher.destroy();
|
|
76
|
+
pool.closeAll();
|
|
77
|
+
app.close(() => process.exit(0));
|
|
78
|
+
setTimeout(() => process.exit(0), 1000);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = devServer;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/dev/logger.js — Terminal output helpers
|
|
3
|
+
*
|
|
4
|
+
* Provides styled console output for the dev server: startup banner,
|
|
5
|
+
* timestamped file-change messages, and error formatting.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// ANSI colour helpers (works on all modern terminals)
|
|
11
|
+
const c = {
|
|
12
|
+
reset: '\x1b[0m',
|
|
13
|
+
bold: '\x1b[1m',
|
|
14
|
+
dim: '\x1b[2m',
|
|
15
|
+
red: '\x1b[31m',
|
|
16
|
+
green: '\x1b[32m',
|
|
17
|
+
yellow: '\x1b[33m',
|
|
18
|
+
cyan: '\x1b[36m',
|
|
19
|
+
magenta: '\x1b[35m',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function timestamp() {
|
|
23
|
+
return new Date().toLocaleTimeString();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Event-level log helpers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function logCSS(relPath) {
|
|
31
|
+
console.log(` ${timestamp()} ${c.magenta} css ${c.reset} ${relPath}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function logReload(relPath) {
|
|
35
|
+
console.log(` ${timestamp()} ${c.cyan} reload ${c.reset} ${relPath}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function logError(descriptor) {
|
|
39
|
+
const t = timestamp();
|
|
40
|
+
console.log(` ${t} ${c.red} error ${c.reset} ${descriptor.file}`);
|
|
41
|
+
console.log(` ${c.red}${descriptor.type}: ${descriptor.message}${c.reset}`);
|
|
42
|
+
if (descriptor.line) {
|
|
43
|
+
console.log(` ${c.dim}at line ${descriptor.line}${descriptor.column ? ':' + descriptor.column : ''}${c.reset}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Startup banner
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
function printBanner({ port, root, htmlEntry, noIntercept, watchDirCount }) {
|
|
52
|
+
const rule = c.dim + '-'.repeat(40) + c.reset;
|
|
53
|
+
console.log(`\n ${c.bold}zQuery Dev Server${c.reset}`);
|
|
54
|
+
console.log(` ${rule}`);
|
|
55
|
+
console.log(` Local: ${c.cyan}http://localhost:${port}/${c.reset}`);
|
|
56
|
+
console.log(` Root: ${root}`);
|
|
57
|
+
if (htmlEntry !== 'index.html') {
|
|
58
|
+
console.log(` HTML: ${c.cyan}${htmlEntry}${c.reset}`);
|
|
59
|
+
}
|
|
60
|
+
console.log(` Live Reload: ${c.green}enabled${c.reset} (SSE)`);
|
|
61
|
+
console.log(` Overlay: ${c.green}enabled${c.reset} (syntax + runtime + ZQueryError)`);
|
|
62
|
+
if (noIntercept) {
|
|
63
|
+
console.log(` Intercept: ${c.yellow}disabled${c.reset} (--no-intercept)`);
|
|
64
|
+
}
|
|
65
|
+
console.log(` Watching: all files in ${watchDirCount} director${watchDirCount === 1 ? 'y' : 'ies'}`);
|
|
66
|
+
console.log(` ${rule}`);
|
|
67
|
+
console.log(` Press Ctrl+C to stop\n`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { logCSS, logReload, logError, printBanner };
|
|
@@ -0,0 +1,317 @@
|
|
|
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
|
+
'>×</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() {
|
|
285
|
+
var sheets = document.querySelectorAll('link[rel="stylesheet"]');
|
|
286
|
+
sheets.forEach(function(l) {
|
|
287
|
+
var href = l.getAttribute('href');
|
|
288
|
+
if (!href) return;
|
|
289
|
+
var sep = href.indexOf('?') >= 0 ? '&' : '?';
|
|
290
|
+
l.setAttribute('href', href.replace(/[?&]_zqr=\\\\d+/, '') + sep + '_zqr=' + Date.now());
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
es.addEventListener('error:syntax', function(e) {
|
|
295
|
+
try { var data = JSON.parse(e.data); createOverlay(data); logToConsole(data); } catch(_){}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
es.addEventListener('error:runtime', function(e) {
|
|
299
|
+
try { var data = JSON.parse(e.data); createOverlay(data); logToConsole(data); } catch(_){}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
es.addEventListener('error:clear', function() {
|
|
303
|
+
removeOverlay();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
es.onerror = function() {
|
|
307
|
+
es.close();
|
|
308
|
+
clearTimeout(reconnectTimer);
|
|
309
|
+
reconnectTimer = setTimeout(connect, 2000);
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
connect();
|
|
314
|
+
})();
|
|
315
|
+
</script>`;
|
|
316
|
+
|
|
317
|
+
module.exports = OVERLAY_SCRIPT;
|
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
* @param {object} opts
|
|
49
|
+
* @param {string} opts.root — absolute path to project root
|
|
50
|
+
* @param {string} opts.htmlEntry — e.g. 'index.html'
|
|
51
|
+
* @param {number} opts.port
|
|
52
|
+
* @param {boolean} opts.noIntercept — skip zquery.min.js auto-resolve
|
|
53
|
+
* @returns {{ app, pool: SSEPool, listen: Function }}
|
|
54
|
+
*/
|
|
55
|
+
function createServer({ root, htmlEntry, port, noIntercept }) {
|
|
56
|
+
let zeroHttp;
|
|
57
|
+
try {
|
|
58
|
+
zeroHttp = require('zero-http');
|
|
59
|
+
} catch {
|
|
60
|
+
console.error(`\n \u2717 zero-http is required for the dev server.`);
|
|
61
|
+
console.error(` Install it: npm install zero-http --save-dev\n`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { createApp, static: serveStatic } = zeroHttp;
|
|
66
|
+
|
|
67
|
+
const app = createApp();
|
|
68
|
+
const pool = new SSEPool();
|
|
69
|
+
|
|
70
|
+
// ---- SSE endpoint ----
|
|
71
|
+
app.get('/__zq_reload', (req, res) => {
|
|
72
|
+
const sse = res.sse({ keepAlive: 30000, keepAliveComment: 'ping' });
|
|
73
|
+
pool.add(sse);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ---- Auto-resolve zquery.min.js ----
|
|
77
|
+
const pkgRoot = path.resolve(__dirname, '..', '..', '..');
|
|
78
|
+
|
|
79
|
+
app.use((req, res, next) => {
|
|
80
|
+
if (noIntercept) return next();
|
|
81
|
+
const basename = path.basename(req.url.split('?')[0]).toLowerCase();
|
|
82
|
+
if (basename !== 'zquery.min.js') return next();
|
|
83
|
+
|
|
84
|
+
const candidates = [
|
|
85
|
+
path.join(pkgRoot, 'dist', 'zquery.min.js'),
|
|
86
|
+
path.join(root, 'node_modules', 'zero-query', 'dist', 'zquery.min.js'),
|
|
87
|
+
];
|
|
88
|
+
for (const p of candidates) {
|
|
89
|
+
if (fs.existsSync(p)) {
|
|
90
|
+
res.set('Content-Type', 'application/javascript; charset=utf-8');
|
|
91
|
+
res.set('Cache-Control', 'no-cache');
|
|
92
|
+
res.send(fs.readFileSync(p, 'utf-8'));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
next();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ---- Static files ----
|
|
100
|
+
app.use(serveStatic(root, { index: false, dotfiles: 'ignore' }));
|
|
101
|
+
|
|
102
|
+
// ---- SPA fallback — inject overlay/SSE snippet ----
|
|
103
|
+
app.get('*', (req, res) => {
|
|
104
|
+
if (path.extname(req.url) && path.extname(req.url) !== '.html') {
|
|
105
|
+
res.status(404).send('Not Found');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const indexPath = path.join(root, htmlEntry);
|
|
109
|
+
if (!fs.existsSync(indexPath)) {
|
|
110
|
+
res.status(404).send(`${htmlEntry} not found`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
let html = fs.readFileSync(indexPath, 'utf-8');
|
|
114
|
+
if (html.includes('</body>')) {
|
|
115
|
+
html = html.replace('</body>', OVERLAY_SCRIPT + '\n</body>');
|
|
116
|
+
} else {
|
|
117
|
+
html += OVERLAY_SCRIPT;
|
|
118
|
+
}
|
|
119
|
+
res.html(html);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
function listen(cb) {
|
|
123
|
+
app.listen(port, cb);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { app, pool, listen };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = { createServer, SSEPool };
|