zero-query 0.9.8 → 0.9.9
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 +26 -3
- package/cli/commands/create.js +39 -5
- package/cli/help.js +2 -0
- package/cli/scaffold/ssr/app/app.js +30 -0
- package/cli/scaffold/ssr/app/components/about.js +28 -0
- package/cli/scaffold/ssr/app/components/home.js +37 -0
- package/cli/scaffold/ssr/app/components/not-found.js +15 -0
- package/cli/scaffold/ssr/app/routes.js +6 -0
- package/cli/scaffold/ssr/global.css +113 -0
- package/cli/scaffold/ssr/index.html +31 -0
- package/cli/scaffold/ssr/package.json +8 -0
- package/cli/scaffold/ssr/server/index.js +118 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +64 -8
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +11 -1
- package/index.js +4 -2
- package/package.json +8 -2
- package/src/errors.js +59 -5
- package/src/package.json +1 -0
- package/src/ssr.js +116 -22
- package/tests/errors.test.js +423 -145
- package/tests/ssr.test.js +435 -3
- package/types/errors.d.ts +34 -2
- package/types/ssr.d.ts +21 -1
package/index.js
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
capitalize, truncate, clamp,
|
|
28
28
|
memoize, retry, timeout,
|
|
29
29
|
} from './src/utils.js';
|
|
30
|
-
import { ZQueryError, ErrorCode, onError, reportError, guardCallback, validate } from './src/errors.js';
|
|
30
|
+
import { ZQueryError, ErrorCode, onError, reportError, guardCallback, guardAsync, validate, formatError } from './src/errors.js';
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
@@ -173,7 +173,9 @@ $.onError = onError;
|
|
|
173
173
|
$.ZQueryError = ZQueryError;
|
|
174
174
|
$.ErrorCode = ErrorCode;
|
|
175
175
|
$.guardCallback = guardCallback;
|
|
176
|
+
$.guardAsync = guardAsync;
|
|
176
177
|
$.validate = validate;
|
|
178
|
+
$.formatError = formatError;
|
|
177
179
|
|
|
178
180
|
// --- Meta ------------------------------------------------------------------
|
|
179
181
|
$.version = '__VERSION__';
|
|
@@ -213,7 +215,7 @@ export {
|
|
|
213
215
|
createRouter, getRouter,
|
|
214
216
|
createStore, getStore,
|
|
215
217
|
http,
|
|
216
|
-
ZQueryError, ErrorCode, onError, reportError, guardCallback, validate,
|
|
218
|
+
ZQueryError, ErrorCode, onError, reportError, guardCallback, guardAsync, validate, formatError,
|
|
217
219
|
debounce, throttle, pipe, once, sleep,
|
|
218
220
|
escapeHtml, stripHtml, html, trust, TrustedHTML, uuid, camelCase, kebabCase,
|
|
219
221
|
deepClone, deepMerge, isEqual, param, parseQuery,
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zero-query",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9",
|
|
4
4
|
"description": "Lightweight modern frontend library — jQuery-like selectors, reactive components, SPA router, and state management with zero dependencies.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./ssr": "./src/ssr.js",
|
|
10
|
+
"./src/*": "./src/*"
|
|
11
|
+
},
|
|
7
12
|
"bin": {
|
|
8
13
|
"zquery": "cli/index.js"
|
|
9
14
|
},
|
|
@@ -26,7 +31,8 @@
|
|
|
26
31
|
"bundle": "node cli/index.js bundle",
|
|
27
32
|
"bundle:app": "node cli/index.js bundle zquery-website",
|
|
28
33
|
"test": "vitest run",
|
|
29
|
-
"test:watch": "vitest"
|
|
34
|
+
"test:watch": "vitest",
|
|
35
|
+
"test:ssr": "node scripts/test-ssr.js"
|
|
30
36
|
},
|
|
31
37
|
"keywords": [
|
|
32
38
|
"dom",
|
package/src/errors.js
CHANGED
|
@@ -49,6 +49,12 @@ export const ErrorCode = Object.freeze({
|
|
|
49
49
|
HTTP_INTERCEPTOR: 'ZQ_HTTP_INTERCEPTOR',
|
|
50
50
|
HTTP_PARSE: 'ZQ_HTTP_PARSE',
|
|
51
51
|
|
|
52
|
+
// SSR
|
|
53
|
+
SSR_RENDER: 'ZQ_SSR_RENDER',
|
|
54
|
+
SSR_COMPONENT: 'ZQ_SSR_COMPONENT',
|
|
55
|
+
SSR_HYDRATION: 'ZQ_SSR_HYDRATION',
|
|
56
|
+
SSR_PAGE: 'ZQ_SSR_PAGE',
|
|
57
|
+
|
|
52
58
|
// General
|
|
53
59
|
INVALID_ARGUMENT: 'ZQ_INVALID_ARGUMENT',
|
|
54
60
|
});
|
|
@@ -77,16 +83,28 @@ export class ZQueryError extends Error {
|
|
|
77
83
|
// ---------------------------------------------------------------------------
|
|
78
84
|
// Global error handler
|
|
79
85
|
// ---------------------------------------------------------------------------
|
|
80
|
-
let
|
|
86
|
+
let _errorHandlers = [];
|
|
81
87
|
|
|
82
88
|
/**
|
|
83
89
|
* Register a global error handler.
|
|
84
90
|
* Called whenever zQuery catches an error internally.
|
|
91
|
+
* Multiple handlers are supported — each receives the error.
|
|
92
|
+
* Pass `null` to clear all handlers.
|
|
85
93
|
*
|
|
86
94
|
* @param {Function|null} handler — (error: ZQueryError) => void
|
|
95
|
+
* @returns {Function} unsubscribe function to remove this handler
|
|
87
96
|
*/
|
|
88
97
|
export function onError(handler) {
|
|
89
|
-
|
|
98
|
+
if (handler === null) {
|
|
99
|
+
_errorHandlers = [];
|
|
100
|
+
return () => {};
|
|
101
|
+
}
|
|
102
|
+
if (typeof handler !== 'function') return () => {};
|
|
103
|
+
_errorHandlers.push(handler);
|
|
104
|
+
return () => {
|
|
105
|
+
const idx = _errorHandlers.indexOf(handler);
|
|
106
|
+
if (idx !== -1) _errorHandlers.splice(idx, 1);
|
|
107
|
+
};
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
/**
|
|
@@ -103,9 +121,9 @@ export function reportError(code, message, context = {}, cause) {
|
|
|
103
121
|
? cause
|
|
104
122
|
: new ZQueryError(code, message, context, cause);
|
|
105
123
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
try {
|
|
124
|
+
// Notify all registered handlers
|
|
125
|
+
for (const handler of _errorHandlers) {
|
|
126
|
+
try { handler(err); } catch { /* prevent handler from crashing framework */ }
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
// Always log for developer visibility
|
|
@@ -153,3 +171,39 @@ export function validate(value, name, expectedType) {
|
|
|
153
171
|
);
|
|
154
172
|
}
|
|
155
173
|
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Format a ZQueryError into a structured object suitable for overlays/logging.
|
|
177
|
+
* @param {ZQueryError|Error} err
|
|
178
|
+
* @returns {{ code: string, type: string, message: string, context: object, stack: string }}
|
|
179
|
+
*/
|
|
180
|
+
export function formatError(err) {
|
|
181
|
+
const isZQ = err instanceof ZQueryError;
|
|
182
|
+
return {
|
|
183
|
+
code: isZQ ? err.code : '',
|
|
184
|
+
type: isZQ ? 'ZQueryError' : (err.name || 'Error'),
|
|
185
|
+
message: err.message || 'Unknown error',
|
|
186
|
+
context: isZQ ? err.context : {},
|
|
187
|
+
stack: err.stack || '',
|
|
188
|
+
cause: err.cause ? formatError(err.cause) : null,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Async version of guardCallback — wraps an async function so that
|
|
194
|
+
* rejections are caught, reported, and don't crash execution.
|
|
195
|
+
*
|
|
196
|
+
* @param {Function} fn — async function
|
|
197
|
+
* @param {string} code — ErrorCode to use
|
|
198
|
+
* @param {object} [context]
|
|
199
|
+
* @returns {Function}
|
|
200
|
+
*/
|
|
201
|
+
export function guardAsync(fn, code, context = {}) {
|
|
202
|
+
return async (...args) => {
|
|
203
|
+
try {
|
|
204
|
+
return await fn(...args);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
reportError(code, err.message || 'Async callback error', context, err);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
package/src/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "type": "module" }
|
package/src/ssr.js
CHANGED
|
@@ -21,14 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
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
|
-
}
|
|
24
|
+
import { reportError, ErrorCode, ZQueryError } from './errors.js';
|
|
32
25
|
|
|
33
26
|
// ---------------------------------------------------------------------------
|
|
34
27
|
// SSR Component renderer
|
|
@@ -54,7 +47,14 @@ class SSRComponent {
|
|
|
54
47
|
if (definition.computed) {
|
|
55
48
|
for (const [name, fn] of Object.entries(definition.computed)) {
|
|
56
49
|
Object.defineProperty(this.computed, name, {
|
|
57
|
-
get: () =>
|
|
50
|
+
get: () => {
|
|
51
|
+
try {
|
|
52
|
+
return fn.call(this, this.state);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
reportError(ErrorCode.SSR_RENDER, `Computed property "${name}" threw during SSR`, { property: name }, err);
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
58
|
enumerable: true
|
|
59
59
|
});
|
|
60
60
|
}
|
|
@@ -67,15 +67,26 @@ class SSRComponent {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// Init
|
|
71
|
-
if (definition.init)
|
|
70
|
+
// Init lifecycle — guarded so a broken init doesn't crash the whole render
|
|
71
|
+
if (definition.init) {
|
|
72
|
+
try {
|
|
73
|
+
definition.init.call(this);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
reportError(ErrorCode.SSR_RENDER, 'Component init() threw during SSR', {}, err);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
render() {
|
|
75
81
|
if (this._def.render) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
try {
|
|
83
|
+
let html = this._def.render.call(this);
|
|
84
|
+
html = this._interpolate(html);
|
|
85
|
+
return html;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
reportError(ErrorCode.SSR_RENDER, 'Component render() threw during SSR', {}, err);
|
|
88
|
+
return `<!-- SSR render error: ${_escapeHtml(err.message)} -->`;
|
|
89
|
+
}
|
|
79
90
|
}
|
|
80
91
|
return '';
|
|
81
92
|
}
|
|
@@ -83,11 +94,16 @@ class SSRComponent {
|
|
|
83
94
|
// Basic {{expression}} interpolation for SSR
|
|
84
95
|
_interpolate(html) {
|
|
85
96
|
return html.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
try {
|
|
98
|
+
const result = safeEval(expr.trim(), [
|
|
99
|
+
this.state,
|
|
100
|
+
{ props: this.props, computed: this.computed }
|
|
101
|
+
]);
|
|
102
|
+
return result != null ? _escapeHtml(String(result)) : '';
|
|
103
|
+
} catch (err) {
|
|
104
|
+
reportError(ErrorCode.SSR_RENDER, `Expression "{{${expr.trim()}}}" failed during SSR`, { expression: expr.trim() }, err);
|
|
105
|
+
return '';
|
|
106
|
+
}
|
|
91
107
|
});
|
|
92
108
|
}
|
|
93
109
|
}
|
|
@@ -120,10 +136,25 @@ class SSRApp {
|
|
|
120
136
|
* @param {object} definition
|
|
121
137
|
*/
|
|
122
138
|
component(name, definition) {
|
|
139
|
+
if (typeof name !== 'string' || !name) {
|
|
140
|
+
throw new ZQueryError(ErrorCode.SSR_COMPONENT, 'Component name must be a non-empty string');
|
|
141
|
+
}
|
|
142
|
+
if (!definition || typeof definition !== 'object') {
|
|
143
|
+
throw new ZQueryError(ErrorCode.SSR_COMPONENT, `Invalid definition for component "${name}"`);
|
|
144
|
+
}
|
|
123
145
|
this._registry.set(name, definition);
|
|
124
146
|
return this;
|
|
125
147
|
}
|
|
126
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Check whether a component is registered.
|
|
151
|
+
* @param {string} name
|
|
152
|
+
* @returns {boolean}
|
|
153
|
+
*/
|
|
154
|
+
has(name) {
|
|
155
|
+
return this._registry.has(name);
|
|
156
|
+
}
|
|
157
|
+
|
|
127
158
|
/**
|
|
128
159
|
* Render a component to an HTML string.
|
|
129
160
|
*
|
|
@@ -131,11 +162,18 @@ class SSRApp {
|
|
|
131
162
|
* @param {object} [props] — props to pass
|
|
132
163
|
* @param {object} [options] — rendering options
|
|
133
164
|
* @param {boolean} [options.hydrate=true] — add hydration marker
|
|
165
|
+
* @param {string} [options.mode='html'] — 'html' (default) or 'fragment' (no wrapper tag)
|
|
134
166
|
* @returns {Promise<string>} — rendered HTML
|
|
135
167
|
*/
|
|
136
168
|
async renderToString(componentName, props = {}, options = {}) {
|
|
137
169
|
const def = this._registry.get(componentName);
|
|
138
|
-
if (!def)
|
|
170
|
+
if (!def) {
|
|
171
|
+
throw new ZQueryError(
|
|
172
|
+
ErrorCode.SSR_COMPONENT,
|
|
173
|
+
`SSR: Component "${componentName}" not registered`,
|
|
174
|
+
{ component: componentName }
|
|
175
|
+
);
|
|
176
|
+
}
|
|
139
177
|
|
|
140
178
|
const instance = new SSRComponent(def, props);
|
|
141
179
|
let html = instance.render();
|
|
@@ -147,12 +185,27 @@ class SSRApp {
|
|
|
147
185
|
html = html.replace(/\s*@[\w.]+="[^"]*"/g, ''); // Remove event bindings
|
|
148
186
|
html = html.replace(/\s*z-on:[\w.]+="[^"]*"/g, '');
|
|
149
187
|
|
|
188
|
+
// Fragment mode — return inner HTML without wrapper tag
|
|
189
|
+
if (options.mode === 'fragment') return html;
|
|
190
|
+
|
|
150
191
|
const hydrate = options.hydrate !== false;
|
|
151
192
|
const marker = hydrate ? ' data-zq-ssr' : '';
|
|
152
193
|
|
|
153
194
|
return `<${componentName}${marker}>${html}</${componentName}>`;
|
|
154
195
|
}
|
|
155
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Render multiple components as a batch.
|
|
199
|
+
*
|
|
200
|
+
* @param {Array<{ name: string, props?: object, options?: object }>} entries
|
|
201
|
+
* @returns {Promise<string[]>} — array of rendered HTML strings
|
|
202
|
+
*/
|
|
203
|
+
async renderBatch(entries) {
|
|
204
|
+
return Promise.all(
|
|
205
|
+
entries.map(({ name, props, options }) => this.renderToString(name, props, options))
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
156
209
|
/**
|
|
157
210
|
* Render a full HTML page with a component mounted in a shell.
|
|
158
211
|
*
|
|
@@ -160,10 +213,15 @@ class SSRApp {
|
|
|
160
213
|
* @param {string} options.component — component name to render
|
|
161
214
|
* @param {object} [options.props] — props
|
|
162
215
|
* @param {string} [options.title] — page title
|
|
216
|
+
* @param {string} [options.description] — meta description for SEO
|
|
163
217
|
* @param {string[]} [options.styles] — CSS file paths
|
|
164
218
|
* @param {string[]} [options.scripts] — JS file paths
|
|
165
219
|
* @param {string} [options.lang] — html lang attribute
|
|
166
220
|
* @param {string} [options.meta] — additional head content
|
|
221
|
+
* @param {string} [options.bodyAttrs] — extra body attributes
|
|
222
|
+
* @param {object} [options.head] — structured head options
|
|
223
|
+
* @param {string} [options.head.canonical] — canonical URL
|
|
224
|
+
* @param {object} [options.head.og] — Open Graph tags
|
|
167
225
|
* @returns {Promise<string>}
|
|
168
226
|
*/
|
|
169
227
|
async renderPage(options = {}) {
|
|
@@ -171,25 +229,49 @@ class SSRApp {
|
|
|
171
229
|
component: comp,
|
|
172
230
|
props = {},
|
|
173
231
|
title = '',
|
|
232
|
+
description = '',
|
|
174
233
|
styles = [],
|
|
175
234
|
scripts = [],
|
|
176
235
|
lang = 'en',
|
|
177
236
|
meta = '',
|
|
178
237
|
bodyAttrs = '',
|
|
238
|
+
head = {},
|
|
179
239
|
} = options;
|
|
180
240
|
|
|
181
|
-
|
|
241
|
+
let content = '';
|
|
242
|
+
if (comp) {
|
|
243
|
+
try {
|
|
244
|
+
content = await this.renderToString(comp, props);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
reportError(ErrorCode.SSR_PAGE, `renderPage failed for component "${comp}"`, { component: comp }, err);
|
|
247
|
+
content = `<!-- SSR error: ${_escapeHtml(err.message)} -->`;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
182
250
|
|
|
183
251
|
const styleLinks = styles.map(s => `<link rel="stylesheet" href="${_escapeHtml(s)}">`).join('\n ');
|
|
184
252
|
const scriptTags = scripts.map(s => `<script src="${_escapeHtml(s)}"></script>`).join('\n ');
|
|
185
253
|
|
|
254
|
+
// Build SEO / structured head tags
|
|
255
|
+
let headExtra = meta;
|
|
256
|
+
if (description) {
|
|
257
|
+
headExtra += `\n <meta name="description" content="${_escapeHtml(description)}">`;
|
|
258
|
+
}
|
|
259
|
+
if (head.canonical) {
|
|
260
|
+
headExtra += `\n <link rel="canonical" href="${_escapeHtml(head.canonical)}">`;
|
|
261
|
+
}
|
|
262
|
+
if (head.og) {
|
|
263
|
+
for (const [key, val] of Object.entries(head.og)) {
|
|
264
|
+
headExtra += `\n <meta property="og:${_escapeHtml(key)}" content="${_escapeHtml(String(val))}">`;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
186
268
|
return `<!DOCTYPE html>
|
|
187
269
|
<html lang="${_escapeHtml(lang)}">
|
|
188
270
|
<head>
|
|
189
271
|
<meta charset="UTF-8">
|
|
190
272
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
191
273
|
<title>${_escapeHtml(title)}</title>
|
|
192
|
-
${
|
|
274
|
+
${headExtra}
|
|
193
275
|
${styleLinks}
|
|
194
276
|
</head>
|
|
195
277
|
<body ${bodyAttrs.replace(/on\w+\s*=/gi, '').replace(/javascript\s*:/gi, '')}>
|
|
@@ -219,6 +301,18 @@ export function createSSRApp() {
|
|
|
219
301
|
* @returns {string}
|
|
220
302
|
*/
|
|
221
303
|
export function renderToString(definition, props = {}) {
|
|
304
|
+
if (!definition || typeof definition !== 'object') {
|
|
305
|
+
throw new ZQueryError(ErrorCode.SSR_COMPONENT, 'renderToString requires a component definition object');
|
|
306
|
+
}
|
|
222
307
|
const instance = new SSRComponent(definition, props);
|
|
223
308
|
return instance.render();
|
|
224
309
|
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Escape HTML entities — exposed for use in SSR templates.
|
|
313
|
+
* @param {string} str
|
|
314
|
+
* @returns {string}
|
|
315
|
+
*/
|
|
316
|
+
export function escapeHtml(str) {
|
|
317
|
+
return _escapeHtml(String(str));
|
|
318
|
+
}
|