zero-query 1.1.1 → 1.2.0
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/LICENSE +21 -21
- package/README.md +2 -0
- package/cli/args.js +33 -33
- package/cli/commands/build-api.js +443 -442
- package/cli/commands/build.js +254 -247
- package/cli/commands/bundle.js +1228 -1224
- package/cli/commands/create.js +137 -121
- package/cli/commands/dev/devtools/index.js +56 -56
- package/cli/commands/dev/devtools/js/components.js +49 -49
- package/cli/commands/dev/devtools/js/core.js +423 -423
- package/cli/commands/dev/devtools/js/elements.js +421 -421
- package/cli/commands/dev/devtools/js/network.js +166 -166
- package/cli/commands/dev/devtools/js/performance.js +73 -73
- package/cli/commands/dev/devtools/js/router.js +105 -105
- package/cli/commands/dev/devtools/js/source.js +132 -132
- package/cli/commands/dev/devtools/js/stats.js +35 -35
- package/cli/commands/dev/devtools/js/tabs.js +79 -79
- package/cli/commands/dev/devtools/panel.html +95 -95
- package/cli/commands/dev/devtools/styles.css +244 -244
- package/cli/commands/dev/index.js +107 -107
- package/cli/commands/dev/logger.js +75 -75
- package/cli/commands/dev/overlay.js +858 -858
- package/cli/commands/dev/server.js +220 -220
- package/cli/commands/dev/validator.js +94 -94
- package/cli/commands/dev/watcher.js +172 -172
- package/cli/help.js +114 -112
- package/cli/index.js +52 -52
- package/cli/scaffold/default/LICENSE +21 -21
- package/cli/scaffold/default/app/app.js +207 -207
- package/cli/scaffold/default/app/components/about.js +201 -201
- package/cli/scaffold/default/app/components/api-demo.js +143 -143
- package/cli/scaffold/default/app/components/contact-card.js +231 -231
- package/cli/scaffold/default/app/components/contacts/contacts.css +706 -706
- package/cli/scaffold/default/app/components/contacts/contacts.html +200 -200
- package/cli/scaffold/default/app/components/contacts/contacts.js +196 -196
- package/cli/scaffold/default/app/components/counter.js +127 -127
- package/cli/scaffold/default/app/components/home.js +249 -249
- package/cli/scaffold/default/app/components/not-found.js +16 -16
- package/cli/scaffold/default/app/components/playground/playground.css +115 -115
- package/cli/scaffold/default/app/components/playground/playground.html +161 -161
- package/cli/scaffold/default/app/components/playground/playground.js +116 -116
- package/cli/scaffold/default/app/components/todos.js +225 -225
- package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -97
- package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -146
- package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -280
- package/cli/scaffold/default/app/routes.js +15 -15
- package/cli/scaffold/default/app/store.js +101 -101
- package/cli/scaffold/default/global.css +552 -552
- package/cli/scaffold/default/index.html +99 -99
- package/cli/scaffold/minimal/app/app.js +85 -85
- package/cli/scaffold/minimal/app/components/about.js +68 -68
- package/cli/scaffold/minimal/app/components/counter.js +122 -122
- package/cli/scaffold/minimal/app/components/home.js +68 -68
- package/cli/scaffold/minimal/app/components/not-found.js +16 -16
- package/cli/scaffold/minimal/app/routes.js +9 -9
- package/cli/scaffold/minimal/app/store.js +36 -36
- package/cli/scaffold/minimal/global.css +300 -300
- package/cli/scaffold/minimal/index.html +44 -44
- package/cli/scaffold/ssr/app/app.js +41 -41
- package/cli/scaffold/ssr/app/components/about.js +55 -55
- package/cli/scaffold/ssr/app/components/blog/index.js +65 -65
- package/cli/scaffold/ssr/app/components/blog/post.js +86 -86
- package/cli/scaffold/ssr/app/components/home.js +37 -37
- package/cli/scaffold/ssr/app/components/not-found.js +15 -15
- package/cli/scaffold/ssr/app/routes.js +8 -8
- package/cli/scaffold/ssr/global.css +228 -228
- package/cli/scaffold/ssr/index.html +37 -37
- package/cli/scaffold/ssr/package.json +8 -8
- package/cli/scaffold/ssr/server/data/posts.js +144 -144
- package/cli/scaffold/ssr/server/index.js +213 -213
- package/cli/scaffold/webrtc/app/app.js +11 -0
- package/cli/scaffold/webrtc/app/components/video-room.js +295 -0
- package/cli/scaffold/webrtc/app/lib/room.js +252 -0
- package/cli/scaffold/webrtc/assets/.gitkeep +0 -0
- package/cli/scaffold/webrtc/global.css +250 -0
- package/cli/scaffold/webrtc/index.html +21 -0
- package/cli/utils.js +305 -287
- package/dist/API.md +661 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +10313 -6614
- package/dist/zquery.min.js +8 -631
- package/index.d.ts +570 -371
- package/index.js +311 -240
- package/package.json +76 -70
- package/src/component.js +1709 -1691
- package/src/core.js +921 -921
- package/src/diff.js +497 -497
- package/src/errors.js +209 -209
- package/src/expression.js +922 -922
- package/src/http.js +242 -242
- package/src/package.json +1 -1
- package/src/reactive.js +255 -255
- package/src/router.js +843 -843
- package/src/ssr.js +418 -418
- package/src/store.js +318 -318
- package/src/utils.js +515 -515
- package/src/webrtc/e2ee.js +351 -0
- package/src/webrtc/errors.js +116 -0
- package/src/webrtc/ice.js +301 -0
- package/src/webrtc/index.js +131 -0
- package/src/webrtc/joinToken.js +119 -0
- package/src/webrtc/observe.js +172 -0
- package/src/webrtc/peer.js +351 -0
- package/src/webrtc/reactive.js +268 -0
- package/src/webrtc/room.js +625 -0
- package/src/webrtc/sdp.js +302 -0
- package/src/webrtc/sfu/index.js +43 -0
- package/src/webrtc/sfu/livekit.js +131 -0
- package/src/webrtc/sfu/mediasoup.js +150 -0
- package/src/webrtc/signaling.js +373 -0
- package/src/webrtc/turn.js +237 -0
- package/tests/_helpers/webrtcFakes.js +289 -0
- package/tests/audit.test.js +4158 -4158
- package/tests/cli.test.js +1136 -1103
- package/tests/compare.test.js +497 -486
- package/tests/component.test.js +3969 -3938
- package/tests/core.test.js +1910 -1910
- package/tests/dev-server.test.js +489 -489
- package/tests/diff.test.js +1416 -1416
- package/tests/docs.test.js +1664 -1650
- package/tests/electron-features.test.js +864 -864
- package/tests/errors.test.js +619 -619
- package/tests/expression.test.js +1056 -1056
- package/tests/http.test.js +648 -648
- package/tests/reactive.test.js +819 -819
- package/tests/router.test.js +2327 -2327
- package/tests/ssr.test.js +870 -870
- package/tests/store.test.js +830 -830
- package/tests/test-minifier.js +153 -153
- package/tests/test-ssr.js +27 -27
- package/tests/utils.test.js +1377 -1377
- package/tests/webrtc/e2ee.test.js +283 -0
- package/tests/webrtc/ice.test.js +202 -0
- package/tests/webrtc/joinToken.test.js +89 -0
- package/tests/webrtc/observe.test.js +111 -0
- package/tests/webrtc/peer.test.js +373 -0
- package/tests/webrtc/reactive.test.js +235 -0
- package/tests/webrtc/room.test.js +406 -0
- package/tests/webrtc/sdp.test.js +151 -0
- package/tests/webrtc/sfu-livekit.test.js +119 -0
- package/tests/webrtc/sfu.test.js +160 -0
- package/tests/webrtc/signaling.test.js +251 -0
- package/tests/webrtc/turn.test.js +256 -0
- package/types/collection.d.ts +383 -383
- package/types/component.d.ts +186 -186
- package/types/errors.d.ts +135 -135
- package/types/http.d.ts +92 -92
- package/types/misc.d.ts +201 -201
- package/types/reactive.d.ts +98 -98
- package/types/router.d.ts +190 -190
- package/types/ssr.d.ts +102 -102
- package/types/store.d.ts +146 -146
- package/types/utils.d.ts +245 -245
- package/types/webrtc.d.ts +653 -0
|
@@ -1,220 +1,220 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cli/commands/dev/server.js - HTTP server & SSE broadcasting
|
|
3
|
-
*
|
|
4
|
-
* Creates the zero-http app, serves static files, injects the
|
|
5
|
-
* error-overlay snippet into HTML responses, and manages the
|
|
6
|
-
* SSE connection pool for live-reload events.
|
|
7
|
-
*
|
|
8
|
-
* Uses zero-http middleware:
|
|
9
|
-
* - helmet() → security headers (relaxed CSP for dev inline scripts)
|
|
10
|
-
* - compress() → brotli/gzip/deflate response compression
|
|
11
|
-
* - cors() → allow cross-origin requests in development
|
|
12
|
-
* - serveStatic() → static file serving with ETag & Cache-Control
|
|
13
|
-
* - SSE with keepAlive, retry, pad for proxy compatibility
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
'use strict';
|
|
17
|
-
|
|
18
|
-
const fs = require('fs');
|
|
19
|
-
const path = require('path');
|
|
20
|
-
const OVERLAY_SCRIPT = require('./overlay');
|
|
21
|
-
const DEVTOOLS_HTML = require('./devtools');
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// SSE client pool
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
class SSEPool {
|
|
28
|
-
constructor() {
|
|
29
|
-
this._clients = new Set();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** @param {import('zero-http').SSEStream} sse */
|
|
33
|
-
add(sse) {
|
|
34
|
-
this._clients.add(sse);
|
|
35
|
-
sse.on('close', () => this._clients.delete(sse));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
broadcast(eventType, data) {
|
|
39
|
-
for (const sse of this._clients) {
|
|
40
|
-
try { sse.event(eventType, data || ''); } catch { this._clients.delete(sse); }
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Number of connected SSE clients. */
|
|
45
|
-
get size() { return this._clients.size; }
|
|
46
|
-
|
|
47
|
-
closeAll() {
|
|
48
|
-
for (const sse of this._clients) {
|
|
49
|
-
try { sse.close(); } catch { /* ignore */ }
|
|
50
|
-
}
|
|
51
|
-
this._clients.clear();
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
// Server factory
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Prompt the user to auto-install zero-http when it isn't found.
|
|
61
|
-
* Resolves `true` if the user accepts, `false` otherwise.
|
|
62
|
-
*/
|
|
63
|
-
function promptInstall() {
|
|
64
|
-
const rl = require('readline').createInterface({
|
|
65
|
-
input: process.stdin,
|
|
66
|
-
output: process.stdout,
|
|
67
|
-
});
|
|
68
|
-
return new Promise((resolve) => {
|
|
69
|
-
rl.question(
|
|
70
|
-
'\n The local dev server requires zero-http, which is not installed.\n' +
|
|
71
|
-
' This package is only used during development and is not needed\n' +
|
|
72
|
-
' for building, bundling, or production.\n' +
|
|
73
|
-
' Install it now? (y/n): ',
|
|
74
|
-
(answer) => {
|
|
75
|
-
rl.close();
|
|
76
|
-
resolve(answer.trim().toLowerCase() === 'y');
|
|
77
|
-
}
|
|
78
|
-
);
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* @param {object} opts
|
|
84
|
-
* @param {string} opts.root - absolute path to project root
|
|
85
|
-
* @param {string} opts.htmlEntry - e.g. 'index.html'
|
|
86
|
-
* @param {number} opts.port
|
|
87
|
-
* @param {boolean} opts.noIntercept - skip zquery.min.js auto-resolve
|
|
88
|
-
* @returns {Promise<{ app, pool: SSEPool, listen: Function }>}
|
|
89
|
-
*/
|
|
90
|
-
async function createServer({ root, htmlEntry, port, noIntercept }) {
|
|
91
|
-
let zeroHttp;
|
|
92
|
-
try {
|
|
93
|
-
zeroHttp = require('zero-http');
|
|
94
|
-
} catch {
|
|
95
|
-
const ok = await promptInstall();
|
|
96
|
-
if (!ok) {
|
|
97
|
-
console.error('\n ✖ Cannot start dev server without zero-http.\n');
|
|
98
|
-
process.exit(1);
|
|
99
|
-
}
|
|
100
|
-
const { execSync } = require('child_process');
|
|
101
|
-
console.log('\n Installing zero-http...\n');
|
|
102
|
-
execSync('npm install zero-http --save-dev', { stdio: 'inherit' });
|
|
103
|
-
zeroHttp = require('zero-http');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const {
|
|
107
|
-
createApp,
|
|
108
|
-
static: serveStatic,
|
|
109
|
-
helmet,
|
|
110
|
-
compress,
|
|
111
|
-
cors,
|
|
112
|
-
debug,
|
|
113
|
-
} = zeroHttp;
|
|
114
|
-
|
|
115
|
-
debug.level('silent');
|
|
116
|
-
|
|
117
|
-
const app = createApp();
|
|
118
|
-
const pool = new SSEPool();
|
|
119
|
-
|
|
120
|
-
// ---- Security headers (dev-friendly CSP) ----
|
|
121
|
-
app.use(helmet({
|
|
122
|
-
contentSecurityPolicy: {
|
|
123
|
-
directives: {
|
|
124
|
-
defaultSrc: ["'self'"],
|
|
125
|
-
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
|
|
126
|
-
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
127
|
-
imgSrc: ["'self'", 'data:', 'blob:'],
|
|
128
|
-
connectSrc: ["'self'", 'ws:', 'wss:'],
|
|
129
|
-
fontSrc: ["'self'", 'data:'],
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
// SPA dev server runs over plain HTTP
|
|
133
|
-
hsts: false,
|
|
134
|
-
// Allow framing for devtools panel
|
|
135
|
-
frameguard: false,
|
|
136
|
-
}));
|
|
137
|
-
|
|
138
|
-
// ---- CORS (allow cross-origin during development) ----
|
|
139
|
-
app.use(cors());
|
|
140
|
-
|
|
141
|
-
// ---- Compression (brotli > gzip > deflate) ----
|
|
142
|
-
app.use(compress({
|
|
143
|
-
threshold: 1024,
|
|
144
|
-
}));
|
|
145
|
-
|
|
146
|
-
// ---- SSE endpoint ----
|
|
147
|
-
app.get('/__zq_reload', (req, res) => {
|
|
148
|
-
const sse = res.sse({
|
|
149
|
-
keepAlive: 30000,
|
|
150
|
-
keepAliveComment: 'ping',
|
|
151
|
-
retry: 3000,
|
|
152
|
-
pad: 2048,
|
|
153
|
-
});
|
|
154
|
-
pool.add(sse);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// ---- DevTools panel ----
|
|
158
|
-
app.get('/_devtools', (req, res) => {
|
|
159
|
-
res.set('Content-Type', 'text/html; charset=utf-8');
|
|
160
|
-
res.set('Cache-Control', 'no-cache');
|
|
161
|
-
res.send(DEVTOOLS_HTML);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
// ---- Auto-resolve zquery.min.js ----
|
|
165
|
-
const pkgRoot = path.resolve(__dirname, '..', '..', '..');
|
|
166
|
-
|
|
167
|
-
app.use((req, res, next) => {
|
|
168
|
-
if (noIntercept) return next();
|
|
169
|
-
const basename = path.basename(req.url.split('?')[0]).toLowerCase();
|
|
170
|
-
if (basename !== 'zquery.min.js') return next();
|
|
171
|
-
|
|
172
|
-
const candidates = [
|
|
173
|
-
path.join(pkgRoot, 'dist', 'zquery.min.js'),
|
|
174
|
-
path.join(root, 'node_modules', 'zero-query', 'dist', 'zquery.min.js'),
|
|
175
|
-
];
|
|
176
|
-
for (const p of candidates) {
|
|
177
|
-
if (fs.existsSync(p)) {
|
|
178
|
-
res.set('Content-Type', 'application/javascript; charset=utf-8');
|
|
179
|
-
res.set('Cache-Control', 'no-cache');
|
|
180
|
-
res.send(fs.readFileSync(p, 'utf-8'));
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
next();
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// ---- Static files ----
|
|
188
|
-
app.use(serveStatic(root, { index: false, dotfiles: 'ignore' }));
|
|
189
|
-
|
|
190
|
-
// ---- SPA fallback - inject overlay/SSE snippet ----
|
|
191
|
-
app.get('*', (req, res) => {
|
|
192
|
-
if (path.extname(req.url) && path.extname(req.url) !== '.html') {
|
|
193
|
-
res.status(404).send('Not Found');
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
const indexPath = path.join(root, htmlEntry);
|
|
197
|
-
if (!fs.existsSync(indexPath)) {
|
|
198
|
-
res.status(404).send(`${htmlEntry} not found`);
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
let html = fs.readFileSync(indexPath, 'utf-8');
|
|
202
|
-
if (html.includes('</body>')) {
|
|
203
|
-
html = html.replace('</body>', OVERLAY_SCRIPT + '\n</body>');
|
|
204
|
-
} else {
|
|
205
|
-
html += OVERLAY_SCRIPT;
|
|
206
|
-
}
|
|
207
|
-
res.html(html);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
function listen(cb) {
|
|
211
|
-
const server = app.listen(port, cb);
|
|
212
|
-
server.keepAliveTimeout = 65000;
|
|
213
|
-
server.headersTimeout = 66000;
|
|
214
|
-
return server;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return { app, pool, listen };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
module.exports = { createServer, SSEPool };
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/dev/server.js - HTTP server & SSE broadcasting
|
|
3
|
+
*
|
|
4
|
+
* Creates the zero-http app, serves static files, injects the
|
|
5
|
+
* error-overlay snippet into HTML responses, and manages the
|
|
6
|
+
* SSE connection pool for live-reload events.
|
|
7
|
+
*
|
|
8
|
+
* Uses zero-http middleware:
|
|
9
|
+
* - helmet() → security headers (relaxed CSP for dev inline scripts)
|
|
10
|
+
* - compress() → brotli/gzip/deflate response compression
|
|
11
|
+
* - cors() → allow cross-origin requests in development
|
|
12
|
+
* - serveStatic() → static file serving with ETag & Cache-Control
|
|
13
|
+
* - SSE with keepAlive, retry, pad for proxy compatibility
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const OVERLAY_SCRIPT = require('./overlay');
|
|
21
|
+
const DEVTOOLS_HTML = require('./devtools');
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// SSE client pool
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
class SSEPool {
|
|
28
|
+
constructor() {
|
|
29
|
+
this._clients = new Set();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @param {import('zero-http').SSEStream} sse */
|
|
33
|
+
add(sse) {
|
|
34
|
+
this._clients.add(sse);
|
|
35
|
+
sse.on('close', () => this._clients.delete(sse));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
broadcast(eventType, data) {
|
|
39
|
+
for (const sse of this._clients) {
|
|
40
|
+
try { sse.event(eventType, data || ''); } catch { this._clients.delete(sse); }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Number of connected SSE clients. */
|
|
45
|
+
get size() { return this._clients.size; }
|
|
46
|
+
|
|
47
|
+
closeAll() {
|
|
48
|
+
for (const sse of this._clients) {
|
|
49
|
+
try { sse.close(); } catch { /* ignore */ }
|
|
50
|
+
}
|
|
51
|
+
this._clients.clear();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Server factory
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Prompt the user to auto-install zero-http when it isn't found.
|
|
61
|
+
* Resolves `true` if the user accepts, `false` otherwise.
|
|
62
|
+
*/
|
|
63
|
+
function promptInstall() {
|
|
64
|
+
const rl = require('readline').createInterface({
|
|
65
|
+
input: process.stdin,
|
|
66
|
+
output: process.stdout,
|
|
67
|
+
});
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
rl.question(
|
|
70
|
+
'\n The local dev server requires zero-http, which is not installed.\n' +
|
|
71
|
+
' This package is only used during development and is not needed\n' +
|
|
72
|
+
' for building, bundling, or production.\n' +
|
|
73
|
+
' Install it now? (y/n): ',
|
|
74
|
+
(answer) => {
|
|
75
|
+
rl.close();
|
|
76
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {object} opts
|
|
84
|
+
* @param {string} opts.root - absolute path to project root
|
|
85
|
+
* @param {string} opts.htmlEntry - e.g. 'index.html'
|
|
86
|
+
* @param {number} opts.port
|
|
87
|
+
* @param {boolean} opts.noIntercept - skip zquery.min.js auto-resolve
|
|
88
|
+
* @returns {Promise<{ app, pool: SSEPool, listen: Function }>}
|
|
89
|
+
*/
|
|
90
|
+
async function createServer({ root, htmlEntry, port, noIntercept }) {
|
|
91
|
+
let zeroHttp;
|
|
92
|
+
try {
|
|
93
|
+
zeroHttp = require('zero-http');
|
|
94
|
+
} catch {
|
|
95
|
+
const ok = await promptInstall();
|
|
96
|
+
if (!ok) {
|
|
97
|
+
console.error('\n ✖ Cannot start dev server without zero-http.\n');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
const { execSync } = require('child_process');
|
|
101
|
+
console.log('\n Installing zero-http...\n');
|
|
102
|
+
execSync('npm install zero-http --save-dev', { stdio: 'inherit' });
|
|
103
|
+
zeroHttp = require('zero-http');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const {
|
|
107
|
+
createApp,
|
|
108
|
+
static: serveStatic,
|
|
109
|
+
helmet,
|
|
110
|
+
compress,
|
|
111
|
+
cors,
|
|
112
|
+
debug,
|
|
113
|
+
} = zeroHttp;
|
|
114
|
+
|
|
115
|
+
debug.level('silent');
|
|
116
|
+
|
|
117
|
+
const app = createApp();
|
|
118
|
+
const pool = new SSEPool();
|
|
119
|
+
|
|
120
|
+
// ---- Security headers (dev-friendly CSP) ----
|
|
121
|
+
app.use(helmet({
|
|
122
|
+
contentSecurityPolicy: {
|
|
123
|
+
directives: {
|
|
124
|
+
defaultSrc: ["'self'"],
|
|
125
|
+
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
|
|
126
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
127
|
+
imgSrc: ["'self'", 'data:', 'blob:'],
|
|
128
|
+
connectSrc: ["'self'", 'ws:', 'wss:'],
|
|
129
|
+
fontSrc: ["'self'", 'data:'],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
// SPA dev server runs over plain HTTP
|
|
133
|
+
hsts: false,
|
|
134
|
+
// Allow framing for devtools panel
|
|
135
|
+
frameguard: false,
|
|
136
|
+
}));
|
|
137
|
+
|
|
138
|
+
// ---- CORS (allow cross-origin during development) ----
|
|
139
|
+
app.use(cors());
|
|
140
|
+
|
|
141
|
+
// ---- Compression (brotli > gzip > deflate) ----
|
|
142
|
+
app.use(compress({
|
|
143
|
+
threshold: 1024,
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
// ---- SSE endpoint ----
|
|
147
|
+
app.get('/__zq_reload', (req, res) => {
|
|
148
|
+
const sse = res.sse({
|
|
149
|
+
keepAlive: 30000,
|
|
150
|
+
keepAliveComment: 'ping',
|
|
151
|
+
retry: 3000,
|
|
152
|
+
pad: 2048,
|
|
153
|
+
});
|
|
154
|
+
pool.add(sse);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ---- DevTools panel ----
|
|
158
|
+
app.get('/_devtools', (req, res) => {
|
|
159
|
+
res.set('Content-Type', 'text/html; charset=utf-8');
|
|
160
|
+
res.set('Cache-Control', 'no-cache');
|
|
161
|
+
res.send(DEVTOOLS_HTML);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ---- Auto-resolve zquery.min.js ----
|
|
165
|
+
const pkgRoot = path.resolve(__dirname, '..', '..', '..');
|
|
166
|
+
|
|
167
|
+
app.use((req, res, next) => {
|
|
168
|
+
if (noIntercept) return next();
|
|
169
|
+
const basename = path.basename(req.url.split('?')[0]).toLowerCase();
|
|
170
|
+
if (basename !== 'zquery.min.js') return next();
|
|
171
|
+
|
|
172
|
+
const candidates = [
|
|
173
|
+
path.join(pkgRoot, 'dist', 'zquery.min.js'),
|
|
174
|
+
path.join(root, 'node_modules', 'zero-query', 'dist', 'zquery.min.js'),
|
|
175
|
+
];
|
|
176
|
+
for (const p of candidates) {
|
|
177
|
+
if (fs.existsSync(p)) {
|
|
178
|
+
res.set('Content-Type', 'application/javascript; charset=utf-8');
|
|
179
|
+
res.set('Cache-Control', 'no-cache');
|
|
180
|
+
res.send(fs.readFileSync(p, 'utf-8'));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
next();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ---- Static files ----
|
|
188
|
+
app.use(serveStatic(root, { index: false, dotfiles: 'ignore' }));
|
|
189
|
+
|
|
190
|
+
// ---- SPA fallback - inject overlay/SSE snippet ----
|
|
191
|
+
app.get('*', (req, res) => {
|
|
192
|
+
if (path.extname(req.url) && path.extname(req.url) !== '.html') {
|
|
193
|
+
res.status(404).send('Not Found');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const indexPath = path.join(root, htmlEntry);
|
|
197
|
+
if (!fs.existsSync(indexPath)) {
|
|
198
|
+
res.status(404).send(`${htmlEntry} not found`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
let html = fs.readFileSync(indexPath, 'utf-8');
|
|
202
|
+
if (html.includes('</body>')) {
|
|
203
|
+
html = html.replace('</body>', OVERLAY_SCRIPT + '\n</body>');
|
|
204
|
+
} else {
|
|
205
|
+
html += OVERLAY_SCRIPT;
|
|
206
|
+
}
|
|
207
|
+
res.html(html);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
function listen(cb) {
|
|
211
|
+
const server = app.listen(port, cb);
|
|
212
|
+
server.keepAliveTimeout = 65000;
|
|
213
|
+
server.headersTimeout = 66000;
|
|
214
|
+
return server;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { app, pool, listen };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = { createServer, SSEPool };
|
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cli/commands/dev/validator.js - JS syntax validation
|
|
3
|
-
*
|
|
4
|
-
* Pre-validates JavaScript files on save using Node's VM module.
|
|
5
|
-
* Returns structured error descriptors with code frames compatible
|
|
6
|
-
* with the browser error overlay.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
'use strict';
|
|
10
|
-
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const vm = require('vm');
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Code frame generator
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Build a code frame showing ~4 lines of context around the error
|
|
20
|
-
* with a caret pointer at the offending column.
|
|
21
|
-
*
|
|
22
|
-
* @param {string} source - full file contents
|
|
23
|
-
* @param {number} line - 1-based line number
|
|
24
|
-
* @param {number} column - 1-based column number
|
|
25
|
-
* @returns {string}
|
|
26
|
-
*/
|
|
27
|
-
function generateCodeFrame(source, line, column) {
|
|
28
|
-
const lines = source.split('\n');
|
|
29
|
-
const start = Math.max(0, line - 4);
|
|
30
|
-
const end = Math.min(lines.length, line + 3);
|
|
31
|
-
const pad = String(end).length;
|
|
32
|
-
const frame = [];
|
|
33
|
-
|
|
34
|
-
for (let i = start; i < end; i++) {
|
|
35
|
-
const num = String(i + 1).padStart(pad);
|
|
36
|
-
const marker = i === line - 1 ? '>' : ' ';
|
|
37
|
-
frame.push(`${marker} ${num} | ${lines[i]}`);
|
|
38
|
-
if (i === line - 1 && column > 0) {
|
|
39
|
-
frame.push(` ${' '.repeat(pad)} | ${' '.repeat(column - 1)}^`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return frame.join('\n');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
// JS validation
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Validate a JavaScript file for syntax errors.
|
|
51
|
-
*
|
|
52
|
-
* Strips ESM import/export statements (preserving line numbers) so the
|
|
53
|
-
* VM can parse module-style code, then compiles via vm.Script.
|
|
54
|
-
*
|
|
55
|
-
* @param {string} filePath - absolute path to the file
|
|
56
|
-
* @param {string} relPath - display-friendly relative path
|
|
57
|
-
* @returns {object|null} - error descriptor, or null if valid
|
|
58
|
-
*/
|
|
59
|
-
function validateJS(filePath, relPath) {
|
|
60
|
-
let source;
|
|
61
|
-
try { source = fs.readFileSync(filePath, 'utf-8'); } catch { return null; }
|
|
62
|
-
|
|
63
|
-
const normalized = source.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
64
|
-
const stripped = normalized.split('\n').map(line => {
|
|
65
|
-
if (/^\s*import\s+.*from\s+['"]/.test(line)) return ' '.repeat(line.length);
|
|
66
|
-
if (/^\s*import\s+['"]/.test(line)) return ' '.repeat(line.length);
|
|
67
|
-
if (/^\s*export\s*\{/.test(line)) return ' '.repeat(line.length);
|
|
68
|
-
line = line.replace(/^(\s*)export\s+default\s+/, '$1');
|
|
69
|
-
line = line.replace(/^(\s*)export\s+(const|let|var|function|class|async\s+function)\s/, '$1$2 ');
|
|
70
|
-
line = line.replace(/import\.meta\.url/g, "'__meta__'");
|
|
71
|
-
line = line.replace(/import\.meta/g, '({})');
|
|
72
|
-
return line;
|
|
73
|
-
}).join('\n');
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
new vm.Script(stripped, { filename: relPath });
|
|
77
|
-
return null;
|
|
78
|
-
} catch (err) {
|
|
79
|
-
const line = err.stack ? parseInt((err.stack.match(/:(\d+)/) || [])[1]) || 0 : 0;
|
|
80
|
-
const col = err.stack ? parseInt((err.stack.match(/:(\d+):(\d+)/) || [])[2]) || 0 : 0;
|
|
81
|
-
const frame = line > 0 ? generateCodeFrame(source, line, col) : '';
|
|
82
|
-
return {
|
|
83
|
-
code: 'ZQ_DEV_SYNTAX',
|
|
84
|
-
type: err.constructor.name || 'SyntaxError',
|
|
85
|
-
message: err.message,
|
|
86
|
-
file: relPath,
|
|
87
|
-
line,
|
|
88
|
-
column: col,
|
|
89
|
-
frame,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
module.exports = { generateCodeFrame, validateJS };
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/dev/validator.js - JS syntax validation
|
|
3
|
+
*
|
|
4
|
+
* Pre-validates JavaScript files on save using Node's VM module.
|
|
5
|
+
* Returns structured error descriptors with code frames compatible
|
|
6
|
+
* with the browser error overlay.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const vm = require('vm');
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Code frame generator
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build a code frame showing ~4 lines of context around the error
|
|
20
|
+
* with a caret pointer at the offending column.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} source - full file contents
|
|
23
|
+
* @param {number} line - 1-based line number
|
|
24
|
+
* @param {number} column - 1-based column number
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
function generateCodeFrame(source, line, column) {
|
|
28
|
+
const lines = source.split('\n');
|
|
29
|
+
const start = Math.max(0, line - 4);
|
|
30
|
+
const end = Math.min(lines.length, line + 3);
|
|
31
|
+
const pad = String(end).length;
|
|
32
|
+
const frame = [];
|
|
33
|
+
|
|
34
|
+
for (let i = start; i < end; i++) {
|
|
35
|
+
const num = String(i + 1).padStart(pad);
|
|
36
|
+
const marker = i === line - 1 ? '>' : ' ';
|
|
37
|
+
frame.push(`${marker} ${num} | ${lines[i]}`);
|
|
38
|
+
if (i === line - 1 && column > 0) {
|
|
39
|
+
frame.push(` ${' '.repeat(pad)} | ${' '.repeat(column - 1)}^`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return frame.join('\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// JS validation
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validate a JavaScript file for syntax errors.
|
|
51
|
+
*
|
|
52
|
+
* Strips ESM import/export statements (preserving line numbers) so the
|
|
53
|
+
* VM can parse module-style code, then compiles via vm.Script.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} filePath - absolute path to the file
|
|
56
|
+
* @param {string} relPath - display-friendly relative path
|
|
57
|
+
* @returns {object|null} - error descriptor, or null if valid
|
|
58
|
+
*/
|
|
59
|
+
function validateJS(filePath, relPath) {
|
|
60
|
+
let source;
|
|
61
|
+
try { source = fs.readFileSync(filePath, 'utf-8'); } catch { return null; }
|
|
62
|
+
|
|
63
|
+
const normalized = source.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
64
|
+
const stripped = normalized.split('\n').map(line => {
|
|
65
|
+
if (/^\s*import\s+.*from\s+['"]/.test(line)) return ' '.repeat(line.length);
|
|
66
|
+
if (/^\s*import\s+['"]/.test(line)) return ' '.repeat(line.length);
|
|
67
|
+
if (/^\s*export\s*\{/.test(line)) return ' '.repeat(line.length);
|
|
68
|
+
line = line.replace(/^(\s*)export\s+default\s+/, '$1');
|
|
69
|
+
line = line.replace(/^(\s*)export\s+(const|let|var|function|class|async\s+function)\s/, '$1$2 ');
|
|
70
|
+
line = line.replace(/import\.meta\.url/g, "'__meta__'");
|
|
71
|
+
line = line.replace(/import\.meta/g, '({})');
|
|
72
|
+
return line;
|
|
73
|
+
}).join('\n');
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
new vm.Script(stripped, { filename: relPath });
|
|
77
|
+
return null;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
const line = err.stack ? parseInt((err.stack.match(/:(\d+)/) || [])[1]) || 0 : 0;
|
|
80
|
+
const col = err.stack ? parseInt((err.stack.match(/:(\d+):(\d+)/) || [])[2]) || 0 : 0;
|
|
81
|
+
const frame = line > 0 ? generateCodeFrame(source, line, col) : '';
|
|
82
|
+
return {
|
|
83
|
+
code: 'ZQ_DEV_SYNTAX',
|
|
84
|
+
type: err.constructor.name || 'SyntaxError',
|
|
85
|
+
message: err.message,
|
|
86
|
+
file: relPath,
|
|
87
|
+
line,
|
|
88
|
+
column: col,
|
|
89
|
+
frame,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { generateCodeFrame, validateJS };
|