bitwrench 2.0.16 → 2.0.18

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 (68) hide show
  1. package/README.md +127 -38
  2. package/dist/bitwrench-bccl.cjs.js +13 -9
  3. package/dist/bitwrench-bccl.cjs.min.js +2 -2
  4. package/dist/bitwrench-bccl.esm.js +13 -9
  5. package/dist/bitwrench-bccl.esm.min.js +2 -2
  6. package/dist/bitwrench-bccl.umd.js +13 -9
  7. package/dist/bitwrench-bccl.umd.min.js +2 -2
  8. package/dist/bitwrench-code-edit.cjs.js +1 -1
  9. package/dist/bitwrench-code-edit.cjs.min.js +1 -1
  10. package/dist/bitwrench-code-edit.es5.js +1 -1
  11. package/dist/bitwrench-code-edit.es5.min.js +1 -1
  12. package/dist/bitwrench-code-edit.esm.js +1 -1
  13. package/dist/bitwrench-code-edit.esm.min.js +1 -1
  14. package/dist/bitwrench-code-edit.umd.js +1 -1
  15. package/dist/bitwrench-code-edit.umd.min.js +1 -1
  16. package/dist/bitwrench-lean.cjs.js +1438 -920
  17. package/dist/bitwrench-lean.cjs.min.js +20 -20
  18. package/dist/bitwrench-lean.es5.js +1518 -1105
  19. package/dist/bitwrench-lean.es5.min.js +18 -18
  20. package/dist/bitwrench-lean.esm.js +1437 -920
  21. package/dist/bitwrench-lean.esm.min.js +20 -20
  22. package/dist/bitwrench-lean.umd.js +1438 -920
  23. package/dist/bitwrench-lean.umd.min.js +20 -20
  24. package/dist/bitwrench-util-css.cjs.js +236 -0
  25. package/dist/bitwrench-util-css.cjs.min.js +22 -0
  26. package/dist/bitwrench-util-css.es5.js +414 -0
  27. package/dist/bitwrench-util-css.es5.min.js +21 -0
  28. package/dist/bitwrench-util-css.esm.js +230 -0
  29. package/dist/bitwrench-util-css.esm.min.js +21 -0
  30. package/dist/bitwrench-util-css.umd.js +242 -0
  31. package/dist/bitwrench-util-css.umd.min.js +21 -0
  32. package/dist/bitwrench.cjs.js +1450 -928
  33. package/dist/bitwrench.cjs.min.js +21 -21
  34. package/dist/bitwrench.css +456 -132
  35. package/dist/bitwrench.es5.js +1563 -1140
  36. package/dist/bitwrench.es5.min.js +19 -19
  37. package/dist/bitwrench.esm.js +1450 -929
  38. package/dist/bitwrench.esm.min.js +21 -21
  39. package/dist/bitwrench.min.css +1 -1
  40. package/dist/bitwrench.umd.js +1450 -928
  41. package/dist/bitwrench.umd.min.js +21 -21
  42. package/dist/builds.json +178 -90
  43. package/dist/bwserve.cjs.js +528 -68
  44. package/dist/bwserve.esm.js +527 -69
  45. package/dist/sri.json +44 -36
  46. package/package.json +5 -2
  47. package/readme.html +136 -49
  48. package/src/bitwrench-bccl.js +12 -8
  49. package/src/bitwrench-color-utils.js +31 -9
  50. package/src/bitwrench-esm-entry.js +11 -0
  51. package/src/bitwrench-styles.js +439 -232
  52. package/src/bitwrench-util-css.js +229 -0
  53. package/src/bitwrench.js +979 -630
  54. package/src/bwserve/attach.js +57 -0
  55. package/src/bwserve/bwclient.js +141 -0
  56. package/src/bwserve/bwshell.js +102 -0
  57. package/src/bwserve/client.js +151 -1
  58. package/src/bwserve/index.js +139 -29
  59. package/src/cli/attach.js +555 -0
  60. package/src/cli/convert.js +2 -5
  61. package/src/cli/index.js +7 -0
  62. package/src/cli/inject.js +1 -1
  63. package/src/cli/layout-default.js +47 -32
  64. package/src/cli/serve.js +6 -2
  65. package/src/generate-css.js +11 -4
  66. package/src/vendor/html2canvas.min.js +20 -0
  67. package/src/version.js +3 -3
  68. package/src/bwserve/shell.js +0 -103
@@ -1,9 +1,16 @@
1
- /*! bwserve v2.0.16 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bwserve v2.0.18 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
2
2
  import { fileURLToPath } from 'url';
3
3
  import { dirname, resolve, join, extname } from 'path';
4
4
  import { createServer } from 'http';
5
5
  import { existsSync, statSync, readFileSync } from 'fs';
6
6
 
7
+ /**
8
+ * Auto-generated version file from package.json
9
+ * DO NOT EDIT DIRECTLY - Use npm run generate-version
10
+ */
11
+
12
+ const VERSION = '2.0.18';
13
+
7
14
  /**
8
15
  * BwServeClient — per-client connection for bwserve.
9
16
  *
@@ -23,15 +30,19 @@ import { existsSync, statSync, readFileSync } from 'fs';
23
30
  * @module bwserve/client
24
31
  */
25
32
 
33
+
26
34
  /**
27
35
  * BwServeClient — one connected browser tab.
28
36
  */
29
37
  class BwServeClient {
38
+ /** bwserve version (from package.json) */
39
+ static version = VERSION;
30
40
  constructor(id, res) {
31
41
  this.id = id;
32
42
  this._res = res; // SSE response stream (null in stub)
33
43
  this._handlers = {}; // action name → handler
34
44
  this._closed = false;
45
+ this._pending = {}; // requestId → { resolve, reject, timer }
35
46
  }
36
47
 
37
48
  /**
@@ -110,7 +121,7 @@ class BwServeClient {
110
121
  /**
111
122
  * Call a previously registered or built-in function on the client.
112
123
  *
113
- * Built-in functions (always available, no registration needed):
124
+ * Built-in functions (registered by bwclient on connection):
114
125
  * scrollTo, focus, download, clipboard, redirect, log
115
126
  *
116
127
  * @param {string} name - Function name (registered or built-in)
@@ -173,6 +184,151 @@ class BwServeClient {
173
184
  }
174
185
  }
175
186
 
187
+ // ── Pending promise mechanism ──
188
+
189
+ /**
190
+ * Create a pending promise with a unique requestId and timeout.
191
+ *
192
+ * @param {number} [timeout=10000] - Timeout in ms
193
+ * @returns {{ requestId: string, promise: Promise }}
194
+ * @private
195
+ */
196
+ _pend(timeout) {
197
+ var self = this;
198
+ timeout = timeout || 10000;
199
+ var requestId = 'req_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
200
+
201
+ var promise = new Promise(function(resolve, reject) {
202
+ var timer = setTimeout(function() {
203
+ delete self._pending[requestId];
204
+ reject(new Error('Request timeout after ' + timeout + 'ms'));
205
+ }, timeout);
206
+
207
+ self._pending[requestId] = { resolve: resolve, reject: reject, timer: timer };
208
+ });
209
+
210
+ return { requestId: requestId, promise: promise };
211
+ }
212
+
213
+ /**
214
+ * Resolve a pending promise by requestId.
215
+ * Called by the server route handler when a POST-back arrives.
216
+ *
217
+ * @param {string} requestId
218
+ * @param {Object} data - Response data (may contain .error)
219
+ * @returns {boolean} true if a pending request was found and resolved
220
+ * @private
221
+ */
222
+ _resolvePending(requestId, data) {
223
+ var pending = this._pending[requestId];
224
+ if (!pending) return false;
225
+
226
+ clearTimeout(pending.timer);
227
+ delete this._pending[requestId];
228
+
229
+ if (data.error) {
230
+ pending.reject(new Error(data.error));
231
+ } else {
232
+ pending.resolve(data.result !== undefined ? data.result : data);
233
+ }
234
+ return true;
235
+ }
236
+
237
+ // ── Query ──
238
+
239
+ /**
240
+ * Execute code on the client and get the result back.
241
+ *
242
+ * @param {string} code - JavaScript code to evaluate (return value is sent back)
243
+ * @param {Object} [options]
244
+ * @param {number} [options.timeout=5000] - Timeout in ms
245
+ * @returns {Promise<*>} The result of evaluating the code
246
+ */
247
+ query(code, options) {
248
+ var opts = options || {};
249
+ var pend = this._pend(opts.timeout || 5000);
250
+ this.call('_bw_query', { code: code, requestId: pend.requestId });
251
+ return pend.promise;
252
+ }
253
+
254
+ // ── Mount ──
255
+
256
+ /**
257
+ * Mount a BCCL component or factory function on the client.
258
+ *
259
+ * @param {string} selector - CSS selector of target element
260
+ * @param {string} factory - BCCL component name (e.g. 'accordion') or JS factory code
261
+ * @param {Object} [props] - Props to pass to the component/factory
262
+ * @param {Object} [options]
263
+ * @param {number} [options.timeout=10000] - Timeout in ms
264
+ * @returns {Promise<Object>} Resolves with { mounted: true } on success
265
+ */
266
+ mount(selector, factory, props, options) {
267
+ var opts = options || {};
268
+ var pend = this._pend(opts.timeout || 10000);
269
+ this.call('_bw_mount', {
270
+ target: selector,
271
+ factory: factory,
272
+ props: props || {},
273
+ requestId: pend.requestId
274
+ });
275
+ return pend.promise;
276
+ }
277
+
278
+ // ── Screenshot ──
279
+
280
+ /**
281
+ * Capture a screenshot of the client's page or a specific element.
282
+ *
283
+ * Requires the server to be created with `{ allowScreenshot: true }`.
284
+ * Uses html2canvas on the client side (lazy-loaded on first call).
285
+ *
286
+ * @param {string} [selector='body'] - CSS selector of element to capture
287
+ * @param {Object} [options]
288
+ * @param {string} [options.format='png'] - 'png' or 'jpeg'
289
+ * @param {number} [options.quality=0.85] - JPEG quality 0-1 (ignored for PNG)
290
+ * @param {number} [options.maxWidth] - Resize if wider (preserves aspect ratio)
291
+ * @param {number} [options.maxHeight] - Resize if taller (preserves aspect ratio)
292
+ * @param {number} [options.scale=1] - Device pixel ratio override
293
+ * @param {number} [options.timeout=10000] - Reject after ms
294
+ * @returns {Promise<Object>} { data: Buffer, width, height, format }
295
+ */
296
+ screenshot(selector, options) {
297
+ var self = this;
298
+ var opts = options || {};
299
+ var timeout = opts.timeout || 10000;
300
+
301
+ if (!self._allowScreenshot) {
302
+ return Promise.reject(new Error('Screenshot not enabled. Set allowScreenshot: true in server options.'));
303
+ }
304
+
305
+ var pend = self._pend(timeout);
306
+
307
+ // Call the bwclient-registered capture function
308
+ self.call('_bw_screenshot', {
309
+ requestId: pend.requestId,
310
+ selector: selector || 'body',
311
+ format: opts.format || 'png',
312
+ quality: opts.quality || 0.85,
313
+ maxWidth: opts.maxWidth || null,
314
+ maxHeight: opts.maxHeight || null,
315
+ scale: opts.scale || 1,
316
+ captureUrl: '/bw/lib/vendor/html2canvas.min.js'
317
+ });
318
+
319
+ // Transform the raw response into { data: Buffer, width, height, format }
320
+ return pend.promise.then(function(result) {
321
+ if (!result || !result.data) return result;
322
+ var base64 = result.data.split(',')[1];
323
+ return {
324
+ data: Buffer.from(base64, 'base64'),
325
+ width: result.width,
326
+ height: result.height,
327
+ format: result.format
328
+ };
329
+ });
330
+ }
331
+
176
332
  /**
177
333
  * Dispatch an incoming action from the client.
178
334
  * @private
@@ -187,20 +343,161 @@ class BwServeClient {
187
343
  }
188
344
  }
189
345
 
346
+ /**
347
+ * bwclient.js — Browser-side protocol client for bwserve.
348
+ *
349
+ * Injected inline by bwshell. Requires window.bw (bitwrench loaded first).
350
+ * NOT bundled into bitwrench dist — this is a bwserve runtime asset.
351
+ *
352
+ * Responsibilities:
353
+ * - SSE connection lifecycle (connect, reconnect, status)
354
+ * - Unified POST-back via /bw/return/<route>/<clientId>
355
+ * - Register built-in client functions (scrollTo, focus, etc.)
356
+ * - data-bw-action click/key delegation
357
+ * - Attach mode for remote-controlling any bitwrench page
358
+ *
359
+ * @module bwserve/bwclient
360
+ */
361
+
362
+
363
+ /**
364
+ * Return the bwclient source as a string for inline injection into the shell.
365
+ * The version is embedded at serve-time from package.json via version.js.
366
+ * @returns {string} JavaScript source code
367
+ */
368
+ function getBwClientSource() {
369
+ return BWCLIENT_SOURCE.replace('__BW_VERSION__', VERSION);
370
+ }
371
+
372
+ var BWCLIENT_SOURCE = '(function(bw) {\n'
373
+ + ' "use strict";\n'
374
+ + ' if (!bw) return;\n'
375
+ + '\n'
376
+ + ' var _client = {\n'
377
+ + ' id: null,\n'
378
+ + ' version: "__BW_VERSION__",\n'
379
+ + ' status: "idle",\n'
380
+ + ' _es: null\n'
381
+ + ' };\n'
382
+ + '\n'
383
+ + ' // ── Unified POST-back ──\n'
384
+ + ' _client.respond = function(route, requestId, result, error) {\n'
385
+ + ' fetch("/bw/return/" + route + "/" + _client.id, {\n'
386
+ + ' method: "POST",\n'
387
+ + ' headers: { "Content-Type": "application/json" },\n'
388
+ + ' body: JSON.stringify({ requestId: requestId, route: route, result: result, error: error || null })\n'
389
+ + ' }).catch(function() {});\n'
390
+ + ' };\n'
391
+ + '\n'
392
+ + ' // ── SSE connect ──\n'
393
+ + ' _client.connect = function(url, opts) {\n'
394
+ + ' opts = opts || {};\n'
395
+ + ' var onStatus = opts.onStatus || function() {};\n'
396
+ + ' function setStatus(s) { _client.status = s; onStatus(s); }\n'
397
+ + ' setStatus("connecting");\n'
398
+ + ' if (typeof EventSource === "undefined") return;\n'
399
+ + ' var es = new EventSource(url);\n'
400
+ + ' _client._es = es;\n'
401
+ + ' es.onopen = function() { setStatus("connected"); };\n'
402
+ + ' es.onmessage = function(e) {\n'
403
+ + ' try {\n'
404
+ + ' var msg = typeof e.data === "string" ? bw.parseJSONFlex(e.data) : e.data;\n'
405
+ + ' bw.apply(msg);\n'
406
+ + ' } catch (err) {\n'
407
+ + ' if (typeof console !== "undefined") console.error("[bwclient]", err);\n'
408
+ + ' }\n'
409
+ + ' };\n'
410
+ + ' es.onerror = function() {\n'
411
+ + ' if (_client.status === "connected") setStatus("disconnected");\n'
412
+ + ' };\n'
413
+ + ' };\n'
414
+ + '\n'
415
+ + ' // ── Attach mode ──\n'
416
+ + ' _client.attach = function(url, opts) {\n'
417
+ + ' opts = opts || {};\n'
418
+ + ' _client.id = opts.clientId || "att_" + Math.random().toString(36).slice(2, 10);\n'
419
+ + ' if (opts.allowExec) bw._allowExec = true;\n'
420
+ + ' _client._registerBuiltins();\n'
421
+ + ' _client._wireActions();\n'
422
+ + ' _client.connect(url + "/bw/events/" + _client.id, opts);\n'
423
+ + ' };\n'
424
+ + '\n'
425
+ + ' // ── Send action to server ──\n'
426
+ + ' _client.sendAction = function(action, data) {\n'
427
+ + ' _client.respond("action", null, { action: action, data: data || {} });\n'
428
+ + ' };\n'
429
+ + '\n'
430
+ + ' // ── Register built-in functions ──\n'
431
+ + ' _client._registerBuiltins = function() {\n'
432
+ + ' var builtins = {\n'
433
+ + ' scrollTo: "function(sel){var el=bw._el(sel);if(el)el.scrollTop=el.scrollHeight;}",\n'
434
+ + ' focus: "function(sel){var el=bw._el(sel);if(el&&typeof el.focus===\\"function\\")el.focus();}",\n'
435
+ + ' download: "function(fn,c,m){if(typeof document===\\"undefined\\")return;var b=new Blob([c],{type:m||\\"text/plain\\"});var a=document.createElement(\\"a\\");a.href=URL.createObjectURL(b);a.download=fn;a.click();URL.revokeObjectURL(a.href);}",\n'
436
+ + ' clipboard: "function(t){if(typeof navigator!==\\"undefined\\"&&navigator.clipboard)navigator.clipboard.writeText(t);}",\n'
437
+ + ' redirect: "function(u){if(typeof window!==\\"undefined\\")window.location.href=u;}",\n'
438
+ + ' log: "function(){console.log.apply(console,arguments);}",\n'
439
+ + ' _bw_query: "function(opts){if(!bw._bwClient)return;try{var r=new Function(opts.code)();if(r&&typeof r.then===\\"function\\"){r.then(function(v){bw._bwClient.respond(\\"query\\",opts.requestId,v);}).catch(function(e){bw._bwClient.respond(\\"query\\",opts.requestId,null,e.message);});}else{bw._bwClient.respond(\\"query\\",opts.requestId,r);}}catch(e){bw._bwClient.respond(\\"query\\",opts.requestId,null,e.message);}}",\n'
440
+ + ' _bw_mount: "function(opts){if(!bw._bwClient)return;try{var taco;var f=opts.factory;var n=f.replace(/-([a-z])/g,function(_,c){return c.toUpperCase();});if(bw.BCCL&&bw.BCCL[n]){taco=bw.make(n,opts.props||{});}else if(bw._allowExec){taco=new Function(\\"props\\",f)(opts.props||{});}else{throw new Error(\\"Unknown component and allowExec disabled\\");}bw.DOM(opts.target,taco);bw._bwClient.respond(\\"mount\\",opts.requestId,{mounted:true});}catch(e){bw._bwClient.respond(\\"mount\\",opts.requestId,null,e.message);}}",\n'
441
+ + ' _bw_screenshot: "function(opts){if(!bw._bwClient)return;var sel=opts.selector||\\"body\\";var el=document.querySelector(sel);if(!el){bw._bwClient.respond(\\"screenshot\\",opts.requestId,null,\\"Element not found: \\"+sel);return;}function _ls(url){return new Promise(function(res,rej){var s=document.createElement(\\"script\\");s.src=url;s.onload=function(){res(window.html2canvas);};s.onerror=function(){rej(new Error(\\"Failed to load html2canvas\\"));};document.head.appendChild(s);});}var p=window.html2canvas?Promise.resolve(window.html2canvas):_ls(opts.captureUrl||\\"/bw/lib/vendor/html2canvas.min.js\\");p.then(function(h2c){return h2c(el,{scale:opts.scale||1,useCORS:true});}).then(function(canvas){var out=canvas;var mw=opts.maxWidth;var mh=opts.maxHeight;if((mw&&canvas.width>mw)||(mh&&canvas.height>mh)){var sw=mw?mw/canvas.width:1;var sh=mh?mh/canvas.height:1;var sc=Math.min(sw,sh);out=document.createElement(\\"canvas\\");out.width=Math.round(canvas.width*sc);out.height=Math.round(canvas.height*sc);out.getContext(\\"2d\\").drawImage(canvas,0,0,out.width,out.height);}var fmt=opts.format===\\"jpeg\\"?\\"image/jpeg\\":\\"image/png\\";var q=opts.format===\\"jpeg\\"?(opts.quality||0.85):undefined;var dataUrl=out.toDataURL(fmt,q);bw._bwClient.respond(\\"screenshot\\",opts.requestId,{data:dataUrl,width:out.width,height:out.height,format:opts.format||\\"png\\"});}).catch(function(err){bw._bwClient.respond(\\"screenshot\\",opts.requestId,null,err.message||String(err));});}",\n'
442
+ + ' _bw_tree: "function(opts){if(!bw._bwClient)return;var sel=opts.selector||\\"body\\";var depth=opts.depth||3;function walk(el,d){if(!el||d>depth)return null;var info={tag:el.tagName?el.tagName.toLowerCase():\\"#text\\"};if(el.id)info.id=el.id;if(el.className&&typeof el.className===\\"string\\")info.cls=el.className.split(\\" \\").slice(0,5).join(\\" \\");if(el.children&&el.children.length>0&&d<depth){info.children=[];for(var i=0;i<Math.min(el.children.length,20);i++){var c=walk(el.children[i],d+1);if(c)info.children.push(c);}}return info;}var root=document.querySelector(sel);bw._bwClient.respond(\\"query\\",opts.requestId,walk(root,0));}",\n'
443
+ + ' _bw_listen: "function(opts){if(!bw._bwClient)return;if(!bw._bwClient._listeners)bw._bwClient._listeners={};var key=opts.selector+\\":::\\"+opts.event;if(bw._bwClient._listeners[key])return;var fn=function(e){var el=e.target.closest?e.target.closest(opts.selector):null;if(!el)return;bw._bwClient.respond(\\"event\\",null,{event:opts.event,selector:opts.selector,tagName:el.tagName,id:el.id||null,text:(el.textContent||\\"\\").slice(0,100)});};document.addEventListener(opts.event,fn,true);bw._bwClient._listeners[key]={fn:fn,event:opts.event};}",\n'
444
+ + ' _bw_unlisten: "function(opts){if(!bw._bwClient||!bw._bwClient._listeners)return;var key=opts.selector+\\":::\\"+opts.event;var entry=bw._bwClient._listeners[key];if(!entry)return;document.removeEventListener(entry.event,entry.fn,true);delete bw._bwClient._listeners[key];}"\n'
445
+ + ' };\n'
446
+ + ' Object.keys(builtins).forEach(function(name) {\n'
447
+ + ' bw.apply({ type: "register", name: name, body: builtins[name] });\n'
448
+ + ' });\n'
449
+ + ' };\n'
450
+ + '\n'
451
+ + ' // ── Wire up data-bw-action click delegation ──\n'
452
+ + ' _client._wireActions = function() {\n'
453
+ + ' document.addEventListener("click", function(e) {\n'
454
+ + ' var el = e.target.closest ? e.target.closest("[data-bw-action]") : null;\n'
455
+ + ' if (!el) return;\n'
456
+ + ' e.preventDefault();\n'
457
+ + ' var actionData = {};\n'
458
+ + ' if (el.getAttribute("data-bw-id")) actionData.bwId = el.getAttribute("data-bw-id");\n'
459
+ + ' var form = el.closest("div") || document;\n'
460
+ + ' var inp = form.querySelector("input[type=text],input:not([type])");\n'
461
+ + ' if (inp) { actionData.inputValue = inp.value; inp.value = ""; }\n'
462
+ + ' _client.sendAction(el.getAttribute("data-bw-action"), actionData);\n'
463
+ + ' });\n'
464
+ + ' document.addEventListener("keydown", function(e) {\n'
465
+ + ' if (e.key === "Enter" && e.target.tagName === "INPUT") {\n'
466
+ + ' var form = e.target.closest("div") || document;\n'
467
+ + ' var btn = form.querySelector("[data-bw-action]");\n'
468
+ + ' if (btn) {\n'
469
+ + ' _client.sendAction(btn.getAttribute("data-bw-action"), { inputValue: e.target.value });\n'
470
+ + ' e.target.value = "";\n'
471
+ + ' }\n'
472
+ + ' }\n'
473
+ + ' });\n'
474
+ + ' };\n'
475
+ + '\n'
476
+ + ' // ── Event delegation helper ──\n'
477
+ + ' _client.listen = function(selector, event, action) {\n'
478
+ + ' document.addEventListener(event, function(e) {\n'
479
+ + ' var el = e.target.closest ? e.target.closest(selector) : null;\n'
480
+ + ' if (el) _client.sendAction(action, { selector: selector, event: event });\n'
481
+ + ' });\n'
482
+ + ' };\n'
483
+ + '\n'
484
+ + ' bw._bwClient = _client;\n'
485
+ + '})(window.bw);\n';
486
+
190
487
  /**
191
488
  * bwserve shell — generates the HTML page shell served to browsers.
192
489
  *
193
490
  * The shell is a minimal HTML doc that:
194
- * - Loads bitwrench UMD + CSS from /__bw/ routes
195
- * - Calls bw.loadDefaultStyles()
196
- * - Optionally applies a theme
491
+ * - Loads bitwrench UMD + CSS from /bw/lib/ routes
492
+ * - Calls bw.loadStyles()
493
+ * - Optionally applies a custom theme
197
494
  * - Creates a #app div
198
- * - Opens an SSE connection via bw.clientConnect()
199
- * - Delegates data-bw-action clicks to the server via POST
495
+ * - Inlines bwclient.js for SSE, action delegation, and built-ins
200
496
  *
201
- * @module bwserve/shell
497
+ * @module bwserve/bwshell
202
498
  */
203
499
 
500
+
204
501
  /**
205
502
  * Generate the shell HTML page for a bwserve app.
206
503
  *
@@ -209,6 +506,7 @@ class BwServeClient {
209
506
  * @param {string} [opts.title='bwserve'] - Page title
210
507
  * @param {string} [opts.theme] - Theme preset name or config
211
508
  * @param {boolean} [opts.injectBitwrench=true] - Whether to inject bitwrench scripts
509
+ * @param {boolean} [opts.allowExec=false] - Enable exec message type
212
510
  * @returns {string} Complete HTML document
213
511
  */
214
512
  function generateShell(opts) {
@@ -223,12 +521,13 @@ function generateShell(opts) {
223
521
  '<head>',
224
522
  '<meta charset="UTF-8">',
225
523
  '<meta name="viewport" content="width=device-width, initial-scale=1.0">',
226
- '<title>' + title + '</title>'
524
+ '<title>' + title + '</title>',
525
+ '<meta name="generator" content="bwserve ' + VERSION + '">'
227
526
  ];
228
527
 
229
528
  if (inject) {
230
- head.push('<script src="/__bw/bitwrench.umd.js"></script>');
231
- head.push('<link rel="stylesheet" href="/__bw/bitwrench.css">');
529
+ head.push('<script src="/bw/lib/bitwrench.umd.js"></script>');
530
+ head.push('<link rel="stylesheet" href="/bw/lib/bitwrench.css">');
232
531
  }
233
532
 
234
533
  head.push('</head>');
@@ -239,58 +538,109 @@ function generateShell(opts) {
239
538
  '<script>',
240
539
  '(function() {',
241
540
  ' "use strict";',
242
- ' bw.loadDefaultStyles();'
541
+ ' bw.loadStyles();'
243
542
  ];
244
543
 
245
544
  if (opts.theme) {
246
- script.push(' bw.generateTheme("bwserve", ' + JSON.stringify(
545
+ script.push(' bw.loadStyles(' + JSON.stringify(
247
546
  typeof opts.theme === 'string'
248
547
  ? { primary: '#006666', secondary: '#333333' }
249
548
  : opts.theme
250
549
  ) + ');');
251
550
  }
252
551
 
552
+ script.push('})();');
553
+ script.push('</script>');
554
+
555
+ // Inline bwclient.js
556
+ script.push('<script>');
557
+ script.push(getBwClientSource());
558
+ script.push('</script>');
559
+
560
+ // Init script: wire up bwclient
561
+ script.push('<script>');
562
+ script.push('(function() {');
563
+ script.push(' "use strict";');
253
564
  script.push(' var clientId = ' + JSON.stringify(clientId) + ';');
254
- script.push(' var conn = bw.clientConnect("/__bw/events/" + clientId, {');
255
- script.push(' actionUrl: "/__bw/action/" + clientId,');
565
+ if (opts.allowExec) {
566
+ script.push(' bw._allowExec = true;');
567
+ }
568
+ script.push(' bw._bwClient.id = clientId;');
569
+ script.push(' bw._bwClient._registerBuiltins();');
570
+ script.push(' bw._bwClient._wireActions();');
571
+ script.push(' bw._bwClient.connect("/bw/events/" + clientId, {');
256
572
  script.push(' onStatus: function(s) {');
257
573
  script.push(' if (typeof console !== "undefined") console.log("[bwserve] " + s);');
258
574
  script.push(' }');
259
575
  script.push(' });');
260
-
261
- // data-bw-action click delegation
262
- script.push(' document.addEventListener("click", function(e) {');
263
- script.push(' var el = e.target.closest ? e.target.closest("[data-bw-action]") : null;');
264
- script.push(' if (!el) return;');
265
- script.push(' e.preventDefault();');
266
- script.push(' var actionData = {};');
267
- script.push(' if (el.getAttribute("data-bw-id")) actionData.bwId = el.getAttribute("data-bw-id");');
268
- script.push(' var form = el.closest("div") || document;');
269
- script.push(' var inp = form.querySelector("input[type=text],input:not([type])");');
270
- script.push(' if (inp) { actionData.inputValue = inp.value; inp.value = ""; }');
271
- script.push(' conn.sendAction(el.getAttribute("data-bw-action"), actionData);');
272
- script.push(' });');
273
-
274
- // Enter key on inputs
275
- script.push(' document.addEventListener("keydown", function(e) {');
276
- script.push(' if (e.key === "Enter" && e.target.tagName === "INPUT") {');
277
- script.push(' var form = e.target.closest("div") || document;');
278
- script.push(' var btn = form.querySelector("[data-bw-action]");');
279
- script.push(' if (btn) {');
280
- script.push(' conn.sendAction(btn.getAttribute("data-bw-action"), { inputValue: e.target.value });');
281
- script.push(' e.target.value = "";');
282
- script.push(' }');
283
- script.push(' }');
284
- script.push(' });');
285
-
286
576
  script.push('})();');
287
577
  script.push('</script>');
578
+
288
579
  script.push('</body>');
289
580
  script.push('</html>');
290
581
 
291
582
  return head.concat(script).join('\n');
292
583
  }
293
584
 
585
+ /** bwshell version (from package.json) */
586
+ generateShell.version = VERSION;
587
+
588
+ /**
589
+ * bwserve attach — self-contained drop-in script generator.
590
+ *
591
+ * Generates JS that loads bitwrench + bwclient and auto-connects
592
+ * to a bwserve instance. When loaded in any browser page, it
593
+ * establishes an SSE connection for remote debugging.
594
+ *
595
+ * Usage:
596
+ * <script src="http://localhost:7902/bw/attach.js"></script>
597
+ *
598
+ * @module bwserve/attach
599
+ */
600
+
601
+
602
+ /**
603
+ * Generate the self-contained attach script.
604
+ *
605
+ * The returned JS string, when evaluated in a browser:
606
+ * 1. Checks if bw is already loaded; if not, injects bitwrench UMD
607
+ * 2. Evaluates bwclient source to set up bw._bwClient
608
+ * 3. Calls bw._bwClient.attach() to connect via SSE
609
+ *
610
+ * @param {Object} [opts]
611
+ * @param {string} [opts.origin=''] - Server origin (empty = same origin)
612
+ * @returns {string} JavaScript source code
613
+ */
614
+ function generateAttachScript(opts) {
615
+ opts = opts || {};
616
+ var origin = opts.origin || '';
617
+
618
+ var clientSource = getBwClientSource();
619
+
620
+ return '(function() {\n'
621
+ + ' "use strict";\n'
622
+ + ' var origin = ' + JSON.stringify(origin) + ';\n'
623
+ + ' function _go() {\n'
624
+ + ' ' + clientSource + '\n'
625
+ + ' bw._bwClient.attach(origin, {\n'
626
+ + ' allowExec: true,\n'
627
+ + ' onStatus: function(s) { console.log("[bw-attach] " + s); }\n'
628
+ + ' });\n'
629
+ + ' console.log("[bw-attach] v' + VERSION + ' connecting to " + (origin || location.origin));\n'
630
+ + ' }\n'
631
+ + ' if (window.bw) { _go(); return; }\n'
632
+ + ' var s = document.createElement("script");\n'
633
+ + ' s.src = (origin || "") + "/bw/lib/bitwrench.umd.js";\n'
634
+ + ' s.onload = function() {\n'
635
+ + ' if (typeof bw !== "undefined" && bw.loadStyles) bw.loadStyles();\n'
636
+ + ' _go();\n'
637
+ + ' };\n'
638
+ + ' document.head.appendChild(s);\n'
639
+ + '})();\n';
640
+ }
641
+
642
+ generateAttachScript.version = VERSION;
643
+
294
644
  /**
295
645
  * bwserve — Server-driven UI library for bitwrench
296
646
  *
@@ -311,7 +661,16 @@ function generateShell(opts) {
311
661
 
312
662
 
313
663
  var __dirname$1 = dirname(fileURLToPath(import.meta.url));
664
+
665
+ // Resolve dist/ — try source layout (src/bwserve/), then npm install layout,
666
+ // then dist/ itself (when running from dist/bwserve.esm.js)
314
667
  var DIST_DIR = resolve(__dirname$1, '..', '..', 'dist');
668
+ if (!existsSync(DIST_DIR)) {
669
+ DIST_DIR = resolve(__dirname$1, '..', 'dist');
670
+ }
671
+ if (!existsSync(DIST_DIR)) {
672
+ DIST_DIR = __dirname$1;
673
+ }
315
674
 
316
675
  // MIME type lookup for static file serving
317
676
  var MIME_TYPES = {
@@ -340,6 +699,7 @@ var MIME_TYPES = {
340
699
  * @param {string} [opts.static] - Directory to serve static files from
341
700
  * @param {boolean} [opts.injectBitwrench=true] - Auto-inject bitwrench client JS
342
701
  * @param {string|Object} [opts.theme] - Theme preset name or config object
702
+ * @param {boolean} [opts.allowScreenshot=false] - Enable client.screenshot() capability
343
703
  * @returns {BwServeApp} Application instance
344
704
  */
345
705
  function create(opts) {
@@ -358,11 +718,13 @@ class BwServeApp {
358
718
  this.staticDir = opts.static || null;
359
719
  this.injectBitwrench = opts.injectBitwrench !== false;
360
720
  this.theme = opts.theme || null;
721
+ this.allowExec = opts.allowExec || false;
722
+ this.allowScreenshot = opts.allowScreenshot || false;
361
723
  this.keepAliveInterval = opts.keepAliveInterval || 15000;
362
724
  this._pages = new Map();
363
725
  this._clients = new Map();
364
- this._server = null;
365
726
  this._clientCounter = 0;
727
+ this._server = null;
366
728
  }
367
729
 
368
730
  /**
@@ -470,31 +832,61 @@ class BwServeApp {
470
832
  // Parse URL path (strip query string)
471
833
  var path = url.split('?')[0];
472
834
 
473
- // /__bw/bitwrench.umd.js — serve bitwrench client library
474
- if (path === '/__bw/bitwrench.umd.js' && method === 'GET') {
835
+ // /bw/attach.js — self-contained attach script for remote debugging
836
+ if (path === '/bw/attach.js' && method === 'GET') {
837
+ return this._serveAttachScript(req, res);
838
+ }
839
+
840
+ // /bw/lib/bitwrench.umd.js — serve bitwrench client library
841
+ if (path === '/bw/lib/bitwrench.umd.js' && method === 'GET') {
475
842
  return this._serveDistFile(res, 'bitwrench.umd.js');
476
843
  }
477
844
 
478
- // /__bw/bitwrench.umd.min.js — serve minified
479
- if (path === '/__bw/bitwrench.umd.min.js' && method === 'GET') {
845
+ // /bw/lib/bitwrench.umd.min.js — serve minified
846
+ if (path === '/bw/lib/bitwrench.umd.min.js' && method === 'GET') {
480
847
  return this._serveDistFile(res, 'bitwrench.umd.min.js');
481
848
  }
482
849
 
483
- // /__bw/bitwrench.css — serve bitwrench CSS
484
- if (path === '/__bw/bitwrench.css' && method === 'GET') {
850
+ // /bw/lib/bitwrench.css — serve bitwrench CSS
851
+ if (path === '/bw/lib/bitwrench.css' && method === 'GET') {
485
852
  return this._serveDistFile(res, 'bitwrench.css');
486
853
  }
487
854
 
488
- // /__bw/events/:clientId — SSE stream
489
- if (path.startsWith('/__bw/events/') && method === 'GET') {
490
- var clientId = path.slice('/__bw/events/'.length);
855
+ // /bw/events/:clientId — SSE stream
856
+ if (path.startsWith('/bw/events/') && method === 'GET') {
857
+ var clientId = path.slice('/bw/events/'.length);
491
858
  return this._handleSSE(req, res, clientId);
492
859
  }
493
860
 
494
- // /__bw/action/:clientId action POST
495
- if (path.startsWith('/__bw/action/') && method === 'POST') {
496
- var actionClientId = path.slice('/__bw/action/'.length);
497
- return this._handleAction(req, res, actionClientId);
861
+ // CORS preflight for /bw/return/ (needed for cross-origin attach)
862
+ if (method === 'OPTIONS' && path.startsWith('/bw/return/')) {
863
+ res.writeHead(204, {
864
+ 'Access-Control-Allow-Origin': '*',
865
+ 'Access-Control-Allow-Methods': 'POST',
866
+ 'Access-Control-Allow-Headers': 'Content-Type'
867
+ });
868
+ res.end();
869
+ return;
870
+ }
871
+
872
+ // /bw/return/<route>/<clientId> — unified return channel
873
+ if (method === 'POST' && path.startsWith('/bw/return/')) {
874
+ var rest = path.slice('/bw/return/'.length);
875
+ var slash = rest.indexOf('/');
876
+ if (slash === -1) {
877
+ res.writeHead(400, { 'Content-Type': 'application/json' });
878
+ res.end(JSON.stringify({ error: 'Invalid return path' }));
879
+ return;
880
+ }
881
+ var route = rest.slice(0, slash);
882
+ var returnClientId = rest.slice(slash + 1);
883
+ return this._handleReturn(req, res, route, returnClientId);
884
+ }
885
+
886
+ // /bw/lib/vendor/:filename — serve vendored libraries (allowlisted)
887
+ if (path.startsWith('/bw/lib/vendor/') && method === 'GET') {
888
+ var vendorFile = path.slice('/bw/lib/vendor/'.length);
889
+ return this._serveVendorFile(res, vendorFile);
498
890
  }
499
891
 
500
892
  // Registered page routes — serve shell HTML
@@ -504,7 +896,8 @@ class BwServeApp {
504
896
  clientId: clientId2,
505
897
  title: this.title,
506
898
  theme: this.theme,
507
- injectBitwrench: this.injectBitwrench
899
+ injectBitwrench: this.injectBitwrench,
900
+ allowExec: this.allowExec
508
901
  });
509
902
  // Store the page path for this client so SSE knows which handler to call
510
903
  this._clients.set(clientId2, { pagePath: path, client: null });
@@ -569,6 +962,7 @@ class BwServeApp {
569
962
 
570
963
  // Create client instance
571
964
  var client = new BwServeClient(clientId, res);
965
+ client._allowScreenshot = this.allowScreenshot;
572
966
 
573
967
  // Look up the pending client record (set during page serve)
574
968
  var pending = self._clients.get(clientId);
@@ -601,38 +995,102 @@ class BwServeApp {
601
995
  }
602
996
 
603
997
  /**
604
- * Handle an action POST from a client.
998
+ * Unified return channel handler.
999
+ * Handles all client-to-server POST-backs via /bw/return/<route>/<clientId>.
1000
+ *
1001
+ * Routes:
1002
+ * action — fire-and-forget action dispatch (no requestId)
1003
+ * query — resolve pending query promise
1004
+ * mount — resolve pending mount promise
1005
+ * screenshot — resolve pending screenshot promise
1006
+ *
605
1007
  * @private
606
1008
  */
607
- _handleAction(req, res, clientId) {
1009
+ _handleReturn(req, res, route, clientId) {
608
1010
  var record = this._clients.get(clientId);
609
1011
  if (!record || !record.client) {
610
- res.writeHead(404, { 'Content-Type': 'application/json' });
1012
+ res.writeHead(404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
611
1013
  res.end(JSON.stringify({ error: 'Unknown client' }));
612
1014
  return;
613
1015
  }
614
1016
 
615
1017
  var body = '';
616
- req.on('data', function(chunk) {
617
- body += chunk;
618
- });
1018
+ req.on('data', function(chunk) { body += chunk; });
619
1019
  req.on('end', function() {
620
1020
  try {
621
1021
  var data = JSON.parse(body);
622
- var action = data.action;
623
- var payload = data.data || data;
624
- record.client._dispatch(action, payload);
625
- res.writeHead(200, { 'Content-Type': 'application/json' });
1022
+ if (route === 'action' || route === 'event') {
1023
+ // Action/event dispatch (no requestId/pending pattern)
1024
+ var action = route === 'event'
1025
+ ? '_bw_event'
1026
+ : (data.result ? data.result.action : data.action);
1027
+ var payload = route === 'event'
1028
+ ? (data.result || data)
1029
+ : (data.result ? data.result.data : data.data || data);
1030
+ record.client._dispatch(action, payload);
1031
+ } else {
1032
+ // All other routes: resolve pending promise
1033
+ record.client._resolvePending(data.requestId, data);
1034
+ }
1035
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
626
1036
  res.end(JSON.stringify({ ok: true }));
627
1037
  } catch (e) {
628
- res.writeHead(400, { 'Content-Type': 'application/json' });
1038
+ res.writeHead(400, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
629
1039
  res.end(JSON.stringify({ error: e.message }));
630
1040
  }
631
1041
  });
632
1042
  }
1043
+
1044
+ /**
1045
+ * Serve the self-contained attach script at /bw/attach.js.
1046
+ * Loads bitwrench + bwclient and auto-connects via SSE.
1047
+ * @private
1048
+ */
1049
+ _serveAttachScript(req, res) {
1050
+ try {
1051
+ var js = generateAttachScript({ origin: '' });
1052
+ res.writeHead(200, {
1053
+ 'Content-Type': 'application/javascript; charset=utf-8',
1054
+ 'Access-Control-Allow-Origin': '*',
1055
+ 'Cache-Control': 'no-cache'
1056
+ });
1057
+ res.end(js);
1058
+ } catch (err) {
1059
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
1060
+ res.end('Error generating attach script: ' + err.message);
1061
+ }
1062
+ }
1063
+
1064
+ /**
1065
+ * Serve a vendored library file (allowlisted filenames only).
1066
+ * @private
1067
+ */
1068
+ _serveVendorFile(res, filename) {
1069
+ var allowed = ['html2canvas.min.js'];
1070
+ if (allowed.indexOf(filename) === -1) {
1071
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
1072
+ res.end('Not found');
1073
+ return;
1074
+ }
1075
+ var vendorDir = resolve(__dirname$1, '..', 'vendor');
1076
+ var filePath = join(vendorDir, filename);
1077
+ if (!existsSync(filePath)) {
1078
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
1079
+ res.end('Vendor file not found: ' + filename);
1080
+ return;
1081
+ }
1082
+ var content = readFileSync(filePath);
1083
+ res.writeHead(200, {
1084
+ 'Content-Type': 'application/javascript; charset=utf-8',
1085
+ 'Cache-Control': 'public, max-age=86400'
1086
+ });
1087
+ res.end(content);
1088
+ }
633
1089
  }
634
1090
 
635
- var index = { create, BwServeApp, BwServeClient };
1091
+ var version = VERSION;
1092
+
1093
+ var index = { create, version: VERSION, BwServeApp, BwServeClient, generateShell };
636
1094
 
637
- export { BwServeApp, BwServeClient, create, index as default };
1095
+ export { BwServeApp, BwServeClient, create, index as default, generateShell, version };
638
1096
  //# sourceMappingURL=bwserve.esm.js.map