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