zero-query 1.0.9 → 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.
Files changed (154) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +2 -0
  3. package/cli/args.js +33 -33
  4. package/cli/commands/build-api.js +443 -0
  5. package/cli/commands/build.js +254 -216
  6. package/cli/commands/bundle.js +1228 -1183
  7. package/cli/commands/create.js +137 -121
  8. package/cli/commands/dev/devtools/index.js +56 -56
  9. package/cli/commands/dev/devtools/js/components.js +49 -49
  10. package/cli/commands/dev/devtools/js/core.js +423 -423
  11. package/cli/commands/dev/devtools/js/elements.js +421 -421
  12. package/cli/commands/dev/devtools/js/network.js +166 -166
  13. package/cli/commands/dev/devtools/js/performance.js +73 -73
  14. package/cli/commands/dev/devtools/js/router.js +105 -105
  15. package/cli/commands/dev/devtools/js/source.js +132 -132
  16. package/cli/commands/dev/devtools/js/stats.js +35 -35
  17. package/cli/commands/dev/devtools/js/tabs.js +79 -79
  18. package/cli/commands/dev/devtools/panel.html +95 -95
  19. package/cli/commands/dev/devtools/styles.css +244 -244
  20. package/cli/commands/dev/index.js +107 -107
  21. package/cli/commands/dev/logger.js +75 -75
  22. package/cli/commands/dev/overlay.js +858 -858
  23. package/cli/commands/dev/server.js +220 -167
  24. package/cli/commands/dev/validator.js +94 -94
  25. package/cli/commands/dev/watcher.js +172 -172
  26. package/cli/help.js +114 -112
  27. package/cli/index.js +52 -52
  28. package/cli/scaffold/default/LICENSE +21 -21
  29. package/cli/scaffold/default/app/app.js +207 -207
  30. package/cli/scaffold/default/app/components/about.js +201 -201
  31. package/cli/scaffold/default/app/components/api-demo.js +143 -143
  32. package/cli/scaffold/default/app/components/contact-card.js +231 -231
  33. package/cli/scaffold/default/app/components/contacts/contacts.css +706 -706
  34. package/cli/scaffold/default/app/components/contacts/contacts.html +200 -200
  35. package/cli/scaffold/default/app/components/contacts/contacts.js +196 -196
  36. package/cli/scaffold/default/app/components/counter.js +127 -127
  37. package/cli/scaffold/default/app/components/home.js +249 -249
  38. package/cli/scaffold/default/app/components/not-found.js +16 -16
  39. package/cli/scaffold/default/app/components/playground/playground.css +115 -115
  40. package/cli/scaffold/default/app/components/playground/playground.html +161 -161
  41. package/cli/scaffold/default/app/components/playground/playground.js +116 -116
  42. package/cli/scaffold/default/app/components/todos.js +225 -225
  43. package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -97
  44. package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -146
  45. package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -280
  46. package/cli/scaffold/default/app/routes.js +15 -15
  47. package/cli/scaffold/default/app/store.js +101 -101
  48. package/cli/scaffold/default/global.css +552 -552
  49. package/cli/scaffold/default/index.html +99 -99
  50. package/cli/scaffold/minimal/app/app.js +85 -85
  51. package/cli/scaffold/minimal/app/components/about.js +68 -68
  52. package/cli/scaffold/minimal/app/components/counter.js +122 -122
  53. package/cli/scaffold/minimal/app/components/home.js +68 -68
  54. package/cli/scaffold/minimal/app/components/not-found.js +16 -16
  55. package/cli/scaffold/minimal/app/routes.js +9 -9
  56. package/cli/scaffold/minimal/app/store.js +36 -36
  57. package/cli/scaffold/minimal/global.css +300 -300
  58. package/cli/scaffold/minimal/index.html +44 -44
  59. package/cli/scaffold/ssr/app/app.js +41 -41
  60. package/cli/scaffold/ssr/app/components/about.js +55 -55
  61. package/cli/scaffold/ssr/app/components/blog/index.js +65 -65
  62. package/cli/scaffold/ssr/app/components/blog/post.js +86 -86
  63. package/cli/scaffold/ssr/app/components/home.js +37 -37
  64. package/cli/scaffold/ssr/app/components/not-found.js +15 -15
  65. package/cli/scaffold/ssr/app/routes.js +8 -8
  66. package/cli/scaffold/ssr/global.css +228 -228
  67. package/cli/scaffold/ssr/index.html +37 -37
  68. package/cli/scaffold/ssr/package.json +8 -8
  69. package/cli/scaffold/ssr/server/data/posts.js +144 -144
  70. package/cli/scaffold/ssr/server/index.js +213 -213
  71. package/cli/scaffold/webrtc/app/app.js +11 -0
  72. package/cli/scaffold/webrtc/app/components/video-room.js +295 -0
  73. package/cli/scaffold/webrtc/app/lib/room.js +252 -0
  74. package/cli/scaffold/webrtc/assets/.gitkeep +0 -0
  75. package/cli/scaffold/webrtc/global.css +250 -0
  76. package/cli/scaffold/webrtc/index.html +21 -0
  77. package/cli/utils.js +305 -287
  78. package/dist/API.md +7264 -0
  79. package/dist/zquery.dist.zip +0 -0
  80. package/dist/zquery.js +10313 -6252
  81. package/dist/zquery.min.js +8 -601
  82. package/index.d.ts +570 -365
  83. package/index.js +311 -232
  84. package/package.json +76 -69
  85. package/src/component.js +1709 -1454
  86. package/src/core.js +921 -921
  87. package/src/diff.js +497 -497
  88. package/src/errors.js +209 -209
  89. package/src/expression.js +922 -922
  90. package/src/http.js +242 -242
  91. package/src/package.json +1 -1
  92. package/src/reactive.js +255 -254
  93. package/src/router.js +843 -773
  94. package/src/ssr.js +418 -418
  95. package/src/store.js +318 -272
  96. package/src/utils.js +515 -515
  97. package/src/webrtc/e2ee.js +351 -0
  98. package/src/webrtc/errors.js +116 -0
  99. package/src/webrtc/ice.js +301 -0
  100. package/src/webrtc/index.js +131 -0
  101. package/src/webrtc/joinToken.js +119 -0
  102. package/src/webrtc/observe.js +172 -0
  103. package/src/webrtc/peer.js +351 -0
  104. package/src/webrtc/reactive.js +268 -0
  105. package/src/webrtc/room.js +625 -0
  106. package/src/webrtc/sdp.js +302 -0
  107. package/src/webrtc/sfu/index.js +43 -0
  108. package/src/webrtc/sfu/livekit.js +131 -0
  109. package/src/webrtc/sfu/mediasoup.js +150 -0
  110. package/src/webrtc/signaling.js +373 -0
  111. package/src/webrtc/turn.js +237 -0
  112. package/tests/_helpers/webrtcFakes.js +289 -0
  113. package/tests/audit.test.js +4158 -4158
  114. package/tests/cli.test.js +1136 -1023
  115. package/tests/compare.test.js +497 -0
  116. package/tests/component.test.js +3969 -3938
  117. package/tests/core.test.js +1910 -1910
  118. package/tests/dev-server.test.js +489 -0
  119. package/tests/diff.test.js +1416 -1416
  120. package/tests/docs.test.js +1664 -0
  121. package/tests/electron-features.test.js +864 -0
  122. package/tests/errors.test.js +619 -619
  123. package/tests/expression.test.js +1056 -1056
  124. package/tests/http.test.js +648 -648
  125. package/tests/reactive.test.js +819 -819
  126. package/tests/router.test.js +2327 -2327
  127. package/tests/ssr.test.js +870 -870
  128. package/tests/store.test.js +830 -830
  129. package/tests/test-minifier.js +153 -153
  130. package/tests/test-ssr.js +27 -27
  131. package/tests/utils.test.js +1377 -1377
  132. package/tests/webrtc/e2ee.test.js +283 -0
  133. package/tests/webrtc/ice.test.js +202 -0
  134. package/tests/webrtc/joinToken.test.js +89 -0
  135. package/tests/webrtc/observe.test.js +111 -0
  136. package/tests/webrtc/peer.test.js +373 -0
  137. package/tests/webrtc/reactive.test.js +235 -0
  138. package/tests/webrtc/room.test.js +406 -0
  139. package/tests/webrtc/sdp.test.js +151 -0
  140. package/tests/webrtc/sfu-livekit.test.js +119 -0
  141. package/tests/webrtc/sfu.test.js +160 -0
  142. package/tests/webrtc/signaling.test.js +251 -0
  143. package/tests/webrtc/turn.test.js +256 -0
  144. package/types/collection.d.ts +383 -383
  145. package/types/component.d.ts +186 -186
  146. package/types/errors.d.ts +135 -135
  147. package/types/http.d.ts +92 -92
  148. package/types/misc.d.ts +201 -201
  149. package/types/reactive.d.ts +98 -98
  150. package/types/router.d.ts +190 -190
  151. package/types/ssr.d.ts +102 -102
  152. package/types/store.d.ts +146 -145
  153. package/types/utils.d.ts +245 -245
  154. package/types/webrtc.d.ts +653 -0
@@ -1,167 +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
-
9
- 'use strict';
10
-
11
- const fs = require('fs');
12
- const path = require('path');
13
- const OVERLAY_SCRIPT = require('./overlay');
14
- const DEVTOOLS_HTML = require('./devtools');
15
-
16
- // ---------------------------------------------------------------------------
17
- // SSE client pool
18
- // ---------------------------------------------------------------------------
19
-
20
- class SSEPool {
21
- constructor() {
22
- this._clients = new Set();
23
- }
24
-
25
- add(sse) {
26
- this._clients.add(sse);
27
- sse.on('close', () => this._clients.delete(sse));
28
- }
29
-
30
- broadcast(eventType, data) {
31
- for (const sse of this._clients) {
32
- try { sse.event(eventType, data || ''); } catch { this._clients.delete(sse); }
33
- }
34
- }
35
-
36
- closeAll() {
37
- for (const sse of this._clients) {
38
- try { sse.close(); } catch { /* ignore */ }
39
- }
40
- this._clients.clear();
41
- }
42
- }
43
-
44
- // ---------------------------------------------------------------------------
45
- // Server factory
46
- // ---------------------------------------------------------------------------
47
-
48
- /**
49
- * Prompt the user to auto-install zero-http when it isn't found.
50
- * Resolves `true` if the user accepts, `false` otherwise.
51
- */
52
- function promptInstall() {
53
- const rl = require('readline').createInterface({
54
- input: process.stdin,
55
- output: process.stdout,
56
- });
57
- return new Promise((resolve) => {
58
- rl.question(
59
- '\n The local dev server requires zero-http, which is not installed.\n' +
60
- ' This package is only used during development and is not needed\n' +
61
- ' for building, bundling, or production.\n' +
62
- ' Install it now? (y/n): ',
63
- (answer) => {
64
- rl.close();
65
- resolve(answer.trim().toLowerCase() === 'y');
66
- }
67
- );
68
- });
69
- }
70
-
71
- /**
72
- * @param {object} opts
73
- * @param {string} opts.root - absolute path to project root
74
- * @param {string} opts.htmlEntry - e.g. 'index.html'
75
- * @param {number} opts.port
76
- * @param {boolean} opts.noIntercept - skip zquery.min.js auto-resolve
77
- * @returns {Promise<{ app, pool: SSEPool, listen: Function }>}
78
- */
79
- async function createServer({ root, htmlEntry, port, noIntercept }) {
80
- let zeroHttp;
81
- try {
82
- zeroHttp = require('zero-http');
83
- } catch {
84
- const ok = await promptInstall();
85
- if (!ok) {
86
- console.error('\n ✖ Cannot start dev server without zero-http.\n');
87
- process.exit(1);
88
- }
89
- const { execSync } = require('child_process');
90
- console.log('\n Installing zero-http...\n');
91
- execSync('npm install zero-http --save-dev', { stdio: 'inherit' });
92
- zeroHttp = require('zero-http');
93
- }
94
-
95
- const { createApp, static: serveStatic, debug } = zeroHttp;
96
- debug.level('silent');
97
-
98
- const app = createApp();
99
- const pool = new SSEPool();
100
-
101
- // ---- SSE endpoint ----
102
- app.get('/__zq_reload', (req, res) => {
103
- const sse = res.sse({ keepAlive: 30000, keepAliveComment: 'ping' });
104
- pool.add(sse);
105
- });
106
-
107
- // ---- DevTools panel ----
108
- app.get('/_devtools', (req, res) => {
109
- res.set('Content-Type', 'text/html; charset=utf-8');
110
- res.set('Cache-Control', 'no-cache');
111
- res.send(DEVTOOLS_HTML);
112
- });
113
-
114
- // ---- Auto-resolve zquery.min.js ----
115
- const pkgRoot = path.resolve(__dirname, '..', '..', '..');
116
-
117
- app.use((req, res, next) => {
118
- if (noIntercept) return next();
119
- const basename = path.basename(req.url.split('?')[0]).toLowerCase();
120
- if (basename !== 'zquery.min.js') return next();
121
-
122
- const candidates = [
123
- path.join(pkgRoot, 'dist', 'zquery.min.js'),
124
- path.join(root, 'node_modules', 'zero-query', 'dist', 'zquery.min.js'),
125
- ];
126
- for (const p of candidates) {
127
- if (fs.existsSync(p)) {
128
- res.set('Content-Type', 'application/javascript; charset=utf-8');
129
- res.set('Cache-Control', 'no-cache');
130
- res.send(fs.readFileSync(p, 'utf-8'));
131
- return;
132
- }
133
- }
134
- next();
135
- });
136
-
137
- // ---- Static files ----
138
- app.use(serveStatic(root, { index: false, dotfiles: 'ignore' }));
139
-
140
- // ---- SPA fallback - inject overlay/SSE snippet ----
141
- app.get('*', (req, res) => {
142
- if (path.extname(req.url) && path.extname(req.url) !== '.html') {
143
- res.status(404).send('Not Found');
144
- return;
145
- }
146
- const indexPath = path.join(root, htmlEntry);
147
- if (!fs.existsSync(indexPath)) {
148
- res.status(404).send(`${htmlEntry} not found`);
149
- return;
150
- }
151
- let html = fs.readFileSync(indexPath, 'utf-8');
152
- if (html.includes('</body>')) {
153
- html = html.replace('</body>', OVERLAY_SCRIPT + '\n</body>');
154
- } else {
155
- html += OVERLAY_SCRIPT;
156
- }
157
- res.html(html);
158
- });
159
-
160
- function listen(cb) {
161
- app.listen(port, cb);
162
- }
163
-
164
- return { app, pool, listen };
165
- }
166
-
167
- 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 };