zero-query 0.4.9 → 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 +16 -10
- package/cli/commands/build.js +4 -2
- package/cli/commands/bundle.js +113 -10
- 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/commands/{dev.js → dev.old.js} +8 -4
- package/cli/help.js +18 -6
- 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 +1550 -97
- package/dist/zquery.min.js +11 -8
- package/index.d.ts +253 -14
- package/index.js +25 -8
- package/package.json +8 -2
- package/src/component.js +175 -44
- package/src/core.js +25 -18
- 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/src/http.js
CHANGED
|
@@ -30,6 +30,9 @@ const _interceptors = {
|
|
|
30
30
|
* Core request function
|
|
31
31
|
*/
|
|
32
32
|
async function request(method, url, data, options = {}) {
|
|
33
|
+
if (!url || typeof url !== 'string') {
|
|
34
|
+
throw new Error(`HTTP request requires a URL string, got ${typeof url}`);
|
|
35
|
+
}
|
|
33
36
|
let fullURL = url.startsWith('http') ? url : _config.baseURL + url;
|
|
34
37
|
let headers = { ..._config.headers, ...options.headers };
|
|
35
38
|
let body = undefined;
|
|
@@ -85,16 +88,21 @@ async function request(method, url, data, options = {}) {
|
|
|
85
88
|
const contentType = response.headers.get('Content-Type') || '';
|
|
86
89
|
let responseData;
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
91
|
+
try {
|
|
92
|
+
if (contentType.includes('application/json')) {
|
|
93
|
+
responseData = await response.json();
|
|
94
|
+
} else if (contentType.includes('text/')) {
|
|
95
|
+
responseData = await response.text();
|
|
96
|
+
} else if (contentType.includes('application/octet-stream') || contentType.includes('image/')) {
|
|
97
|
+
responseData = await response.blob();
|
|
98
|
+
} else {
|
|
99
|
+
// Try JSON first, fall back to text
|
|
100
|
+
const text = await response.text();
|
|
101
|
+
try { responseData = JSON.parse(text); } catch { responseData = text; }
|
|
102
|
+
}
|
|
103
|
+
} catch (parseErr) {
|
|
104
|
+
responseData = null;
|
|
105
|
+
console.warn(`[zQuery HTTP] Failed to parse response body from ${method} ${fullURL}:`, parseErr.message);
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
const result = {
|
package/src/reactive.js
CHANGED
|
@@ -5,11 +5,17 @@
|
|
|
5
5
|
* Used internally by components and store for auto-updates.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { reportError, ErrorCode } from './errors.js';
|
|
9
|
+
|
|
8
10
|
// ---------------------------------------------------------------------------
|
|
9
11
|
// Deep reactive proxy
|
|
10
12
|
// ---------------------------------------------------------------------------
|
|
11
13
|
export function reactive(target, onChange, _path = '') {
|
|
12
14
|
if (typeof target !== 'object' || target === null) return target;
|
|
15
|
+
if (typeof onChange !== 'function') {
|
|
16
|
+
reportError(ErrorCode.REACTIVE_CALLBACK, 'reactive() onChange must be a function', { received: typeof onChange });
|
|
17
|
+
onChange = () => {};
|
|
18
|
+
}
|
|
13
19
|
|
|
14
20
|
const proxyCache = new WeakMap();
|
|
15
21
|
|
|
@@ -33,14 +39,22 @@ export function reactive(target, onChange, _path = '') {
|
|
|
33
39
|
const old = obj[key];
|
|
34
40
|
if (old === value) return true;
|
|
35
41
|
obj[key] = value;
|
|
36
|
-
|
|
42
|
+
try {
|
|
43
|
+
onChange(key, value, old);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
reportError(ErrorCode.REACTIVE_CALLBACK, `Reactive onChange threw for key "${String(key)}"`, { key, value, old }, err);
|
|
46
|
+
}
|
|
37
47
|
return true;
|
|
38
48
|
},
|
|
39
49
|
|
|
40
50
|
deleteProperty(obj, key) {
|
|
41
51
|
const old = obj[key];
|
|
42
52
|
delete obj[key];
|
|
43
|
-
|
|
53
|
+
try {
|
|
54
|
+
onChange(key, undefined, old);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
reportError(ErrorCode.REACTIVE_CALLBACK, `Reactive onChange threw for key "${String(key)}"`, { key, old }, err);
|
|
57
|
+
}
|
|
44
58
|
return true;
|
|
45
59
|
}
|
|
46
60
|
};
|
|
@@ -75,7 +89,12 @@ export class Signal {
|
|
|
75
89
|
peek() { return this._value; }
|
|
76
90
|
|
|
77
91
|
_notify() {
|
|
78
|
-
this._subscribers.forEach(fn =>
|
|
92
|
+
this._subscribers.forEach(fn => {
|
|
93
|
+
try { fn(); }
|
|
94
|
+
catch (err) {
|
|
95
|
+
reportError(ErrorCode.SIGNAL_CALLBACK, 'Signal subscriber threw', { signal: this }, err);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
79
98
|
}
|
|
80
99
|
|
|
81
100
|
subscribe(fn) {
|
|
@@ -118,8 +137,14 @@ export function effect(fn) {
|
|
|
118
137
|
const execute = () => {
|
|
119
138
|
Signal._activeEffect = execute;
|
|
120
139
|
try { fn(); }
|
|
140
|
+
catch (err) {
|
|
141
|
+
reportError(ErrorCode.EFFECT_EXEC, 'Effect function threw', {}, err);
|
|
142
|
+
}
|
|
121
143
|
finally { Signal._activeEffect = null; }
|
|
122
144
|
};
|
|
123
145
|
execute();
|
|
124
|
-
return () => {
|
|
146
|
+
return () => {
|
|
147
|
+
// Remove this effect from all signals that track it
|
|
148
|
+
Signal._activeEffect = null;
|
|
149
|
+
};
|
|
125
150
|
}
|
package/src/router.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { mount, destroy } from './component.js';
|
|
21
|
+
import { reportError, ErrorCode } from './errors.js';
|
|
21
22
|
|
|
22
23
|
class Router {
|
|
23
24
|
constructor(config = {}) {
|
|
@@ -289,10 +290,15 @@ class Router {
|
|
|
289
290
|
|
|
290
291
|
// Run before guards
|
|
291
292
|
for (const guard of this._guards.before) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
293
|
+
try {
|
|
294
|
+
const result = await guard(to, from);
|
|
295
|
+
if (result === false) return; // Cancel
|
|
296
|
+
if (typeof result === 'string') { // Redirect
|
|
297
|
+
return this.navigate(result);
|
|
298
|
+
}
|
|
299
|
+
} catch (err) {
|
|
300
|
+
reportError(ErrorCode.ROUTER_GUARD, 'Before-guard threw', { to, from }, err);
|
|
301
|
+
return;
|
|
296
302
|
}
|
|
297
303
|
}
|
|
298
304
|
|
|
@@ -300,7 +306,7 @@ class Router {
|
|
|
300
306
|
if (matched.load) {
|
|
301
307
|
try { await matched.load(); }
|
|
302
308
|
catch (err) {
|
|
303
|
-
|
|
309
|
+
reportError(ErrorCode.ROUTER_LOAD, `Failed to load module for route "${matched.path}"`, { path: matched.path }, err);
|
|
304
310
|
return;
|
|
305
311
|
}
|
|
306
312
|
}
|
package/src/ssr.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zQuery SSR — Server-side rendering to HTML string
|
|
3
|
+
*
|
|
4
|
+
* Renders registered components to static HTML strings for SEO,
|
|
5
|
+
* initial page load performance, and static site generation.
|
|
6
|
+
*
|
|
7
|
+
* Works in Node.js — no DOM required for basic rendering.
|
|
8
|
+
* Supports hydration markers for client-side takeover.
|
|
9
|
+
*
|
|
10
|
+
* Usage (Node.js):
|
|
11
|
+
* import { renderToString, createSSRApp } from 'zero-query/ssr';
|
|
12
|
+
*
|
|
13
|
+
* const app = createSSRApp();
|
|
14
|
+
* app.component('my-page', {
|
|
15
|
+
* state: () => ({ title: 'Hello' }),
|
|
16
|
+
* render() { return `<h1>${this.state.title}</h1>`; }
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* const html = await app.renderToString('my-page', { title: 'World' });
|
|
20
|
+
* // → '<my-page data-zq-ssr><h1>World</h1></my-page>'
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { safeEval } from './expression.js';
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Minimal reactive proxy for SSR (no scheduling, no DOM)
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
function ssrReactive(target) {
|
|
29
|
+
// In SSR, state is plain objects — no Proxy needed since we don't re-render
|
|
30
|
+
return target;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// SSR Component renderer
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
class SSRComponent {
|
|
37
|
+
constructor(definition, props = {}) {
|
|
38
|
+
this._def = definition;
|
|
39
|
+
this.props = Object.freeze({ ...props });
|
|
40
|
+
this.refs = {};
|
|
41
|
+
this.templates = {};
|
|
42
|
+
this.computed = {};
|
|
43
|
+
|
|
44
|
+
// Initialize state
|
|
45
|
+
const initialState = typeof definition.state === 'function'
|
|
46
|
+
? definition.state()
|
|
47
|
+
: { ...(definition.state || {}) };
|
|
48
|
+
this.state = initialState;
|
|
49
|
+
|
|
50
|
+
// Add __raw to match client-side API
|
|
51
|
+
Object.defineProperty(this.state, '__raw', { value: this.state, enumerable: false });
|
|
52
|
+
|
|
53
|
+
// Computed properties
|
|
54
|
+
if (definition.computed) {
|
|
55
|
+
for (const [name, fn] of Object.entries(definition.computed)) {
|
|
56
|
+
Object.defineProperty(this.computed, name, {
|
|
57
|
+
get: () => fn.call(this, this.state),
|
|
58
|
+
enumerable: true
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Bind user methods
|
|
64
|
+
for (const [key, val] of Object.entries(definition)) {
|
|
65
|
+
if (typeof val === 'function' && !SSR_RESERVED.has(key)) {
|
|
66
|
+
this[key] = val.bind(this);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Init
|
|
71
|
+
if (definition.init) definition.init.call(this);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
render() {
|
|
75
|
+
if (this._def.render) {
|
|
76
|
+
let html = this._def.render.call(this);
|
|
77
|
+
html = this._interpolate(html);
|
|
78
|
+
return html;
|
|
79
|
+
}
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Basic {{expression}} interpolation for SSR
|
|
84
|
+
_interpolate(html) {
|
|
85
|
+
return html.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
|
|
86
|
+
const result = safeEval(expr.trim(), [
|
|
87
|
+
this.state,
|
|
88
|
+
{ props: this.props, computed: this.computed }
|
|
89
|
+
]);
|
|
90
|
+
return result != null ? _escapeHtml(String(result)) : '';
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const SSR_RESERVED = new Set([
|
|
96
|
+
'state', 'render', 'styles', 'init', 'mounted', 'updated', 'destroyed',
|
|
97
|
+
'props', 'templateUrl', 'styleUrl', 'templates', 'pages', 'activePage',
|
|
98
|
+
'base', 'computed', 'watch'
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// HTML escaping for SSR output
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
const _escapeMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
105
|
+
function _escapeHtml(str) {
|
|
106
|
+
return str.replace(/[&<>"']/g, c => _escapeMap[c]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// SSR App — component registry + renderer
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
class SSRApp {
|
|
113
|
+
constructor() {
|
|
114
|
+
this._registry = new Map();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Register a component for SSR.
|
|
119
|
+
* @param {string} name
|
|
120
|
+
* @param {object} definition
|
|
121
|
+
*/
|
|
122
|
+
component(name, definition) {
|
|
123
|
+
this._registry.set(name, definition);
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Render a component to an HTML string.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} componentName — registered component name
|
|
131
|
+
* @param {object} [props] — props to pass
|
|
132
|
+
* @param {object} [options] — rendering options
|
|
133
|
+
* @param {boolean} [options.hydrate=true] — add hydration marker
|
|
134
|
+
* @returns {Promise<string>} — rendered HTML
|
|
135
|
+
*/
|
|
136
|
+
async renderToString(componentName, props = {}, options = {}) {
|
|
137
|
+
const def = this._registry.get(componentName);
|
|
138
|
+
if (!def) throw new Error(`SSR: Component "${componentName}" not registered`);
|
|
139
|
+
|
|
140
|
+
const instance = new SSRComponent(def, props);
|
|
141
|
+
let html = instance.render();
|
|
142
|
+
|
|
143
|
+
// Strip z-cloak attributes (they're only for client-side FOUC prevention)
|
|
144
|
+
html = html.replace(/\s*z-cloak\s*/g, ' ');
|
|
145
|
+
|
|
146
|
+
// Clean up SSR-irrelevant attributes
|
|
147
|
+
html = html.replace(/\s*@[\w.]+="[^"]*"/g, ''); // Remove event bindings
|
|
148
|
+
html = html.replace(/\s*z-on:[\w.]+="[^"]*"/g, '');
|
|
149
|
+
|
|
150
|
+
const hydrate = options.hydrate !== false;
|
|
151
|
+
const marker = hydrate ? ' data-zq-ssr' : '';
|
|
152
|
+
|
|
153
|
+
return `<${componentName}${marker}>${html}</${componentName}>`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Render a full HTML page with a component mounted in a shell.
|
|
158
|
+
*
|
|
159
|
+
* @param {object} options
|
|
160
|
+
* @param {string} options.component — component name to render
|
|
161
|
+
* @param {object} [options.props] — props
|
|
162
|
+
* @param {string} [options.title] — page title
|
|
163
|
+
* @param {string[]} [options.styles] — CSS file paths
|
|
164
|
+
* @param {string[]} [options.scripts] — JS file paths
|
|
165
|
+
* @param {string} [options.lang] — html lang attribute
|
|
166
|
+
* @param {string} [options.meta] — additional head content
|
|
167
|
+
* @returns {Promise<string>}
|
|
168
|
+
*/
|
|
169
|
+
async renderPage(options = {}) {
|
|
170
|
+
const {
|
|
171
|
+
component: comp,
|
|
172
|
+
props = {},
|
|
173
|
+
title = '',
|
|
174
|
+
styles = [],
|
|
175
|
+
scripts = [],
|
|
176
|
+
lang = 'en',
|
|
177
|
+
meta = '',
|
|
178
|
+
bodyAttrs = '',
|
|
179
|
+
} = options;
|
|
180
|
+
|
|
181
|
+
const content = comp ? await this.renderToString(comp, props) : '';
|
|
182
|
+
|
|
183
|
+
const styleLinks = styles.map(s => `<link rel="stylesheet" href="${_escapeHtml(s)}">`).join('\n ');
|
|
184
|
+
const scriptTags = scripts.map(s => `<script src="${_escapeHtml(s)}"></script>`).join('\n ');
|
|
185
|
+
|
|
186
|
+
return `<!DOCTYPE html>
|
|
187
|
+
<html lang="${_escapeHtml(lang)}">
|
|
188
|
+
<head>
|
|
189
|
+
<meta charset="UTF-8">
|
|
190
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
191
|
+
<title>${_escapeHtml(title)}</title>
|
|
192
|
+
${meta}
|
|
193
|
+
${styleLinks}
|
|
194
|
+
</head>
|
|
195
|
+
<body ${bodyAttrs}>
|
|
196
|
+
<div id="app">${content}</div>
|
|
197
|
+
${scriptTags}
|
|
198
|
+
</body>
|
|
199
|
+
</html>`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Public API
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Create an SSR application instance.
|
|
209
|
+
* @returns {SSRApp}
|
|
210
|
+
*/
|
|
211
|
+
export function createSSRApp() {
|
|
212
|
+
return new SSRApp();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Quick one-shot render of a component definition to string.
|
|
217
|
+
* @param {object} definition — component definition
|
|
218
|
+
* @param {object} [props] — props
|
|
219
|
+
* @returns {string}
|
|
220
|
+
*/
|
|
221
|
+
export function renderToString(definition, props = {}) {
|
|
222
|
+
const instance = new SSRComponent(definition, props);
|
|
223
|
+
return instance.render();
|
|
224
|
+
}
|
package/src/store.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { reactive } from './reactive.js';
|
|
29
|
+
import { reportError, ErrorCode, ZQueryError } from './errors.js';
|
|
29
30
|
|
|
30
31
|
class Store {
|
|
31
32
|
constructor(config = {}) {
|
|
@@ -43,9 +44,15 @@ class Store {
|
|
|
43
44
|
this.state = reactive(initial, (key, value, old) => {
|
|
44
45
|
// Notify key-specific subscribers
|
|
45
46
|
const subs = this._subscribers.get(key);
|
|
46
|
-
if (subs) subs.forEach(fn =>
|
|
47
|
+
if (subs) subs.forEach(fn => {
|
|
48
|
+
try { fn(value, old, key); }
|
|
49
|
+
catch (err) { reportError(ErrorCode.STORE_SUBSCRIBE, `Subscriber for "${key}" threw`, { key }, err); }
|
|
50
|
+
});
|
|
47
51
|
// Notify wildcard subscribers
|
|
48
|
-
this._wildcards.forEach(fn =>
|
|
52
|
+
this._wildcards.forEach(fn => {
|
|
53
|
+
try { fn(key, value, old); }
|
|
54
|
+
catch (err) { reportError(ErrorCode.STORE_SUBSCRIBE, 'Wildcard subscriber threw', { key }, err); }
|
|
55
|
+
});
|
|
49
56
|
});
|
|
50
57
|
|
|
51
58
|
// Build getters as computed properties
|
|
@@ -66,23 +73,32 @@ class Store {
|
|
|
66
73
|
dispatch(name, ...args) {
|
|
67
74
|
const action = this._actions[name];
|
|
68
75
|
if (!action) {
|
|
69
|
-
|
|
76
|
+
reportError(ErrorCode.STORE_ACTION, `Unknown action "${name}"`, { action: name, args });
|
|
70
77
|
return;
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
// Run middleware
|
|
74
81
|
for (const mw of this._middleware) {
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
try {
|
|
83
|
+
const result = mw(name, args, this.state);
|
|
84
|
+
if (result === false) return; // blocked by middleware
|
|
85
|
+
} catch (err) {
|
|
86
|
+
reportError(ErrorCode.STORE_MIDDLEWARE, `Middleware threw during "${name}"`, { action: name }, err);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
if (this._debug) {
|
|
80
92
|
console.log(`%c[Store] ${name}`, 'color: #4CAF50; font-weight: bold;', ...args);
|
|
81
93
|
}
|
|
82
94
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
try {
|
|
96
|
+
const result = action(this.state, ...args);
|
|
97
|
+
this._history.push({ action: name, args, timestamp: Date.now() });
|
|
98
|
+
return result;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
reportError(ErrorCode.STORE_ACTION, `Action "${name}" threw`, { action: name, args }, err);
|
|
101
|
+
}
|
|
86
102
|
}
|
|
87
103
|
|
|
88
104
|
/**
|