novac 2.2.2 → 2.4.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/bin/novac +49 -12
- package/bin/novac.cmd +2 -0
- package/bin/nvc.cmd +2 -0
- package/bin/nvml +267 -224
- package/bin/nvml.cmd +2 -0
- package/examples/bf.nv +3 -39
- package/kits/kitarcade/kitdef.js +2709 -0
- package/kits/kitascii/kitdef.js +839 -0
- package/kits/kitlife/kitdef.js +575 -0
- package/kits/kitmisc/kitdef.js +2037 -0
- package/kits/kitnovacweb/nvml/index.js +9 -3
- package/kits/kitnovacweb/nvml/renderer.js +113 -83
- package/kits/kitpet/kitdef.js +752 -0
- package/kits/libtasker/kitdef.js +1395 -106
- package/package.json +1 -1
package/bin/nvml
CHANGED
|
@@ -15,11 +15,21 @@
|
|
|
15
15
|
* nvml --version Show version
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
const fs
|
|
18
|
+
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
20
|
const http = require('http');
|
|
21
|
-
const os
|
|
22
|
-
const url
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const url = require('url');
|
|
23
|
+
const crypto = require('crypto');
|
|
24
|
+
|
|
25
|
+
// ── CSRF token — one per server process, embedded in every compiled page ──
|
|
26
|
+
const CSRF_TOKEN = crypto.randomBytes(32).toString('hex');
|
|
27
|
+
|
|
28
|
+
// ── Named action registry — populated by parsing the NVML file ────────────
|
|
29
|
+
// { actionName → { code, type: 'nova'|'nodejs' } }
|
|
30
|
+
// Actions are registered when the file is first compiled (or re-compiled).
|
|
31
|
+
// The browser calls /_nvml/action/:name — no code ever travels over the wire.
|
|
32
|
+
const _actions = new Map();
|
|
23
33
|
|
|
24
34
|
const chalk = (() => {
|
|
25
35
|
try { return require('chalk').default ?? require('chalk'); } catch { return null; }
|
|
@@ -29,11 +39,11 @@ const VERSION = '1.0.0';
|
|
|
29
39
|
|
|
30
40
|
// ── colour helpers (graceful fallback if chalk absent) ────────
|
|
31
41
|
const c = {
|
|
32
|
-
green:
|
|
33
|
-
red:
|
|
34
|
-
cyan:
|
|
35
|
-
bold:
|
|
36
|
-
dim:
|
|
42
|
+
green: s => chalk ? chalk.green(s) : s,
|
|
43
|
+
red: s => chalk ? chalk.red(s) : s,
|
|
44
|
+
cyan: s => chalk ? chalk.cyan(s) : s,
|
|
45
|
+
bold: s => chalk ? chalk.bold(s) : s,
|
|
46
|
+
dim: s => chalk ? chalk.dim(s) : s,
|
|
37
47
|
yellow: s => chalk ? chalk.yellow(s) : s,
|
|
38
48
|
};
|
|
39
49
|
|
|
@@ -112,11 +122,11 @@ function findElementById(elements, id) {
|
|
|
112
122
|
|
|
113
123
|
function makeDocumentProxy(doc) {
|
|
114
124
|
return {
|
|
115
|
-
setConfig: (k, v)
|
|
116
|
-
getConfig: (k)
|
|
117
|
-
setTitle:
|
|
118
|
-
setMeta:
|
|
119
|
-
set: (id, content)
|
|
125
|
+
setConfig: (k, v) => { doc.config[k] = v; },
|
|
126
|
+
getConfig: (k) => doc.config[k],
|
|
127
|
+
setTitle: (t) => { doc.config.title = t; },
|
|
128
|
+
setMeta: (k, v) => { doc.config[k] = v; },
|
|
129
|
+
set: (id, content) => {
|
|
120
130
|
const el = findElementById(doc.visual, id);
|
|
121
131
|
if (el) el.textValue = String(content);
|
|
122
132
|
},
|
|
@@ -155,15 +165,15 @@ function makeDocumentProxy(doc) {
|
|
|
155
165
|
const el = findElementById(doc.visual, id);
|
|
156
166
|
if (el) el.ss = (el.ss ? el.ss + '\n' : '') + `display: ${display || 'block'};`;
|
|
157
167
|
},
|
|
158
|
-
alert: (_msg) => {},
|
|
168
|
+
alert: (_msg) => { },
|
|
159
169
|
config: doc.config,
|
|
160
|
-
_doc:
|
|
170
|
+
_doc: doc,
|
|
161
171
|
};
|
|
162
172
|
}
|
|
163
173
|
|
|
164
174
|
// ── error page (served when compile fails during --serve) ─────
|
|
165
175
|
function errorPage(file, err) {
|
|
166
|
-
const esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
176
|
+
const esc = s => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
167
177
|
return `<!DOCTYPE html>
|
|
168
178
|
<html><head><meta charset="UTF-8"><title>NVML Error</title>
|
|
169
179
|
<style>
|
|
@@ -184,7 +194,7 @@ function errorPage(file, err) {
|
|
|
184
194
|
|
|
185
195
|
// ── serve ─────────────────────────────────────────────────────
|
|
186
196
|
function cmdServe(port, nvmlFile, route) {
|
|
187
|
-
const nvml
|
|
197
|
+
const nvml = loadKit();
|
|
188
198
|
const absFile = path.resolve(process.cwd(), nvmlFile);
|
|
189
199
|
|
|
190
200
|
if (!fs.existsSync(absFile)) die(`File not found: ${absFile}`);
|
|
@@ -199,233 +209,266 @@ function cmdServe(port, nvmlFile, route) {
|
|
|
199
209
|
);
|
|
200
210
|
}
|
|
201
211
|
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const scope = {
|
|
207
|
-
document: makeDocumentProxy(doc),
|
|
208
|
-
request: reqCtx || {},
|
|
209
|
-
};
|
|
210
|
-
novaRun(code, scope);
|
|
211
|
-
};
|
|
212
|
+
// ── registerAction — called by renderer at compile time ─────────────────
|
|
213
|
+
// Stores the action's server code by name so the browser can call it by name only.
|
|
214
|
+
function registerAction(name, code, type) {
|
|
215
|
+
_actions.set(name, { code, type: type || 'nova' });
|
|
212
216
|
}
|
|
213
217
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
// text / html
|
|
224
|
-
set: (id, val) => mutations.push({ type: 'setText', id: String(id), value: String(val) }),
|
|
225
|
-
setHTML: (id, val) => mutations.push({ type: 'setHTML', id: String(id), value: String(val) }),
|
|
226
|
-
// props / attrs
|
|
227
|
-
setProp: (id, k, v) => mutations.push({ type: 'setProp', id: String(id), key: String(k), value: String(v) }),
|
|
228
|
-
setAttr: (id, k, v) => mutations.push({ type: 'setAttr', id: String(id), key: String(k), value: String(v) }),
|
|
229
|
-
removeAttr: (id, k) => mutations.push({ type: 'removeAttr', id: String(id), key: String(k) }),
|
|
230
|
-
// classes
|
|
231
|
-
addClass: (id, cls) => mutations.push({ type: 'addClass', id: String(id), value: String(cls) }),
|
|
232
|
-
removeClass: (id, cls) => mutations.push({ type: 'removeClass', id: String(id), value: String(cls) }),
|
|
233
|
-
toggleClass: (id, cls, f) => mutations.push({ type: 'toggleClass', id: String(id), value: String(cls), force: f }),
|
|
234
|
-
setClass: (id, cls) => mutations.push({ type: 'setClass', id: String(id), value: String(cls) }),
|
|
235
|
-
// style
|
|
236
|
-
addStyle: (css) => mutations.push({ type: 'addStyle', value: String(css) }),
|
|
237
|
-
addElementStyle:(id, css) => mutations.push({ type: 'addStyle', id: String(id), value: String(css) }),
|
|
238
|
-
setStyle: (id, k, v) => mutations.push({ type: 'setStyle', id: String(id), key: String(k), value: String(v) }),
|
|
239
|
-
setCSSVar: (name, val) => mutations.push({ type: 'setCSSVar', name: String(name), value: String(val) }),
|
|
240
|
-
// visibility
|
|
241
|
-
hide: (id, t) => mutations.push({ type: 'hide', id: String(id), transition: t || null }),
|
|
242
|
-
show: (id, d, t) => mutations.push({ type: 'show', id: String(id), value: d || 'block', transition: t || null }),
|
|
243
|
-
// DOM manipulation
|
|
244
|
-
remove: (id, t) => mutations.push({ type: 'remove', id: String(id), transition: t || null }),
|
|
245
|
-
appendChild: (id, html) => mutations.push({ type: 'appendChild', id: String(id), html: String(html) }),
|
|
246
|
-
insertBefore: (id, html) => mutations.push({ type: 'insertBefore', id: String(id), html: String(html) }),
|
|
247
|
-
insertAfter: (id, html) => mutations.push({ type: 'insertAfter', id: String(id), html: String(html) }),
|
|
248
|
-
// focus / scroll
|
|
249
|
-
focus: (id) => mutations.push({ type: 'focus', id: String(id) }),
|
|
250
|
-
blur: (id) => mutations.push({ type: 'blur', id: String(id) }),
|
|
251
|
-
scroll: (id, beh) => mutations.push({ type: 'scroll', id: String(id), behavior: beh || 'smooth' }),
|
|
252
|
-
// signals
|
|
253
|
-
setSignal: (name, val) => mutations.push({ type: 'setSignal', name: String(name), value: val }),
|
|
254
|
-
// list patch
|
|
255
|
-
patchList: (id, items, tpl, key) => mutations.push({ type: 'patchList', id: String(id), items, template: String(tpl), key: key || null }),
|
|
256
|
-
// navigation
|
|
257
|
-
navigate: (path) => mutations.push({ type: 'navigate', path: String(path) }),
|
|
258
|
-
reload: () => mutations.push({ type: 'reload' }),
|
|
259
|
-
redirect: (url) => mutations.push({ type: 'redirect', url: String(url) }),
|
|
260
|
-
// feedback
|
|
261
|
-
alert: (msg) => mutations.push({ type: 'alert', value: String(msg) }),
|
|
262
|
-
toast: (msg, d, t) => mutations.push({ type: 'toast', value: String(msg), duration: d || 3000, type: t || 'info' }),
|
|
263
|
-
console: (msg, lvl) => mutations.push({ type: 'console', value: String(msg), level: lvl || 'log' }),
|
|
264
|
-
// reads from live snapshot
|
|
265
|
-
get: (id) => (live.elements && live.elements[id]) ? live.elements[id].text : null,
|
|
266
|
-
getValue: (id) => (live.elements && live.elements[id]) ? live.elements[id].value : null,
|
|
267
|
-
getClass: (id) => (live.elements && live.elements[id]) ? live.elements[id].class : null,
|
|
268
|
-
getConfig: (k) => live[k] ?? null,
|
|
269
|
-
getSignal: (n) => (live.state && live.state[n]) ?? null,
|
|
270
|
-
config: live,
|
|
271
|
-
// SSE push (server → all connected clients)
|
|
272
|
-
push: (name, val) => {
|
|
273
|
-
const msg = `event: signal\ndata: ${JSON.stringify({ name, value: val })}\n\n`;
|
|
274
|
-
_sseClients.forEach(c => { try { c.write(msg); } catch(_) {} });
|
|
275
|
-
},
|
|
276
|
-
pushMutations: (muts) => {
|
|
277
|
-
const msg = `event: mutations\ndata: ${JSON.stringify(muts)}\n\n`;
|
|
278
|
-
_sseClients.forEach(c => { try { c.write(msg); } catch(_) {} });
|
|
279
|
-
},
|
|
280
|
-
};
|
|
218
|
+
// ── Compile the NVML file (re-reads + re-registers actions on every request) ──
|
|
219
|
+
function compileFile(reqCtx) {
|
|
220
|
+
const source = fs.readFileSync(absFile, 'utf8');
|
|
221
|
+
return nvml.compile(source, {
|
|
222
|
+
novaRunner: makeNovaRunner(reqCtx),
|
|
223
|
+
novaEmitter: null,
|
|
224
|
+
registerAction: registerAction,
|
|
225
|
+
csrfToken: CSRF_TOKEN,
|
|
226
|
+
});
|
|
281
227
|
}
|
|
228
|
+
if (!novaRun) return null;
|
|
229
|
+
return (code, doc) => {
|
|
230
|
+
const scope = {
|
|
231
|
+
document: makeDocumentProxy(doc),
|
|
232
|
+
request: reqCtx || {},
|
|
233
|
+
};
|
|
234
|
+
novaRun(code, scope);
|
|
235
|
+
};
|
|
236
|
+
}
|
|
282
237
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const urlPath = parsed.pathname || '/';
|
|
238
|
+
// Build a mutation-capturing proxy for /_nvml/run
|
|
239
|
+
const _sseClients = new Set(); // Server-Sent Events connections
|
|
286
240
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
241
|
+
function makeMutationProxy(mutations, live) {
|
|
242
|
+
return {
|
|
243
|
+
// config
|
|
244
|
+
setConfig: (k, v) => mutations.push({ type: 'setConfig', key: String(k), value: v }),
|
|
245
|
+
setTitle: (t) => mutations.push({ type: 'setConfig', key: 'title', value: t }),
|
|
246
|
+
setMeta: (k, v) => mutations.push({ type: 'setConfig', key: String(k), value: v }),
|
|
247
|
+
// text / html
|
|
248
|
+
set: (id, val) => mutations.push({ type: 'setText', id: String(id), value: String(val) }),
|
|
249
|
+
setHTML: (id, val) => mutations.push({ type: 'setHTML', id: String(id), value: String(val) }),
|
|
250
|
+
// props / attrs
|
|
251
|
+
setProp: (id, k, v) => mutations.push({ type: 'setProp', id: String(id), key: String(k), value: String(v) }),
|
|
252
|
+
setAttr: (id, k, v) => mutations.push({ type: 'setAttr', id: String(id), key: String(k), value: String(v) }),
|
|
253
|
+
removeAttr: (id, k) => mutations.push({ type: 'removeAttr', id: String(id), key: String(k) }),
|
|
254
|
+
// classes
|
|
255
|
+
addClass: (id, cls) => mutations.push({ type: 'addClass', id: String(id), value: String(cls) }),
|
|
256
|
+
removeClass: (id, cls) => mutations.push({ type: 'removeClass', id: String(id), value: String(cls) }),
|
|
257
|
+
toggleClass: (id, cls, f) => mutations.push({ type: 'toggleClass', id: String(id), value: String(cls), force: f }),
|
|
258
|
+
setClass: (id, cls) => mutations.push({ type: 'setClass', id: String(id), value: String(cls) }),
|
|
259
|
+
// style
|
|
260
|
+
addStyle: (css) => mutations.push({ type: 'addStyle', value: String(css) }),
|
|
261
|
+
addElementStyle: (id, css) => mutations.push({ type: 'addStyle', id: String(id), value: String(css) }),
|
|
262
|
+
setStyle: (id, k, v) => mutations.push({ type: 'setStyle', id: String(id), key: String(k), value: String(v) }),
|
|
263
|
+
setCSSVar: (name, val) => mutations.push({ type: 'setCSSVar', name: String(name), value: String(val) }),
|
|
264
|
+
// visibility
|
|
265
|
+
hide: (id, t) => mutations.push({ type: 'hide', id: String(id), transition: t || null }),
|
|
266
|
+
show: (id, d, t) => mutations.push({ type: 'show', id: String(id), value: d || 'block', transition: t || null }),
|
|
267
|
+
// DOM manipulation
|
|
268
|
+
remove: (id, t) => mutations.push({ type: 'remove', id: String(id), transition: t || null }),
|
|
269
|
+
appendChild: (id, html) => mutations.push({ type: 'appendChild', id: String(id), html: String(html) }),
|
|
270
|
+
insertBefore: (id, html) => mutations.push({ type: 'insertBefore', id: String(id), html: String(html) }),
|
|
271
|
+
insertAfter: (id, html) => mutations.push({ type: 'insertAfter', id: String(id), html: String(html) }),
|
|
272
|
+
// focus / scroll
|
|
273
|
+
focus: (id) => mutations.push({ type: 'focus', id: String(id) }),
|
|
274
|
+
blur: (id) => mutations.push({ type: 'blur', id: String(id) }),
|
|
275
|
+
scroll: (id, beh) => mutations.push({ type: 'scroll', id: String(id), behavior: beh || 'smooth' }),
|
|
276
|
+
// signals
|
|
277
|
+
setSignal: (name, val) => mutations.push({ type: 'setSignal', name: String(name), value: val }),
|
|
278
|
+
// list patch
|
|
279
|
+
patchList: (id, items, tpl, key) => mutations.push({ type: 'patchList', id: String(id), items, template: String(tpl), key: key || null }),
|
|
280
|
+
// navigation
|
|
281
|
+
navigate: (path) => mutations.push({ type: 'navigate', path: String(path) }),
|
|
282
|
+
reload: () => mutations.push({ type: 'reload' }),
|
|
283
|
+
redirect: (url) => mutations.push({ type: 'redirect', url: String(url) }),
|
|
284
|
+
// feedback
|
|
285
|
+
alert: (msg) => mutations.push({ type: 'alert', value: String(msg) }),
|
|
286
|
+
toast: (msg, d, t) => mutations.push({ type: 'toast', value: String(msg), duration: d || 3000, type: t || 'info' }),
|
|
287
|
+
console: (msg, lvl) => mutations.push({ type: 'console', value: String(msg), level: lvl || 'log' }),
|
|
288
|
+
// reads from live snapshot
|
|
289
|
+
get: (id) => (live.elements && live.elements[id]) ? live.elements[id].text : null,
|
|
290
|
+
getValue: (id) => (live.elements && live.elements[id]) ? live.elements[id].value : null,
|
|
291
|
+
getClass: (id) => (live.elements && live.elements[id]) ? live.elements[id].class : null,
|
|
292
|
+
getConfig: (k) => live[k] ?? null,
|
|
293
|
+
getSignal: (n) => (live.state && live.state[n]) ?? null,
|
|
294
|
+
config: live,
|
|
295
|
+
// SSE push (server → all connected clients)
|
|
296
|
+
push: (name, val) => {
|
|
297
|
+
const msg = `event: signal\ndata: ${JSON.stringify({ name, value: val })}\n\n`;
|
|
298
|
+
_sseClients.forEach(c => { try { c.write(msg); } catch (_) { } });
|
|
299
|
+
},
|
|
300
|
+
pushMutations: (muts) => {
|
|
301
|
+
const msg = `event: mutations\ndata: ${JSON.stringify(muts)}\n\n`;
|
|
302
|
+
_sseClients.forEach(c => { try { c.write(msg); } catch (_) { } });
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const server = http.createServer((req, res) => {
|
|
308
|
+
const parsed = url.parse(req.url || '/');
|
|
309
|
+
const urlPath = parsed.pathname || '/';
|
|
310
|
+
|
|
311
|
+
// ── Localhost-only guard for all internal /_nvml/* endpoints ─────────
|
|
312
|
+
if (urlPath.startsWith('/_nvml/')) {
|
|
313
|
+
const ip = req.socket.remoteAddress;
|
|
314
|
+
if (ip !== '127.0.0.1' && ip !== '::1' && ip !== '::ffff:127.0.0.1') {
|
|
315
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
316
|
+
res.end('403 Forbidden');
|
|
304
317
|
return;
|
|
305
318
|
}
|
|
319
|
+
}
|
|
306
320
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
live = body.live || {};
|
|
317
|
-
} catch (e) {
|
|
318
|
-
return sendJSON(res, 400, { error: 'Invalid JSON body' });
|
|
319
|
-
}
|
|
321
|
+
// CORS — open for the main page, locked for internal endpoints
|
|
322
|
+
if (urlPath.startsWith('/_nvml/')) {
|
|
323
|
+
res.setHeader('Access-Control-Allow-Origin', `http://localhost:${port}`);
|
|
324
|
+
} else {
|
|
325
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
326
|
+
}
|
|
327
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
328
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-CSRF-Token');
|
|
329
|
+
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
320
330
|
|
|
321
|
-
|
|
331
|
+
// ── CSRF validation helper ────────────────────────────────────────────
|
|
332
|
+
function validCSRF() {
|
|
333
|
+
return (req.headers['x-csrf-token'] || '') === CSRF_TOKEN;
|
|
334
|
+
}
|
|
322
335
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
336
|
+
// ── /_nvml/sse — Server-Sent Events ──────────────────────────────────
|
|
337
|
+
if (urlPath === '/_nvml/sse' && req.method === 'GET') {
|
|
338
|
+
res.writeHead(200, {
|
|
339
|
+
'Content-Type': 'text/event-stream',
|
|
340
|
+
'Cache-Control': 'no-cache',
|
|
341
|
+
'Connection': 'keep-alive',
|
|
342
|
+
'X-Accel-Buffering': 'no',
|
|
343
|
+
});
|
|
344
|
+
res.write('retry: 3000\n\n');
|
|
345
|
+
_sseClients.add(res);
|
|
346
|
+
req.on('close', () => _sseClients.delete(res));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
326
349
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
process.stderr.write(c.red(`[nvml] /_nvml/run error: ${e.message}`) + '\n');
|
|
334
|
-
return sendJSON(res, 500, { error: e.message, mutations });
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
350
|
+
// ── /_nvml/action/:name — named server action dispatcher ─────────────
|
|
351
|
+
// This is the only server-execution endpoint. The browser calls it by
|
|
352
|
+
// action name (declared in NVML source), never sends executable code.
|
|
353
|
+
const actionMatch = urlPath.match(/^\/_nvml\/action\/([a-zA-Z0-9_-]+)$/);
|
|
354
|
+
if (actionMatch && req.method === 'POST') {
|
|
355
|
+
if (!validCSRF()) return sendJSON(res, 403, { error: 'Invalid CSRF token' });
|
|
339
356
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
res.end(`404 — this server only handles: ${route}`);
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
357
|
+
const actionName = actionMatch[1];
|
|
358
|
+
const action = _actions.get(actionName);
|
|
359
|
+
if (!action) return sendJSON(res, 404, { error: `Unknown action: ${actionName}` });
|
|
346
360
|
|
|
347
|
-
let
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
path: urlPath,
|
|
356
|
-
query: Object.fromEntries(new URLSearchParams(parsed.query || '')),
|
|
357
|
-
headers: req.headers,
|
|
358
|
-
params: {},
|
|
359
|
-
body: null,
|
|
360
|
-
};
|
|
361
|
+
let rawBody = '';
|
|
362
|
+
req.on('data', chunk => { rawBody += chunk; });
|
|
363
|
+
req.on('end', () => {
|
|
364
|
+
let live = {};
|
|
365
|
+
try { live = JSON.parse(rawBody).live || {}; } catch (_) { }
|
|
366
|
+
|
|
367
|
+
const mutations = [];
|
|
368
|
+
const docProxy = makeMutationProxy(mutations, live);
|
|
361
369
|
|
|
362
|
-
const compileAndSend = () => {
|
|
363
|
-
let html;
|
|
364
370
|
try {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
})
|
|
371
|
+
if (action.type === 'nova') {
|
|
372
|
+
if (!novaRun) return sendJSON(res, 503, { error: 'Nova runtime not available' });
|
|
373
|
+
novaRun(action.code, { document: docProxy, request: live });
|
|
374
|
+
} else if (action.type === 'nodejs') {
|
|
375
|
+
const vm = require('vm');
|
|
376
|
+
vm.runInNewContext(action.code, {
|
|
377
|
+
document: docProxy, request: live,
|
|
378
|
+
require, console, process,
|
|
379
|
+
}, { timeout: 5000 });
|
|
380
|
+
}
|
|
381
|
+
return sendJSON(res, 200, { mutations });
|
|
369
382
|
} catch (e) {
|
|
370
|
-
process.stderr.write(c.red(`[nvml]
|
|
371
|
-
|
|
372
|
-
res.writeHead(500, {
|
|
373
|
-
'Content-Type': 'text/html; charset=UTF-8',
|
|
374
|
-
'Content-Length': Buffer.byteLength(errHtml),
|
|
375
|
-
});
|
|
376
|
-
res.end(errHtml);
|
|
377
|
-
return;
|
|
383
|
+
process.stderr.write(c.red(`[nvml] action "${actionName}" error: ${e.message}`) + '\n');
|
|
384
|
+
return sendJSON(res, 500, { error: e.message, mutations });
|
|
378
385
|
}
|
|
386
|
+
});
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
379
389
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
res.end(html);
|
|
387
|
-
process.stdout.write(c.dim(` ${req.method} ${urlPath} → 200`) + '\n');
|
|
388
|
-
};
|
|
390
|
+
// ── main route ───────────────────────────────────────────
|
|
391
|
+
if (urlPath !== route) {
|
|
392
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
393
|
+
res.end(`404 — this server only handles: ${route}`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
389
396
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
+
// Build request context
|
|
398
|
+
const reqContext = {
|
|
399
|
+
method: req.method,
|
|
400
|
+
url: req.url,
|
|
401
|
+
path: urlPath,
|
|
402
|
+
query: Object.fromEntries(new URLSearchParams(parsed.query || '')),
|
|
403
|
+
headers: req.headers,
|
|
404
|
+
params: {},
|
|
405
|
+
body: null,
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const compileAndSend = () => {
|
|
409
|
+
let html;
|
|
410
|
+
try {
|
|
411
|
+
html = compileFile(reqContext);
|
|
412
|
+
} catch (e) {
|
|
413
|
+
process.stderr.write(c.red(`[nvml] compile error: ${e.message}`) + '\n');
|
|
414
|
+
const errHtml = errorPage(nvmlFile, e);
|
|
415
|
+
res.writeHead(500, {
|
|
416
|
+
'Content-Type': 'text/html; charset=UTF-8',
|
|
417
|
+
'Content-Length': Buffer.byteLength(errHtml),
|
|
397
418
|
});
|
|
398
|
-
|
|
399
|
-
|
|
419
|
+
res.end(errHtml);
|
|
420
|
+
return;
|
|
400
421
|
}
|
|
401
|
-
});
|
|
402
422
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
423
|
+
res.writeHead(200, {
|
|
424
|
+
'Content-Type': 'text/html; charset=UTF-8',
|
|
425
|
+
'Content-Length': Buffer.byteLength(html),
|
|
426
|
+
'Cache-Control': 'no-store',
|
|
427
|
+
'X-Powered-By': `nvml/${VERSION}`,
|
|
428
|
+
});
|
|
429
|
+
res.end(html);
|
|
430
|
+
process.stdout.write(c.dim(` ${req.method} ${urlPath} → 200`) + '\n');
|
|
431
|
+
};
|
|
407
432
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
433
|
+
// Collect body for POST/PUT/PATCH before compiling
|
|
434
|
+
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
|
435
|
+
let rawBody = '';
|
|
436
|
+
req.on('data', chunk => { rawBody += chunk; });
|
|
437
|
+
req.on('end', () => {
|
|
438
|
+
try { reqContext.body = JSON.parse(rawBody); } catch { reqContext.body = rawBody; }
|
|
439
|
+
compileAndSend();
|
|
440
|
+
});
|
|
441
|
+
} else {
|
|
442
|
+
compileAndSend();
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
server.on('error', e => {
|
|
447
|
+
if (e.code === 'EADDRINUSE') die(`Port ${port} is already in use.`);
|
|
448
|
+
die(`Server error: ${e.message}`);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
server.listen(port, '127.0.0.1', () => {
|
|
452
|
+
process.stdout.write(
|
|
453
|
+
c.green('\n nvml server running') + '\n' +
|
|
454
|
+
c.cyan(` http://localhost:${port}${route}`) + '\n\n' +
|
|
455
|
+
c.dim(` file : ${absFile}`) + '\n' +
|
|
456
|
+
c.dim(` route : ${route}`) + '\n' +
|
|
457
|
+
c.dim(` reload : automatic (re-reads file on every request)`) + '\n\n' +
|
|
458
|
+
` Press ${c.bold('Ctrl+C')} to stop.\n\n`
|
|
459
|
+
);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
process.on('SIGINT', () => { process.stdout.write('\n'); process.exit(0); });
|
|
463
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
418
464
|
|
|
419
|
-
process.on('SIGINT', () => { process.stdout.write('\n'); process.exit(0); });
|
|
420
|
-
process.on('SIGTERM', () => process.exit(0));
|
|
421
|
-
}
|
|
422
465
|
|
|
423
466
|
// ── helpers ───────────────────────────────────────────────────
|
|
424
467
|
function sendJSON(res, code, obj) {
|
|
425
468
|
const body = JSON.stringify(obj);
|
|
426
469
|
res.writeHead(code, {
|
|
427
|
-
'Content-Type':
|
|
428
|
-
'Content-Length':
|
|
470
|
+
'Content-Type': 'application/json; charset=UTF-8',
|
|
471
|
+
'Content-Length': Buffer.byteLength(body),
|
|
429
472
|
'Access-Control-Allow-Origin': '*',
|
|
430
473
|
});
|
|
431
474
|
res.end(body);
|
|
@@ -433,7 +476,7 @@ function sendJSON(res, code, obj) {
|
|
|
433
476
|
|
|
434
477
|
// ── compile ───────────────────────────────────────────────────
|
|
435
478
|
function cmdCompile(nvmlFile) {
|
|
436
|
-
const nvml
|
|
479
|
+
const nvml = loadKit();
|
|
437
480
|
const absFile = path.resolve(process.cwd(), nvmlFile);
|
|
438
481
|
if (!fs.existsSync(absFile)) die(`File not found: ${absFile}`);
|
|
439
482
|
const novaRun = resolveNovaRun();
|
|
@@ -449,7 +492,7 @@ function cmdCompile(nvmlFile) {
|
|
|
449
492
|
|
|
450
493
|
// ── ast ───────────────────────────────────────────────────────
|
|
451
494
|
function cmdAst(nvmlFile) {
|
|
452
|
-
const nvml
|
|
495
|
+
const nvml = loadKit();
|
|
453
496
|
const absFile = path.resolve(process.cwd(), nvmlFile);
|
|
454
497
|
if (!fs.existsSync(absFile)) die(`File not found: ${absFile}`);
|
|
455
498
|
try {
|
|
@@ -462,7 +505,7 @@ function cmdAst(nvmlFile) {
|
|
|
462
505
|
// ── tokens ────────────────────────────────────────────────────
|
|
463
506
|
function cmdTokens(nvmlFile) {
|
|
464
507
|
const { Lexer } = loadLexer();
|
|
465
|
-
const absFile
|
|
508
|
+
const absFile = path.resolve(process.cwd(), nvmlFile);
|
|
466
509
|
if (!fs.existsSync(absFile)) die(`File not found: ${absFile}`);
|
|
467
510
|
try {
|
|
468
511
|
process.stdout.write(JSON.stringify(new Lexer(fs.readFileSync(absFile, 'utf8')).tokenize(), null, 2) + '\n');
|
|
@@ -473,7 +516,7 @@ function cmdTokens(nvmlFile) {
|
|
|
473
516
|
|
|
474
517
|
// ── check ─────────────────────────────────────────────────────
|
|
475
518
|
function cmdCheck(nvmlFile) {
|
|
476
|
-
const nvml
|
|
519
|
+
const nvml = loadKit();
|
|
477
520
|
const absFile = path.resolve(process.cwd(), nvmlFile);
|
|
478
521
|
if (!fs.existsSync(absFile)) die(`File not found: ${absFile}`);
|
|
479
522
|
try {
|
|
@@ -527,13 +570,13 @@ if (args[0] === '--version' || args[0] === '-v') {
|
|
|
527
570
|
// subcommands
|
|
528
571
|
switch (args[0]) {
|
|
529
572
|
case 'compile': if (!args[1]) die('Usage: nvml compile <file.nvml>'); cmdCompile(args[1]); break;
|
|
530
|
-
case 'ast':
|
|
531
|
-
case 'tokens':
|
|
532
|
-
case 'check':
|
|
573
|
+
case 'ast': if (!args[1]) die('Usage: nvml ast <file.nvml>'); cmdAst(args[1]); break;
|
|
574
|
+
case 'tokens': if (!args[1]) die('Usage: nvml tokens <file.nvml>'); cmdTokens(args[1]); break;
|
|
575
|
+
case 'check': if (!args[1]) die('Usage: nvml check <file.nvml>'); cmdCheck(args[1]); break;
|
|
533
576
|
default: {
|
|
534
577
|
// nvml <port> <file> [route]
|
|
535
|
-
const port
|
|
536
|
-
const file
|
|
578
|
+
const port = parseInt(args[0], 10);
|
|
579
|
+
const file = args[1];
|
|
537
580
|
const route = args[2] || '/';
|
|
538
581
|
if (!port || port < 1 || port > 65535) die(`Invalid port: "${args[0]}"\nUsage: nvml <port> <file.nvml>`);
|
|
539
582
|
if (!file) die('Usage: nvml <port> <file.nvml> [route]');
|
package/bin/nvml.cmd
ADDED