bitwrench 2.0.17 → 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 (67) hide show
  1. package/README.md +127 -38
  2. package/dist/bitwrench-bccl.cjs.js +8 -8
  3. package/dist/bitwrench-bccl.cjs.min.js +3 -3
  4. package/dist/bitwrench-bccl.esm.js +8 -8
  5. package/dist/bitwrench-bccl.esm.min.js +3 -3
  6. package/dist/bitwrench-bccl.umd.js +8 -8
  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 +941 -775
  17. package/dist/bitwrench-lean.cjs.min.js +20 -20
  18. package/dist/bitwrench-lean.es5.js +1012 -961
  19. package/dist/bitwrench-lean.es5.min.js +18 -18
  20. package/dist/bitwrench-lean.esm.js +941 -775
  21. package/dist/bitwrench-lean.esm.min.js +20 -20
  22. package/dist/bitwrench-lean.umd.js +941 -775
  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 +948 -782
  33. package/dist/bitwrench.cjs.min.js +21 -21
  34. package/dist/bitwrench.css +456 -132
  35. package/dist/bitwrench.es5.js +1024 -970
  36. package/dist/bitwrench.es5.min.js +19 -19
  37. package/dist/bitwrench.esm.js +949 -783
  38. package/dist/bitwrench.esm.min.js +21 -21
  39. package/dist/bitwrench.min.css +1 -1
  40. package/dist/bitwrench.umd.js +948 -782
  41. package/dist/bitwrench.umd.min.js +21 -21
  42. package/dist/builds.json +178 -90
  43. package/dist/bwserve.cjs.js +514 -68
  44. package/dist/bwserve.esm.js +513 -69
  45. package/dist/sri.json +44 -36
  46. package/package.json +3 -2
  47. package/readme.html +136 -49
  48. package/src/bitwrench-bccl.js +7 -7
  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 +483 -485
  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 +127 -28
  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/serve.js +6 -2
  64. package/src/generate-css.js +11 -4
  65. package/src/vendor/html2canvas.min.js +20 -0
  66. package/src/version.js +3 -3
  67. package/src/bwserve/shell.js +0 -106
@@ -1,9 +1,16 @@
1
- /*! bwserve v2.0.17 | 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,61 +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,');
256
565
  if (opts.allowExec) {
257
- script.push(' allowExec: true,');
566
+ script.push(' bw._allowExec = true;');
258
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, {');
259
572
  script.push(' onStatus: function(s) {');
260
573
  script.push(' if (typeof console !== "undefined") console.log("[bwserve] " + s);');
261
574
  script.push(' }');
262
575
  script.push(' });');
263
-
264
- // data-bw-action click delegation
265
- script.push(' document.addEventListener("click", function(e) {');
266
- script.push(' var el = e.target.closest ? e.target.closest("[data-bw-action]") : null;');
267
- script.push(' if (!el) return;');
268
- script.push(' e.preventDefault();');
269
- script.push(' var actionData = {};');
270
- script.push(' if (el.getAttribute("data-bw-id")) actionData.bwId = el.getAttribute("data-bw-id");');
271
- script.push(' var form = el.closest("div") || document;');
272
- script.push(' var inp = form.querySelector("input[type=text],input:not([type])");');
273
- script.push(' if (inp) { actionData.inputValue = inp.value; inp.value = ""; }');
274
- script.push(' conn.sendAction(el.getAttribute("data-bw-action"), actionData);');
275
- script.push(' });');
276
-
277
- // Enter key on inputs
278
- script.push(' document.addEventListener("keydown", function(e) {');
279
- script.push(' if (e.key === "Enter" && e.target.tagName === "INPUT") {');
280
- script.push(' var form = e.target.closest("div") || document;');
281
- script.push(' var btn = form.querySelector("[data-bw-action]");');
282
- script.push(' if (btn) {');
283
- script.push(' conn.sendAction(btn.getAttribute("data-bw-action"), { inputValue: e.target.value });');
284
- script.push(' e.target.value = "";');
285
- script.push(' }');
286
- script.push(' }');
287
- script.push(' });');
288
-
289
576
  script.push('})();');
290
577
  script.push('</script>');
578
+
291
579
  script.push('</body>');
292
580
  script.push('</html>');
293
581
 
294
582
  return head.concat(script).join('\n');
295
583
  }
296
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
+
297
644
  /**
298
645
  * bwserve — Server-driven UI library for bitwrench
299
646
  *
@@ -352,6 +699,7 @@ var MIME_TYPES = {
352
699
  * @param {string} [opts.static] - Directory to serve static files from
353
700
  * @param {boolean} [opts.injectBitwrench=true] - Auto-inject bitwrench client JS
354
701
  * @param {string|Object} [opts.theme] - Theme preset name or config object
702
+ * @param {boolean} [opts.allowScreenshot=false] - Enable client.screenshot() capability
355
703
  * @returns {BwServeApp} Application instance
356
704
  */
357
705
  function create(opts) {
@@ -371,11 +719,12 @@ class BwServeApp {
371
719
  this.injectBitwrench = opts.injectBitwrench !== false;
372
720
  this.theme = opts.theme || null;
373
721
  this.allowExec = opts.allowExec || false;
722
+ this.allowScreenshot = opts.allowScreenshot || false;
374
723
  this.keepAliveInterval = opts.keepAliveInterval || 15000;
375
724
  this._pages = new Map();
376
725
  this._clients = new Map();
377
- this._server = null;
378
726
  this._clientCounter = 0;
727
+ this._server = null;
379
728
  }
380
729
 
381
730
  /**
@@ -483,31 +832,61 @@ class BwServeApp {
483
832
  // Parse URL path (strip query string)
484
833
  var path = url.split('?')[0];
485
834
 
486
- // /__bw/bitwrench.umd.js — serve bitwrench client library
487
- 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') {
488
842
  return this._serveDistFile(res, 'bitwrench.umd.js');
489
843
  }
490
844
 
491
- // /__bw/bitwrench.umd.min.js — serve minified
492
- 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') {
493
847
  return this._serveDistFile(res, 'bitwrench.umd.min.js');
494
848
  }
495
849
 
496
- // /__bw/bitwrench.css — serve bitwrench CSS
497
- if (path === '/__bw/bitwrench.css' && method === 'GET') {
850
+ // /bw/lib/bitwrench.css — serve bitwrench CSS
851
+ if (path === '/bw/lib/bitwrench.css' && method === 'GET') {
498
852
  return this._serveDistFile(res, 'bitwrench.css');
499
853
  }
500
854
 
501
- // /__bw/events/:clientId — SSE stream
502
- if (path.startsWith('/__bw/events/') && method === 'GET') {
503
- 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);
504
858
  return this._handleSSE(req, res, clientId);
505
859
  }
506
860
 
507
- // /__bw/action/:clientId action POST
508
- if (path.startsWith('/__bw/action/') && method === 'POST') {
509
- var actionClientId = path.slice('/__bw/action/'.length);
510
- 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);
511
890
  }
512
891
 
513
892
  // Registered page routes — serve shell HTML
@@ -583,6 +962,7 @@ class BwServeApp {
583
962
 
584
963
  // Create client instance
585
964
  var client = new BwServeClient(clientId, res);
965
+ client._allowScreenshot = this.allowScreenshot;
586
966
 
587
967
  // Look up the pending client record (set during page serve)
588
968
  var pending = self._clients.get(clientId);
@@ -615,38 +995,102 @@ class BwServeApp {
615
995
  }
616
996
 
617
997
  /**
618
- * 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
+ *
619
1007
  * @private
620
1008
  */
621
- _handleAction(req, res, clientId) {
1009
+ _handleReturn(req, res, route, clientId) {
622
1010
  var record = this._clients.get(clientId);
623
1011
  if (!record || !record.client) {
624
- res.writeHead(404, { 'Content-Type': 'application/json' });
1012
+ res.writeHead(404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
625
1013
  res.end(JSON.stringify({ error: 'Unknown client' }));
626
1014
  return;
627
1015
  }
628
1016
 
629
1017
  var body = '';
630
- req.on('data', function(chunk) {
631
- body += chunk;
632
- });
1018
+ req.on('data', function(chunk) { body += chunk; });
633
1019
  req.on('end', function() {
634
1020
  try {
635
1021
  var data = JSON.parse(body);
636
- var action = data.action;
637
- var payload = data.data || data;
638
- record.client._dispatch(action, payload);
639
- 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': '*' });
640
1036
  res.end(JSON.stringify({ ok: true }));
641
1037
  } catch (e) {
642
- res.writeHead(400, { 'Content-Type': 'application/json' });
1038
+ res.writeHead(400, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
643
1039
  res.end(JSON.stringify({ error: e.message }));
644
1040
  }
645
1041
  });
646
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
+ }
647
1089
  }
648
1090
 
649
- var index = { create, BwServeApp, BwServeClient };
1091
+ var version = VERSION;
1092
+
1093
+ var index = { create, version: VERSION, BwServeApp, BwServeClient, generateShell };
650
1094
 
651
- export { BwServeApp, BwServeClient, create, index as default };
1095
+ export { BwServeApp, BwServeClient, create, index as default, generateShell, version };
652
1096
  //# sourceMappingURL=bwserve.esm.js.map