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.
Files changed (138) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +0 -0
  3. package/demo.nv +0 -0
  4. package/demo_builtins.nv +0 -0
  5. package/demo_http.nv +0 -0
  6. package/examples/bf.nv +69 -0
  7. package/examples/math.nv +21 -0
  8. package/kits/birdAPI/kitdef.js +954 -0
  9. package/kits/kitRNG/kitdef.js +740 -0
  10. package/kits/kitSSH/kitdef.js +1272 -0
  11. package/kits/kitadb/kitdef.js +606 -0
  12. package/kits/kitai/kitdef.js +2185 -0
  13. package/kits/kitcanvas/kitdef.js +914 -0
  14. package/kits/kitclippy/kitdef.js +925 -0
  15. package/kits/kitgps/kitdef.js +1862 -0
  16. package/kits/kitlibproc/kitdef.js +3 -2
  17. package/kits/kitmorse/kitdef.js +229 -0
  18. package/kits/kitmpatch/kitdef.js +906 -0
  19. package/kits/kitnet/kitdef.js +1401 -0
  20. package/kits/kitproto/kitdef.js +613 -0
  21. package/kits/kitqr/kitdef.js +637 -0
  22. package/kits/kitrequire/kitdef.js +1599 -0
  23. package/kits/libtea/kitdef.js +2691 -0
  24. package/kits/libterm/kitdef.js +2 -0
  25. package/novac/LICENSE +21 -0
  26. package/novac/README.md +1823 -0
  27. package/novac/bin/novac +950 -0
  28. package/novac/bin/nvc +522 -0
  29. package/novac/bin/nvml +542 -0
  30. package/novac/demo.nv +245 -0
  31. package/novac/demo_builtins.nv +209 -0
  32. package/novac/demo_http.nv +62 -0
  33. package/novac/examples/bf.nv +69 -0
  34. package/novac/examples/math.nv +21 -0
  35. package/novac/kits/kitai/kitdef.js +2185 -0
  36. package/novac/kits/kitansi/kitdef.js +1402 -0
  37. package/novac/kits/kitformat/kitdef.js +1485 -0
  38. package/novac/kits/kitgps/kitdef.js +1862 -0
  39. package/novac/kits/kitlibfs/kitdef.js +231 -0
  40. package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
  41. package/novac/kits/kitmatrix/ex.js +19 -0
  42. package/novac/kits/kitmatrix/kitdef.js +960 -0
  43. package/novac/kits/kitmpatch/kitdef.js +906 -0
  44. package/novac/kits/kitnovacweb/README.md +1572 -0
  45. package/novac/kits/kitnovacweb/demo.nv +12 -0
  46. package/novac/kits/kitnovacweb/demo.nvml +71 -0
  47. package/novac/kits/kitnovacweb/index.nova +12 -0
  48. package/novac/kits/kitnovacweb/kitdef.js +692 -0
  49. package/novac/kits/kitnovacweb/nova.kit.json +8 -0
  50. package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
  51. package/novac/kits/kitnovacweb/nvml/index.js +67 -0
  52. package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
  53. package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
  54. package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
  55. package/novac/kits/kitparse/kitdef.js +1688 -0
  56. package/novac/kits/kitregex++/kitdef.js +1353 -0
  57. package/novac/kits/kitrequire/kitdef.js +1599 -0
  58. package/novac/kits/kitx11/kitdef.js +1 -0
  59. package/novac/kits/kitx11/kitx11.js +2472 -0
  60. package/novac/kits/kitx11/kitx11_conn.js +948 -0
  61. package/novac/kits/kitx11/kitx11_worker.js +121 -0
  62. package/novac/kits/libterm/ex.js +285 -0
  63. package/novac/kits/libterm/kitdef.js +1927 -0
  64. package/novac/node_modules/chalk/license +9 -0
  65. package/novac/node_modules/chalk/package.json +83 -0
  66. package/novac/node_modules/chalk/readme.md +297 -0
  67. package/novac/node_modules/chalk/source/index.d.ts +325 -0
  68. package/novac/node_modules/chalk/source/index.js +225 -0
  69. package/novac/node_modules/chalk/source/utilities.js +33 -0
  70. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  71. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  72. package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  73. package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  74. package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  75. package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  76. package/novac/node_modules/commander/LICENSE +22 -0
  77. package/novac/node_modules/commander/Readme.md +1176 -0
  78. package/novac/node_modules/commander/esm.mjs +16 -0
  79. package/novac/node_modules/commander/index.js +24 -0
  80. package/novac/node_modules/commander/lib/argument.js +150 -0
  81. package/novac/node_modules/commander/lib/command.js +2777 -0
  82. package/novac/node_modules/commander/lib/error.js +39 -0
  83. package/novac/node_modules/commander/lib/help.js +747 -0
  84. package/novac/node_modules/commander/lib/option.js +380 -0
  85. package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
  86. package/novac/node_modules/commander/package-support.json +19 -0
  87. package/novac/node_modules/commander/package.json +82 -0
  88. package/novac/node_modules/commander/typings/esm.d.mts +3 -0
  89. package/novac/node_modules/commander/typings/index.d.ts +1113 -0
  90. package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
  91. package/novac/node_modules/node-addon-api/README.md +95 -0
  92. package/novac/node_modules/node-addon-api/common.gypi +21 -0
  93. package/novac/node_modules/node-addon-api/except.gypi +25 -0
  94. package/novac/node_modules/node-addon-api/index.js +14 -0
  95. package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
  96. package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
  97. package/novac/node_modules/node-addon-api/napi.h +3364 -0
  98. package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
  99. package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
  100. package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
  101. package/novac/node_modules/node-addon-api/package-support.json +21 -0
  102. package/novac/node_modules/node-addon-api/package.json +480 -0
  103. package/novac/node_modules/node-addon-api/tools/README.md +73 -0
  104. package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
  105. package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
  106. package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
  107. package/novac/node_modules/serialize-javascript/LICENSE +27 -0
  108. package/novac/node_modules/serialize-javascript/README.md +149 -0
  109. package/novac/node_modules/serialize-javascript/index.js +297 -0
  110. package/novac/node_modules/serialize-javascript/package.json +33 -0
  111. package/novac/package.json +27 -0
  112. package/novac/scripts/update-bin.js +24 -0
  113. package/novac/src/core/bstd.js +1035 -0
  114. package/novac/src/core/config.js +155 -0
  115. package/novac/src/core/describe.js +187 -0
  116. package/novac/src/core/emitter.js +499 -0
  117. package/novac/src/core/error.js +86 -0
  118. package/novac/src/core/executor.js +5606 -0
  119. package/novac/src/core/formatter.js +686 -0
  120. package/novac/src/core/lexer.js +1026 -0
  121. package/novac/src/core/nova_builtins.js +717 -0
  122. package/novac/src/core/nova_thread_worker.js +166 -0
  123. package/novac/src/core/parser.js +2181 -0
  124. package/novac/src/core/types.js +112 -0
  125. package/novac/src/index.js +28 -0
  126. package/novac/src/runtime/stdlib.js +244 -0
  127. package/package.json +3 -2
  128. package/scripts/update-bin.js +0 -0
  129. package/src/core/bstd.js +835 -361
  130. package/src/core/executor.js +427 -246
  131. package/src/core/lexer.js +19 -2
  132. package/src/core/parser.js +13 -0
  133. package/src/index.js +0 -0
  134. package/examples/example-project/README.md +0 -3
  135. package/examples/example-project/src/main.nova +0 -3
  136. package/src/core/environment.js +0 -0
  137. /package/{kits → novac/kits}/libtea/tf.js +0 -0
  138. /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,'&lt;')}</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 };
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "kitnovacweb",
3
+ "version": "1.0.0",
4
+ "description": "Nova web kit — NVML markup language with SSR, client scripts, and HTTP server",
5
+ "main": "index.nova",
6
+ "author": "",
7
+ "license": "MIT"
8
+ }