bitwrench 2.0.32 → 2.1.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/README.md +210 -125
- package/dist/bitwrench-bccl.cjs.js +349 -188
- package/dist/bitwrench-bccl.cjs.min.js +2 -39
- package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
- package/dist/bitwrench-bccl.esm.js +349 -188
- package/dist/bitwrench-bccl.esm.min.js +2 -39
- package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
- package/dist/bitwrench-bccl.umd.js +349 -188
- package/dist/bitwrench-bccl.umd.min.js +2 -39
- package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
- package/dist/bitwrench-code-edit.cjs.js +17 -6
- package/dist/bitwrench-code-edit.cjs.min.js +2 -20
- package/dist/bitwrench-code-edit.es5.js +8 -3
- package/dist/bitwrench-code-edit.es5.min.js +2 -19
- package/dist/bitwrench-code-edit.esm.js +17 -6
- package/dist/bitwrench-code-edit.esm.min.js +2 -19
- package/dist/bitwrench-code-edit.umd.js +17 -6
- package/dist/bitwrench-code-edit.umd.min.js +2 -19
- package/dist/bitwrench-code-edit.umd.min.js.gz +0 -0
- package/dist/bitwrench-debug.js +1 -1
- package/dist/bitwrench-debug.min.js +1 -1
- package/dist/bitwrench-lean.cjs.js +2492 -1628
- package/dist/bitwrench-lean.cjs.min.js +2 -80
- package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
- package/dist/bitwrench-lean.es5.js +2740 -1838
- package/dist/bitwrench-lean.es5.min.js +2 -49
- package/dist/bitwrench-lean.es5.min.js.gz +0 -0
- package/dist/bitwrench-lean.esm.js +2492 -1628
- package/dist/bitwrench-lean.esm.min.js +2 -80
- package/dist/bitwrench-lean.esm.min.js.gz +0 -0
- package/dist/bitwrench-lean.umd.js +2492 -1628
- package/dist/bitwrench-lean.umd.min.js +2 -80
- package/dist/bitwrench-lean.umd.min.js.gz +0 -0
- package/dist/bitwrench-util-color.cjs.js +251 -0
- package/dist/bitwrench-util-color.cjs.min.js +3 -0
- package/dist/bitwrench-util-color.es5.js +256 -0
- package/dist/bitwrench-util-color.es5.min.js +3 -0
- package/dist/bitwrench-util-color.esm.js +241 -0
- package/dist/bitwrench-util-color.esm.min.js +3 -0
- package/dist/bitwrench-util-color.umd.js +257 -0
- package/dist/bitwrench-util-color.umd.min.js +3 -0
- package/dist/bitwrench-util-css.cjs.js +2 -1
- package/dist/bitwrench-util-css.cjs.min.js +2 -21
- package/dist/bitwrench-util-css.es5.js +2 -1
- package/dist/bitwrench-util-css.es5.min.js +2 -20
- package/dist/bitwrench-util-css.esm.js +2 -1
- package/dist/bitwrench-util-css.esm.min.js +1 -19
- package/dist/bitwrench-util-css.umd.js +2 -1
- package/dist/bitwrench-util-css.umd.min.js +2 -20
- package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
- package/dist/bitwrench.cjs.js +2826 -1801
- package/dist/bitwrench.cjs.min.js +2 -99
- package/dist/bitwrench.cjs.min.js.gz +0 -0
- package/dist/bitwrench.css +403 -479
- package/dist/bitwrench.d.ts +70 -73
- package/dist/bitwrench.es5.js +3106 -2020
- package/dist/bitwrench.es5.min.js +2 -67
- package/dist/bitwrench.es5.min.js.gz +0 -0
- package/dist/bitwrench.esm.js +2826 -1801
- package/dist/bitwrench.esm.min.js +2 -99
- package/dist/bitwrench.esm.min.js.gz +0 -0
- package/dist/bitwrench.min.css +1 -1
- package/dist/bitwrench.umd.js +2826 -1801
- package/dist/bitwrench.umd.min.js +2 -99
- package/dist/bitwrench.umd.min.js.gz +0 -0
- package/dist/builds.json +222 -134
- package/dist/bwserve.cjs.js +289 -282
- package/dist/bwserve.d.ts +157 -0
- package/dist/bwserve.esm.js +290 -283
- package/dist/sri.json +54 -46
- package/docs/README.md +6 -3
- package/docs/app-patterns.md +7 -6
- package/docs/bitwrench-for-wasm.md +53 -54
- package/docs/bitwrench-mcp.md +2 -2
- package/docs/bitwrench-northstar-principles.md +406 -0
- package/docs/bitwrench-taco-schema-discussion.md +2 -2
- package/docs/bitwrench_api.md +191 -106
- package/docs/bitwrench_typescript_usage.md +5 -5
- package/docs/bw-attach.md +29 -75
- package/docs/bwserve.md +200 -168
- package/docs/cli.md +36 -12
- package/docs/component-cheatsheet.md +2 -2
- package/docs/component-library.md +4 -4
- package/docs/component-lifecycle.md +234 -0
- package/docs/drift-lint.md +268 -0
- package/docs/framework-translation-table.md +4 -4
- package/docs/llm-bitwrench-guide.md +60 -50
- package/docs/routing.md +11 -13
- package/docs/state-management.md +110 -109
- package/docs/taco-format.md +13 -14
- package/docs/theming.md +13 -3
- package/docs/thinking-in-bitwrench.md +858 -983
- package/docs/tutorial-bwserve.md +37 -36
- package/docs/tutorial-embedded.md +10 -21
- package/docs/tutorial-website.md +2 -2
- package/package.json +38 -7
- package/readme.html +262 -160
- package/src/bitwrench-bccl-entry.js +2 -2
- package/src/bitwrench-bccl.js +346 -185
- package/src/bitwrench-code-edit.js +16 -5
- package/src/bitwrench-color-utils.js +117 -181
- package/src/bitwrench-file-ops.js +2 -2
- package/src/bitwrench-lean.js +4 -3
- package/src/bitwrench-router.js +5 -2
- package/src/bitwrench-styles.js +420 -504
- package/src/bitwrench-util-color.js +240 -0
- package/src/bitwrench-util-css.js +1 -0
- package/src/bitwrench-utils.js +4 -0
- package/src/bitwrench.d.ts +70 -73
- package/src/bitwrench.h +5 -0
- package/src/bitwrench.js +1939 -933
- package/src/bwserve/attach.js +0 -1
- package/src/bwserve/bwclient.js +172 -32
- package/src/bwserve/bwshell.js +0 -4
- package/src/bwserve/client.js +59 -220
- package/src/bwserve/index.js +59 -26
- package/src/bwserve.d.ts +157 -0
- package/src/bwserve.h +5 -0
- package/src/cli/attach.js +12 -75
- package/src/cli/convert.js +2 -2
- package/src/cli/serve.js +37 -35
- package/src/generate-css.js +1 -1
- package/src/mcp/knowledge.js +4 -4
- package/src/mcp/live.js +21 -13
- package/src/mcp/tools.js +0 -1
- package/src/version.js +3 -7
package/src/bwserve/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* import bwserve from 'bitwrench/bwserve';
|
|
10
10
|
* const app = bwserve.create({ port: 7902 });
|
|
11
11
|
* app.page('/', (client) => {
|
|
12
|
-
* client.
|
|
12
|
+
* client.mount('#app', bw.makeCard({ title: 'Hello' }));
|
|
13
13
|
* });
|
|
14
14
|
* app.listen();
|
|
15
15
|
*
|
|
@@ -23,7 +23,7 @@ import { VERSION } from '../version.js';
|
|
|
23
23
|
|
|
24
24
|
// Resolve dist/ paths relative to the package root
|
|
25
25
|
import { fileURLToPath } from 'url';
|
|
26
|
-
import { dirname, resolve, join, extname } from 'path';
|
|
26
|
+
import { dirname, resolve, join, extname, sep } from 'path';
|
|
27
27
|
import { createServer } from 'http';
|
|
28
28
|
import { readFileSync, readdirSync, existsSync, statSync } from 'fs';
|
|
29
29
|
|
|
@@ -32,9 +32,11 @@ var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
32
32
|
// Resolve dist/ — try source layout (src/bwserve/), then npm install layout,
|
|
33
33
|
// then dist/ itself (when running from dist/bwserve.esm.js)
|
|
34
34
|
var DIST_DIR = resolve(__dirname, '..', '..', 'dist');
|
|
35
|
+
/* c8 ignore next 3 -- DIST_DIR fallback at module load; only triggers in npm install layout */
|
|
35
36
|
if (!existsSync(DIST_DIR)) {
|
|
36
37
|
DIST_DIR = resolve(__dirname, '..', 'dist');
|
|
37
38
|
}
|
|
39
|
+
/* c8 ignore next 3 -- DIST_DIR fallback at module load; only triggers when no dist/ exists */
|
|
38
40
|
if (!existsSync(DIST_DIR)) {
|
|
39
41
|
DIST_DIR = __dirname;
|
|
40
42
|
}
|
|
@@ -96,15 +98,14 @@ export function create(opts) {
|
|
|
96
98
|
*/
|
|
97
99
|
class BwServeApp {
|
|
98
100
|
constructor(opts) {
|
|
99
|
-
this.port = opts.port
|
|
101
|
+
this.port = opts.port != null ? opts.port : 7902;
|
|
100
102
|
this.title = opts.title || 'bwserve';
|
|
101
103
|
this.staticDir = opts.static || null;
|
|
102
104
|
this.injectBitwrench = opts.injectBitwrench !== false;
|
|
103
105
|
this.theme = opts.theme || null;
|
|
104
|
-
this.allowExec = opts.allowExec || false;
|
|
105
106
|
this.allowScreenshot = opts.allowScreenshot || false;
|
|
106
107
|
this.dirList = opts.dirList !== false;
|
|
107
|
-
this.host = opts.host || '
|
|
108
|
+
this.host = opts.host || '127.0.0.1';
|
|
108
109
|
this.keepAliveInterval = opts.keepAliveInterval || 15000;
|
|
109
110
|
this._pages = new Map();
|
|
110
111
|
this._clients = new Map();
|
|
@@ -139,6 +140,11 @@ class BwServeApp {
|
|
|
139
140
|
});
|
|
140
141
|
|
|
141
142
|
self._server.listen(self.port, self.host, function() {
|
|
143
|
+
// Update port to the actual bound port (important when port 0 is used)
|
|
144
|
+
var addr = self._server.address();
|
|
145
|
+
if (addr && addr.port) {
|
|
146
|
+
self.port = addr.port;
|
|
147
|
+
}
|
|
142
148
|
if (callback) callback();
|
|
143
149
|
res();
|
|
144
150
|
});
|
|
@@ -197,9 +203,9 @@ class BwServeApp {
|
|
|
197
203
|
return 0;
|
|
198
204
|
}
|
|
199
205
|
var count = 0;
|
|
200
|
-
for (var
|
|
201
|
-
if (
|
|
202
|
-
|
|
206
|
+
for (var rec of this._clients.values()) {
|
|
207
|
+
if (rec.client && !rec.client._closed) {
|
|
208
|
+
rec.client._send(msg);
|
|
203
209
|
count++;
|
|
204
210
|
}
|
|
205
211
|
}
|
|
@@ -218,6 +224,7 @@ class BwServeApp {
|
|
|
218
224
|
var path = url.split('?')[0];
|
|
219
225
|
|
|
220
226
|
// /bw/attach.js — self-contained attach script for remote debugging
|
|
227
|
+
/* c8 ignore next 3 -- covered in isolation; flaky in combined suite due to server state */
|
|
221
228
|
if (path === '/bw/attach.js' && method === 'GET') {
|
|
222
229
|
return this._serveAttachScript(req, res);
|
|
223
230
|
}
|
|
@@ -244,6 +251,7 @@ class BwServeApp {
|
|
|
244
251
|
}
|
|
245
252
|
|
|
246
253
|
// CORS preflight for /bw/return/ (needed for cross-origin attach)
|
|
254
|
+
/* c8 ignore next 9 -- covered in isolation; flaky in combined suite */
|
|
247
255
|
if (method === 'OPTIONS' && path.startsWith('/bw/return/')) {
|
|
248
256
|
res.writeHead(204, {
|
|
249
257
|
'Access-Control-Allow-Origin': '*',
|
|
@@ -258,6 +266,7 @@ class BwServeApp {
|
|
|
258
266
|
if (method === 'POST' && path.startsWith('/bw/return/')) {
|
|
259
267
|
var rest = path.slice('/bw/return/'.length);
|
|
260
268
|
var slash = rest.indexOf('/');
|
|
269
|
+
/* c8 ignore next 4 -- covered in isolation; flaky in combined suite */
|
|
261
270
|
if (slash === -1) {
|
|
262
271
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
263
272
|
res.end(JSON.stringify({ error: 'Invalid return path' }));
|
|
@@ -278,18 +287,26 @@ class BwServeApp {
|
|
|
278
287
|
// so that bwserve works as a drop-in static server (like python -m
|
|
279
288
|
// http.server or npx serve) with opt-in bwserve superpowers.
|
|
280
289
|
if (method === 'GET' && this.staticDir) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
290
|
+
// Path traversal guard: resolve to absolute and verify containment
|
|
291
|
+
var resolvedBase = resolve(this.staticDir);
|
|
292
|
+
var resolvedPath = resolve(resolvedBase, '.' + path);
|
|
293
|
+
if (resolvedPath !== resolvedBase && !resolvedPath.startsWith(resolvedBase + sep)) {
|
|
294
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
295
|
+
res.end('Forbidden');
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (existsSync(resolvedPath) && statSync(resolvedPath).isFile()) {
|
|
300
|
+
var ext = extname(resolvedPath);
|
|
284
301
|
var mime = MIME_TYPES[ext] || 'application/octet-stream';
|
|
285
|
-
var content = readFileSync(
|
|
302
|
+
var content = readFileSync(resolvedPath);
|
|
286
303
|
res.writeHead(200, { 'Content-Type': mime });
|
|
287
304
|
res.end(content);
|
|
288
305
|
return;
|
|
289
306
|
}
|
|
290
307
|
// Directory index resolution: /foo/ => /foo/index.html
|
|
291
308
|
if (path.endsWith('/')) {
|
|
292
|
-
var indexPath = join(
|
|
309
|
+
var indexPath = join(resolvedPath, 'index.html');
|
|
293
310
|
if (existsSync(indexPath) && statSync(indexPath).isFile()) {
|
|
294
311
|
var indexContent = readFileSync(indexPath);
|
|
295
312
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
@@ -297,16 +314,15 @@ class BwServeApp {
|
|
|
297
314
|
return;
|
|
298
315
|
}
|
|
299
316
|
// Directory listing when no index.html
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
var listing = this._generateDirListing(path, dirPath);
|
|
317
|
+
if (this.dirList && existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
318
|
+
var listing = this._generateDirListing(path, resolvedPath);
|
|
303
319
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
304
320
|
res.end(listing);
|
|
305
321
|
return;
|
|
306
322
|
}
|
|
307
323
|
}
|
|
308
324
|
// Bare directory without trailing slash: /foo => 301 to /foo/
|
|
309
|
-
if (!path.endsWith('/') && existsSync(
|
|
325
|
+
if (!path.endsWith('/') && existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
310
326
|
var qs = url.split('?')[1];
|
|
311
327
|
var location = path + '/' + (qs ? '?' + qs : '');
|
|
312
328
|
res.writeHead(301, { 'Location': location });
|
|
@@ -323,8 +339,7 @@ class BwServeApp {
|
|
|
323
339
|
clientId: clientId2,
|
|
324
340
|
title: this.title,
|
|
325
341
|
theme: this.theme,
|
|
326
|
-
injectBitwrench: this.injectBitwrench
|
|
327
|
-
allowExec: this.allowExec
|
|
342
|
+
injectBitwrench: this.injectBitwrench
|
|
328
343
|
});
|
|
329
344
|
// Store the page path for this client so SSE knows which handler to call
|
|
330
345
|
this._clients.set(clientId2, { pagePath: path, client: null });
|
|
@@ -383,9 +398,13 @@ class BwServeApp {
|
|
|
383
398
|
var pagePath = pending ? pending.pagePath : '/';
|
|
384
399
|
self._clients.set(clientId, { pagePath: pagePath, client: client });
|
|
385
400
|
|
|
401
|
+
// Send the handshake as the very first SSE event
|
|
402
|
+
client._send({ type: 'hello' });
|
|
403
|
+
|
|
386
404
|
// Keep-alive: send SSE comment periodically
|
|
387
405
|
var keepAlive = setInterval(function() {
|
|
388
406
|
if (!client._closed) {
|
|
407
|
+
/* c8 ignore next -- keepalive write failure only on socket close */
|
|
389
408
|
try { res.write(':keepalive\n\n'); } catch (e) { /* ignore */ }
|
|
390
409
|
}
|
|
391
410
|
}, self.keepAliveInterval);
|
|
@@ -397,11 +416,12 @@ class BwServeApp {
|
|
|
397
416
|
self._clients.delete(clientId);
|
|
398
417
|
});
|
|
399
418
|
|
|
400
|
-
// Call the page handler
|
|
419
|
+
// Call the page handler (runs on every connection, including reconnects)
|
|
401
420
|
var handler = self._pages.get(pagePath);
|
|
402
421
|
if (handler) {
|
|
403
422
|
try {
|
|
404
423
|
handler(client);
|
|
424
|
+
/* c8 ignore next 3 -- page handler error catch; requires throwing handler in SSE context */
|
|
405
425
|
} catch (e) {
|
|
406
426
|
console.error('[bwserve] Page handler error:', e);
|
|
407
427
|
}
|
|
@@ -414,8 +434,8 @@ class BwServeApp {
|
|
|
414
434
|
*
|
|
415
435
|
* Routes:
|
|
416
436
|
* action — fire-and-forget action dispatch (no requestId)
|
|
417
|
-
*
|
|
418
|
-
*
|
|
437
|
+
* event — event dispatch from client listeners
|
|
438
|
+
* topic — topic dispatch from client pub/sub
|
|
419
439
|
* screenshot — resolve pending screenshot promise
|
|
420
440
|
*
|
|
421
441
|
* @private
|
|
@@ -433,18 +453,26 @@ class BwServeApp {
|
|
|
433
453
|
req.on('end', function() {
|
|
434
454
|
try {
|
|
435
455
|
var data = JSON.parse(body);
|
|
436
|
-
if (route === '
|
|
456
|
+
if (route === 'topic') {
|
|
457
|
+
// Topic dispatch — forward to the listen handler registered on the client
|
|
458
|
+
var topic = data.topic;
|
|
459
|
+
var topicData = data.data;
|
|
460
|
+
record.client._dispatch('_topic:' + topic, topicData);
|
|
461
|
+
} else if (route === 'action' || route === 'event') {
|
|
437
462
|
// Action/event dispatch (no requestId/pending pattern)
|
|
438
463
|
var action = route === 'event'
|
|
439
464
|
? '_bw_event'
|
|
440
465
|
: (data.result ? data.result.action : data.action);
|
|
466
|
+
/* c8 ignore next 3 -- data.data fallback; covered in isolation */
|
|
441
467
|
var payload = route === 'event'
|
|
442
468
|
? (data.result || data)
|
|
443
469
|
: (data.result ? data.result.data : data.data || data);
|
|
444
470
|
record.client._dispatch(action, payload);
|
|
445
471
|
} else {
|
|
446
|
-
// All other routes: resolve pending promise
|
|
447
|
-
record.client._resolvePending
|
|
472
|
+
// All other routes: resolve pending promise if mechanism exists
|
|
473
|
+
if (record.client._resolvePending) {
|
|
474
|
+
record.client._resolvePending(data.requestId, data);
|
|
475
|
+
}
|
|
448
476
|
}
|
|
449
477
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
450
478
|
res.end(JSON.stringify({ ok: true }));
|
|
@@ -462,13 +490,18 @@ class BwServeApp {
|
|
|
462
490
|
*/
|
|
463
491
|
_serveAttachScript(req, res) {
|
|
464
492
|
try {
|
|
465
|
-
var
|
|
493
|
+
var h = req.headers || {};
|
|
494
|
+
var proto = (h['x-forwarded-proto'] || 'http');
|
|
495
|
+
var host = h['x-forwarded-host'] || h.host || '';
|
|
496
|
+
var origin = host ? (proto + '://' + host) : '';
|
|
497
|
+
var js = generateAttachScript({ origin: origin });
|
|
466
498
|
res.writeHead(200, {
|
|
467
499
|
'Content-Type': 'application/javascript; charset=utf-8',
|
|
468
500
|
'Access-Control-Allow-Origin': '*',
|
|
469
501
|
'Cache-Control': 'no-cache'
|
|
470
502
|
});
|
|
471
503
|
res.end(js);
|
|
504
|
+
/* c8 ignore next 4 -- generateAttachScript is a pure template; cannot throw */
|
|
472
505
|
} catch (err) {
|
|
473
506
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
474
507
|
res.end('Error generating attach script: ' + err.message);
|
package/src/bwserve.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for bitwrench/bwserve — server-driven UI over SSE.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
6
|
+
|
|
7
|
+
/** Options for creating a BwServeApp instance. */
|
|
8
|
+
export interface BwServeOptions {
|
|
9
|
+
/** Port to listen on (default: 7902) */
|
|
10
|
+
port?: number;
|
|
11
|
+
/** Page title (default: 'bwserve') */
|
|
12
|
+
title?: string;
|
|
13
|
+
/** Directory path for static file serving */
|
|
14
|
+
static?: string;
|
|
15
|
+
/** Auto-inject bitwrench client JS (default: true) */
|
|
16
|
+
injectBitwrench?: boolean;
|
|
17
|
+
/** Theme preset name or config object */
|
|
18
|
+
theme?: string | Record<string, string>;
|
|
19
|
+
/** Enable client.screenshot() (default: false) */
|
|
20
|
+
allowScreenshot?: boolean;
|
|
21
|
+
/** Enable directory listings (default: true) */
|
|
22
|
+
dirList?: boolean;
|
|
23
|
+
/** Host/address to bind to (default: '127.0.0.1') */
|
|
24
|
+
host?: string;
|
|
25
|
+
/** SSE keep-alive interval in ms (default: 15000) */
|
|
26
|
+
keepAliveInterval?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Options for generateShell(). */
|
|
30
|
+
export interface ShellOptions {
|
|
31
|
+
/** Unique client ID for this connection */
|
|
32
|
+
clientId: string;
|
|
33
|
+
/** Page title (default: 'bwserve') */
|
|
34
|
+
title?: string;
|
|
35
|
+
/** Theme preset name or config object */
|
|
36
|
+
theme?: string | Record<string, string>;
|
|
37
|
+
/** Whether to inject bitwrench scripts (default: true) */
|
|
38
|
+
injectBitwrench?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A TACO object — Tag, Attributes, Content, Options. */
|
|
42
|
+
export interface Taco {
|
|
43
|
+
t?: string;
|
|
44
|
+
a?: Record<string, any>;
|
|
45
|
+
c?: string | Taco | (string | Taco)[];
|
|
46
|
+
o?: Record<string, any>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** SSE protocol message. */
|
|
50
|
+
export interface BwServeMessage {
|
|
51
|
+
v?: number;
|
|
52
|
+
type?: string;
|
|
53
|
+
ref?: string;
|
|
54
|
+
taco?: Taco;
|
|
55
|
+
[key: string]: any;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Batch operation entry. */
|
|
59
|
+
export interface BatchOp {
|
|
60
|
+
type: string;
|
|
61
|
+
ref?: string;
|
|
62
|
+
taco?: Taco;
|
|
63
|
+
[key: string]: any;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Server-side client connection — represents one browser tab via SSE. */
|
|
67
|
+
export declare class BwServeClient {
|
|
68
|
+
/** bwserve version string */
|
|
69
|
+
static version: string;
|
|
70
|
+
|
|
71
|
+
/** Unique client identifier */
|
|
72
|
+
id: string;
|
|
73
|
+
|
|
74
|
+
constructor(id: string, res: ServerResponse);
|
|
75
|
+
|
|
76
|
+
/** Mount TACO at selector, replacing contents */
|
|
77
|
+
mount(selector: string, taco: Taco): void;
|
|
78
|
+
|
|
79
|
+
/** @deprecated Use mount() instead */
|
|
80
|
+
render(selector: string, taco: Taco): void;
|
|
81
|
+
|
|
82
|
+
/** Patch element properties without full rebuild */
|
|
83
|
+
patch(ref: string, fields: Record<string, any>): void;
|
|
84
|
+
|
|
85
|
+
/** Append TACO as child of target element */
|
|
86
|
+
append(selector: string, taco: Taco): void;
|
|
87
|
+
|
|
88
|
+
/** Remove element from DOM with cleanup */
|
|
89
|
+
remove(selector: string): void;
|
|
90
|
+
|
|
91
|
+
/** Send array of operations as single batch message */
|
|
92
|
+
batch(ops: BatchOp[]): void;
|
|
93
|
+
|
|
94
|
+
/** Dispatch to component's el.bw[action](data) */
|
|
95
|
+
message(ref: string, action: string, data?: any): void;
|
|
96
|
+
|
|
97
|
+
/** Call registered or built-in client function */
|
|
98
|
+
call(name: string, ...args: any[]): void;
|
|
99
|
+
|
|
100
|
+
/** Subscribe to client topic. Returns this for chaining. */
|
|
101
|
+
listen(topic: string, handler: (data: any) => void): BwServeClient;
|
|
102
|
+
|
|
103
|
+
/** Register handler for client action. Returns this for chaining. */
|
|
104
|
+
on(action: string, handler: (data: any, client: BwServeClient) => void): BwServeClient;
|
|
105
|
+
|
|
106
|
+
/** Close SSE connection */
|
|
107
|
+
close(): void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Main bwserve application — HTTP + SSE server for server-driven UI. */
|
|
111
|
+
export declare class BwServeApp {
|
|
112
|
+
port: number;
|
|
113
|
+
title: string;
|
|
114
|
+
staticDir: string | null;
|
|
115
|
+
injectBitwrench: boolean;
|
|
116
|
+
theme: string | Record<string, string> | null;
|
|
117
|
+
allowScreenshot: boolean;
|
|
118
|
+
dirList: boolean;
|
|
119
|
+
host: string;
|
|
120
|
+
keepAliveInterval: number;
|
|
121
|
+
|
|
122
|
+
/** Number of active client connections */
|
|
123
|
+
readonly clientCount: number;
|
|
124
|
+
|
|
125
|
+
constructor(opts?: BwServeOptions);
|
|
126
|
+
|
|
127
|
+
/** Register a page handler. Returns this for chaining. */
|
|
128
|
+
page(path: string, handler: (client: BwServeClient) => void): BwServeApp;
|
|
129
|
+
|
|
130
|
+
/** Start HTTP server. Returns Promise that resolves when listening. */
|
|
131
|
+
listen(callback?: () => void): Promise<void>;
|
|
132
|
+
|
|
133
|
+
/** Stop server and close all client connections. */
|
|
134
|
+
close(): Promise<void>;
|
|
135
|
+
|
|
136
|
+
/** Send protocol message to all clients (or specific client if msg.clientId is set). Returns count of recipients. */
|
|
137
|
+
broadcast(msg: BwServeMessage): number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Generate a complete HTML shell document for SSE connection. */
|
|
141
|
+
export declare function generateShell(opts?: ShellOptions): string;
|
|
142
|
+
|
|
143
|
+
/** Create a new BwServeApp instance. */
|
|
144
|
+
export declare function create(opts?: BwServeOptions): BwServeApp;
|
|
145
|
+
|
|
146
|
+
/** bwserve version string */
|
|
147
|
+
export declare const version: string;
|
|
148
|
+
|
|
149
|
+
/** Default export */
|
|
150
|
+
declare const bwserve: {
|
|
151
|
+
create: typeof create;
|
|
152
|
+
version: string;
|
|
153
|
+
BwServeApp: typeof BwServeApp;
|
|
154
|
+
BwServeClient: typeof BwServeClient;
|
|
155
|
+
generateShell: typeof generateShell;
|
|
156
|
+
};
|
|
157
|
+
export default bwserve;
|
package/src/bwserve.h
ADDED
package/src/cli/attach.js
CHANGED
|
@@ -29,8 +29,8 @@ Usage:
|
|
|
29
29
|
Description:
|
|
30
30
|
Starts a bwserve instance and waits for a browser to connect via the
|
|
31
31
|
drop-in attach script. Once connected, you get an interactive REPL
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
for inspecting the DOM, mounting components, taking screenshots,
|
|
33
|
+
and listening to events — all from your terminal.
|
|
34
34
|
|
|
35
35
|
To connect a page, either:
|
|
36
36
|
1. Add <script src="http://localhost:<port>/bw/attach.js"></script>
|
|
@@ -44,7 +44,6 @@ Options:
|
|
|
44
44
|
-h, --help Print this help
|
|
45
45
|
|
|
46
46
|
REPL Commands:
|
|
47
|
-
<expression> Evaluate JS in the connected browser (e.g., document.title)
|
|
48
47
|
/help Show command reference
|
|
49
48
|
/quit, /q Exit
|
|
50
49
|
/inspect [selector] [depth] Show DOM tree summary (default: body, depth 3)
|
|
@@ -55,7 +54,6 @@ REPL Commands:
|
|
|
55
54
|
/listen <sel> <event> Start listening for DOM events (e.g., /listen button click)
|
|
56
55
|
/unlisten <sel> <event> Stop listening for a previously added listener
|
|
57
56
|
/clients List connected clients
|
|
58
|
-
/exec <code> Execute JS without capturing return value
|
|
59
57
|
|
|
60
58
|
Examples:
|
|
61
59
|
bwcli attach Start on default port 7902
|
|
@@ -64,8 +62,6 @@ Examples:
|
|
|
64
62
|
bwcli attach -v Verbose mode (shows protocol)
|
|
65
63
|
|
|
66
64
|
# In the REPL:
|
|
67
|
-
bw> document.title
|
|
68
|
-
bw> bw.$('.bw-card').length
|
|
69
65
|
bw> /inspect #app 2
|
|
70
66
|
bw> /screenshot body page.png
|
|
71
67
|
bw> /listen .bw-btn click
|
|
@@ -80,21 +76,7 @@ Examples:
|
|
|
80
76
|
bw> /listen .bw-btn click
|
|
81
77
|
`.trim();
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
* Wrap a JS expression for client.query().
|
|
85
|
-
* Statements (var, let, const, if, for, etc.) are sent as-is.
|
|
86
|
-
* Expressions are wrapped in return() so the result comes back.
|
|
87
|
-
*
|
|
88
|
-
* @param {string} code - User input
|
|
89
|
-
* @returns {string} Code suitable for new Function()
|
|
90
|
-
*/
|
|
91
|
-
export function wrapExpression(code) {
|
|
92
|
-
code = code.trim();
|
|
93
|
-
if (/^(var |let |const |if |for |while |function |try |switch |throw |class |\{)/.test(code)) {
|
|
94
|
-
return code;
|
|
95
|
-
}
|
|
96
|
-
return 'return (' + code + ')';
|
|
97
|
-
}
|
|
79
|
+
// v2.1: wrapExpression and bare JS eval removed — no code-over-wire.
|
|
98
80
|
|
|
99
81
|
/**
|
|
100
82
|
* Run the attach subcommand.
|
|
@@ -160,7 +142,6 @@ export function startAttach(bwserve, opts) {
|
|
|
160
142
|
var app = bwserve.create({
|
|
161
143
|
port: opts.port,
|
|
162
144
|
title: 'bwcli attach',
|
|
163
|
-
allowExec: true,
|
|
164
145
|
allowScreenshot: opts.allowScreenshot
|
|
165
146
|
});
|
|
166
147
|
|
|
@@ -266,33 +247,9 @@ export function startAttach(bwserve, opts) {
|
|
|
266
247
|
return;
|
|
267
248
|
}
|
|
268
249
|
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
safePrompt();
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
var code = wrapExpression(line);
|
|
277
|
-
if (opts.verbose) {
|
|
278
|
-
console.log('[query] ' + code);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
activeClient.query(code, { timeout: 10000 }).then(function(result) {
|
|
282
|
-
if (result !== undefined && result !== null) {
|
|
283
|
-
try {
|
|
284
|
-
console.log(typeof result === 'string' ? result : JSON.stringify(result, null, 2));
|
|
285
|
-
} catch (e) {
|
|
286
|
-
console.log(String(result));
|
|
287
|
-
}
|
|
288
|
-
} else {
|
|
289
|
-
console.log('undefined');
|
|
290
|
-
}
|
|
291
|
-
safePrompt();
|
|
292
|
-
}).catch(function(err) {
|
|
293
|
-
console.error('[error] ' + err.message);
|
|
294
|
-
safePrompt();
|
|
295
|
-
});
|
|
250
|
+
// Non-slash input: show hint (no more bare JS eval)
|
|
251
|
+
console.log('Unknown input. Type /help for available commands.');
|
|
252
|
+
safePrompt();
|
|
296
253
|
});
|
|
297
254
|
|
|
298
255
|
rl.on('close', function() {
|
|
@@ -417,13 +374,13 @@ export function handleSlashCommand(line, activeClient, clients, opts, rl) {
|
|
|
417
374
|
break;
|
|
418
375
|
}
|
|
419
376
|
}
|
|
420
|
-
activeClient.
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
console.error('[error] ' + err.message);
|
|
425
|
-
rl.prompt();
|
|
377
|
+
activeClient.call('_bw_mount', {
|
|
378
|
+
target: mountSel,
|
|
379
|
+
factory: mountComp,
|
|
380
|
+
props: mountProps
|
|
426
381
|
});
|
|
382
|
+
console.log('Mounted ' + mountComp + ' at ' + mountSel);
|
|
383
|
+
rl.prompt();
|
|
427
384
|
break;
|
|
428
385
|
|
|
429
386
|
case '/render':
|
|
@@ -508,22 +465,6 @@ export function handleSlashCommand(line, activeClient, clients, opts, rl) {
|
|
|
508
465
|
rl.prompt();
|
|
509
466
|
break;
|
|
510
467
|
|
|
511
|
-
case '/exec':
|
|
512
|
-
if (!activeClient) {
|
|
513
|
-
console.log('No client connected.');
|
|
514
|
-
rl.prompt();
|
|
515
|
-
break;
|
|
516
|
-
}
|
|
517
|
-
if (parts.length < 2) {
|
|
518
|
-
console.log('Usage: /exec <code>');
|
|
519
|
-
rl.prompt();
|
|
520
|
-
break;
|
|
521
|
-
}
|
|
522
|
-
activeClient.exec(parts.slice(1).join(' '));
|
|
523
|
-
console.log('Executed.');
|
|
524
|
-
rl.prompt();
|
|
525
|
-
break;
|
|
526
|
-
|
|
527
468
|
default:
|
|
528
469
|
console.log('Unknown command: ' + cmd + '. Type /help for available commands.');
|
|
529
470
|
rl.prompt();
|
|
@@ -558,9 +499,6 @@ export function printHelp() {
|
|
|
558
499
|
'',
|
|
559
500
|
'bwcli attach — REPL Commands',
|
|
560
501
|
'',
|
|
561
|
-
' <expression> Evaluate JS in the browser and print result',
|
|
562
|
-
' Examples: document.title, bw.$(".card").length',
|
|
563
|
-
'',
|
|
564
502
|
' /help, /h Show this help',
|
|
565
503
|
' /quit, /q Exit',
|
|
566
504
|
'',
|
|
@@ -577,7 +515,6 @@ export function printHelp() {
|
|
|
577
515
|
' /listen <sel> <event> Listen for DOM events (prints inline)',
|
|
578
516
|
' Example: /listen button click',
|
|
579
517
|
' /unlisten <sel> <event> Remove a listener',
|
|
580
|
-
' /exec <code> Execute JS without capturing return value',
|
|
581
518
|
' /clients List connected clients',
|
|
582
519
|
'',
|
|
583
520
|
' Workflow — build and push a component:',
|
package/src/cli/convert.js
CHANGED
|
@@ -7,7 +7,7 @@ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
|
7
7
|
import { resolve, extname, basename, dirname } from 'node:path';
|
|
8
8
|
import quikdown from '../vendor/quikdown.js';
|
|
9
9
|
import bw from '../bitwrench.js';
|
|
10
|
-
import {
|
|
10
|
+
import { THEME_PRESETS } from '../bitwrench-styles.js';
|
|
11
11
|
import { getInjectionHead, getInjectionBodyEnd } from './inject.js';
|
|
12
12
|
import { makePageLayout } from './layout-default.js';
|
|
13
13
|
|
|
@@ -110,7 +110,7 @@ export function convertFile(inputPath, flags = {}) {
|
|
|
110
110
|
const raw = readFileSync(absInput, 'utf8');
|
|
111
111
|
const ext = extname(absInput).toLowerCase();
|
|
112
112
|
|
|
113
|
-
let bodyHTML
|
|
113
|
+
let bodyHTML;
|
|
114
114
|
let autoTitle = null;
|
|
115
115
|
|
|
116
116
|
// Process based on file extension
|