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/README.md
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
| **HTTP** | Fetch wrapper with auto-JSON, interceptors (with unsubscribe & clear), HEAD requests, parallel requests (`http.all`), config inspection (`getConfig`), timeout/abort, base URL |
|
|
31
31
|
| **Utils** | debounce, throttle, pipe, once, sleep, memoize, escapeHtml, stripHtml, uuid, capitalize, truncate, range, chunk, groupBy, unique, pick, omit, getPath/setPath, isEmpty, clamp, retry, timeout, deepClone, deepMerge, storage/session wrappers, event bus |
|
|
32
32
|
| **Dev Tools** | CLI dev server with live-reload, CSS hot-swap, full-screen error overlay, floating toolbar, dark-themed inspector panel (Router view, DOM tree, network log, component viewer, performance dashboard), fetch interceptor, render instrumentation, CLI bundler for single-file production builds |
|
|
33
|
+
| **SSR** | Server-side rendering to HTML strings in Node.js — `createSSRApp()`, `renderToString()`, `renderPage()` with SEO/Open Graph support, `renderBatch()` for parallel rendering, fragment mode, hydration markers, graceful error handling, `escapeHtml()` utility |
|
|
33
34
|
|
|
34
35
|
---
|
|
35
36
|
|
|
@@ -53,7 +54,7 @@ npx zquery dev my-app
|
|
|
53
54
|
|
|
54
55
|
> **Tip:** Stay in the project root (where `node_modules` lives) instead of `cd`-ing into `my-app`. This keeps `index.d.ts` accessible to your IDE for full type/intellisense support.
|
|
55
56
|
|
|
56
|
-
The `create` command generates a ready-to-run project with a sidebar layout, router, multiple components (including folder components with external templates and styles), and responsive styles. Use `--minimal` (or `-m`) to scaffold a lightweight 3-page starter instead. The dev server watches for file changes, hot-swaps CSS in-place, full-reloads on other changes, and handles SPA fallback routing.
|
|
57
|
+
The `create` command generates a ready-to-run project with a sidebar layout, router, multiple components (including folder components with external templates and styles), and responsive styles. Use `--minimal` (or `-m`) to scaffold a lightweight 3-page starter instead. Use `--ssr` (or `-s`) to scaffold a project with a Node.js server-side rendering example. The dev server watches for file changes, hot-swaps CSS in-place, full-reloads on other changes, and handles SPA fallback routing.
|
|
57
58
|
|
|
58
59
|
#### Error Overlay
|
|
59
60
|
|
|
@@ -186,6 +187,26 @@ my-app/ ← minimal scaffold (npx zquery create my-app
|
|
|
186
187
|
assets/
|
|
187
188
|
```
|
|
188
189
|
|
|
190
|
+
Use `--ssr` for a project with server-side rendering:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
my-app/ ← SSR scaffold (npx zquery create my-app --ssr)
|
|
194
|
+
index.html ← client HTML shell
|
|
195
|
+
global.css
|
|
196
|
+
app/
|
|
197
|
+
app.js ← client entry — registers shared components
|
|
198
|
+
routes.js ← shared route definitions
|
|
199
|
+
components/
|
|
200
|
+
home.js ← shared component (SSR + client)
|
|
201
|
+
about.js
|
|
202
|
+
not-found.js
|
|
203
|
+
server/
|
|
204
|
+
index.js ← SSR HTTP server
|
|
205
|
+
assets/
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Components in `app/components/` export plain definition objects — the client registers them with `$.component()`, the server with `app.component()`. The `--ssr` flag handles everything automatically — installs dependencies, starts the server at `http://localhost:3000`, and opens the browser.
|
|
209
|
+
|
|
189
210
|
- One component per file inside `components/`.
|
|
190
211
|
- Names **must contain a hyphen** (Web Component convention): `home-page`, `app-counter`, etc.
|
|
191
212
|
- Components with external templates or styles can use a subfolder (e.g. `contacts/contacts.js` + `contacts.html` + `contacts.css`).
|
|
@@ -298,14 +319,16 @@ location / {
|
|
|
298
319
|
| `$.retry` `$.timeout` | Async utils |
|
|
299
320
|
| `$.param` `$.parseQuery` | URL utils |
|
|
300
321
|
| `$.storage` `$.session` | Storage wrappers |
|
|
301
|
-
| `$.EventBus` `$.bus` | Event bus
|
|
322
|
+
| `$.EventBus` `$.bus` | Event bus |
|
|
323
|
+
| `$.onError` `$.ZQueryError` `$.ErrorCode` `$.guardCallback` `$.guardAsync` `$.formatError` `$.validate` | Error handling |
|
|
324
|
+
| `$.version` | Library version |\n| `$.libSize` | Minified bundle size string (e.g. `\"~100 KB\"`) |
|
|
302
325
|
| `$.unitTests` | Build-time test results `{ passed, failed, total, suites, duration, ok }` |
|
|
303
326
|
| `$.meta` | Build metadata (populated by CLI bundler) |
|
|
304
327
|
| `$.noConflict` | Release `$` global |
|
|
305
328
|
|
|
306
329
|
| CLI Command | Description |
|
|
307
330
|
| --- | --- |
|
|
308
|
-
| `zquery create [dir]` | Scaffold a new project. Default: full-featured app. `--minimal` / `-m`: lightweight 3-page starter with
|
|
331
|
+
| `zquery create [dir]` | Scaffold a new project. Default: full-featured app. `--minimal` / `-m`: lightweight 3-page starter. `--ssr` / `-s`: SSR project with shared components and HTTP server. |
|
|
309
332
|
| `zquery dev [root]` | Dev server with live-reload, CSS hot-swap, error overlay, expandable floating toolbar & five-tab inspector panel (port 3100). Visit `/_devtools` for the standalone panel. `--index` for custom HTML, `--bundle` for bundled mode, `--no-intercept` to skip CDN intercept. |
|
|
310
333
|
| `zquery bundle [dir\|file]` | Bundle app into a single IIFE file. Accepts dir or direct entry file. |
|
|
311
334
|
| `zquery build` | Build the zQuery library (`dist/zquery.min.js`) |
|
package/cli/commands/create.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
const { execSync, spawn } = require('child_process');
|
|
9
10
|
const { flag } = require('../args');
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -30,11 +31,15 @@ function createProject(args) {
|
|
|
30
31
|
const target = dirArg ? path.resolve(dirArg) : process.cwd();
|
|
31
32
|
const name = path.basename(target);
|
|
32
33
|
|
|
33
|
-
// Determine scaffold variant: --minimal / -m or default
|
|
34
|
-
const variant = flag('minimal', 'm') ? 'minimal'
|
|
34
|
+
// Determine scaffold variant: --minimal / -m or --ssr / -s or default
|
|
35
|
+
const variant = flag('minimal', 'm') ? 'minimal'
|
|
36
|
+
: flag('ssr', 's') ? 'ssr'
|
|
37
|
+
: 'default';
|
|
35
38
|
|
|
36
39
|
// Guard: refuse to overwrite existing files
|
|
37
|
-
const
|
|
40
|
+
const checkFiles = ['index.html', 'global.css', 'app', 'assets'];
|
|
41
|
+
if (variant === 'ssr') checkFiles.push('server');
|
|
42
|
+
const conflicts = checkFiles.filter(f =>
|
|
38
43
|
fs.existsSync(path.join(target, f))
|
|
39
44
|
);
|
|
40
45
|
if (conflicts.length) {
|
|
@@ -70,11 +75,40 @@ function createProject(args) {
|
|
|
70
75
|
console.log(` ✓ ${rel}`);
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
|
|
78
|
+
const devCmd = `npx zquery dev${target !== process.cwd() ? ` ${dirArg}` : ''}`;
|
|
79
|
+
|
|
80
|
+
if (variant === 'ssr') {
|
|
81
|
+
console.log(`\n Installing dependencies...\n`);
|
|
82
|
+
// Install zero-query from the same package that provides this CLI
|
|
83
|
+
// (works both in local dev and when installed from npm)
|
|
84
|
+
const zqRoot = path.resolve(__dirname, '..', '..');
|
|
85
|
+
try {
|
|
86
|
+
execSync(`npm install "${zqRoot}"`, { cwd: target, stdio: 'inherit' });
|
|
87
|
+
} catch {
|
|
88
|
+
console.error(`\n ✗ npm install failed. Run it manually:\n\n cd ${dirArg || '.'}\n npm install\n node server/index.js\n`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(`\n Starting SSR server...\n`);
|
|
93
|
+
const child = spawn('node', ['server/index.js'], { cwd: target, stdio: 'inherit' });
|
|
94
|
+
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
const open = process.platform === 'win32' ? 'start'
|
|
97
|
+
: process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
98
|
+
try { execSync(`${open} http://localhost:3000`, { stdio: 'ignore' }); } catch {}
|
|
99
|
+
}, 500);
|
|
100
|
+
|
|
101
|
+
process.on('SIGINT', () => { child.kill(); process.exit(); });
|
|
102
|
+
process.on('SIGTERM', () => { child.kill(); process.exit(); });
|
|
103
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
104
|
+
return; // keep process alive for the server
|
|
105
|
+
} else {
|
|
106
|
+
console.log(`
|
|
74
107
|
Done! Next steps:
|
|
75
108
|
|
|
76
|
-
|
|
109
|
+
${devCmd}
|
|
77
110
|
`);
|
|
111
|
+
}
|
|
78
112
|
}
|
|
79
113
|
|
|
80
114
|
module.exports = createProject;
|
package/cli/help.js
CHANGED
|
@@ -11,6 +11,8 @@ function showHelp() {
|
|
|
11
11
|
(defaults to the current directory)
|
|
12
12
|
--minimal, -m Use the minimal template (home, counter, about)
|
|
13
13
|
instead of the full-featured default scaffold
|
|
14
|
+
--ssr, -s Use the SSR template — includes server/index.js
|
|
15
|
+
SSR HTTP server with shared component definitions
|
|
14
16
|
|
|
15
17
|
dev [root] Start a dev server with live-reload
|
|
16
18
|
--port, -p <number> Port number (default: 3100)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// app.js — Client entry point
|
|
2
|
+
//
|
|
3
|
+
// Imports shared component definitions and registers them with zQuery.
|
|
4
|
+
// The SSR server imports the same definitions via createSSRApp().
|
|
5
|
+
|
|
6
|
+
import { homePage } from './components/home.js';
|
|
7
|
+
import { aboutPage } from './components/about.js';
|
|
8
|
+
import { notFound } from './components/not-found.js';
|
|
9
|
+
import { routes } from './routes.js';
|
|
10
|
+
|
|
11
|
+
// Register shared component definitions on the client
|
|
12
|
+
$.component('home-page', homePage);
|
|
13
|
+
$.component('about-page', aboutPage);
|
|
14
|
+
$.component('not-found', notFound);
|
|
15
|
+
|
|
16
|
+
// Client-side router
|
|
17
|
+
const router = $.router({
|
|
18
|
+
el: '#app',
|
|
19
|
+
routes,
|
|
20
|
+
fallback: 'not-found',
|
|
21
|
+
mode: 'history'
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Active nav highlighting
|
|
25
|
+
router.onChange((to) => {
|
|
26
|
+
$.qsa('.nav-link').forEach(link => {
|
|
27
|
+
const href = link.getAttribute('z-link') || link.getAttribute('href');
|
|
28
|
+
link.classList.toggle('active', href === to.path);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// about.js — About page component
|
|
2
|
+
|
|
3
|
+
export const aboutPage = {
|
|
4
|
+
state: () => ({
|
|
5
|
+
features: [
|
|
6
|
+
'createSSRApp() — isolated component registry for Node.js',
|
|
7
|
+
'renderToString() — render a component to an HTML string',
|
|
8
|
+
'renderPage() — full HTML document with meta tags',
|
|
9
|
+
'renderBatch() — render multiple components in one call',
|
|
10
|
+
'Hydration markers (data-zq-ssr) for client takeover',
|
|
11
|
+
'SEO: description, canonical URL, Open Graph tags',
|
|
12
|
+
]
|
|
13
|
+
}),
|
|
14
|
+
|
|
15
|
+
render() {
|
|
16
|
+
const list = this.state.features.map(f => `<li>${f}</li>`).join('');
|
|
17
|
+
return `
|
|
18
|
+
<div class="page-header">
|
|
19
|
+
<h1>About</h1>
|
|
20
|
+
<p class="subtitle">SSR capabilities in this scaffold.</p>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="card">
|
|
23
|
+
<h3>SSR API</h3>
|
|
24
|
+
<ul style="padding-left:1.2rem; line-height:2;">${list}</ul>
|
|
25
|
+
</div>
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// home.js — Home page component
|
|
2
|
+
//
|
|
3
|
+
// Exports a plain definition object that works on both client and server.
|
|
4
|
+
// The client registers it with $.component(), the server with app.component().
|
|
5
|
+
|
|
6
|
+
export const homePage = {
|
|
7
|
+
state: () => ({
|
|
8
|
+
greeting: 'Hello',
|
|
9
|
+
timestamp: new Date().toLocaleTimeString(),
|
|
10
|
+
}),
|
|
11
|
+
|
|
12
|
+
// init() runs on both client and server — no DOM required
|
|
13
|
+
init() {
|
|
14
|
+
const hour = new Date().getHours();
|
|
15
|
+
this.state.greeting =
|
|
16
|
+
hour < 12 ? 'Good morning' :
|
|
17
|
+
hour < 18 ? 'Good afternoon' : 'Good evening';
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
render() {
|
|
21
|
+
return `
|
|
22
|
+
<div class="page-header">
|
|
23
|
+
<h1>${this.state.greeting} 👋</h1>
|
|
24
|
+
<p class="subtitle">Rendered with zQuery SSR</p>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="card">
|
|
27
|
+
<h3>Server-Side Rendering</h3>
|
|
28
|
+
<p>
|
|
29
|
+
This page was rendered to HTML on the server and served as a complete
|
|
30
|
+
document. The same component definition powers both the SSR server and
|
|
31
|
+
the client-side SPA.
|
|
32
|
+
</p>
|
|
33
|
+
<p>Rendered at <strong>${this.state.timestamp}</strong></p>
|
|
34
|
+
</div>
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// not-found.js — 404 fallback component
|
|
2
|
+
|
|
3
|
+
export const notFound = {
|
|
4
|
+
render() {
|
|
5
|
+
return `
|
|
6
|
+
<div class="page-header" style="text-align:center; margin-top:4rem;">
|
|
7
|
+
<h1>404</h1>
|
|
8
|
+
<p class="subtitle">Page not found.</p>
|
|
9
|
+
<p style="margin-top:1rem;">
|
|
10
|
+
<a href="/" style="color:var(--accent);">← Home</a>
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/* global.css — SSR Scaffold Styles */
|
|
2
|
+
|
|
3
|
+
*, *::before, *::after {
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
margin: 0;
|
|
6
|
+
padding: 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #0f0f0f;
|
|
11
|
+
--surface: #1a1a2e;
|
|
12
|
+
--text: #e0e0e0;
|
|
13
|
+
--muted: #888;
|
|
14
|
+
--accent: #00d4ff;
|
|
15
|
+
--accent-hover: #00b8d9;
|
|
16
|
+
--radius: 8px;
|
|
17
|
+
--gap: 1.5rem;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
body {
|
|
21
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
22
|
+
background: var(--bg);
|
|
23
|
+
color: var(--text);
|
|
24
|
+
line-height: 1.6;
|
|
25
|
+
min-height: 100vh;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* -- Navbar -- */
|
|
29
|
+
.navbar {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
padding: 1rem 2rem;
|
|
34
|
+
background: var(--surface);
|
|
35
|
+
border-bottom: 1px solid rgba(255,255,255,0.06);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.brand {
|
|
39
|
+
font-weight: 700;
|
|
40
|
+
font-size: 1.2rem;
|
|
41
|
+
color: var(--accent);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.nav-links { display: flex; gap: 1rem; }
|
|
45
|
+
|
|
46
|
+
.nav-link {
|
|
47
|
+
color: var(--muted);
|
|
48
|
+
text-decoration: none;
|
|
49
|
+
padding: 0.4rem 0.8rem;
|
|
50
|
+
border-radius: var(--radius);
|
|
51
|
+
transition: color 0.2s, background 0.2s;
|
|
52
|
+
}
|
|
53
|
+
.nav-link:hover, .nav-link.active {
|
|
54
|
+
color: var(--accent);
|
|
55
|
+
background: rgba(0, 212, 255, 0.08);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* -- Main content -- */
|
|
59
|
+
#app {
|
|
60
|
+
max-width: 800px;
|
|
61
|
+
margin: 2rem auto;
|
|
62
|
+
padding: 0 1.5rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
[z-cloak] { display: none !important; }
|
|
66
|
+
|
|
67
|
+
.page-header {
|
|
68
|
+
margin-bottom: var(--gap);
|
|
69
|
+
}
|
|
70
|
+
.page-header h1 {
|
|
71
|
+
font-size: 2rem;
|
|
72
|
+
margin-bottom: 0.5rem;
|
|
73
|
+
}
|
|
74
|
+
.page-header .subtitle {
|
|
75
|
+
color: var(--muted);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.card {
|
|
79
|
+
background: var(--surface);
|
|
80
|
+
border: 1px solid rgba(255,255,255,0.06);
|
|
81
|
+
border-radius: var(--radius);
|
|
82
|
+
padding: 1.5rem;
|
|
83
|
+
margin-bottom: var(--gap);
|
|
84
|
+
}
|
|
85
|
+
.card h3 {
|
|
86
|
+
margin-bottom: 0.75rem;
|
|
87
|
+
color: var(--accent);
|
|
88
|
+
}
|
|
89
|
+
.card code {
|
|
90
|
+
background: rgba(255,255,255,0.06);
|
|
91
|
+
padding: 0.15em 0.4em;
|
|
92
|
+
border-radius: 4px;
|
|
93
|
+
font-size: 0.9em;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.badge {
|
|
97
|
+
display: inline-block;
|
|
98
|
+
padding: 2px 8px;
|
|
99
|
+
border-radius: 4px;
|
|
100
|
+
font-size: 0.8em;
|
|
101
|
+
font-weight: 600;
|
|
102
|
+
}
|
|
103
|
+
.badge-ssr { background: rgba(0,212,255,0.15); color: var(--accent); }
|
|
104
|
+
.badge-csr { background: rgba(255,165,0,0.15); color: #ffa500; }
|
|
105
|
+
|
|
106
|
+
/* -- Footer -- */
|
|
107
|
+
.footer {
|
|
108
|
+
text-align: center;
|
|
109
|
+
padding: 2rem;
|
|
110
|
+
color: var(--muted);
|
|
111
|
+
border-top: 1px solid rgba(255,255,255,0.06);
|
|
112
|
+
margin-top: 3rem;
|
|
113
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{{NAME}}</title>
|
|
7
|
+
<base href="/">
|
|
8
|
+
<link rel="stylesheet" href="global.css">
|
|
9
|
+
<script src="zquery.min.js"></script>
|
|
10
|
+
<script type="module" src="app/app.js"></script>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
|
|
14
|
+
<!-- Navigation -->
|
|
15
|
+
<nav class="navbar">
|
|
16
|
+
<span class="brand">⚡ {{NAME}}</span>
|
|
17
|
+
<div class="nav-links">
|
|
18
|
+
<a z-link="/" class="nav-link">Home</a>
|
|
19
|
+
<a z-link="/about" class="nav-link">About</a>
|
|
20
|
+
</div>
|
|
21
|
+
</nav>
|
|
22
|
+
|
|
23
|
+
<!-- Main content — SSR output is injected here by the server -->
|
|
24
|
+
<main id="app" z-cloak></main>
|
|
25
|
+
|
|
26
|
+
<footer class="footer">
|
|
27
|
+
<small>Built with zQuery · SSR Scaffold</small>
|
|
28
|
+
</footer>
|
|
29
|
+
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// server/index.js — SSR HTTP server
|
|
2
|
+
//
|
|
3
|
+
// Renders routes to HTML with zQuery SSR and serves them over HTTP.
|
|
4
|
+
// Components are imported from app/components/ — the same definitions
|
|
5
|
+
// the client uses.
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// node server/index.js
|
|
9
|
+
// PORT=8080 node server/index.js
|
|
10
|
+
|
|
11
|
+
import { createServer } from 'node:http';
|
|
12
|
+
import { readFile } from 'node:fs/promises';
|
|
13
|
+
import { join, extname, resolve } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { createSSRApp } from 'zero-query/ssr';
|
|
16
|
+
|
|
17
|
+
// Shared component definitions — same ones the client registers
|
|
18
|
+
import { homePage } from '../app/components/home.js';
|
|
19
|
+
import { aboutPage } from '../app/components/about.js';
|
|
20
|
+
import { notFound } from '../app/components/not-found.js';
|
|
21
|
+
import { routes } from '../app/routes.js';
|
|
22
|
+
|
|
23
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
24
|
+
const ROOT = join(__dirname, '..');
|
|
25
|
+
const PORT = parseInt(process.env.PORT || '3000', 10);
|
|
26
|
+
|
|
27
|
+
// --- SSR app ----------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const app = createSSRApp();
|
|
30
|
+
app.component('home-page', homePage);
|
|
31
|
+
app.component('about-page', aboutPage);
|
|
32
|
+
app.component('not-found', notFound);
|
|
33
|
+
|
|
34
|
+
// --- Route matching ---------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
function matchRoute(pathname) {
|
|
37
|
+
const route = routes.find(r => r.path === pathname);
|
|
38
|
+
return route ? route.component : 'not-found';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- Render a full HTML page ------------------------------------------------
|
|
42
|
+
|
|
43
|
+
async function render(pathname) {
|
|
44
|
+
const component = matchRoute(pathname);
|
|
45
|
+
const body = await app.renderToString(component);
|
|
46
|
+
|
|
47
|
+
return `<!DOCTYPE html>
|
|
48
|
+
<html lang="en">
|
|
49
|
+
<head>
|
|
50
|
+
<meta charset="UTF-8">
|
|
51
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
52
|
+
<title>{{NAME}}</title>
|
|
53
|
+
<link rel="stylesheet" href="/global.css">
|
|
54
|
+
</head>
|
|
55
|
+
<body>
|
|
56
|
+
<nav class="navbar">
|
|
57
|
+
<span class="brand">⚡ {{NAME}}</span>
|
|
58
|
+
<div class="nav-links">
|
|
59
|
+
${routes.map(r =>
|
|
60
|
+
` <a href="${r.path}" class="nav-link${r.path === pathname ? ' active' : ''}">${
|
|
61
|
+
r.path === '/' ? 'Home' : r.path.slice(1)[0].toUpperCase() + r.path.slice(2)
|
|
62
|
+
}</a>`).join('\n')}
|
|
63
|
+
</div>
|
|
64
|
+
</nav>
|
|
65
|
+
<main id="app">${body}</main>
|
|
66
|
+
<footer class="footer"><small>Built with zQuery · SSR</small></footer>
|
|
67
|
+
</body>
|
|
68
|
+
</html>`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- Static files -----------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
const MIME = {
|
|
74
|
+
'.css': 'text/css', '.js': 'text/javascript', '.json': 'application/json',
|
|
75
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml',
|
|
76
|
+
'.ico': 'image/x-icon', '.woff2': 'font/woff2',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
async function serveStatic(res, pathname) {
|
|
80
|
+
const ext = extname(pathname);
|
|
81
|
+
if (!MIME[ext]) return false;
|
|
82
|
+
|
|
83
|
+
const filePath = join(ROOT, pathname);
|
|
84
|
+
if (!resolve(filePath).startsWith(resolve(ROOT))) return false;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const data = await readFile(filePath);
|
|
88
|
+
res.writeHead(200, { 'Content-Type': MIME[ext] });
|
|
89
|
+
res.end(data);
|
|
90
|
+
return true;
|
|
91
|
+
} catch { return false; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- HTTP server ------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
createServer(async (req, res) => {
|
|
97
|
+
const { pathname } = new URL(req.url, `http://localhost:${PORT}`);
|
|
98
|
+
|
|
99
|
+
// Static assets (CSS, images, etc.)
|
|
100
|
+
if (pathname !== '/' && await serveStatic(res, pathname)) return;
|
|
101
|
+
|
|
102
|
+
// SSR route
|
|
103
|
+
try {
|
|
104
|
+
const html = await render(pathname);
|
|
105
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
106
|
+
res.end(html);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error('SSR error:', err);
|
|
109
|
+
if (!res.headersSent) {
|
|
110
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
111
|
+
}
|
|
112
|
+
res.end('Internal Server Error');
|
|
113
|
+
}
|
|
114
|
+
}).listen(PORT, () => {
|
|
115
|
+
console.log(`\n ⚡ SSR server → http://localhost:${PORT}\n`);
|
|
116
|
+
routes.forEach(r => console.log(` ${r.path.padEnd(10)} → ${r.component}`));
|
|
117
|
+
console.log(` * → not-found\n`);
|
|
118
|
+
});
|
package/dist/zquery.dist.zip
CHANGED
|
Binary file
|
package/dist/zquery.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery (zeroQuery) v0.9.
|
|
2
|
+
* zQuery (zeroQuery) v0.9.9
|
|
3
3
|
* Lightweight Frontend Library
|
|
4
4
|
* https://github.com/tonywied17/zero-query
|
|
5
5
|
* (c) 2026 Anthony Wiedman - MIT License
|
|
@@ -59,6 +59,12 @@ const ErrorCode = Object.freeze({
|
|
|
59
59
|
HTTP_INTERCEPTOR: 'ZQ_HTTP_INTERCEPTOR',
|
|
60
60
|
HTTP_PARSE: 'ZQ_HTTP_PARSE',
|
|
61
61
|
|
|
62
|
+
// SSR
|
|
63
|
+
SSR_RENDER: 'ZQ_SSR_RENDER',
|
|
64
|
+
SSR_COMPONENT: 'ZQ_SSR_COMPONENT',
|
|
65
|
+
SSR_HYDRATION: 'ZQ_SSR_HYDRATION',
|
|
66
|
+
SSR_PAGE: 'ZQ_SSR_PAGE',
|
|
67
|
+
|
|
62
68
|
// General
|
|
63
69
|
INVALID_ARGUMENT: 'ZQ_INVALID_ARGUMENT',
|
|
64
70
|
});
|
|
@@ -87,16 +93,28 @@ class ZQueryError extends Error {
|
|
|
87
93
|
// ---------------------------------------------------------------------------
|
|
88
94
|
// Global error handler
|
|
89
95
|
// ---------------------------------------------------------------------------
|
|
90
|
-
let
|
|
96
|
+
let _errorHandlers = [];
|
|
91
97
|
|
|
92
98
|
/**
|
|
93
99
|
* Register a global error handler.
|
|
94
100
|
* Called whenever zQuery catches an error internally.
|
|
101
|
+
* Multiple handlers are supported — each receives the error.
|
|
102
|
+
* Pass `null` to clear all handlers.
|
|
95
103
|
*
|
|
96
104
|
* @param {Function|null} handler — (error: ZQueryError) => void
|
|
105
|
+
* @returns {Function} unsubscribe function to remove this handler
|
|
97
106
|
*/
|
|
98
107
|
function onError(handler) {
|
|
99
|
-
|
|
108
|
+
if (handler === null) {
|
|
109
|
+
_errorHandlers = [];
|
|
110
|
+
return () => {};
|
|
111
|
+
}
|
|
112
|
+
if (typeof handler !== 'function') return () => {};
|
|
113
|
+
_errorHandlers.push(handler);
|
|
114
|
+
return () => {
|
|
115
|
+
const idx = _errorHandlers.indexOf(handler);
|
|
116
|
+
if (idx !== -1) _errorHandlers.splice(idx, 1);
|
|
117
|
+
};
|
|
100
118
|
}
|
|
101
119
|
|
|
102
120
|
/**
|
|
@@ -113,9 +131,9 @@ function reportError(code, message, context = {}, cause) {
|
|
|
113
131
|
? cause
|
|
114
132
|
: new ZQueryError(code, message, context, cause);
|
|
115
133
|
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
try {
|
|
134
|
+
// Notify all registered handlers
|
|
135
|
+
for (const handler of _errorHandlers) {
|
|
136
|
+
try { handler(err); } catch { /* prevent handler from crashing framework */ }
|
|
119
137
|
}
|
|
120
138
|
|
|
121
139
|
// Always log for developer visibility
|
|
@@ -162,6 +180,42 @@ function validate(value, name, expectedType) {
|
|
|
162
180
|
`"${name}" must be a ${expectedType}, got ${typeof value}`
|
|
163
181
|
);
|
|
164
182
|
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Format a ZQueryError into a structured object suitable for overlays/logging.
|
|
187
|
+
* @param {ZQueryError|Error} err
|
|
188
|
+
* @returns {{ code: string, type: string, message: string, context: object, stack: string }}
|
|
189
|
+
*/
|
|
190
|
+
function formatError(err) {
|
|
191
|
+
const isZQ = err instanceof ZQueryError;
|
|
192
|
+
return {
|
|
193
|
+
code: isZQ ? err.code : '',
|
|
194
|
+
type: isZQ ? 'ZQueryError' : (err.name || 'Error'),
|
|
195
|
+
message: err.message || 'Unknown error',
|
|
196
|
+
context: isZQ ? err.context : {},
|
|
197
|
+
stack: err.stack || '',
|
|
198
|
+
cause: err.cause ? formatError(err.cause) : null,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Async version of guardCallback — wraps an async function so that
|
|
204
|
+
* rejections are caught, reported, and don't crash execution.
|
|
205
|
+
*
|
|
206
|
+
* @param {Function} fn — async function
|
|
207
|
+
* @param {string} code — ErrorCode to use
|
|
208
|
+
* @param {object} [context]
|
|
209
|
+
* @returns {Function}
|
|
210
|
+
*/
|
|
211
|
+
function guardAsync(fn, code, context = {}) {
|
|
212
|
+
return async (...args) => {
|
|
213
|
+
try {
|
|
214
|
+
return await fn(...args);
|
|
215
|
+
} catch (err) {
|
|
216
|
+
reportError(code, err.message || 'Async callback error', context, err);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
165
219
|
}
|
|
166
220
|
|
|
167
221
|
// --- src/reactive.js ---------------------------------------------
|
|
@@ -5849,12 +5903,14 @@ $.onError = onError;
|
|
|
5849
5903
|
$.ZQueryError = ZQueryError;
|
|
5850
5904
|
$.ErrorCode = ErrorCode;
|
|
5851
5905
|
$.guardCallback = guardCallback;
|
|
5906
|
+
$.guardAsync = guardAsync;
|
|
5852
5907
|
$.validate = validate;
|
|
5908
|
+
$.formatError = formatError;
|
|
5853
5909
|
|
|
5854
5910
|
// --- Meta ------------------------------------------------------------------
|
|
5855
|
-
$.version = '0.9.
|
|
5911
|
+
$.version = '0.9.9';
|
|
5856
5912
|
$.libSize = '~101 KB';
|
|
5857
|
-
$.unitTests = {"passed":
|
|
5913
|
+
$.unitTests = {"passed":1808,"failed":0,"total":1808,"suites":493,"duration":2896,"ok":true};
|
|
5858
5914
|
$.meta = {}; // populated at build time by CLI bundler
|
|
5859
5915
|
|
|
5860
5916
|
$.noConflict = () => {
|