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.
- package/README.md +127 -38
- package/dist/bitwrench-bccl.cjs.js +8 -8
- package/dist/bitwrench-bccl.cjs.min.js +3 -3
- package/dist/bitwrench-bccl.esm.js +8 -8
- package/dist/bitwrench-bccl.esm.min.js +3 -3
- package/dist/bitwrench-bccl.umd.js +8 -8
- package/dist/bitwrench-bccl.umd.min.js +2 -2
- package/dist/bitwrench-code-edit.cjs.js +1 -1
- package/dist/bitwrench-code-edit.cjs.min.js +1 -1
- package/dist/bitwrench-code-edit.es5.js +1 -1
- package/dist/bitwrench-code-edit.es5.min.js +1 -1
- package/dist/bitwrench-code-edit.esm.js +1 -1
- package/dist/bitwrench-code-edit.esm.min.js +1 -1
- package/dist/bitwrench-code-edit.umd.js +1 -1
- package/dist/bitwrench-code-edit.umd.min.js +1 -1
- package/dist/bitwrench-lean.cjs.js +941 -775
- package/dist/bitwrench-lean.cjs.min.js +20 -20
- package/dist/bitwrench-lean.es5.js +1012 -961
- package/dist/bitwrench-lean.es5.min.js +18 -18
- package/dist/bitwrench-lean.esm.js +941 -775
- package/dist/bitwrench-lean.esm.min.js +20 -20
- package/dist/bitwrench-lean.umd.js +941 -775
- package/dist/bitwrench-lean.umd.min.js +20 -20
- package/dist/bitwrench-util-css.cjs.js +236 -0
- package/dist/bitwrench-util-css.cjs.min.js +22 -0
- package/dist/bitwrench-util-css.es5.js +414 -0
- package/dist/bitwrench-util-css.es5.min.js +21 -0
- package/dist/bitwrench-util-css.esm.js +230 -0
- package/dist/bitwrench-util-css.esm.min.js +21 -0
- package/dist/bitwrench-util-css.umd.js +242 -0
- package/dist/bitwrench-util-css.umd.min.js +21 -0
- package/dist/bitwrench.cjs.js +948 -782
- package/dist/bitwrench.cjs.min.js +21 -21
- package/dist/bitwrench.css +456 -132
- package/dist/bitwrench.es5.js +1024 -970
- package/dist/bitwrench.es5.min.js +19 -19
- package/dist/bitwrench.esm.js +949 -783
- package/dist/bitwrench.esm.min.js +21 -21
- package/dist/bitwrench.min.css +1 -1
- package/dist/bitwrench.umd.js +948 -782
- package/dist/bitwrench.umd.min.js +21 -21
- package/dist/builds.json +178 -90
- package/dist/bwserve.cjs.js +514 -68
- package/dist/bwserve.esm.js +513 -69
- package/dist/sri.json +44 -36
- package/package.json +3 -2
- package/readme.html +136 -49
- package/src/bitwrench-bccl.js +7 -7
- package/src/bitwrench-color-utils.js +31 -9
- package/src/bitwrench-esm-entry.js +11 -0
- package/src/bitwrench-styles.js +439 -232
- package/src/bitwrench-util-css.js +229 -0
- package/src/bitwrench.js +483 -485
- package/src/bwserve/attach.js +57 -0
- package/src/bwserve/bwclient.js +141 -0
- package/src/bwserve/bwshell.js +102 -0
- package/src/bwserve/client.js +151 -1
- package/src/bwserve/index.js +127 -28
- package/src/cli/attach.js +555 -0
- package/src/cli/convert.js +2 -5
- package/src/cli/index.js +7 -0
- package/src/cli/inject.js +1 -1
- package/src/cli/serve.js +6 -2
- package/src/generate-css.js +11 -4
- package/src/vendor/html2canvas.min.js +20 -0
- package/src/version.js +3 -3
- package/src/bwserve/shell.js +0 -106
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bwserve attach — self-contained drop-in script generator.
|
|
3
|
+
*
|
|
4
|
+
* Generates JS that loads bitwrench + bwclient and auto-connects
|
|
5
|
+
* to a bwserve instance. When loaded in any browser page, it
|
|
6
|
+
* establishes an SSE connection for remote debugging.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* <script src="http://localhost:7902/bw/attach.js"></script>
|
|
10
|
+
*
|
|
11
|
+
* @module bwserve/attach
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getBwClientSource } from './bwclient.js';
|
|
15
|
+
import { VERSION } from '../version.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate the self-contained attach script.
|
|
19
|
+
*
|
|
20
|
+
* The returned JS string, when evaluated in a browser:
|
|
21
|
+
* 1. Checks if bw is already loaded; if not, injects bitwrench UMD
|
|
22
|
+
* 2. Evaluates bwclient source to set up bw._bwClient
|
|
23
|
+
* 3. Calls bw._bwClient.attach() to connect via SSE
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} [opts]
|
|
26
|
+
* @param {string} [opts.origin=''] - Server origin (empty = same origin)
|
|
27
|
+
* @returns {string} JavaScript source code
|
|
28
|
+
*/
|
|
29
|
+
export function generateAttachScript(opts) {
|
|
30
|
+
opts = opts || {};
|
|
31
|
+
var origin = opts.origin || '';
|
|
32
|
+
|
|
33
|
+
var clientSource = getBwClientSource();
|
|
34
|
+
|
|
35
|
+
return '(function() {\n'
|
|
36
|
+
+ ' "use strict";\n'
|
|
37
|
+
+ ' var origin = ' + JSON.stringify(origin) + ';\n'
|
|
38
|
+
+ ' function _go() {\n'
|
|
39
|
+
+ ' ' + clientSource + '\n'
|
|
40
|
+
+ ' bw._bwClient.attach(origin, {\n'
|
|
41
|
+
+ ' allowExec: true,\n'
|
|
42
|
+
+ ' onStatus: function(s) { console.log("[bw-attach] " + s); }\n'
|
|
43
|
+
+ ' });\n'
|
|
44
|
+
+ ' console.log("[bw-attach] v' + VERSION + ' connecting to " + (origin || location.origin));\n'
|
|
45
|
+
+ ' }\n'
|
|
46
|
+
+ ' if (window.bw) { _go(); return; }\n'
|
|
47
|
+
+ ' var s = document.createElement("script");\n'
|
|
48
|
+
+ ' s.src = (origin || "") + "/bw/lib/bitwrench.umd.js";\n'
|
|
49
|
+
+ ' s.onload = function() {\n'
|
|
50
|
+
+ ' if (typeof bw !== "undefined" && bw.loadStyles) bw.loadStyles();\n'
|
|
51
|
+
+ ' _go();\n'
|
|
52
|
+
+ ' };\n'
|
|
53
|
+
+ ' document.head.appendChild(s);\n'
|
|
54
|
+
+ '})();\n';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
generateAttachScript.version = VERSION;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bwclient.js — Browser-side protocol client for bwserve.
|
|
3
|
+
*
|
|
4
|
+
* Injected inline by bwshell. Requires window.bw (bitwrench loaded first).
|
|
5
|
+
* NOT bundled into bitwrench dist — this is a bwserve runtime asset.
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities:
|
|
8
|
+
* - SSE connection lifecycle (connect, reconnect, status)
|
|
9
|
+
* - Unified POST-back via /bw/return/<route>/<clientId>
|
|
10
|
+
* - Register built-in client functions (scrollTo, focus, etc.)
|
|
11
|
+
* - data-bw-action click/key delegation
|
|
12
|
+
* - Attach mode for remote-controlling any bitwrench page
|
|
13
|
+
*
|
|
14
|
+
* @module bwserve/bwclient
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { VERSION } from '../version.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Return the bwclient source as a string for inline injection into the shell.
|
|
21
|
+
* The version is embedded at serve-time from package.json via version.js.
|
|
22
|
+
* @returns {string} JavaScript source code
|
|
23
|
+
*/
|
|
24
|
+
export function getBwClientSource() {
|
|
25
|
+
return BWCLIENT_SOURCE.replace('__BW_VERSION__', VERSION);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var BWCLIENT_SOURCE = '(function(bw) {\n'
|
|
29
|
+
+ ' "use strict";\n'
|
|
30
|
+
+ ' if (!bw) return;\n'
|
|
31
|
+
+ '\n'
|
|
32
|
+
+ ' var _client = {\n'
|
|
33
|
+
+ ' id: null,\n'
|
|
34
|
+
+ ' version: "__BW_VERSION__",\n'
|
|
35
|
+
+ ' status: "idle",\n'
|
|
36
|
+
+ ' _es: null\n'
|
|
37
|
+
+ ' };\n'
|
|
38
|
+
+ '\n'
|
|
39
|
+
+ ' // ── Unified POST-back ──\n'
|
|
40
|
+
+ ' _client.respond = function(route, requestId, result, error) {\n'
|
|
41
|
+
+ ' fetch("/bw/return/" + route + "/" + _client.id, {\n'
|
|
42
|
+
+ ' method: "POST",\n'
|
|
43
|
+
+ ' headers: { "Content-Type": "application/json" },\n'
|
|
44
|
+
+ ' body: JSON.stringify({ requestId: requestId, route: route, result: result, error: error || null })\n'
|
|
45
|
+
+ ' }).catch(function() {});\n'
|
|
46
|
+
+ ' };\n'
|
|
47
|
+
+ '\n'
|
|
48
|
+
+ ' // ── SSE connect ──\n'
|
|
49
|
+
+ ' _client.connect = function(url, opts) {\n'
|
|
50
|
+
+ ' opts = opts || {};\n'
|
|
51
|
+
+ ' var onStatus = opts.onStatus || function() {};\n'
|
|
52
|
+
+ ' function setStatus(s) { _client.status = s; onStatus(s); }\n'
|
|
53
|
+
+ ' setStatus("connecting");\n'
|
|
54
|
+
+ ' if (typeof EventSource === "undefined") return;\n'
|
|
55
|
+
+ ' var es = new EventSource(url);\n'
|
|
56
|
+
+ ' _client._es = es;\n'
|
|
57
|
+
+ ' es.onopen = function() { setStatus("connected"); };\n'
|
|
58
|
+
+ ' es.onmessage = function(e) {\n'
|
|
59
|
+
+ ' try {\n'
|
|
60
|
+
+ ' var msg = typeof e.data === "string" ? bw.parseJSONFlex(e.data) : e.data;\n'
|
|
61
|
+
+ ' bw.apply(msg);\n'
|
|
62
|
+
+ ' } catch (err) {\n'
|
|
63
|
+
+ ' if (typeof console !== "undefined") console.error("[bwclient]", err);\n'
|
|
64
|
+
+ ' }\n'
|
|
65
|
+
+ ' };\n'
|
|
66
|
+
+ ' es.onerror = function() {\n'
|
|
67
|
+
+ ' if (_client.status === "connected") setStatus("disconnected");\n'
|
|
68
|
+
+ ' };\n'
|
|
69
|
+
+ ' };\n'
|
|
70
|
+
+ '\n'
|
|
71
|
+
+ ' // ── Attach mode ──\n'
|
|
72
|
+
+ ' _client.attach = function(url, opts) {\n'
|
|
73
|
+
+ ' opts = opts || {};\n'
|
|
74
|
+
+ ' _client.id = opts.clientId || "att_" + Math.random().toString(36).slice(2, 10);\n'
|
|
75
|
+
+ ' if (opts.allowExec) bw._allowExec = true;\n'
|
|
76
|
+
+ ' _client._registerBuiltins();\n'
|
|
77
|
+
+ ' _client._wireActions();\n'
|
|
78
|
+
+ ' _client.connect(url + "/bw/events/" + _client.id, opts);\n'
|
|
79
|
+
+ ' };\n'
|
|
80
|
+
+ '\n'
|
|
81
|
+
+ ' // ── Send action to server ──\n'
|
|
82
|
+
+ ' _client.sendAction = function(action, data) {\n'
|
|
83
|
+
+ ' _client.respond("action", null, { action: action, data: data || {} });\n'
|
|
84
|
+
+ ' };\n'
|
|
85
|
+
+ '\n'
|
|
86
|
+
+ ' // ── Register built-in functions ──\n'
|
|
87
|
+
+ ' _client._registerBuiltins = function() {\n'
|
|
88
|
+
+ ' var builtins = {\n'
|
|
89
|
+
+ ' scrollTo: "function(sel){var el=bw._el(sel);if(el)el.scrollTop=el.scrollHeight;}",\n'
|
|
90
|
+
+ ' focus: "function(sel){var el=bw._el(sel);if(el&&typeof el.focus===\\"function\\")el.focus();}",\n'
|
|
91
|
+
+ ' 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'
|
|
92
|
+
+ ' clipboard: "function(t){if(typeof navigator!==\\"undefined\\"&&navigator.clipboard)navigator.clipboard.writeText(t);}",\n'
|
|
93
|
+
+ ' redirect: "function(u){if(typeof window!==\\"undefined\\")window.location.href=u;}",\n'
|
|
94
|
+
+ ' log: "function(){console.log.apply(console,arguments);}",\n'
|
|
95
|
+
+ ' _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'
|
|
96
|
+
+ ' _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'
|
|
97
|
+
+ ' _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'
|
|
98
|
+
+ ' _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'
|
|
99
|
+
+ ' _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'
|
|
100
|
+
+ ' _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'
|
|
101
|
+
+ ' };\n'
|
|
102
|
+
+ ' Object.keys(builtins).forEach(function(name) {\n'
|
|
103
|
+
+ ' bw.apply({ type: "register", name: name, body: builtins[name] });\n'
|
|
104
|
+
+ ' });\n'
|
|
105
|
+
+ ' };\n'
|
|
106
|
+
+ '\n'
|
|
107
|
+
+ ' // ── Wire up data-bw-action click delegation ──\n'
|
|
108
|
+
+ ' _client._wireActions = function() {\n'
|
|
109
|
+
+ ' document.addEventListener("click", function(e) {\n'
|
|
110
|
+
+ ' var el = e.target.closest ? e.target.closest("[data-bw-action]") : null;\n'
|
|
111
|
+
+ ' if (!el) return;\n'
|
|
112
|
+
+ ' e.preventDefault();\n'
|
|
113
|
+
+ ' var actionData = {};\n'
|
|
114
|
+
+ ' if (el.getAttribute("data-bw-id")) actionData.bwId = el.getAttribute("data-bw-id");\n'
|
|
115
|
+
+ ' var form = el.closest("div") || document;\n'
|
|
116
|
+
+ ' var inp = form.querySelector("input[type=text],input:not([type])");\n'
|
|
117
|
+
+ ' if (inp) { actionData.inputValue = inp.value; inp.value = ""; }\n'
|
|
118
|
+
+ ' _client.sendAction(el.getAttribute("data-bw-action"), actionData);\n'
|
|
119
|
+
+ ' });\n'
|
|
120
|
+
+ ' document.addEventListener("keydown", function(e) {\n'
|
|
121
|
+
+ ' if (e.key === "Enter" && e.target.tagName === "INPUT") {\n'
|
|
122
|
+
+ ' var form = e.target.closest("div") || document;\n'
|
|
123
|
+
+ ' var btn = form.querySelector("[data-bw-action]");\n'
|
|
124
|
+
+ ' if (btn) {\n'
|
|
125
|
+
+ ' _client.sendAction(btn.getAttribute("data-bw-action"), { inputValue: e.target.value });\n'
|
|
126
|
+
+ ' e.target.value = "";\n'
|
|
127
|
+
+ ' }\n'
|
|
128
|
+
+ ' }\n'
|
|
129
|
+
+ ' });\n'
|
|
130
|
+
+ ' };\n'
|
|
131
|
+
+ '\n'
|
|
132
|
+
+ ' // ── Event delegation helper ──\n'
|
|
133
|
+
+ ' _client.listen = function(selector, event, action) {\n'
|
|
134
|
+
+ ' document.addEventListener(event, function(e) {\n'
|
|
135
|
+
+ ' var el = e.target.closest ? e.target.closest(selector) : null;\n'
|
|
136
|
+
+ ' if (el) _client.sendAction(action, { selector: selector, event: event });\n'
|
|
137
|
+
+ ' });\n'
|
|
138
|
+
+ ' };\n'
|
|
139
|
+
+ '\n'
|
|
140
|
+
+ ' bw._bwClient = _client;\n'
|
|
141
|
+
+ '})(window.bw);\n';
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bwserve shell — generates the HTML page shell served to browsers.
|
|
3
|
+
*
|
|
4
|
+
* The shell is a minimal HTML doc that:
|
|
5
|
+
* - Loads bitwrench UMD + CSS from /bw/lib/ routes
|
|
6
|
+
* - Calls bw.loadStyles()
|
|
7
|
+
* - Optionally applies a custom theme
|
|
8
|
+
* - Creates a #app div
|
|
9
|
+
* - Inlines bwclient.js for SSE, action delegation, and built-ins
|
|
10
|
+
*
|
|
11
|
+
* @module bwserve/bwshell
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getBwClientSource } from './bwclient.js';
|
|
15
|
+
import { VERSION } from '../version.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate the shell HTML page for a bwserve app.
|
|
19
|
+
*
|
|
20
|
+
* @param {Object} opts
|
|
21
|
+
* @param {string} opts.clientId - Unique client ID for this connection
|
|
22
|
+
* @param {string} [opts.title='bwserve'] - Page title
|
|
23
|
+
* @param {string} [opts.theme] - Theme preset name or config
|
|
24
|
+
* @param {boolean} [opts.injectBitwrench=true] - Whether to inject bitwrench scripts
|
|
25
|
+
* @param {boolean} [opts.allowExec=false] - Enable exec message type
|
|
26
|
+
* @returns {string} Complete HTML document
|
|
27
|
+
*/
|
|
28
|
+
export function generateShell(opts) {
|
|
29
|
+
opts = opts || {};
|
|
30
|
+
var clientId = opts.clientId || 'default';
|
|
31
|
+
var title = opts.title || 'bwserve';
|
|
32
|
+
var inject = opts.injectBitwrench !== false;
|
|
33
|
+
|
|
34
|
+
var head = [
|
|
35
|
+
'<!DOCTYPE html>',
|
|
36
|
+
'<html lang="en">',
|
|
37
|
+
'<head>',
|
|
38
|
+
'<meta charset="UTF-8">',
|
|
39
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1.0">',
|
|
40
|
+
'<title>' + title + '</title>',
|
|
41
|
+
'<meta name="generator" content="bwserve ' + VERSION + '">'
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
if (inject) {
|
|
45
|
+
head.push('<script src="/bw/lib/bitwrench.umd.js"></script>');
|
|
46
|
+
head.push('<link rel="stylesheet" href="/bw/lib/bitwrench.css">');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
head.push('</head>');
|
|
50
|
+
head.push('<body>');
|
|
51
|
+
head.push('<div id="app"></div>');
|
|
52
|
+
|
|
53
|
+
var script = [
|
|
54
|
+
'<script>',
|
|
55
|
+
'(function() {',
|
|
56
|
+
' "use strict";',
|
|
57
|
+
' bw.loadStyles();'
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
if (opts.theme) {
|
|
61
|
+
script.push(' bw.loadStyles(' + JSON.stringify(
|
|
62
|
+
typeof opts.theme === 'string'
|
|
63
|
+
? { primary: '#006666', secondary: '#333333' }
|
|
64
|
+
: opts.theme
|
|
65
|
+
) + ');');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
script.push('})();');
|
|
69
|
+
script.push('</script>');
|
|
70
|
+
|
|
71
|
+
// Inline bwclient.js
|
|
72
|
+
script.push('<script>');
|
|
73
|
+
script.push(getBwClientSource());
|
|
74
|
+
script.push('</script>');
|
|
75
|
+
|
|
76
|
+
// Init script: wire up bwclient
|
|
77
|
+
script.push('<script>');
|
|
78
|
+
script.push('(function() {');
|
|
79
|
+
script.push(' "use strict";');
|
|
80
|
+
script.push(' var clientId = ' + JSON.stringify(clientId) + ';');
|
|
81
|
+
if (opts.allowExec) {
|
|
82
|
+
script.push(' bw._allowExec = true;');
|
|
83
|
+
}
|
|
84
|
+
script.push(' bw._bwClient.id = clientId;');
|
|
85
|
+
script.push(' bw._bwClient._registerBuiltins();');
|
|
86
|
+
script.push(' bw._bwClient._wireActions();');
|
|
87
|
+
script.push(' bw._bwClient.connect("/bw/events/" + clientId, {');
|
|
88
|
+
script.push(' onStatus: function(s) {');
|
|
89
|
+
script.push(' if (typeof console !== "undefined") console.log("[bwserve] " + s);');
|
|
90
|
+
script.push(' }');
|
|
91
|
+
script.push(' });');
|
|
92
|
+
script.push('})();');
|
|
93
|
+
script.push('</script>');
|
|
94
|
+
|
|
95
|
+
script.push('</body>');
|
|
96
|
+
script.push('</html>');
|
|
97
|
+
|
|
98
|
+
return head.concat(script).join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** bwshell version (from package.json) */
|
|
102
|
+
generateShell.version = VERSION;
|
package/src/bwserve/client.js
CHANGED
|
@@ -17,15 +17,20 @@
|
|
|
17
17
|
* @module bwserve/client
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { VERSION } from '../version.js';
|
|
21
|
+
|
|
20
22
|
/**
|
|
21
23
|
* BwServeClient — one connected browser tab.
|
|
22
24
|
*/
|
|
23
25
|
export class BwServeClient {
|
|
26
|
+
/** bwserve version (from package.json) */
|
|
27
|
+
static version = VERSION;
|
|
24
28
|
constructor(id, res) {
|
|
25
29
|
this.id = id;
|
|
26
30
|
this._res = res; // SSE response stream (null in stub)
|
|
27
31
|
this._handlers = {}; // action name → handler
|
|
28
32
|
this._closed = false;
|
|
33
|
+
this._pending = {}; // requestId → { resolve, reject, timer }
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
/**
|
|
@@ -104,7 +109,7 @@ export class BwServeClient {
|
|
|
104
109
|
/**
|
|
105
110
|
* Call a previously registered or built-in function on the client.
|
|
106
111
|
*
|
|
107
|
-
* Built-in functions (
|
|
112
|
+
* Built-in functions (registered by bwclient on connection):
|
|
108
113
|
* scrollTo, focus, download, clipboard, redirect, log
|
|
109
114
|
*
|
|
110
115
|
* @param {string} name - Function name (registered or built-in)
|
|
@@ -167,6 +172,151 @@ export class BwServeClient {
|
|
|
167
172
|
}
|
|
168
173
|
}
|
|
169
174
|
|
|
175
|
+
// ── Pending promise mechanism ──
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create a pending promise with a unique requestId and timeout.
|
|
179
|
+
*
|
|
180
|
+
* @param {number} [timeout=10000] - Timeout in ms
|
|
181
|
+
* @returns {{ requestId: string, promise: Promise }}
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
_pend(timeout) {
|
|
185
|
+
var self = this;
|
|
186
|
+
timeout = timeout || 10000;
|
|
187
|
+
var requestId = 'req_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
|
|
188
|
+
|
|
189
|
+
var promise = new Promise(function(resolve, reject) {
|
|
190
|
+
var timer = setTimeout(function() {
|
|
191
|
+
delete self._pending[requestId];
|
|
192
|
+
reject(new Error('Request timeout after ' + timeout + 'ms'));
|
|
193
|
+
}, timeout);
|
|
194
|
+
|
|
195
|
+
self._pending[requestId] = { resolve: resolve, reject: reject, timer: timer };
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return { requestId: requestId, promise: promise };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Resolve a pending promise by requestId.
|
|
203
|
+
* Called by the server route handler when a POST-back arrives.
|
|
204
|
+
*
|
|
205
|
+
* @param {string} requestId
|
|
206
|
+
* @param {Object} data - Response data (may contain .error)
|
|
207
|
+
* @returns {boolean} true if a pending request was found and resolved
|
|
208
|
+
* @private
|
|
209
|
+
*/
|
|
210
|
+
_resolvePending(requestId, data) {
|
|
211
|
+
var pending = this._pending[requestId];
|
|
212
|
+
if (!pending) return false;
|
|
213
|
+
|
|
214
|
+
clearTimeout(pending.timer);
|
|
215
|
+
delete this._pending[requestId];
|
|
216
|
+
|
|
217
|
+
if (data.error) {
|
|
218
|
+
pending.reject(new Error(data.error));
|
|
219
|
+
} else {
|
|
220
|
+
pending.resolve(data.result !== undefined ? data.result : data);
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Query ──
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Execute code on the client and get the result back.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} code - JavaScript code to evaluate (return value is sent back)
|
|
231
|
+
* @param {Object} [options]
|
|
232
|
+
* @param {number} [options.timeout=5000] - Timeout in ms
|
|
233
|
+
* @returns {Promise<*>} The result of evaluating the code
|
|
234
|
+
*/
|
|
235
|
+
query(code, options) {
|
|
236
|
+
var opts = options || {};
|
|
237
|
+
var pend = this._pend(opts.timeout || 5000);
|
|
238
|
+
this.call('_bw_query', { code: code, requestId: pend.requestId });
|
|
239
|
+
return pend.promise;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── Mount ──
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Mount a BCCL component or factory function on the client.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} selector - CSS selector of target element
|
|
248
|
+
* @param {string} factory - BCCL component name (e.g. 'accordion') or JS factory code
|
|
249
|
+
* @param {Object} [props] - Props to pass to the component/factory
|
|
250
|
+
* @param {Object} [options]
|
|
251
|
+
* @param {number} [options.timeout=10000] - Timeout in ms
|
|
252
|
+
* @returns {Promise<Object>} Resolves with { mounted: true } on success
|
|
253
|
+
*/
|
|
254
|
+
mount(selector, factory, props, options) {
|
|
255
|
+
var opts = options || {};
|
|
256
|
+
var pend = this._pend(opts.timeout || 10000);
|
|
257
|
+
this.call('_bw_mount', {
|
|
258
|
+
target: selector,
|
|
259
|
+
factory: factory,
|
|
260
|
+
props: props || {},
|
|
261
|
+
requestId: pend.requestId
|
|
262
|
+
});
|
|
263
|
+
return pend.promise;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Screenshot ──
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Capture a screenshot of the client's page or a specific element.
|
|
270
|
+
*
|
|
271
|
+
* Requires the server to be created with `{ allowScreenshot: true }`.
|
|
272
|
+
* Uses html2canvas on the client side (lazy-loaded on first call).
|
|
273
|
+
*
|
|
274
|
+
* @param {string} [selector='body'] - CSS selector of element to capture
|
|
275
|
+
* @param {Object} [options]
|
|
276
|
+
* @param {string} [options.format='png'] - 'png' or 'jpeg'
|
|
277
|
+
* @param {number} [options.quality=0.85] - JPEG quality 0-1 (ignored for PNG)
|
|
278
|
+
* @param {number} [options.maxWidth] - Resize if wider (preserves aspect ratio)
|
|
279
|
+
* @param {number} [options.maxHeight] - Resize if taller (preserves aspect ratio)
|
|
280
|
+
* @param {number} [options.scale=1] - Device pixel ratio override
|
|
281
|
+
* @param {number} [options.timeout=10000] - Reject after ms
|
|
282
|
+
* @returns {Promise<Object>} { data: Buffer, width, height, format }
|
|
283
|
+
*/
|
|
284
|
+
screenshot(selector, options) {
|
|
285
|
+
var self = this;
|
|
286
|
+
var opts = options || {};
|
|
287
|
+
var timeout = opts.timeout || 10000;
|
|
288
|
+
|
|
289
|
+
if (!self._allowScreenshot) {
|
|
290
|
+
return Promise.reject(new Error('Screenshot not enabled. Set allowScreenshot: true in server options.'));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
var pend = self._pend(timeout);
|
|
294
|
+
|
|
295
|
+
// Call the bwclient-registered capture function
|
|
296
|
+
self.call('_bw_screenshot', {
|
|
297
|
+
requestId: pend.requestId,
|
|
298
|
+
selector: selector || 'body',
|
|
299
|
+
format: opts.format || 'png',
|
|
300
|
+
quality: opts.quality || 0.85,
|
|
301
|
+
maxWidth: opts.maxWidth || null,
|
|
302
|
+
maxHeight: opts.maxHeight || null,
|
|
303
|
+
scale: opts.scale || 1,
|
|
304
|
+
captureUrl: '/bw/lib/vendor/html2canvas.min.js'
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Transform the raw response into { data: Buffer, width, height, format }
|
|
308
|
+
return pend.promise.then(function(result) {
|
|
309
|
+
if (!result || !result.data) return result;
|
|
310
|
+
var base64 = result.data.split(',')[1];
|
|
311
|
+
return {
|
|
312
|
+
data: Buffer.from(base64, 'base64'),
|
|
313
|
+
width: result.width,
|
|
314
|
+
height: result.height,
|
|
315
|
+
format: result.format
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
170
320
|
/**
|
|
171
321
|
* Dispatch an incoming action from the client.
|
|
172
322
|
* @private
|