novac 2.1.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 +0 -0
- 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/kitcanvas/kitdef.js +914 -0
- package/kits/kitclippy/kitdef.js +925 -0
- package/kits/kitgps/kitdef.js +1862 -0
- package/kits/kitlibproc/kitdef.js +3 -2
- package/kits/kitmorse/kitdef.js +229 -0
- package/kits/kitmpatch/kitdef.js +906 -0
- package/kits/kitnet/kitdef.js +1401 -0
- package/kits/kitproto/kitdef.js +613 -0
- package/kits/kitqr/kitdef.js +637 -0
- package/kits/kitrequire/kitdef.js +1599 -0
- package/kits/libtea/kitdef.js +2691 -0
- package/kits/libterm/kitdef.js +2 -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/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 +3 -2
- package/scripts/update-bin.js +0 -0
- package/src/core/bstd.js +835 -361
- package/src/core/executor.js +427 -246
- package/src/core/lexer.js +19 -2
- package/src/core/parser.js +13 -0
- 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/{kits → novac/kits}/libtea/tf.js +0 -0
- /package/{examples/example-project/bin/example-project.nv → novac/node_modules/node-addon-api/nothing.c} +0 -0
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* kitnovacweb — Nova web kit
|
|
5
|
+
*
|
|
6
|
+
* Provides:
|
|
7
|
+
* document.setIndex(nvmlFile) — set the default / route
|
|
8
|
+
* document.setPage(route, nvmlFile) — map a route to an nvml file
|
|
9
|
+
* document.setNotFound(nvmlFile) — custom 404 page
|
|
10
|
+
* document.serve(port) — start the server (default 3000)
|
|
11
|
+
* document.stop() — stop the server
|
|
12
|
+
* document.middleware(fn) — add middleware fn(req, res, next)
|
|
13
|
+
* document.static(urlPath, dirPath) — serve static files
|
|
14
|
+
* document.setHeader(key, value) — set a default response header
|
|
15
|
+
*
|
|
16
|
+
* Usage in Nova:
|
|
17
|
+
* import "kitnovacweb"
|
|
18
|
+
* document.setIndex("index.nvml")
|
|
19
|
+
* document.serve(8080)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const http = require('http');
|
|
25
|
+
const url = require('url');
|
|
26
|
+
const nvml = require('./nvml/index');
|
|
27
|
+
|
|
28
|
+
// ── Nova runner (requires executor.js from core novac) ───────
|
|
29
|
+
// We try to load the novac executor from standard install locations.
|
|
30
|
+
function makeNovaRunner() {
|
|
31
|
+
const searchPaths = [
|
|
32
|
+
// kits/kitnovacweb/ → novac_new/src/ (standard shipped layout)
|
|
33
|
+
path.join(__dirname, '..', '..', 'src', 'index.js'),
|
|
34
|
+
// nova_modules/kitnovacweb/ inside a user project → project root / src/
|
|
35
|
+
path.join(process.cwd(), 'src', 'index.js'),
|
|
36
|
+
// standard global install
|
|
37
|
+
path.join(require('os').homedir(), '.novac', 'node_modules', 'novac', 'src', 'index.js'),
|
|
38
|
+
// local node_modules
|
|
39
|
+
path.resolve(process.cwd(), 'node_modules', 'novac', 'src', 'index.js'),
|
|
40
|
+
// sibling directory (developer layout)
|
|
41
|
+
path.join(__dirname, '..', 'src', 'index.js'),
|
|
42
|
+
// bin sibling (when kit is symlinked / standalone)
|
|
43
|
+
path.join(__dirname, '..', '..', 'bin', '..', 'src', 'index.js'),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
let novacIndex = null;
|
|
47
|
+
for (const p of searchPaths) {
|
|
48
|
+
if (fs.existsSync(p)) { novacIndex = p; break; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!novacIndex) {
|
|
52
|
+
process.stderr.write(
|
|
53
|
+
'[kitnovacweb] Warning: novac runtime not found — server-side Nova scripts will not run.\n' +
|
|
54
|
+
' Searched:\n' + searchPaths.map(p => ' ' + p).join('\n') + '\n'
|
|
55
|
+
);
|
|
56
|
+
// Return stubs so the rest of the code doesn't need null-checks
|
|
57
|
+
return {
|
|
58
|
+
runner: (code, doc, reqCtx) => {},
|
|
59
|
+
emitter: null,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let novaRun = null;
|
|
64
|
+
try {
|
|
65
|
+
const novacModule = require(novacIndex);
|
|
66
|
+
// src/index.js exports { run, tokenize, parse, format, Executor }
|
|
67
|
+
novaRun = novacModule.run || novacModule.default?.run || null;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
process.stderr.write(`[kitnovacweb] Warning: failed to load novac runtime: ${e.message}\n`);
|
|
70
|
+
return {
|
|
71
|
+
runner: (code, doc, reqCtx) => {},
|
|
72
|
+
emitter: null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!novaRun) {
|
|
77
|
+
process.stderr.write('[kitnovacweb] Warning: novac runtime loaded but `run` not exported.\n');
|
|
78
|
+
return {
|
|
79
|
+
runner: (code, doc, reqCtx) => {},
|
|
80
|
+
emitter: null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* runner(code, doc, reqContext)
|
|
86
|
+
* - code : Nova source string
|
|
87
|
+
* - doc : live NvmlDocument (mutated in place)
|
|
88
|
+
* - reqContext : { method, url, path, query, headers, params, body }
|
|
89
|
+
*
|
|
90
|
+
* Inside the Nova script the following are available:
|
|
91
|
+
* document — proxy with setConfig / setTitle / set / get / addStyle …
|
|
92
|
+
* request — the HTTP request context object
|
|
93
|
+
*/
|
|
94
|
+
const runner = (code, doc, reqCtx) => {
|
|
95
|
+
const { makeBfObject } = require('./nvml/executor');
|
|
96
|
+
const scope = {
|
|
97
|
+
document: makeDocumentProxy(doc),
|
|
98
|
+
request: reqCtx || {},
|
|
99
|
+
bf: makeBfObject(),
|
|
100
|
+
};
|
|
101
|
+
novaRun(code, scope);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* exec(code, scope) — run Nova with a fully custom scope object.
|
|
106
|
+
* Used by /_nvml/run to pass a mutation-capturing proxy directly.
|
|
107
|
+
*/
|
|
108
|
+
const exec = (code, scope) => {
|
|
109
|
+
const { makeBfObject } = require('./nvml/executor');
|
|
110
|
+
if (!scope.bf) scope.bf = makeBfObject();
|
|
111
|
+
novaRun(code, scope);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return { runner, exec, emitter: null }; // emitter intentionally null: server-side only
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── document proxy (for use inside Nova server scripts) ──────
|
|
118
|
+
// All mutations happen directly on the NvmlDocument so they are
|
|
119
|
+
// reflected in the rendered HTML output.
|
|
120
|
+
|
|
121
|
+
function makeDocumentProxy(doc) {
|
|
122
|
+
return {
|
|
123
|
+
// ── config / meta ──────────────────────────────────────
|
|
124
|
+
setConfig: (k, v) => { doc.config[k] = v; },
|
|
125
|
+
getConfig: (k) => doc.config[k],
|
|
126
|
+
setTitle: (t) => { doc.config.title = t; },
|
|
127
|
+
setMeta: (k, v) => { doc.config[k] = v; },
|
|
128
|
+
|
|
129
|
+
// ── element text content ───────────────────────────────
|
|
130
|
+
/** Set the text content of the element with the given id */
|
|
131
|
+
set: (id, content) => {
|
|
132
|
+
const el = findElementById(doc.visual, id);
|
|
133
|
+
if (el) el.textValue = String(content);
|
|
134
|
+
},
|
|
135
|
+
/** Get the current text content of an element */
|
|
136
|
+
get: (id) => {
|
|
137
|
+
const el = findElementById(doc.visual, id);
|
|
138
|
+
return el ? (el.textValue || null) : null;
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// ── element attributes / props ─────────────────────────
|
|
142
|
+
/** Set a prop/attribute on the element with the given id */
|
|
143
|
+
setProp: (id, key, value) => {
|
|
144
|
+
const el = findElementById(doc.visual, id);
|
|
145
|
+
if (el) el.props[String(key)] = String(value);
|
|
146
|
+
},
|
|
147
|
+
/** Get a prop/attribute from the element with the given id */
|
|
148
|
+
getProp: (id, key) => {
|
|
149
|
+
const el = findElementById(doc.visual, id);
|
|
150
|
+
return el ? (el.props[String(key)] || null) : null;
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// ── element class manipulation ─────────────────────────
|
|
154
|
+
/** Append a CSS class to the element's class list */
|
|
155
|
+
addClass: (id, cls) => {
|
|
156
|
+
const el = findElementById(doc.visual, id);
|
|
157
|
+
if (!el) return;
|
|
158
|
+
const existing = el.props.class || '';
|
|
159
|
+
el.props.class = (existing ? existing + ' ' : '') + cls;
|
|
160
|
+
},
|
|
161
|
+
/** Replace the entire class attribute of an element */
|
|
162
|
+
setClass: (id, cls) => {
|
|
163
|
+
const el = findElementById(doc.visual, id);
|
|
164
|
+
if (el) el.props.class = String(cls);
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
// ── inline styles ─────────────────────────────────────
|
|
168
|
+
/** Append CSS declarations to the element's inline ::ss block */
|
|
169
|
+
addElementStyle: (id, css) => {
|
|
170
|
+
const el = findElementById(doc.visual, id);
|
|
171
|
+
if (el) el.ss = (el.ss ? el.ss + '\n' : '') + css;
|
|
172
|
+
},
|
|
173
|
+
/** Append CSS to the document's global <style> block */
|
|
174
|
+
addStyle: (css) => { doc.globalStyles += '\n' + css; },
|
|
175
|
+
|
|
176
|
+
// ── visibility helpers ────────────────────────────────
|
|
177
|
+
/** Hide an element (sets display:none via inline style) */
|
|
178
|
+
hide: (id) => {
|
|
179
|
+
const el = findElementById(doc.visual, id);
|
|
180
|
+
if (el) el.ss = (el.ss ? el.ss + '\n' : '') + 'display: none;';
|
|
181
|
+
},
|
|
182
|
+
/** Show an element (sets display:block via inline style) */
|
|
183
|
+
show: (id, display) => {
|
|
184
|
+
const el = findElementById(doc.visual, id);
|
|
185
|
+
if (el) el.ss = (el.ss ? el.ss + '\n' : '') + `display: ${display || 'block'};`;
|
|
186
|
+
},
|
|
187
|
+
// alert: no-op when running on the server directly against a real doc
|
|
188
|
+
// (only meaningful via the mutation proxy in /_nvml/run)
|
|
189
|
+
alert: (_msg) => {},
|
|
190
|
+
|
|
191
|
+
// ── raw document reference (for advanced use) ─────────
|
|
192
|
+
config: doc.config,
|
|
193
|
+
_doc: doc,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function findElementById(elements, id) {
|
|
198
|
+
for (const el of elements) {
|
|
199
|
+
if (el.props && el.props.id === id) return el;
|
|
200
|
+
if (el.children) {
|
|
201
|
+
const found = findElementById(el.children, id);
|
|
202
|
+
if (found) return found;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── MIME types for static file serving ──────────────────────
|
|
209
|
+
|
|
210
|
+
const MIME = {
|
|
211
|
+
'.html': 'text/html',
|
|
212
|
+
'.htm': 'text/html',
|
|
213
|
+
'.css': 'text/css',
|
|
214
|
+
'.js': 'text/javascript',
|
|
215
|
+
'.mjs': 'text/javascript',
|
|
216
|
+
'.json': 'application/json',
|
|
217
|
+
'.png': 'image/png',
|
|
218
|
+
'.jpg': 'image/jpeg',
|
|
219
|
+
'.jpeg': 'image/jpeg',
|
|
220
|
+
'.gif': 'image/gif',
|
|
221
|
+
'.svg': 'image/svg+xml',
|
|
222
|
+
'.ico': 'image/x-icon',
|
|
223
|
+
'.woff': 'font/woff',
|
|
224
|
+
'.woff2':'font/woff2',
|
|
225
|
+
'.ttf': 'font/ttf',
|
|
226
|
+
'.otf': 'font/otf',
|
|
227
|
+
'.mp4': 'video/mp4',
|
|
228
|
+
'.webm': 'video/webm',
|
|
229
|
+
'.mp3': 'audio/mpeg',
|
|
230
|
+
'.wav': 'audio/wav',
|
|
231
|
+
'.txt': 'text/plain',
|
|
232
|
+
'.xml': 'application/xml',
|
|
233
|
+
'.pdf': 'application/pdf',
|
|
234
|
+
'.zip': 'application/zip',
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
function mimeFor(filePath) {
|
|
238
|
+
return MIME[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── KitDocument — the main object exposed as `document` ─────
|
|
242
|
+
|
|
243
|
+
class KitDocument {
|
|
244
|
+
constructor() {
|
|
245
|
+
this._routes = new Map(); // urlPath → nvmlFilePath
|
|
246
|
+
this._notFound = null; // nvml file for 404
|
|
247
|
+
this._port = 3000;
|
|
248
|
+
this._server = null;
|
|
249
|
+
this._middlewares = [];
|
|
250
|
+
this._staticMounts= []; // [{ urlPath, dirPath }]
|
|
251
|
+
this._headers = {
|
|
252
|
+
'X-Powered-By': 'NovacWeb/1.0',
|
|
253
|
+
};
|
|
254
|
+
this._nova = null; // { runner, emitter } lazily loaded
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── public API ──────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
setIndex(nvmlFile) {
|
|
260
|
+
this.setPage('/', nvmlFile);
|
|
261
|
+
return this;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
setPage(route, nvmlFile) {
|
|
265
|
+
if (!route.startsWith('/')) route = '/' + route;
|
|
266
|
+
this._routes.set(route, path.resolve(process.cwd(), nvmlFile));
|
|
267
|
+
return this;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
setNotFound(nvmlFile) {
|
|
271
|
+
this._notFound = path.resolve(process.cwd(), nvmlFile);
|
|
272
|
+
return this;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
serve(port) {
|
|
276
|
+
if (port !== undefined) this._port = Number(port);
|
|
277
|
+
this._ensureNova();
|
|
278
|
+
this._startServer();
|
|
279
|
+
return this;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
stop() {
|
|
283
|
+
if (this._server) {
|
|
284
|
+
this._server.close();
|
|
285
|
+
this._server = null;
|
|
286
|
+
}
|
|
287
|
+
return this;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
middleware(fn) {
|
|
291
|
+
this._middlewares.push(fn);
|
|
292
|
+
return this;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
static(urlPath, dirPath) {
|
|
296
|
+
if (!urlPath.startsWith('/')) urlPath = '/' + urlPath;
|
|
297
|
+
const absDir = path.resolve(process.cwd(), dirPath || urlPath.slice(1));
|
|
298
|
+
this._staticMounts.push({ urlPath, dirPath: absDir });
|
|
299
|
+
return this;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
setHeader(key, value) {
|
|
303
|
+
this._headers[key] = value;
|
|
304
|
+
return this;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── internals ───────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
_ensureNova() {
|
|
310
|
+
if (!this._nova) {
|
|
311
|
+
this._nova = makeNovaRunner(); // always returns { runner, emitter }
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
_startServer() {
|
|
316
|
+
if (this._server) {
|
|
317
|
+
process.stdout.write(`[kitnovacweb] Server already running on port ${this._port}\n`);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this._server = http.createServer((req, res) => {
|
|
322
|
+
this._handleRequest(req, res);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
this._server.listen(this._port, () => {
|
|
326
|
+
process.stdout.write(`[kitnovacweb] Server running at http://localhost:${this._port}\n`);
|
|
327
|
+
process.stdout.write(`[kitnovacweb] Routes:\n`);
|
|
328
|
+
for (const [route, file] of this._routes) {
|
|
329
|
+
process.stdout.write(` ${route} → ${file}\n`);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
this._server.on('error', (err) => {
|
|
334
|
+
process.stderr.write(`[kitnovacweb] Server error: ${err.message}\n`);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
_handleRequest(req, res) {
|
|
339
|
+
// run middleware chain
|
|
340
|
+
let idx = 0;
|
|
341
|
+
const next = () => {
|
|
342
|
+
if (idx < this._middlewares.length) {
|
|
343
|
+
const mw = this._middlewares[idx++];
|
|
344
|
+
try { mw(req, res, next); } catch (e) {
|
|
345
|
+
this._sendError(res, 500, e.message);
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
this._dispatch(req, res);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
next();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
_dispatch(req, res) {
|
|
355
|
+
const parsed = url.parse(req.url || '/');
|
|
356
|
+
const urlPath = parsed.pathname || '/';
|
|
357
|
+
|
|
358
|
+
// ── /_nvml/run — Nova server script execution endpoint ──
|
|
359
|
+
if (urlPath === '/_nvml/run' && req.method === 'POST') {
|
|
360
|
+
return this._handleNvRun(req, res);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ── /_nvml/run-node — Node.js server script execution endpoint ──
|
|
364
|
+
if (urlPath === '/_nvml/run-node' && req.method === 'POST') {
|
|
365
|
+
return this._handleNodeRun(req, res);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ── static file serving ────────────────────────────────
|
|
369
|
+
for (const mount of this._staticMounts) {
|
|
370
|
+
if (urlPath.startsWith(mount.urlPath)) {
|
|
371
|
+
const rel = urlPath.slice(mount.urlPath.length) || '/';
|
|
372
|
+
const filePath = path.join(mount.dirPath, rel);
|
|
373
|
+
if (this._serveStaticFile(res, filePath)) return;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── route matching ─────────────────────────────────────
|
|
378
|
+
// Exact match first
|
|
379
|
+
if (this._routes.has(urlPath)) {
|
|
380
|
+
return this._serveNvml(req, res, this._routes.get(urlPath), parsed);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Pattern match: routes with :param segments
|
|
384
|
+
for (const [pattern, file] of this._routes) {
|
|
385
|
+
const params = this._matchRoute(pattern, urlPath);
|
|
386
|
+
if (params !== null) {
|
|
387
|
+
req._params = params;
|
|
388
|
+
return this._serveNvml(req, res, file, parsed);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ── 404 ────────────────────────────────────────────────
|
|
393
|
+
if (this._notFound && fs.existsSync(this._notFound)) {
|
|
394
|
+
return this._serveNvml(req, res, this._notFound, parsed, 404);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
this._sendError(res, 404, `Not Found: ${urlPath}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
_matchRoute(pattern, urlPath) {
|
|
401
|
+
const pSegs = pattern.split('/');
|
|
402
|
+
const uSegs = urlPath.split('/');
|
|
403
|
+
if (pSegs.length !== uSegs.length) return null;
|
|
404
|
+
const params = {};
|
|
405
|
+
for (let i = 0; i < pSegs.length; i++) {
|
|
406
|
+
if (pSegs[i].startsWith(':')) {
|
|
407
|
+
params[pSegs[i].slice(1)] = uSegs[i];
|
|
408
|
+
} else if (pSegs[i] !== uSegs[i]) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return params;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
_serveNvml(req, res, nvmlFilePath, parsedUrl, statusCode = 200) {
|
|
416
|
+
if (!fs.existsSync(nvmlFilePath)) {
|
|
417
|
+
return this._sendError(res, 500, `NVML file not found: ${nvmlFilePath}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
let source;
|
|
421
|
+
try {
|
|
422
|
+
source = fs.readFileSync(nvmlFilePath, 'utf8');
|
|
423
|
+
} catch (e) {
|
|
424
|
+
return this._sendError(res, 500, `Cannot read NVML file: ${e.message}`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Build a request context object accessible in server-side Nova scripts
|
|
428
|
+
const reqContext = {
|
|
429
|
+
method: req.method,
|
|
430
|
+
url: req.url,
|
|
431
|
+
path: parsedUrl.pathname,
|
|
432
|
+
query: Object.fromEntries(new URLSearchParams(parsedUrl.query || '')),
|
|
433
|
+
headers: req.headers,
|
|
434
|
+
params: req._params || {},
|
|
435
|
+
body: req._body || null,
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// If it's a POST/PUT/PATCH, collect body first
|
|
439
|
+
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
|
440
|
+
let rawBody = '';
|
|
441
|
+
req.on('data', chunk => { rawBody += chunk; });
|
|
442
|
+
req.on('end', () => {
|
|
443
|
+
try {
|
|
444
|
+
reqContext.body = JSON.parse(rawBody);
|
|
445
|
+
} catch {
|
|
446
|
+
reqContext.body = rawBody;
|
|
447
|
+
}
|
|
448
|
+
this._compileAndSend(source, nvmlFilePath, reqContext, res, statusCode);
|
|
449
|
+
});
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
this._compileAndSend(source, nvmlFilePath, reqContext, res, statusCode);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
_compileAndSend(source, nvmlFilePath, reqContext, res, statusCode) {
|
|
457
|
+
// Change cwd to the NVML file's directory so relative paths in Nova scripts work
|
|
458
|
+
const nvmlDir = path.dirname(nvmlFilePath);
|
|
459
|
+
|
|
460
|
+
let html;
|
|
461
|
+
try {
|
|
462
|
+
// Wrap the runner so reqContext is forwarded to the Nova scope as `request`.
|
|
463
|
+
// The emitter is intentionally NOT passed — server-side nv scripts must not
|
|
464
|
+
// be compiled to JS and embedded in the page; they only mutate the doc.
|
|
465
|
+
const novaRunner = this._nova.runner
|
|
466
|
+
? (code, doc) => this._nova.runner(code, doc, reqContext)
|
|
467
|
+
: null;
|
|
468
|
+
|
|
469
|
+
html = nvml.compile(source, {
|
|
470
|
+
novaRunner,
|
|
471
|
+
novaEmitter: null, // server-side only; no client-side Nova JS emitting
|
|
472
|
+
});
|
|
473
|
+
} catch (e) {
|
|
474
|
+
process.stderr.write(`[kitnovacweb] NVML compile error (${nvmlFilePath}): ${e.message}\n`);
|
|
475
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
476
|
+
return this._sendError(res, 500,
|
|
477
|
+
`NVML Compile Error in ${path.basename(nvmlFilePath)}:\n${e.message}`);
|
|
478
|
+
}
|
|
479
|
+
return this._sendError(res, 500, 'Internal Server Error');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const headers = {
|
|
483
|
+
'Content-Type': 'text/html; charset=UTF-8',
|
|
484
|
+
'Content-Length': Buffer.byteLength(html),
|
|
485
|
+
...this._headers,
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
res.writeHead(statusCode, headers);
|
|
489
|
+
res.end(html);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ── /_nvml/run — run Nova server script, return mutations ──
|
|
493
|
+
|
|
494
|
+
_handleNvRun(req, res) {
|
|
495
|
+
let rawBody = '';
|
|
496
|
+
req.on('data', chunk => { rawBody += chunk; });
|
|
497
|
+
req.on('end', () => {
|
|
498
|
+
let code, live;
|
|
499
|
+
try {
|
|
500
|
+
const body = JSON.parse(rawBody);
|
|
501
|
+
code = String(body.code || '');
|
|
502
|
+
live = body.live || {};
|
|
503
|
+
} catch (e) {
|
|
504
|
+
return this._sendJSON(res, 400, { error: 'Invalid JSON body' });
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (!code.trim()) {
|
|
508
|
+
return this._sendJSON(res, 200, { mutations: [] });
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (!this._nova || !this._nova.exec) {
|
|
512
|
+
return this._sendJSON(res, 503, { error: 'Nova runtime not available' });
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Mutation-capturing proxy — collects all document mutations as JSON
|
|
516
|
+
// instead of applying them to an NvmlDocument. The client applies them.
|
|
517
|
+
const mutations = [];
|
|
518
|
+
const docProxy = {
|
|
519
|
+
// config / meta
|
|
520
|
+
setConfig: (k, v) => mutations.push({ type: 'setConfig', key: String(k), value: v }),
|
|
521
|
+
setTitle: (t) => mutations.push({ type: 'setConfig', key: 'title', value: t }),
|
|
522
|
+
setMeta: (k, v) => mutations.push({ type: 'setConfig', key: String(k), value: v }),
|
|
523
|
+
// element text / html
|
|
524
|
+
set: (id, val) => mutations.push({ type: 'setText', id: String(id), value: String(val) }),
|
|
525
|
+
setHTML: (id, val) => mutations.push({ type: 'setHTML', id: String(id), value: String(val) }),
|
|
526
|
+
// element attributes
|
|
527
|
+
setProp: (id, key, val) => mutations.push({ type: 'setProp', id: String(id), key: String(key), value: String(val) }),
|
|
528
|
+
// class helpers
|
|
529
|
+
addClass: (id, cls) => mutations.push({ type: 'addClass', id: String(id), value: String(cls) }),
|
|
530
|
+
setClass: (id, cls) => mutations.push({ type: 'setClass', id: String(id), value: String(cls) }),
|
|
531
|
+
// styles
|
|
532
|
+
addStyle: (css) => mutations.push({ type: 'addStyle', value: String(css) }),
|
|
533
|
+
addElementStyle: (id, css) => mutations.push({ type: 'addStyle', id: String(id), value: String(css) }),
|
|
534
|
+
// visibility
|
|
535
|
+
hide: (id) => mutations.push({ type: 'hide', id: String(id) }),
|
|
536
|
+
show: (id, display) => mutations.push({ type: 'show', id: String(id), value: display || 'block' }),
|
|
537
|
+
// browser actions (applied client-side from mutation list)
|
|
538
|
+
alert: (msg) => mutations.push({ type: 'alert', value: String(msg) }),
|
|
539
|
+
// reads from the live snapshot the client sent
|
|
540
|
+
get: (id) => (live.elements && live.elements[id]) ? live.elements[id].text : null,
|
|
541
|
+
getConfig: (k) => live[k] ?? null,
|
|
542
|
+
config: live,
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
this._nova.exec(code, { document: docProxy, request: live });
|
|
547
|
+
return this._sendJSON(res, 200, { mutations });
|
|
548
|
+
} catch (e) {
|
|
549
|
+
process.stderr.write(`[kitnovacweb] /_nvml/run error: ${e.message}\n`);
|
|
550
|
+
return this._sendJSON(res, 500, { error: e.message, mutations });
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ── /_nvml/run-node — run Node.js server script, return mutations ──
|
|
556
|
+
|
|
557
|
+
_handleNodeRun(req, res) {
|
|
558
|
+
let rawBody = '';
|
|
559
|
+
req.on('data', chunk => { rawBody += chunk; });
|
|
560
|
+
req.on('end', () => {
|
|
561
|
+
let code, live;
|
|
562
|
+
try {
|
|
563
|
+
const body = JSON.parse(rawBody);
|
|
564
|
+
code = String(body.code || '');
|
|
565
|
+
live = body.live || {};
|
|
566
|
+
} catch (e) {
|
|
567
|
+
return this._sendJSON(res, 400, { error: 'Invalid JSON body' });
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (!code.trim()) {
|
|
571
|
+
return this._sendJSON(res, 200, { mutations: [] });
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const vm = require('vm');
|
|
575
|
+
const { makeBfObject } = require('./nvml/executor');
|
|
576
|
+
const mutations = [];
|
|
577
|
+
|
|
578
|
+
// Same mutation-capturing document proxy as _handleNvRun
|
|
579
|
+
const docProxy = {
|
|
580
|
+
setConfig: (k, v) => mutations.push({ type: 'setConfig', key: String(k), value: v }),
|
|
581
|
+
setTitle: (t) => mutations.push({ type: 'setConfig', key: 'title', value: t }),
|
|
582
|
+
setMeta: (k, v) => mutations.push({ type: 'setConfig', key: String(k), value: v }),
|
|
583
|
+
set: (id, val) => mutations.push({ type: 'setText', id: String(id), value: String(val) }),
|
|
584
|
+
setHTML: (id, val) => mutations.push({ type: 'setHTML', id: String(id), value: String(val) }),
|
|
585
|
+
setProp: (id, k, v)=> mutations.push({ type: 'setProp', id: String(id), key: String(k), value: String(v) }),
|
|
586
|
+
addClass: (id, cls) => mutations.push({ type: 'addClass', id: String(id), value: String(cls) }),
|
|
587
|
+
setClass: (id, cls) => mutations.push({ type: 'setClass', id: String(id), value: String(cls) }),
|
|
588
|
+
addStyle: (css) => mutations.push({ type: 'addStyle', value: String(css) }),
|
|
589
|
+
addElementStyle: (id, css) => mutations.push({ type: 'addStyle', id: String(id), value: String(css) }),
|
|
590
|
+
hide: (id, tr) => mutations.push({ type: 'hide', id: String(id), transition: tr }),
|
|
591
|
+
show: (id, d, tr)=> mutations.push({ type: 'show', id: String(id), value: d||'block', transition: tr }),
|
|
592
|
+
remove: (id, tr) => mutations.push({ type: 'remove', id: String(id), transition: tr }),
|
|
593
|
+
alert: (msg) => mutations.push({ type: 'alert', value: String(msg) }),
|
|
594
|
+
toast: (msg, dur, typ) => mutations.push({ type: 'toast', value: String(msg), duration: dur, type: typ }),
|
|
595
|
+
console: (msg, lv) => mutations.push({ type: 'console', value: String(msg), level: lv || 'log' }),
|
|
596
|
+
setSignal: (n, v) => mutations.push({ type: 'setSignal', name: String(n), value: v }),
|
|
597
|
+
navigate: (p) => mutations.push({ type: 'navigate', path: String(p) }),
|
|
598
|
+
reload: () => mutations.push({ type: 'reload' }),
|
|
599
|
+
redirect: (url) => mutations.push({ type: 'redirect', url: String(url) }),
|
|
600
|
+
setStyle: (id, k, v)=> mutations.push({ type: 'setStyle', id: String(id), key: String(k), value: String(v) }),
|
|
601
|
+
setCSSVar: (n, v) => mutations.push({ type: 'setCSSVar', name: String(n), value: String(v) }),
|
|
602
|
+
setAttr: (id, k, v)=> mutations.push({ type: 'setAttr', id: String(id), key: String(k), value: String(v) }),
|
|
603
|
+
removeAttr:(id, k) => mutations.push({ type: 'removeAttr',id: String(id), key: String(k) }),
|
|
604
|
+
focus: (id) => mutations.push({ type: 'focus', id: String(id) }),
|
|
605
|
+
blur: (id) => mutations.push({ type: 'blur', id: String(id) }),
|
|
606
|
+
scroll: (id, beh) => mutations.push({ type: 'scroll', id: String(id), behavior: beh }),
|
|
607
|
+
// reads from live snapshot
|
|
608
|
+
get: (id) => (live.elements && live.elements[id]) ? live.elements[id].text : null,
|
|
609
|
+
getSignal: (n) => (live.state && live.state[n]) !== undefined ? live.state[n] : null,
|
|
610
|
+
getConfig: (k) => live[k] ?? null,
|
|
611
|
+
config: live,
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
try {
|
|
615
|
+
const ctx = vm.createContext({
|
|
616
|
+
document: docProxy,
|
|
617
|
+
bf: makeBfObject(),
|
|
618
|
+
request: live,
|
|
619
|
+
console,
|
|
620
|
+
require,
|
|
621
|
+
process,
|
|
622
|
+
__dirname: process.cwd(),
|
|
623
|
+
__filename: '',
|
|
624
|
+
});
|
|
625
|
+
vm.runInContext(code, ctx, { timeout: 10000 });
|
|
626
|
+
return this._sendJSON(res, 200, { mutations });
|
|
627
|
+
} catch (e) {
|
|
628
|
+
process.stderr.write(`[kitnovacweb] /_nvml/run-node error: ${e.message}\n`);
|
|
629
|
+
return this._sendJSON(res, 500, { error: e.message, mutations });
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
const body = JSON.stringify(obj);
|
|
636
|
+
res.writeHead(code, {
|
|
637
|
+
'Content-Type': 'application/json; charset=UTF-8',
|
|
638
|
+
'Content-Length': Buffer.byteLength(body),
|
|
639
|
+
'Access-Control-Allow-Origin': '*',
|
|
640
|
+
...this._headers,
|
|
641
|
+
});
|
|
642
|
+
res.end(body);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
_serveStaticFile(res, filePath) {
|
|
646
|
+
// prevent directory traversal
|
|
647
|
+
const normalized = path.normalize(filePath);
|
|
648
|
+
if (!fs.existsSync(normalized) || !fs.statSync(normalized).isFile()) return false;
|
|
649
|
+
|
|
650
|
+
let data;
|
|
651
|
+
try { data = fs.readFileSync(normalized); } catch { return false; }
|
|
652
|
+
|
|
653
|
+
const mime = mimeFor(normalized);
|
|
654
|
+
res.writeHead(200, {
|
|
655
|
+
'Content-Type': mime,
|
|
656
|
+
'Content-Length': data.length,
|
|
657
|
+
...this._headers,
|
|
658
|
+
});
|
|
659
|
+
res.end(data);
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
_sendError(res, code, msg) {
|
|
664
|
+
const body = `<!DOCTYPE html>
|
|
665
|
+
<html><head><meta charset="UTF-8"><title>${code}</title></head>
|
|
666
|
+
<body><h1>${code}</h1><pre>${msg.replace(/</g,'<')}</pre></body>
|
|
667
|
+
</html>`;
|
|
668
|
+
res.writeHead(code, {
|
|
669
|
+
'Content-Type': 'text/html; charset=UTF-8',
|
|
670
|
+
'Content-Length': Buffer.byteLength(body),
|
|
671
|
+
});
|
|
672
|
+
res.end(body);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ── Kit export ───────────────────────────────────────────────
|
|
677
|
+
|
|
678
|
+
const kitdef = {
|
|
679
|
+
// Singleton document object (mirrors browser `document`)
|
|
680
|
+
document: new KitDocument(),
|
|
681
|
+
|
|
682
|
+
// Also expose the NVML pipeline directly for power users
|
|
683
|
+
nvml,
|
|
684
|
+
|
|
685
|
+
// Compile a raw NVML string to HTML (one-shot, no server)
|
|
686
|
+
compile: (source, opts) => nvml.compile(source, opts),
|
|
687
|
+
|
|
688
|
+
// Parse NVML source to AST
|
|
689
|
+
parse: (source) => nvml.parse(source),
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
module.exports = { kitdef };
|