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
package/types/utils.d.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions — debounce, throttle, strings, objects, URL, storage, event bus.
|
|
3
|
+
*
|
|
4
|
+
* @module utils
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Function Utilities
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
/** Debounced function with a `.cancel()` helper. */
|
|
12
|
+
export interface DebouncedFunction<T extends (...args: any[]) => any> {
|
|
13
|
+
(...args: Parameters<T>): void;
|
|
14
|
+
/** Cancel the pending invocation. */
|
|
15
|
+
cancel(): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns a debounced function that delays execution until `ms` ms of inactivity.
|
|
20
|
+
* @param ms Default `250`.
|
|
21
|
+
*/
|
|
22
|
+
export function debounce<T extends (...args: any[]) => any>(fn: T, ms?: number): DebouncedFunction<T>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns a throttled function that executes at most once per `ms` ms.
|
|
26
|
+
* The return value of the original function is discarded.
|
|
27
|
+
* @param ms Default `250`.
|
|
28
|
+
*/
|
|
29
|
+
export function throttle<T extends (...args: any[]) => any>(fn: T, ms?: number): (...args: Parameters<T>) => void;
|
|
30
|
+
|
|
31
|
+
/** Left-to-right function composition. */
|
|
32
|
+
export function pipe<A, B>(f1: (a: A) => B): (input: A) => B;
|
|
33
|
+
export function pipe<A, B, C>(f1: (a: A) => B, f2: (b: B) => C): (input: A) => C;
|
|
34
|
+
export function pipe<A, B, C, D>(f1: (a: A) => B, f2: (b: B) => C, f3: (c: C) => D): (input: A) => D;
|
|
35
|
+
export function pipe<A, B, C, D, E>(f1: (a: A) => B, f2: (b: B) => C, f3: (c: C) => D, f4: (d: D) => E): (input: A) => E;
|
|
36
|
+
export function pipe<T>(...fns: Array<(value: any) => any>): (input: T) => any;
|
|
37
|
+
|
|
38
|
+
/** Returns a function that only executes once, caching the result. */
|
|
39
|
+
export function once<T extends (...args: any[]) => any>(fn: T): (...args: Parameters<T>) => ReturnType<T>;
|
|
40
|
+
|
|
41
|
+
/** Returns a `Promise` that resolves after `ms` milliseconds. */
|
|
42
|
+
export function sleep(ms: number): Promise<void>;
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// String Utilities
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/** Escape HTML entities: `&`, `<`, `>`, `"`, `'`. */
|
|
49
|
+
export function escapeHtml(str: string): string;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Tagged template literal that auto-escapes interpolated values.
|
|
53
|
+
* Use `$.trust()` to mark values as safe.
|
|
54
|
+
*/
|
|
55
|
+
export function html(strings: TemplateStringsArray, ...values: any[]): string;
|
|
56
|
+
|
|
57
|
+
/** Wrapper that marks an HTML string as trusted (not escaped by `$.html`). */
|
|
58
|
+
export interface TrustedHTML {
|
|
59
|
+
toString(): string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Mark an HTML string as trusted so it won't be escaped in `$.html`. */
|
|
63
|
+
export function trust(htmlStr: string): TrustedHTML;
|
|
64
|
+
|
|
65
|
+
/** Generate a UUID v4 string. */
|
|
66
|
+
export function uuid(): string;
|
|
67
|
+
|
|
68
|
+
/** Convert kebab-case to camelCase. */
|
|
69
|
+
export function camelCase(str: string): string;
|
|
70
|
+
|
|
71
|
+
/** Convert camelCase to kebab-case. */
|
|
72
|
+
export function kebabCase(str: string): string;
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Object Utilities
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/** Deep clone using `structuredClone` (JSON fallback). */
|
|
79
|
+
export function deepClone<T>(obj: T): T;
|
|
80
|
+
|
|
81
|
+
/** Recursively merge objects. Arrays are replaced, not merged. */
|
|
82
|
+
export function deepMerge<T extends object>(target: T, ...sources: Partial<T>[]): T;
|
|
83
|
+
|
|
84
|
+
/** Deep equality comparison. */
|
|
85
|
+
export function isEqual(a: any, b: any): boolean;
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// URL Utilities
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/** Serialize an object to a URL query string. */
|
|
92
|
+
export function param(obj: Record<string, any>): string;
|
|
93
|
+
|
|
94
|
+
/** Parse a URL query string into an object. */
|
|
95
|
+
export function parseQuery(str: string): Record<string, string>;
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Storage Wrappers
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/** JSON-aware storage wrapper (localStorage or sessionStorage). */
|
|
102
|
+
export interface StorageWrapper {
|
|
103
|
+
/**
|
|
104
|
+
* Get and JSON-parse a value.
|
|
105
|
+
* @param key Storage key.
|
|
106
|
+
* @param fallback Value returned if key is missing or parse fails (default `null`).
|
|
107
|
+
*/
|
|
108
|
+
get<T = any>(key: string, fallback?: T): T;
|
|
109
|
+
/** JSON-stringify and store. */
|
|
110
|
+
set(key: string, value: any): void;
|
|
111
|
+
/** Remove a key. */
|
|
112
|
+
remove(key: string): void;
|
|
113
|
+
/** Clear all entries. */
|
|
114
|
+
clear(): void;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** `localStorage` wrapper with auto JSON serialization. */
|
|
118
|
+
export declare const storage: StorageWrapper;
|
|
119
|
+
|
|
120
|
+
/** `sessionStorage` wrapper (same API as `storage`). */
|
|
121
|
+
export declare const session: StorageWrapper;
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Event Bus
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/** Singleton pub/sub event bus for cross-component communication. */
|
|
128
|
+
export interface EventBus {
|
|
129
|
+
/** Subscribe. Returns an unsubscribe function. */
|
|
130
|
+
on(event: string, fn: (...args: any[]) => void): () => void;
|
|
131
|
+
/** Unsubscribe a specific handler. */
|
|
132
|
+
off(event: string, fn: (...args: any[]) => void): void;
|
|
133
|
+
/** Emit an event with arguments. */
|
|
134
|
+
emit(event: string, ...args: any[]): void;
|
|
135
|
+
/** Subscribe for a single invocation. Returns an unsubscribe function. */
|
|
136
|
+
once(event: string, fn: (...args: any[]) => void): () => void;
|
|
137
|
+
/** Remove all listeners. */
|
|
138
|
+
clear(): void;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Global event bus instance. */
|
|
142
|
+
export declare const bus: EventBus;
|
package/cli/commands/dev.old.js
DELETED
|
@@ -1,520 +0,0 @@
|
|
|
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
|
-
'>×</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
|
-
// Custom HTML entry file (default: index.html)
|
|
325
|
-
const htmlEntry = option('index', 'i', 'index.html');
|
|
326
|
-
|
|
327
|
-
// Determine the project root to serve
|
|
328
|
-
let root = null;
|
|
329
|
-
for (let i = 1; i < args.length; i++) {
|
|
330
|
-
if (!args[i].startsWith('-') && args[i - 1] !== '-p' && args[i - 1] !== '--port' && args[i - 1] !== '--index') {
|
|
331
|
-
root = path.resolve(process.cwd(), args[i]);
|
|
332
|
-
break;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
if (!root) {
|
|
336
|
-
const candidates = [
|
|
337
|
-
process.cwd(),
|
|
338
|
-
path.join(process.cwd(), 'public'),
|
|
339
|
-
path.join(process.cwd(), 'src'),
|
|
340
|
-
];
|
|
341
|
-
for (const c of candidates) {
|
|
342
|
-
if (fs.existsSync(path.join(c, htmlEntry))) { root = c; break; }
|
|
343
|
-
}
|
|
344
|
-
if (!root) root = process.cwd();
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const PORT = parseInt(option('port', 'p', '3100'));
|
|
348
|
-
|
|
349
|
-
// SSE clients
|
|
350
|
-
const sseClients = new Set();
|
|
351
|
-
|
|
352
|
-
const app = createApp();
|
|
353
|
-
|
|
354
|
-
// SSE endpoint
|
|
355
|
-
app.get('/__zq_reload', (req, res) => {
|
|
356
|
-
const sse = res.sse({ keepAlive: 30000, keepAliveComment: 'ping' });
|
|
357
|
-
sseClients.add(sse);
|
|
358
|
-
sse.on('close', () => sseClients.delete(sse));
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
// Auto-resolve zquery.min.js
|
|
362
|
-
// __dirname is cli/commands/, package root is two levels up
|
|
363
|
-
const pkgRoot = path.resolve(__dirname, '..', '..');
|
|
364
|
-
const noIntercept = flag('no-intercept');
|
|
365
|
-
|
|
366
|
-
app.use((req, res, next) => {
|
|
367
|
-
if (noIntercept) return next();
|
|
368
|
-
const basename = path.basename(req.url.split('?')[0]).toLowerCase();
|
|
369
|
-
if (basename !== 'zquery.min.js') return next();
|
|
370
|
-
|
|
371
|
-
const candidates = [
|
|
372
|
-
path.join(pkgRoot, 'dist', 'zquery.min.js'),
|
|
373
|
-
path.join(root, 'node_modules', 'zero-query', 'dist', 'zquery.min.js'),
|
|
374
|
-
];
|
|
375
|
-
for (const p of candidates) {
|
|
376
|
-
if (fs.existsSync(p)) {
|
|
377
|
-
res.set('Content-Type', 'application/javascript; charset=utf-8');
|
|
378
|
-
res.set('Cache-Control', 'no-cache');
|
|
379
|
-
res.send(fs.readFileSync(p, 'utf-8'));
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
next();
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
// Static file serving
|
|
387
|
-
app.use(serveStatic(root, { index: false, dotfiles: 'ignore' }));
|
|
388
|
-
|
|
389
|
-
// SPA fallback — inject live-reload
|
|
390
|
-
app.get('*', (req, res) => {
|
|
391
|
-
if (path.extname(req.url) && path.extname(req.url) !== '.html') {
|
|
392
|
-
res.status(404).send('Not Found');
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
const indexPath = path.join(root, htmlEntry);
|
|
396
|
-
if (!fs.existsSync(indexPath)) {
|
|
397
|
-
res.status(404).send(`${htmlEntry} not found`);
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
let html = fs.readFileSync(indexPath, 'utf-8');
|
|
401
|
-
if (html.includes('</body>')) {
|
|
402
|
-
html = html.replace('</body>', LIVE_RELOAD_SNIPPET + '\n</body>');
|
|
403
|
-
} else {
|
|
404
|
-
html += LIVE_RELOAD_SNIPPET;
|
|
405
|
-
}
|
|
406
|
-
res.html(html);
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
// Broadcast helper
|
|
410
|
-
function broadcast(eventType, data) {
|
|
411
|
-
for (const sse of sseClients) {
|
|
412
|
-
try { sse.event(eventType, data || ''); } catch (_) { sseClients.delete(sse); }
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// File watcher
|
|
417
|
-
const IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', '.cache']);
|
|
418
|
-
let debounceTimer;
|
|
419
|
-
|
|
420
|
-
function shouldWatch(filename) {
|
|
421
|
-
if (!filename) return false;
|
|
422
|
-
if (filename.startsWith('.')) return false;
|
|
423
|
-
return true;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function isIgnored(filepath) {
|
|
427
|
-
const parts = filepath.split(path.sep);
|
|
428
|
-
return parts.some(p => IGNORE_DIRS.has(p));
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function collectWatchDirs(dir) {
|
|
432
|
-
const dirs = [dir];
|
|
433
|
-
try {
|
|
434
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
435
|
-
for (const entry of entries) {
|
|
436
|
-
if (!entry.isDirectory()) continue;
|
|
437
|
-
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
438
|
-
const sub = path.join(dir, entry.name);
|
|
439
|
-
dirs.push(...collectWatchDirs(sub));
|
|
440
|
-
}
|
|
441
|
-
} catch (_) {}
|
|
442
|
-
return dirs;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const watchDirs = collectWatchDirs(root);
|
|
446
|
-
const watchers = [];
|
|
447
|
-
|
|
448
|
-
// Track current error state to know when to clear
|
|
449
|
-
let currentError = null;
|
|
450
|
-
|
|
451
|
-
for (const dir of watchDirs) {
|
|
452
|
-
try {
|
|
453
|
-
const watcher = fs.watch(dir, (eventType, filename) => {
|
|
454
|
-
if (!shouldWatch(filename)) return;
|
|
455
|
-
const fullPath = path.join(dir, filename || '');
|
|
456
|
-
if (isIgnored(fullPath)) return;
|
|
457
|
-
|
|
458
|
-
clearTimeout(debounceTimer);
|
|
459
|
-
debounceTimer = setTimeout(() => {
|
|
460
|
-
const rel = path.relative(root, fullPath).replace(/\\/g, '/');
|
|
461
|
-
const ext = path.extname(filename).toLowerCase();
|
|
462
|
-
const now = new Date().toLocaleTimeString();
|
|
463
|
-
|
|
464
|
-
if (ext === '.css') {
|
|
465
|
-
console.log(` ${now} \x1b[35m css \x1b[0m ${rel}`);
|
|
466
|
-
broadcast('css', rel);
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Validate JS files for syntax errors before triggering reload
|
|
471
|
-
if (ext === '.js') {
|
|
472
|
-
const err = validateJS(fullPath, rel);
|
|
473
|
-
if (err) {
|
|
474
|
-
currentError = rel;
|
|
475
|
-
console.log(` ${now} \x1b[31m error \x1b[0m ${rel}`);
|
|
476
|
-
console.log(` \x1b[31m${err.type}: ${err.message}\x1b[0m`);
|
|
477
|
-
if (err.line) console.log(` \x1b[2mat line ${err.line}${err.column ? ':' + err.column : ''}\x1b[0m`);
|
|
478
|
-
broadcast('error:syntax', JSON.stringify(err));
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
// File was fixed — clear previous error if it was in this file
|
|
482
|
-
if (currentError === rel) {
|
|
483
|
-
currentError = null;
|
|
484
|
-
broadcast('error:clear', '');
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
console.log(` ${now} \x1b[36m reload \x1b[0m ${rel}`);
|
|
489
|
-
broadcast('reload', rel);
|
|
490
|
-
}, 100);
|
|
491
|
-
});
|
|
492
|
-
watchers.push(watcher);
|
|
493
|
-
} catch (_) {}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
app.listen(PORT, () => {
|
|
497
|
-
console.log(`\n \x1b[1mzQuery Dev Server\x1b[0m`);
|
|
498
|
-
console.log(` \x1b[2m${'-'.repeat(40)}\x1b[0m`);
|
|
499
|
-
console.log(` Local: \x1b[36mhttp://localhost:${PORT}/\x1b[0m`);
|
|
500
|
-
console.log(` Root: ${path.relative(process.cwd(), root) || '.'}`);
|
|
501
|
-
if (htmlEntry !== 'index.html') console.log(` HTML: \x1b[36m${htmlEntry}\x1b[0m`);
|
|
502
|
-
console.log(` Live Reload: \x1b[32menabled\x1b[0m (SSE)`);
|
|
503
|
-
console.log(` Overlay: \x1b[32menabled\x1b[0m (syntax + runtime errors)`);
|
|
504
|
-
if (noIntercept) console.log(` Intercept: \x1b[33mdisabled\x1b[0m (--no-intercept)`);
|
|
505
|
-
console.log(` Watching: all files in ${watchDirs.length} director${watchDirs.length === 1 ? 'y' : 'ies'}`);
|
|
506
|
-
console.log(` \x1b[2m${'-'.repeat(40)}\x1b[0m`);
|
|
507
|
-
console.log(` Press Ctrl+C to stop\n`);
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
// Graceful shutdown
|
|
511
|
-
process.on('SIGINT', () => {
|
|
512
|
-
console.log('\n Shutting down...');
|
|
513
|
-
watchers.forEach(w => w.close());
|
|
514
|
-
for (const sse of sseClients) { try { sse.close(); } catch (_) {} }
|
|
515
|
-
app.close(() => process.exit(0));
|
|
516
|
-
setTimeout(() => process.exit(0), 1000);
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
module.exports = devServer;
|