zero-query 0.5.2 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -10
- package/cli/commands/build.js +7 -5
- package/cli/commands/bundle.js +286 -8
- package/cli/commands/dev/index.js +82 -0
- package/cli/commands/dev/logger.js +70 -0
- package/cli/commands/dev/overlay.js +366 -0
- package/cli/commands/dev/server.js +158 -0
- package/cli/commands/dev/validator.js +94 -0
- package/cli/commands/dev/watcher.js +147 -0
- package/cli/scaffold/favicon.ico +0 -0
- package/cli/scaffold/index.html +1 -0
- package/cli/scaffold/scripts/app.js +15 -22
- package/cli/scaffold/scripts/components/about.js +14 -2
- package/cli/scaffold/scripts/components/contacts/contacts.css +0 -7
- package/cli/scaffold/scripts/components/contacts/contacts.html +8 -7
- 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/cli/scaffold/styles/styles.css +1 -0
- package/cli/utils.js +111 -6
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +2005 -216
- package/dist/zquery.min.js +3 -13
- package/index.d.ts +149 -1080
- package/index.js +18 -7
- package/package.json +9 -3
- package/src/component.js +186 -45
- package/src/core.js +327 -35
- 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 +59 -6
- package/src/ssr.js +224 -0
- package/src/store.js +24 -8
- package/tests/component.test.js +304 -0
- package/tests/core.test.js +726 -0
- package/tests/diff.test.js +194 -0
- package/tests/errors.test.js +162 -0
- package/tests/expression.test.js +334 -0
- package/tests/http.test.js +181 -0
- package/tests/reactive.test.js +191 -0
- package/tests/router.test.js +332 -0
- package/tests/store.test.js +253 -0
- package/tests/utils.test.js +353 -0
- package/types/collection.d.ts +368 -0
- package/types/component.d.ts +210 -0
- package/types/errors.d.ts +103 -0
- package/types/http.d.ts +81 -0
- package/types/misc.d.ts +166 -0
- package/types/reactive.d.ts +76 -0
- package/types/router.d.ts +132 -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.js → dev.old.js} +0 -0
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 = {}) {
|
|
@@ -77,7 +78,21 @@ class Router {
|
|
|
77
78
|
if (!link) return;
|
|
78
79
|
if (link.getAttribute('target') === '_blank') return;
|
|
79
80
|
e.preventDefault();
|
|
80
|
-
|
|
81
|
+
let href = link.getAttribute('z-link');
|
|
82
|
+
// Support z-link-params for dynamic :param interpolation
|
|
83
|
+
const paramsAttr = link.getAttribute('z-link-params');
|
|
84
|
+
if (paramsAttr) {
|
|
85
|
+
try {
|
|
86
|
+
const params = JSON.parse(paramsAttr);
|
|
87
|
+
href = this._interpolateParams(href, params);
|
|
88
|
+
} catch { /* ignore malformed JSON */ }
|
|
89
|
+
}
|
|
90
|
+
this.navigate(href);
|
|
91
|
+
// z-to-top modifier: scroll to top after navigation
|
|
92
|
+
if (link.hasAttribute('z-to-top')) {
|
|
93
|
+
const scrollBehavior = link.getAttribute('z-to-top') || 'instant';
|
|
94
|
+
window.scrollTo({ top: 0, behavior: scrollBehavior });
|
|
95
|
+
}
|
|
81
96
|
});
|
|
82
97
|
|
|
83
98
|
// Initial resolve
|
|
@@ -121,7 +136,23 @@ class Router {
|
|
|
121
136
|
|
|
122
137
|
// --- Navigation ----------------------------------------------------------
|
|
123
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Interpolate :param placeholders in a path with the given values.
|
|
141
|
+
* @param {string} path — e.g. '/user/:id/posts/:pid'
|
|
142
|
+
* @param {Object} params — e.g. { id: 42, pid: 7 }
|
|
143
|
+
* @returns {string}
|
|
144
|
+
*/
|
|
145
|
+
_interpolateParams(path, params) {
|
|
146
|
+
if (!params || typeof params !== 'object') return path;
|
|
147
|
+
return path.replace(/:([\w]+)/g, (_, key) => {
|
|
148
|
+
const val = params[key];
|
|
149
|
+
return val != null ? encodeURIComponent(String(val)) : ':' + key;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
124
153
|
navigate(path, options = {}) {
|
|
154
|
+
// Interpolate :param placeholders if options.params is provided
|
|
155
|
+
if (options.params) path = this._interpolateParams(path, options.params);
|
|
125
156
|
// Separate hash fragment (e.g. /docs/getting-started#cli-bundler)
|
|
126
157
|
const [cleanPath, fragment] = (path || '').split('#');
|
|
127
158
|
let normalized = this._normalizePath(cleanPath);
|
|
@@ -139,6 +170,8 @@ class Router {
|
|
|
139
170
|
}
|
|
140
171
|
|
|
141
172
|
replace(path, options = {}) {
|
|
173
|
+
// Interpolate :param placeholders if options.params is provided
|
|
174
|
+
if (options.params) path = this._interpolateParams(path, options.params);
|
|
142
175
|
const [cleanPath, fragment] = (path || '').split('#');
|
|
143
176
|
let normalized = this._normalizePath(cleanPath);
|
|
144
177
|
const hash = fragment ? '#' + fragment : '';
|
|
@@ -252,6 +285,7 @@ class Router {
|
|
|
252
285
|
// Prevent re-entrant calls (e.g. listener triggering navigation)
|
|
253
286
|
if (this._resolving) return;
|
|
254
287
|
this._resolving = true;
|
|
288
|
+
this._redirectCount = 0;
|
|
255
289
|
try {
|
|
256
290
|
await this.__resolve();
|
|
257
291
|
} finally {
|
|
@@ -289,10 +323,29 @@ class Router {
|
|
|
289
323
|
|
|
290
324
|
// Run before guards
|
|
291
325
|
for (const guard of this._guards.before) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
326
|
+
try {
|
|
327
|
+
const result = await guard(to, from);
|
|
328
|
+
if (result === false) return; // Cancel
|
|
329
|
+
if (typeof result === 'string') { // Redirect
|
|
330
|
+
if (++this._redirectCount > 10) {
|
|
331
|
+
reportError(ErrorCode.ROUTER_GUARD, 'Too many guard redirects (possible loop)', { to }, null);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// Update URL directly and re-resolve (avoids re-entrancy block)
|
|
335
|
+
const [rPath, rFrag] = result.split('#');
|
|
336
|
+
const rNorm = this._normalizePath(rPath || '/');
|
|
337
|
+
const rHash = rFrag ? '#' + rFrag : '';
|
|
338
|
+
if (this._mode === 'hash') {
|
|
339
|
+
if (rFrag) window.__zqScrollTarget = rFrag;
|
|
340
|
+
window.location.replace('#' + rNorm);
|
|
341
|
+
} else {
|
|
342
|
+
window.history.replaceState({}, '', this._base + rNorm + rHash);
|
|
343
|
+
}
|
|
344
|
+
return this.__resolve();
|
|
345
|
+
}
|
|
346
|
+
} catch (err) {
|
|
347
|
+
reportError(ErrorCode.ROUTER_GUARD, 'Before-guard threw', { to, from }, err);
|
|
348
|
+
return;
|
|
296
349
|
}
|
|
297
350
|
}
|
|
298
351
|
|
|
@@ -300,7 +353,7 @@ class Router {
|
|
|
300
353
|
if (matched.load) {
|
|
301
354
|
try { await matched.load(); }
|
|
302
355
|
catch (err) {
|
|
303
|
-
|
|
356
|
+
reportError(ErrorCode.ROUTER_LOAD, `Failed to load module for route "${matched.path}"`, { path: matched.path }, err);
|
|
304
357
|
return;
|
|
305
358
|
}
|
|
306
359
|
}
|
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
|
/**
|