novac 2.0.1 → 2.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 +1 -1
- package/README.md +1574 -597
- package/bin/novac +468 -171
- package/bin/nvc +522 -0
- package/bin/nvml +78 -17
- package/demo.nv +0 -0
- package/demo_builtins.nv +0 -0
- package/demo_http.nv +0 -0
- package/examples/bf.nv +69 -0
- package/examples/math.nv +21 -0
- package/kits/birdAPI/kitdef.js +954 -0
- package/kits/kitRNG/kitdef.js +740 -0
- package/kits/kitSSH/kitdef.js +1272 -0
- package/kits/kitadb/kitdef.js +606 -0
- package/kits/kitai/kitdef.js +2185 -0
- package/kits/kitansi/kitdef.js +1402 -0
- package/kits/kitcanvas/kitdef.js +914 -0
- package/kits/kitclippy/kitdef.js +925 -0
- package/kits/kitformat/kitdef.js +1485 -0
- package/kits/kitgps/kitdef.js +1862 -0
- package/kits/kitlibproc/kitdef.js +3 -2
- package/kits/kitmatrix/ex.js +19 -0
- package/kits/kitmatrix/kitdef.js +960 -0
- package/kits/kitmorse/kitdef.js +229 -0
- package/kits/kitmpatch/kitdef.js +906 -0
- package/kits/kitnet/kitdef.js +1401 -0
- package/kits/kitnovacweb/README.md +1416 -143
- package/kits/kitnovacweb/kitdef.js +92 -2
- package/kits/kitnovacweb/nvml/executor.js +578 -176
- package/kits/kitnovacweb/nvml/index.js +2 -2
- package/kits/kitnovacweb/nvml/lexer.js +72 -69
- package/kits/kitnovacweb/nvml/parser.js +328 -159
- package/kits/kitnovacweb/nvml/renderer.js +770 -270
- package/kits/kitparse/kitdef.js +1688 -0
- package/kits/kitproto/kitdef.js +613 -0
- package/kits/kitqr/kitdef.js +637 -0
- package/kits/kitregex++/kitdef.js +1353 -0
- package/kits/kitrequire/kitdef.js +1599 -0
- package/kits/kitx11/kitdef.js +1 -0
- package/kits/kitx11/kitx11.js +2472 -0
- package/kits/kitx11/kitx11_conn.js +948 -0
- package/kits/kitx11/kitx11_worker.js +121 -0
- package/kits/libtea/kitdef.js +2691 -0
- package/kits/libterm/ex.js +285 -0
- package/kits/libterm/kitdef.js +1927 -0
- package/novac/LICENSE +21 -0
- package/novac/README.md +1823 -0
- package/novac/bin/novac +950 -0
- package/novac/bin/nvc +522 -0
- package/novac/bin/nvml +542 -0
- package/novac/demo.nv +245 -0
- package/novac/demo_builtins.nv +209 -0
- package/novac/demo_http.nv +62 -0
- package/novac/examples/bf.nv +69 -0
- package/novac/examples/math.nv +21 -0
- package/novac/kits/kitai/kitdef.js +2185 -0
- package/novac/kits/kitansi/kitdef.js +1402 -0
- package/novac/kits/kitformat/kitdef.js +1485 -0
- package/novac/kits/kitgps/kitdef.js +1862 -0
- package/novac/kits/kitlibfs/kitdef.js +231 -0
- package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
- package/novac/kits/kitmatrix/ex.js +19 -0
- package/novac/kits/kitmatrix/kitdef.js +960 -0
- package/novac/kits/kitmpatch/kitdef.js +906 -0
- package/novac/kits/kitnovacweb/README.md +1572 -0
- package/novac/kits/kitnovacweb/demo.nv +12 -0
- package/novac/kits/kitnovacweb/demo.nvml +71 -0
- package/novac/kits/kitnovacweb/index.nova +12 -0
- package/novac/kits/kitnovacweb/kitdef.js +692 -0
- package/novac/kits/kitnovacweb/nova.kit.json +8 -0
- package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
- package/novac/kits/kitnovacweb/nvml/index.js +67 -0
- package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
- package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
- package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
- package/novac/kits/kitparse/kitdef.js +1688 -0
- package/novac/kits/kitregex++/kitdef.js +1353 -0
- package/novac/kits/kitrequire/kitdef.js +1599 -0
- package/novac/kits/kitx11/kitdef.js +1 -0
- package/novac/kits/kitx11/kitx11.js +2472 -0
- package/novac/kits/kitx11/kitx11_conn.js +948 -0
- package/novac/kits/kitx11/kitx11_worker.js +121 -0
- package/novac/kits/libtea/tf.js +2691 -0
- package/novac/kits/libterm/ex.js +285 -0
- package/novac/kits/libterm/kitdef.js +1927 -0
- package/novac/node_modules/chalk/license +9 -0
- package/novac/node_modules/chalk/package.json +83 -0
- package/novac/node_modules/chalk/readme.md +297 -0
- package/novac/node_modules/chalk/source/index.d.ts +325 -0
- package/novac/node_modules/chalk/source/index.js +225 -0
- package/novac/node_modules/chalk/source/utilities.js +33 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/novac/node_modules/commander/LICENSE +22 -0
- package/novac/node_modules/commander/Readme.md +1176 -0
- package/novac/node_modules/commander/esm.mjs +16 -0
- package/novac/node_modules/commander/index.js +24 -0
- package/novac/node_modules/commander/lib/argument.js +150 -0
- package/novac/node_modules/commander/lib/command.js +2777 -0
- package/novac/node_modules/commander/lib/error.js +39 -0
- package/novac/node_modules/commander/lib/help.js +747 -0
- package/novac/node_modules/commander/lib/option.js +380 -0
- package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
- package/novac/node_modules/commander/package-support.json +19 -0
- package/novac/node_modules/commander/package.json +82 -0
- package/novac/node_modules/commander/typings/esm.d.mts +3 -0
- package/novac/node_modules/commander/typings/index.d.ts +1113 -0
- package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
- package/novac/node_modules/node-addon-api/README.md +95 -0
- package/novac/node_modules/node-addon-api/common.gypi +21 -0
- package/novac/node_modules/node-addon-api/except.gypi +25 -0
- package/novac/node_modules/node-addon-api/index.js +14 -0
- package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
- package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
- package/novac/node_modules/node-addon-api/napi.h +3364 -0
- package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
- package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
- package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
- package/novac/node_modules/node-addon-api/package-support.json +21 -0
- package/novac/node_modules/node-addon-api/package.json +480 -0
- package/novac/node_modules/node-addon-api/tools/README.md +73 -0
- package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
- package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
- package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
- package/novac/node_modules/serialize-javascript/LICENSE +27 -0
- package/novac/node_modules/serialize-javascript/README.md +149 -0
- package/novac/node_modules/serialize-javascript/index.js +297 -0
- package/novac/node_modules/serialize-javascript/package.json +33 -0
- package/novac/package.json +27 -0
- package/novac/scripts/update-bin.js +24 -0
- package/novac/src/core/bstd.js +1035 -0
- package/novac/src/core/config.js +155 -0
- package/novac/src/core/describe.js +187 -0
- package/novac/src/core/emitter.js +499 -0
- package/novac/src/core/error.js +86 -0
- package/novac/src/core/executor.js +5606 -0
- package/novac/src/core/formatter.js +686 -0
- package/novac/src/core/lexer.js +1026 -0
- package/novac/src/core/nova_builtins.js +717 -0
- package/novac/src/core/nova_thread_worker.js +166 -0
- package/novac/src/core/parser.js +2181 -0
- package/novac/src/core/types.js +112 -0
- package/novac/src/index.js +28 -0
- package/novac/src/runtime/stdlib.js +244 -0
- package/package.json +6 -3
- package/scripts/update-bin.js +0 -0
- package/src/core/bstd.js +838 -362
- package/src/core/executor.js +2578 -170
- package/src/core/lexer.js +502 -54
- package/src/core/nova_builtins.js +21 -3
- package/src/core/parser.js +413 -72
- package/src/core/types.js +30 -2
- package/src/index.js +0 -0
- package/examples/example-project/README.md +0 -3
- package/examples/example-project/src/main.nova +0 -3
- package/src/core/environment.js +0 -0
- /package/{examples/example-project/bin/example-project.nv → novac/node_modules/node-addon-api/nothing.c} +0 -0
package/novac/bin/nvml
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* nvml — NVML CLI
|
|
6
|
+
* Part of the novac toolchain.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* nvml <port> <file.nvml> [route] Serve file.nvml on localhost:<port>
|
|
10
|
+
* nvml compile <file.nvml> Compile to HTML (stdout)
|
|
11
|
+
* nvml ast <file.nvml> Print AST as JSON
|
|
12
|
+
* nvml tokens <file.nvml> Print token stream as JSON
|
|
13
|
+
* nvml check <file.nvml> Check syntax
|
|
14
|
+
* nvml --help Show help
|
|
15
|
+
* nvml --version Show version
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const http = require('http');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const url = require('url');
|
|
23
|
+
|
|
24
|
+
const chalk = (() => {
|
|
25
|
+
try { return require('chalk').default ?? require('chalk'); } catch { return null; }
|
|
26
|
+
})();
|
|
27
|
+
|
|
28
|
+
const VERSION = '1.0.0';
|
|
29
|
+
|
|
30
|
+
// ── colour helpers (graceful fallback if chalk absent) ────────
|
|
31
|
+
const c = {
|
|
32
|
+
green: s => chalk ? chalk.green(s) : s,
|
|
33
|
+
red: s => chalk ? chalk.red(s) : s,
|
|
34
|
+
cyan: s => chalk ? chalk.cyan(s) : s,
|
|
35
|
+
bold: s => chalk ? chalk.bold(s) : s,
|
|
36
|
+
dim: s => chalk ? chalk.dim(s) : s,
|
|
37
|
+
yellow: s => chalk ? chalk.yellow(s) : s,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function die(msg) {
|
|
41
|
+
process.stderr.write(c.red('[nvml] ' + msg) + '\n');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── locate kitnovacweb/nvml/index.js ─────────────────────────
|
|
46
|
+
function resolveNvmlKit() {
|
|
47
|
+
const candidates = [
|
|
48
|
+
path.join(__dirname, '..', 'kits', 'kitnovacweb', 'nvml', 'index.js'),
|
|
49
|
+
path.join(process.cwd(), 'nova_modules', 'kitnovacweb', 'nvml', 'index.js'),
|
|
50
|
+
path.join(os.homedir(), '.novac', 'nova_modules', 'kitnovacweb', 'nvml', 'index.js'),
|
|
51
|
+
path.join(__dirname, '..', 'node_modules', 'kitnovacweb', 'nvml', 'index.js'),
|
|
52
|
+
];
|
|
53
|
+
for (const p of candidates) {
|
|
54
|
+
if (fs.existsSync(p)) return p;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function loadKit() {
|
|
60
|
+
const kitPath = resolveNvmlKit();
|
|
61
|
+
if (!kitPath) {
|
|
62
|
+
die(
|
|
63
|
+
'kitnovacweb not found.\n\n' +
|
|
64
|
+
' Install it with:\n' +
|
|
65
|
+
' novac etc kit kitnovacweb\n\n' +
|
|
66
|
+
' Or put it in nova_modules/kitnovacweb/'
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return require(kitPath);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function loadLexer() {
|
|
73
|
+
const kitPath = resolveNvmlKit();
|
|
74
|
+
if (!kitPath) die('kitnovacweb not found.');
|
|
75
|
+
return require(path.join(path.dirname(kitPath), 'lexer.js'));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Nova runtime loader ───────────────────────────────────────
|
|
79
|
+
// Finds src/index.js and returns the `run` function, or null if not found.
|
|
80
|
+
function resolveNovaRun() {
|
|
81
|
+
const searchPaths = [
|
|
82
|
+
path.join(__dirname, '..', 'src', 'index.js'),
|
|
83
|
+
path.join(process.cwd(), 'src', 'index.js'),
|
|
84
|
+
path.join(os.homedir(), '.novac', 'node_modules', 'novac', 'src', 'index.js'),
|
|
85
|
+
path.resolve(process.cwd(), 'node_modules', 'novac', 'src', 'index.js'),
|
|
86
|
+
];
|
|
87
|
+
for (const p of searchPaths) {
|
|
88
|
+
if (fs.existsSync(p)) {
|
|
89
|
+
try {
|
|
90
|
+
const m = require(p);
|
|
91
|
+
return m.run || m.default?.run || null;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
process.stderr.write(`[nvml] Warning: failed to load novac runtime from ${p}: ${e.message}\n`);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── document proxy for server-side Nova scripts ───────────────
|
|
102
|
+
function findElementById(elements, id) {
|
|
103
|
+
for (const el of elements) {
|
|
104
|
+
if (el.props && el.props.id === id) return el;
|
|
105
|
+
if (el.children) {
|
|
106
|
+
const found = findElementById(el.children, id);
|
|
107
|
+
if (found) return found;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function makeDocumentProxy(doc) {
|
|
114
|
+
return {
|
|
115
|
+
setConfig: (k, v) => { doc.config[k] = v; },
|
|
116
|
+
getConfig: (k) => doc.config[k],
|
|
117
|
+
setTitle: (t) => { doc.config.title = t; },
|
|
118
|
+
setMeta: (k, v) => { doc.config[k] = v; },
|
|
119
|
+
set: (id, content) => {
|
|
120
|
+
const el = findElementById(doc.visual, id);
|
|
121
|
+
if (el) el.textValue = String(content);
|
|
122
|
+
},
|
|
123
|
+
get: (id) => {
|
|
124
|
+
const el = findElementById(doc.visual, id);
|
|
125
|
+
return el ? (el.textValue || null) : null;
|
|
126
|
+
},
|
|
127
|
+
setProp: (id, key, value) => {
|
|
128
|
+
const el = findElementById(doc.visual, id);
|
|
129
|
+
if (el) el.props[String(key)] = String(value);
|
|
130
|
+
},
|
|
131
|
+
getProp: (id, key) => {
|
|
132
|
+
const el = findElementById(doc.visual, id);
|
|
133
|
+
return el ? (el.props[String(key)] || null) : null;
|
|
134
|
+
},
|
|
135
|
+
addClass: (id, cls) => {
|
|
136
|
+
const el = findElementById(doc.visual, id);
|
|
137
|
+
if (!el) return;
|
|
138
|
+
const existing = el.props.class || '';
|
|
139
|
+
el.props.class = (existing ? existing + ' ' : '') + cls;
|
|
140
|
+
},
|
|
141
|
+
setClass: (id, cls) => {
|
|
142
|
+
const el = findElementById(doc.visual, id);
|
|
143
|
+
if (el) el.props.class = String(cls);
|
|
144
|
+
},
|
|
145
|
+
addElementStyle: (id, css) => {
|
|
146
|
+
const el = findElementById(doc.visual, id);
|
|
147
|
+
if (el) el.ss = (el.ss ? el.ss + '\n' : '') + css;
|
|
148
|
+
},
|
|
149
|
+
addStyle: (css) => { doc.globalStyles += '\n' + css; },
|
|
150
|
+
hide: (id) => {
|
|
151
|
+
const el = findElementById(doc.visual, id);
|
|
152
|
+
if (el) el.ss = (el.ss ? el.ss + '\n' : '') + 'display: none;';
|
|
153
|
+
},
|
|
154
|
+
show: (id, display) => {
|
|
155
|
+
const el = findElementById(doc.visual, id);
|
|
156
|
+
if (el) el.ss = (el.ss ? el.ss + '\n' : '') + `display: ${display || 'block'};`;
|
|
157
|
+
},
|
|
158
|
+
alert: (_msg) => {},
|
|
159
|
+
config: doc.config,
|
|
160
|
+
_doc: doc,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── error page (served when compile fails during --serve) ─────
|
|
165
|
+
function errorPage(file, err) {
|
|
166
|
+
const esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
167
|
+
return `<!DOCTYPE html>
|
|
168
|
+
<html><head><meta charset="UTF-8"><title>NVML Error</title>
|
|
169
|
+
<style>
|
|
170
|
+
body{font-family:monospace;background:#1a1a1a;color:#eee;padding:2rem;margin:0}
|
|
171
|
+
h1{color:#f87171;font-size:1.1rem;margin:0 0 .75rem}
|
|
172
|
+
.file{color:#94a3b8;font-size:.8rem;margin-bottom:1.5rem}
|
|
173
|
+
pre{background:#111;border:1px solid #333;padding:1rem;border-radius:6px;
|
|
174
|
+
white-space:pre-wrap;word-break:break-word;font-size:.85rem;line-height:1.6}
|
|
175
|
+
.msg{color:#fca5a5}
|
|
176
|
+
.stack{color:#475569;font-size:.75rem;margin-top:.75rem}
|
|
177
|
+
</style></head><body>
|
|
178
|
+
<h1>NVML Error</h1>
|
|
179
|
+
<div class="file">${esc(file)}</div>
|
|
180
|
+
<pre class="msg">${esc(err.message)}</pre>
|
|
181
|
+
<pre class="stack">${esc(err.stack || '')}</pre>
|
|
182
|
+
</body></html>`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── serve ─────────────────────────────────────────────────────
|
|
186
|
+
function cmdServe(port, nvmlFile, route) {
|
|
187
|
+
const nvml = loadKit();
|
|
188
|
+
const absFile = path.resolve(process.cwd(), nvmlFile);
|
|
189
|
+
|
|
190
|
+
if (!fs.existsSync(absFile)) die(`File not found: ${absFile}`);
|
|
191
|
+
if (!route.startsWith('/')) route = '/' + route;
|
|
192
|
+
|
|
193
|
+
// Load the Nova runtime once — used for every server-side script block
|
|
194
|
+
const novaRun = resolveNovaRun();
|
|
195
|
+
if (!novaRun) {
|
|
196
|
+
process.stderr.write(
|
|
197
|
+
c.yellow('[nvml] Warning: novac runtime not found — server-side Nova scripts will not run.\n') +
|
|
198
|
+
c.dim(' Searched: src/index.js relative to bin/ and cwd\n\n')
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Build a novaRunner for a given NvmlDocument + request context
|
|
203
|
+
function makeNovaRunner(reqCtx) {
|
|
204
|
+
if (!novaRun) return null;
|
|
205
|
+
return (code, doc) => {
|
|
206
|
+
const scope = {
|
|
207
|
+
document: makeDocumentProxy(doc),
|
|
208
|
+
request: reqCtx || {},
|
|
209
|
+
};
|
|
210
|
+
novaRun(code, scope);
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Build a mutation-capturing proxy for /_nvml/run
|
|
215
|
+
const _sseClients = new Set(); // Server-Sent Events connections
|
|
216
|
+
|
|
217
|
+
function makeMutationProxy(mutations, live) {
|
|
218
|
+
return {
|
|
219
|
+
// config
|
|
220
|
+
setConfig: (k, v) => mutations.push({ type: 'setConfig', key: String(k), value: v }),
|
|
221
|
+
setTitle: (t) => mutations.push({ type: 'setConfig', key: 'title', value: t }),
|
|
222
|
+
setMeta: (k, v) => mutations.push({ type: 'setConfig', key: String(k), value: v }),
|
|
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
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const server = http.createServer((req, res) => {
|
|
284
|
+
const parsed = url.parse(req.url || '/');
|
|
285
|
+
const urlPath = parsed.pathname || '/';
|
|
286
|
+
|
|
287
|
+
// CORS
|
|
288
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
289
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
290
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
291
|
+
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
292
|
+
|
|
293
|
+
// ── /_nvml/sse — Server-Sent Events for server-push signal updates ──
|
|
294
|
+
if (urlPath === '/_nvml/sse' && req.method === 'GET') {
|
|
295
|
+
res.writeHead(200, {
|
|
296
|
+
'Content-Type': 'text/event-stream',
|
|
297
|
+
'Cache-Control': 'no-cache',
|
|
298
|
+
'Connection': 'keep-alive',
|
|
299
|
+
'X-Accel-Buffering': 'no',
|
|
300
|
+
});
|
|
301
|
+
res.write('retry: 3000\n\n'); // tell client to reconnect after 3s if disconnected
|
|
302
|
+
_sseClients.add(res);
|
|
303
|
+
req.on('close', () => _sseClients.delete(res));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── /_nvml/run — triggered server-side Nova scripts ──────
|
|
308
|
+
if (urlPath === '/_nvml/run' && req.method === 'POST') {
|
|
309
|
+
let rawBody = '';
|
|
310
|
+
req.on('data', chunk => { rawBody += chunk; });
|
|
311
|
+
req.on('end', () => {
|
|
312
|
+
let code, live;
|
|
313
|
+
try {
|
|
314
|
+
const body = JSON.parse(rawBody);
|
|
315
|
+
code = String(body.code || '');
|
|
316
|
+
live = body.live || {};
|
|
317
|
+
} catch (e) {
|
|
318
|
+
return sendJSON(res, 400, { error: 'Invalid JSON body' });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!code.trim()) return sendJSON(res, 200, { mutations: [] });
|
|
322
|
+
|
|
323
|
+
if (!novaRun) {
|
|
324
|
+
return sendJSON(res, 503, { error: 'Nova runtime not available' });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const mutations = [];
|
|
328
|
+
const docProxy = makeMutationProxy(mutations, live);
|
|
329
|
+
try {
|
|
330
|
+
novaRun(code, { document: docProxy, request: live });
|
|
331
|
+
return sendJSON(res, 200, { mutations });
|
|
332
|
+
} catch (e) {
|
|
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
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── main route ───────────────────────────────────────────
|
|
341
|
+
if (urlPath !== route) {
|
|
342
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
343
|
+
res.end(`404 — this server only handles: ${route}`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let source;
|
|
348
|
+
try { source = fs.readFileSync(absFile, 'utf8'); }
|
|
349
|
+
catch (e) { res.writeHead(500); res.end(e.message); return; }
|
|
350
|
+
|
|
351
|
+
// Build request context
|
|
352
|
+
const reqContext = {
|
|
353
|
+
method: req.method,
|
|
354
|
+
url: req.url,
|
|
355
|
+
path: urlPath,
|
|
356
|
+
query: Object.fromEntries(new URLSearchParams(parsed.query || '')),
|
|
357
|
+
headers: req.headers,
|
|
358
|
+
params: {},
|
|
359
|
+
body: null,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const compileAndSend = () => {
|
|
363
|
+
let html;
|
|
364
|
+
try {
|
|
365
|
+
html = nvml.compile(source, {
|
|
366
|
+
novaRunner: makeNovaRunner(reqContext),
|
|
367
|
+
novaEmitter: null,
|
|
368
|
+
});
|
|
369
|
+
} catch (e) {
|
|
370
|
+
process.stderr.write(c.red(`[nvml] compile error: ${e.message}`) + '\n');
|
|
371
|
+
const errHtml = errorPage(nvmlFile, e);
|
|
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;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
res.writeHead(200, {
|
|
381
|
+
'Content-Type': 'text/html; charset=UTF-8',
|
|
382
|
+
'Content-Length': Buffer.byteLength(html),
|
|
383
|
+
'Cache-Control': 'no-store',
|
|
384
|
+
'X-Powered-By': `nvml/${VERSION}`,
|
|
385
|
+
});
|
|
386
|
+
res.end(html);
|
|
387
|
+
process.stdout.write(c.dim(` ${req.method} ${urlPath} → 200`) + '\n');
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// Collect body for POST/PUT/PATCH before compiling
|
|
391
|
+
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
|
392
|
+
let rawBody = '';
|
|
393
|
+
req.on('data', chunk => { rawBody += chunk; });
|
|
394
|
+
req.on('end', () => {
|
|
395
|
+
try { reqContext.body = JSON.parse(rawBody); } catch { reqContext.body = rawBody; }
|
|
396
|
+
compileAndSend();
|
|
397
|
+
});
|
|
398
|
+
} else {
|
|
399
|
+
compileAndSend();
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
server.on('error', e => {
|
|
404
|
+
if (e.code === 'EADDRINUSE') die(`Port ${port} is already in use.`);
|
|
405
|
+
die(`Server error: ${e.message}`);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
server.listen(port, '127.0.0.1', () => {
|
|
409
|
+
process.stdout.write(
|
|
410
|
+
c.green('\n nvml server running') + '\n' +
|
|
411
|
+
c.cyan(` http://localhost:${port}${route}`) + '\n\n' +
|
|
412
|
+
c.dim(` file : ${absFile}`) + '\n' +
|
|
413
|
+
c.dim(` route : ${route}`) + '\n' +
|
|
414
|
+
c.dim(` reload : automatic (re-reads file on every request)`) + '\n\n' +
|
|
415
|
+
` Press ${c.bold('Ctrl+C')} to stop.\n\n`
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
process.on('SIGINT', () => { process.stdout.write('\n'); process.exit(0); });
|
|
420
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── helpers ───────────────────────────────────────────────────
|
|
424
|
+
function sendJSON(res, code, obj) {
|
|
425
|
+
const body = JSON.stringify(obj);
|
|
426
|
+
res.writeHead(code, {
|
|
427
|
+
'Content-Type': 'application/json; charset=UTF-8',
|
|
428
|
+
'Content-Length': Buffer.byteLength(body),
|
|
429
|
+
'Access-Control-Allow-Origin': '*',
|
|
430
|
+
});
|
|
431
|
+
res.end(body);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── compile ───────────────────────────────────────────────────
|
|
435
|
+
function cmdCompile(nvmlFile) {
|
|
436
|
+
const nvml = loadKit();
|
|
437
|
+
const absFile = path.resolve(process.cwd(), nvmlFile);
|
|
438
|
+
if (!fs.existsSync(absFile)) die(`File not found: ${absFile}`);
|
|
439
|
+
const novaRun = resolveNovaRun();
|
|
440
|
+
const novaRunner = novaRun
|
|
441
|
+
? (code, doc) => novaRun(code, { document: makeDocumentProxy(doc), request: {} })
|
|
442
|
+
: null;
|
|
443
|
+
try {
|
|
444
|
+
process.stdout.write(nvml.compile(fs.readFileSync(absFile, 'utf8'), { novaRunner, novaEmitter: null }));
|
|
445
|
+
} catch (e) {
|
|
446
|
+
die(`Compile error: ${e.message}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ── ast ───────────────────────────────────────────────────────
|
|
451
|
+
function cmdAst(nvmlFile) {
|
|
452
|
+
const nvml = loadKit();
|
|
453
|
+
const absFile = path.resolve(process.cwd(), nvmlFile);
|
|
454
|
+
if (!fs.existsSync(absFile)) die(`File not found: ${absFile}`);
|
|
455
|
+
try {
|
|
456
|
+
process.stdout.write(JSON.stringify(nvml.parse(fs.readFileSync(absFile, 'utf8')), null, 2) + '\n');
|
|
457
|
+
} catch (e) {
|
|
458
|
+
die(`Parse error: ${e.message}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ── tokens ────────────────────────────────────────────────────
|
|
463
|
+
function cmdTokens(nvmlFile) {
|
|
464
|
+
const { Lexer } = loadLexer();
|
|
465
|
+
const absFile = path.resolve(process.cwd(), nvmlFile);
|
|
466
|
+
if (!fs.existsSync(absFile)) die(`File not found: ${absFile}`);
|
|
467
|
+
try {
|
|
468
|
+
process.stdout.write(JSON.stringify(new Lexer(fs.readFileSync(absFile, 'utf8')).tokenize(), null, 2) + '\n');
|
|
469
|
+
} catch (e) {
|
|
470
|
+
die(`Lex error: ${e.message}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ── check ─────────────────────────────────────────────────────
|
|
475
|
+
function cmdCheck(nvmlFile) {
|
|
476
|
+
const nvml = loadKit();
|
|
477
|
+
const absFile = path.resolve(process.cwd(), nvmlFile);
|
|
478
|
+
if (!fs.existsSync(absFile)) die(`File not found: ${absFile}`);
|
|
479
|
+
try {
|
|
480
|
+
nvml.parse(fs.readFileSync(absFile, 'utf8'));
|
|
481
|
+
process.stdout.write(c.green(` Syntax OK`) + c.dim(` — ${nvmlFile}`) + '\n');
|
|
482
|
+
} catch (e) {
|
|
483
|
+
die(`Syntax error: ${e.message}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ── help ──────────────────────────────────────────────────────
|
|
488
|
+
function printHelp() {
|
|
489
|
+
process.stdout.write(`
|
|
490
|
+
${c.bold('nvml')} v${VERSION} — NVML CLI ${c.dim('(part of novac)')}
|
|
491
|
+
|
|
492
|
+
${c.bold('Usage:')}
|
|
493
|
+
nvml ${c.cyan('<port>')} ${c.cyan('<file.nvml>')} Serve file on localhost:<port> (route: /)
|
|
494
|
+
nvml ${c.cyan('<port>')} ${c.cyan('<file.nvml>')} ${c.cyan('<route>')} Serve on a specific route
|
|
495
|
+
nvml compile ${c.cyan('<file.nvml>')} Compile to HTML and print to stdout
|
|
496
|
+
nvml ast ${c.cyan('<file.nvml>')} Print parsed AST as JSON
|
|
497
|
+
nvml tokens ${c.cyan('<file.nvml>')} Print token stream as JSON
|
|
498
|
+
nvml check ${c.cyan('<file.nvml>')} Validate NVML syntax
|
|
499
|
+
nvml --help Show this help
|
|
500
|
+
nvml --version Show version
|
|
501
|
+
|
|
502
|
+
${c.bold('Examples:')}
|
|
503
|
+
nvml 3000 index.nvml
|
|
504
|
+
nvml 8080 about.nvml /about
|
|
505
|
+
nvml compile index.nvml > index.html
|
|
506
|
+
nvml check index.nvml
|
|
507
|
+
nvml ast index.nvml | head -30
|
|
508
|
+
|
|
509
|
+
${c.bold('Notes:')}
|
|
510
|
+
The server re-reads the NVML file on every request — no restart needed.
|
|
511
|
+
CORS headers are set so nvml-browser can connect without issues.
|
|
512
|
+
|
|
513
|
+
`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ── entry point ───────────────────────────────────────────────
|
|
517
|
+
const args = process.argv.slice(2);
|
|
518
|
+
|
|
519
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
520
|
+
printHelp(); process.exit(0);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (args[0] === '--version' || args[0] === '-v') {
|
|
524
|
+
process.stdout.write(`nvml v${VERSION}\n`); process.exit(0);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// subcommands
|
|
528
|
+
switch (args[0]) {
|
|
529
|
+
case 'compile': if (!args[1]) die('Usage: nvml compile <file.nvml>'); cmdCompile(args[1]); break;
|
|
530
|
+
case 'ast': if (!args[1]) die('Usage: nvml ast <file.nvml>'); cmdAst(args[1]); break;
|
|
531
|
+
case 'tokens': if (!args[1]) die('Usage: nvml tokens <file.nvml>'); cmdTokens(args[1]); break;
|
|
532
|
+
case 'check': if (!args[1]) die('Usage: nvml check <file.nvml>'); cmdCheck(args[1]); break;
|
|
533
|
+
default: {
|
|
534
|
+
// nvml <port> <file> [route]
|
|
535
|
+
const port = parseInt(args[0], 10);
|
|
536
|
+
const file = args[1];
|
|
537
|
+
const route = args[2] || '/';
|
|
538
|
+
if (!port || port < 1 || port > 65535) die(`Invalid port: "${args[0]}"\nUsage: nvml <port> <file.nvml>`);
|
|
539
|
+
if (!file) die('Usage: nvml <port> <file.nvml> [route]');
|
|
540
|
+
cmdServe(port, file, route);
|
|
541
|
+
}
|
|
542
|
+
}
|