bitwrench 2.0.15 → 2.0.16
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 +57 -21
- package/dist/bitwrench-bccl.cjs.js +3746 -0
- package/dist/bitwrench-bccl.cjs.min.js +40 -0
- package/dist/bitwrench-bccl.esm.js +3741 -0
- package/dist/bitwrench-bccl.esm.min.js +40 -0
- package/dist/bitwrench-bccl.umd.js +3752 -0
- package/dist/bitwrench-bccl.umd.min.js +40 -0
- package/dist/bitwrench-code-edit.cjs.js +57 -7
- package/dist/bitwrench-code-edit.cjs.min.js +9 -2
- package/dist/bitwrench-code-edit.es5.js +74 -11
- package/dist/bitwrench-code-edit.es5.min.js +9 -2
- package/dist/bitwrench-code-edit.esm.js +57 -7
- package/dist/bitwrench-code-edit.esm.min.js +9 -2
- package/dist/bitwrench-code-edit.umd.js +57 -7
- package/dist/bitwrench-code-edit.umd.min.js +9 -2
- package/dist/bitwrench-lean.cjs.js +413 -17
- package/dist/bitwrench-lean.cjs.min.js +7 -7
- package/dist/bitwrench-lean.es5.js +428 -16
- package/dist/bitwrench-lean.es5.min.js +5 -5
- package/dist/bitwrench-lean.esm.js +413 -17
- package/dist/bitwrench-lean.esm.min.js +7 -7
- package/dist/bitwrench-lean.umd.js +413 -17
- package/dist/bitwrench-lean.umd.min.js +7 -7
- package/dist/bitwrench.cjs.js +413 -17
- package/dist/bitwrench.cjs.min.js +7 -7
- package/dist/bitwrench.css +60 -17
- package/dist/bitwrench.es5.js +428 -16
- package/dist/bitwrench.es5.min.js +6 -6
- package/dist/bitwrench.esm.js +413 -17
- package/dist/bitwrench.esm.min.js +7 -7
- package/dist/bitwrench.min.css +1 -1
- package/dist/bitwrench.umd.js +413 -17
- package/dist/bitwrench.umd.min.js +7 -7
- package/dist/builds.json +168 -80
- package/dist/bwserve.cjs.js +646 -0
- package/dist/bwserve.esm.js +638 -0
- package/dist/sri.json +36 -28
- package/package.json +18 -3
- package/readme.html +62 -23
- package/src/bitwrench-bccl-entry.js +72 -0
- package/src/bitwrench-code-edit.js +56 -6
- package/src/bitwrench-color-utils.js +5 -6
- package/src/bitwrench-styles.js +20 -8
- package/src/bitwrench.js +385 -0
- package/src/bwserve/client.js +182 -0
- package/src/bwserve/index.js +352 -0
- package/src/bwserve/shell.js +103 -0
- package/src/cli/index.js +36 -15
- package/src/cli/serve.js +325 -0
- package/src/version.js +3 -3
- /package/bin/{bitwrench.js → bwcli.js} +0 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bwserve — Server-driven UI library for bitwrench
|
|
3
|
+
*
|
|
4
|
+
* Programmatic API for building server-push UIs (Streamlit-style).
|
|
5
|
+
* Uses SSE (Server-Sent Events) by default, with WebSocket opt-in.
|
|
6
|
+
* Zero runtime dependencies — only Node.js stdlib (http, fs, path).
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import bwserve from 'bitwrench/bwserve';
|
|
10
|
+
* const app = bwserve.create({ port: 7902 });
|
|
11
|
+
* app.page('/', (client) => {
|
|
12
|
+
* client.render('#app', bw.makeCard({ title: 'Hello' }));
|
|
13
|
+
* });
|
|
14
|
+
* app.listen();
|
|
15
|
+
*
|
|
16
|
+
* @module bwserve
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { BwServeClient } from './client.js';
|
|
20
|
+
import { generateShell } from './shell.js';
|
|
21
|
+
|
|
22
|
+
// Resolve dist/ paths relative to the package root
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
import { dirname, resolve, join, extname } from 'path';
|
|
25
|
+
import { createServer } from 'http';
|
|
26
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
27
|
+
|
|
28
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
var DIST_DIR = resolve(__dirname, '..', '..', 'dist');
|
|
30
|
+
|
|
31
|
+
// MIME type lookup for static file serving
|
|
32
|
+
var MIME_TYPES = {
|
|
33
|
+
'.html': 'text/html; charset=utf-8',
|
|
34
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
35
|
+
'.css': 'text/css; charset=utf-8',
|
|
36
|
+
'.json': 'application/json; charset=utf-8',
|
|
37
|
+
'.png': 'image/png',
|
|
38
|
+
'.jpg': 'image/jpeg',
|
|
39
|
+
'.jpeg': 'image/jpeg',
|
|
40
|
+
'.gif': 'image/gif',
|
|
41
|
+
'.svg': 'image/svg+xml',
|
|
42
|
+
'.ico': 'image/x-icon',
|
|
43
|
+
'.woff': 'font/woff',
|
|
44
|
+
'.woff2': 'font/woff2',
|
|
45
|
+
'.ttf': 'font/ttf',
|
|
46
|
+
'.map': 'application/json'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a bwserve application.
|
|
51
|
+
*
|
|
52
|
+
* @param {Object} opts - Server options
|
|
53
|
+
* @param {number} [opts.port=7902] - Port to listen on
|
|
54
|
+
* @param {string} [opts.title='bwserve'] - Page title
|
|
55
|
+
* @param {string} [opts.static] - Directory to serve static files from
|
|
56
|
+
* @param {boolean} [opts.injectBitwrench=true] - Auto-inject bitwrench client JS
|
|
57
|
+
* @param {string|Object} [opts.theme] - Theme preset name or config object
|
|
58
|
+
* @returns {BwServeApp} Application instance
|
|
59
|
+
*/
|
|
60
|
+
export function create(opts) {
|
|
61
|
+
return new BwServeApp(opts || {});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* BwServeApp — the server application object.
|
|
66
|
+
*
|
|
67
|
+
* Manages pages, client connections, and the HTTP/SSE server.
|
|
68
|
+
*/
|
|
69
|
+
class BwServeApp {
|
|
70
|
+
constructor(opts) {
|
|
71
|
+
this.port = opts.port || 7902;
|
|
72
|
+
this.title = opts.title || 'bwserve';
|
|
73
|
+
this.staticDir = opts.static || null;
|
|
74
|
+
this.injectBitwrench = opts.injectBitwrench !== false;
|
|
75
|
+
this.theme = opts.theme || null;
|
|
76
|
+
this.keepAliveInterval = opts.keepAliveInterval || 15000;
|
|
77
|
+
this._pages = new Map();
|
|
78
|
+
this._clients = new Map();
|
|
79
|
+
this._server = null;
|
|
80
|
+
this._clientCounter = 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Register a page handler.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} path - URL path (e.g., '/', '/dashboard')
|
|
87
|
+
* @param {Function} handler - Called with (client: BwServeClient) on connection
|
|
88
|
+
* @returns {BwServeApp} this (for chaining)
|
|
89
|
+
*/
|
|
90
|
+
page(path, handler) {
|
|
91
|
+
this._pages.set(path, handler);
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Start the HTTP server and begin accepting SSE connections.
|
|
97
|
+
*
|
|
98
|
+
* @param {Function} [callback] - Called when server is listening
|
|
99
|
+
* @returns {Promise<void>}
|
|
100
|
+
*/
|
|
101
|
+
listen(callback) {
|
|
102
|
+
var self = this;
|
|
103
|
+
|
|
104
|
+
return new Promise(function(res) {
|
|
105
|
+
self._server = createServer(function(req, rawRes) {
|
|
106
|
+
self._handleRequest(req, rawRes);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
self._server.listen(self.port, function() {
|
|
110
|
+
if (callback) callback();
|
|
111
|
+
res();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Stop the server and close all client connections.
|
|
118
|
+
*/
|
|
119
|
+
close() {
|
|
120
|
+
var self = this;
|
|
121
|
+
return new Promise(function(res) {
|
|
122
|
+
// Close all SSE streams
|
|
123
|
+
for (var record of self._clients.values()) {
|
|
124
|
+
if (record.client && typeof record.client.close === 'function') {
|
|
125
|
+
record.client.close();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
self._clients.clear();
|
|
129
|
+
|
|
130
|
+
if (self._server) {
|
|
131
|
+
self._server.close(function() {
|
|
132
|
+
self._server = null;
|
|
133
|
+
res();
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
res();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get count of active client connections.
|
|
143
|
+
* @returns {number}
|
|
144
|
+
*/
|
|
145
|
+
get clientCount() {
|
|
146
|
+
return this._clients.size;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Broadcast a protocol message to all connected clients.
|
|
151
|
+
*
|
|
152
|
+
* If msg has a clientId field, send only to that client.
|
|
153
|
+
* Otherwise, broadcast to all.
|
|
154
|
+
*
|
|
155
|
+
* @param {Object} msg - Protocol message (replace, patch, append, remove, batch)
|
|
156
|
+
* @returns {number} Number of clients that received the message
|
|
157
|
+
*/
|
|
158
|
+
broadcast(msg) {
|
|
159
|
+
if (msg.clientId) {
|
|
160
|
+
var record = this._clients.get(msg.clientId);
|
|
161
|
+
if (record && record.client) {
|
|
162
|
+
record.client._send(msg);
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
var count = 0;
|
|
168
|
+
for (var record of this._clients.values()) {
|
|
169
|
+
if (record.client && !record.client._closed) {
|
|
170
|
+
record.client._send(msg);
|
|
171
|
+
count++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return count;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Internal: route incoming HTTP requests.
|
|
179
|
+
* @private
|
|
180
|
+
*/
|
|
181
|
+
_handleRequest(req, res) {
|
|
182
|
+
var url = req.url || '/';
|
|
183
|
+
var method = req.method || 'GET';
|
|
184
|
+
|
|
185
|
+
// Parse URL path (strip query string)
|
|
186
|
+
var path = url.split('?')[0];
|
|
187
|
+
|
|
188
|
+
// /__bw/bitwrench.umd.js — serve bitwrench client library
|
|
189
|
+
if (path === '/__bw/bitwrench.umd.js' && method === 'GET') {
|
|
190
|
+
return this._serveDistFile(res, 'bitwrench.umd.js');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// /__bw/bitwrench.umd.min.js — serve minified
|
|
194
|
+
if (path === '/__bw/bitwrench.umd.min.js' && method === 'GET') {
|
|
195
|
+
return this._serveDistFile(res, 'bitwrench.umd.min.js');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// /__bw/bitwrench.css — serve bitwrench CSS
|
|
199
|
+
if (path === '/__bw/bitwrench.css' && method === 'GET') {
|
|
200
|
+
return this._serveDistFile(res, 'bitwrench.css');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// /__bw/events/:clientId — SSE stream
|
|
204
|
+
if (path.startsWith('/__bw/events/') && method === 'GET') {
|
|
205
|
+
var clientId = path.slice('/__bw/events/'.length);
|
|
206
|
+
return this._handleSSE(req, res, clientId);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// /__bw/action/:clientId — action POST
|
|
210
|
+
if (path.startsWith('/__bw/action/') && method === 'POST') {
|
|
211
|
+
var actionClientId = path.slice('/__bw/action/'.length);
|
|
212
|
+
return this._handleAction(req, res, actionClientId);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Registered page routes — serve shell HTML
|
|
216
|
+
if (method === 'GET' && this._pages.has(path)) {
|
|
217
|
+
var clientId2 = 'c' + (++this._clientCounter);
|
|
218
|
+
var shell = generateShell({
|
|
219
|
+
clientId: clientId2,
|
|
220
|
+
title: this.title,
|
|
221
|
+
theme: this.theme,
|
|
222
|
+
injectBitwrench: this.injectBitwrench
|
|
223
|
+
});
|
|
224
|
+
// Store the page path for this client so SSE knows which handler to call
|
|
225
|
+
this._clients.set(clientId2, { pagePath: path, client: null });
|
|
226
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
227
|
+
res.end(shell);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Static file serving
|
|
232
|
+
if (method === 'GET' && this.staticDir) {
|
|
233
|
+
var filePath = join(this.staticDir, path);
|
|
234
|
+
if (existsSync(filePath) && statSync(filePath).isFile()) {
|
|
235
|
+
var ext = extname(filePath);
|
|
236
|
+
var mime = MIME_TYPES[ext] || 'application/octet-stream';
|
|
237
|
+
var content = readFileSync(filePath);
|
|
238
|
+
res.writeHead(200, { 'Content-Type': mime });
|
|
239
|
+
res.end(content);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 404
|
|
245
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
246
|
+
res.end('Not Found');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Serve a file from the dist/ directory.
|
|
251
|
+
* @private
|
|
252
|
+
*/
|
|
253
|
+
_serveDistFile(res, filename) {
|
|
254
|
+
var filePath = join(DIST_DIR, filename);
|
|
255
|
+
if (!existsSync(filePath)) {
|
|
256
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
257
|
+
res.end('Not Found: ' + filename);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
var ext = extname(filename);
|
|
261
|
+
var mime = MIME_TYPES[ext] || 'application/octet-stream';
|
|
262
|
+
var content = readFileSync(filePath);
|
|
263
|
+
res.writeHead(200, {
|
|
264
|
+
'Content-Type': mime,
|
|
265
|
+
'Cache-Control': 'public, max-age=3600'
|
|
266
|
+
});
|
|
267
|
+
res.end(content);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Handle an SSE connection.
|
|
272
|
+
* @private
|
|
273
|
+
*/
|
|
274
|
+
_handleSSE(req, res, clientId) {
|
|
275
|
+
var self = this;
|
|
276
|
+
|
|
277
|
+
// Set SSE headers
|
|
278
|
+
res.writeHead(200, {
|
|
279
|
+
'Content-Type': 'text/event-stream',
|
|
280
|
+
'Cache-Control': 'no-cache',
|
|
281
|
+
'Connection': 'keep-alive',
|
|
282
|
+
'Access-Control-Allow-Origin': '*'
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Create client instance
|
|
286
|
+
var client = new BwServeClient(clientId, res);
|
|
287
|
+
|
|
288
|
+
// Look up the pending client record (set during page serve)
|
|
289
|
+
var pending = self._clients.get(clientId);
|
|
290
|
+
var pagePath = pending ? pending.pagePath : '/';
|
|
291
|
+
self._clients.set(clientId, { pagePath: pagePath, client: client });
|
|
292
|
+
|
|
293
|
+
// Keep-alive: send SSE comment periodically
|
|
294
|
+
var keepAlive = setInterval(function() {
|
|
295
|
+
if (!client._closed) {
|
|
296
|
+
try { res.write(':keepalive\n\n'); } catch (e) { /* ignore */ }
|
|
297
|
+
}
|
|
298
|
+
}, self.keepAliveInterval);
|
|
299
|
+
|
|
300
|
+
// Clean up on disconnect
|
|
301
|
+
req.on('close', function() {
|
|
302
|
+
clearInterval(keepAlive);
|
|
303
|
+
client._closed = true;
|
|
304
|
+
self._clients.delete(clientId);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Call the page handler
|
|
308
|
+
var handler = self._pages.get(pagePath);
|
|
309
|
+
if (handler) {
|
|
310
|
+
try {
|
|
311
|
+
handler(client);
|
|
312
|
+
} catch (e) {
|
|
313
|
+
console.error('[bwserve] Page handler error:', e);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Handle an action POST from a client.
|
|
320
|
+
* @private
|
|
321
|
+
*/
|
|
322
|
+
_handleAction(req, res, clientId) {
|
|
323
|
+
var record = this._clients.get(clientId);
|
|
324
|
+
if (!record || !record.client) {
|
|
325
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
326
|
+
res.end(JSON.stringify({ error: 'Unknown client' }));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
var body = '';
|
|
331
|
+
req.on('data', function(chunk) {
|
|
332
|
+
body += chunk;
|
|
333
|
+
});
|
|
334
|
+
req.on('end', function() {
|
|
335
|
+
try {
|
|
336
|
+
var data = JSON.parse(body);
|
|
337
|
+
var action = data.action;
|
|
338
|
+
var payload = data.data || data;
|
|
339
|
+
record.client._dispatch(action, payload);
|
|
340
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
341
|
+
res.end(JSON.stringify({ ok: true }));
|
|
342
|
+
} catch (e) {
|
|
343
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
344
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export { BwServeApp, BwServeClient };
|
|
351
|
+
|
|
352
|
+
export default { create, BwServeApp, BwServeClient };
|
|
@@ -0,0 +1,103 @@
|
|
|
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/ routes
|
|
6
|
+
* - Calls bw.loadDefaultStyles()
|
|
7
|
+
* - Optionally applies a theme
|
|
8
|
+
* - Creates a #app div
|
|
9
|
+
* - Opens an SSE connection via bw.clientConnect()
|
|
10
|
+
* - Delegates data-bw-action clicks to the server via POST
|
|
11
|
+
*
|
|
12
|
+
* @module bwserve/shell
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate the shell HTML page for a bwserve app.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} opts
|
|
19
|
+
* @param {string} opts.clientId - Unique client ID for this connection
|
|
20
|
+
* @param {string} [opts.title='bwserve'] - Page title
|
|
21
|
+
* @param {string} [opts.theme] - Theme preset name or config
|
|
22
|
+
* @param {boolean} [opts.injectBitwrench=true] - Whether to inject bitwrench scripts
|
|
23
|
+
* @returns {string} Complete HTML document
|
|
24
|
+
*/
|
|
25
|
+
export function generateShell(opts) {
|
|
26
|
+
opts = opts || {};
|
|
27
|
+
var clientId = opts.clientId || 'default';
|
|
28
|
+
var title = opts.title || 'bwserve';
|
|
29
|
+
var inject = opts.injectBitwrench !== false;
|
|
30
|
+
|
|
31
|
+
var head = [
|
|
32
|
+
'<!DOCTYPE html>',
|
|
33
|
+
'<html lang="en">',
|
|
34
|
+
'<head>',
|
|
35
|
+
'<meta charset="UTF-8">',
|
|
36
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1.0">',
|
|
37
|
+
'<title>' + title + '</title>'
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
if (inject) {
|
|
41
|
+
head.push('<script src="/__bw/bitwrench.umd.js"></script>');
|
|
42
|
+
head.push('<link rel="stylesheet" href="/__bw/bitwrench.css">');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
head.push('</head>');
|
|
46
|
+
head.push('<body>');
|
|
47
|
+
head.push('<div id="app"></div>');
|
|
48
|
+
|
|
49
|
+
var script = [
|
|
50
|
+
'<script>',
|
|
51
|
+
'(function() {',
|
|
52
|
+
' "use strict";',
|
|
53
|
+
' bw.loadDefaultStyles();'
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
if (opts.theme) {
|
|
57
|
+
script.push(' bw.generateTheme("bwserve", ' + JSON.stringify(
|
|
58
|
+
typeof opts.theme === 'string'
|
|
59
|
+
? { primary: '#006666', secondary: '#333333' }
|
|
60
|
+
: opts.theme
|
|
61
|
+
) + ');');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
script.push(' var clientId = ' + JSON.stringify(clientId) + ';');
|
|
65
|
+
script.push(' var conn = bw.clientConnect("/__bw/events/" + clientId, {');
|
|
66
|
+
script.push(' actionUrl: "/__bw/action/" + clientId,');
|
|
67
|
+
script.push(' onStatus: function(s) {');
|
|
68
|
+
script.push(' if (typeof console !== "undefined") console.log("[bwserve] " + s);');
|
|
69
|
+
script.push(' }');
|
|
70
|
+
script.push(' });');
|
|
71
|
+
|
|
72
|
+
// data-bw-action click delegation
|
|
73
|
+
script.push(' document.addEventListener("click", function(e) {');
|
|
74
|
+
script.push(' var el = e.target.closest ? e.target.closest("[data-bw-action]") : null;');
|
|
75
|
+
script.push(' if (!el) return;');
|
|
76
|
+
script.push(' e.preventDefault();');
|
|
77
|
+
script.push(' var actionData = {};');
|
|
78
|
+
script.push(' if (el.getAttribute("data-bw-id")) actionData.bwId = el.getAttribute("data-bw-id");');
|
|
79
|
+
script.push(' var form = el.closest("div") || document;');
|
|
80
|
+
script.push(' var inp = form.querySelector("input[type=text],input:not([type])");');
|
|
81
|
+
script.push(' if (inp) { actionData.inputValue = inp.value; inp.value = ""; }');
|
|
82
|
+
script.push(' conn.sendAction(el.getAttribute("data-bw-action"), actionData);');
|
|
83
|
+
script.push(' });');
|
|
84
|
+
|
|
85
|
+
// Enter key on inputs
|
|
86
|
+
script.push(' document.addEventListener("keydown", function(e) {');
|
|
87
|
+
script.push(' if (e.key === "Enter" && e.target.tagName === "INPUT") {');
|
|
88
|
+
script.push(' var form = e.target.closest("div") || document;');
|
|
89
|
+
script.push(' var btn = form.querySelector("[data-bw-action]");');
|
|
90
|
+
script.push(' if (btn) {');
|
|
91
|
+
script.push(' conn.sendAction(btn.getAttribute("data-bw-action"), { inputValue: e.target.value });');
|
|
92
|
+
script.push(' e.target.value = "";');
|
|
93
|
+
script.push(' }');
|
|
94
|
+
script.push(' }');
|
|
95
|
+
script.push(' });');
|
|
96
|
+
|
|
97
|
+
script.push('})();');
|
|
98
|
+
script.push('</script>');
|
|
99
|
+
script.push('</body>');
|
|
100
|
+
script.push('</html>');
|
|
101
|
+
|
|
102
|
+
return head.concat(script).join('\n');
|
|
103
|
+
}
|
package/src/cli/index.js
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* bwcli — Main entry point for the bitwrench command-line tool
|
|
3
3
|
* Arg parsing with util.parseArgs(), help, version, dispatch
|
|
4
|
+
*
|
|
5
|
+
* Subcommands:
|
|
6
|
+
* bwcli <file> [options] Convert a file to styled HTML
|
|
7
|
+
* bwcli serve [dir] [options] Start bwserve dev server
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
import { parseArgs } from 'node:util';
|
|
7
11
|
import { VERSION } from '../version.js';
|
|
8
12
|
import { convertFile } from './convert.js';
|
|
13
|
+
import { runServe } from './serve.js';
|
|
9
14
|
|
|
10
15
|
const USAGE = `
|
|
11
|
-
|
|
16
|
+
bwcli v${VERSION} — bitwrench command-line tool
|
|
12
17
|
|
|
13
18
|
Usage:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
bwcli <file> [options] Convert a file to styled HTML
|
|
20
|
+
bwcli serve [dir] [options] Start bwserve development server
|
|
21
|
+
bwcli --version Print version
|
|
22
|
+
bwcli --help Print this help
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
Convert options:
|
|
19
25
|
-o, --output <file> Output file path (default: input with .html extension)
|
|
20
26
|
-c, --css <file> Include external CSS file
|
|
21
27
|
-t, --theme <name> Theme preset (ocean, sunset, forest, slate) or hex colors ("#pri,#sec")
|
|
@@ -26,16 +32,26 @@ Options:
|
|
|
26
32
|
-f, --favicon <path> Favicon path or URL
|
|
27
33
|
--highlight Include highlight.js for syntax highlighting
|
|
28
34
|
-v, --verbose Verbose output
|
|
35
|
+
|
|
36
|
+
Serve options:
|
|
37
|
+
-p, --port <number> Port to listen on (default: 7902)
|
|
38
|
+
-t, --theme <name> Theme preset or hex colors
|
|
39
|
+
--open Open browser on start
|
|
40
|
+
-v, --verbose Verbose output
|
|
41
|
+
|
|
42
|
+
General:
|
|
29
43
|
-h, --help Print this help
|
|
30
44
|
--version Print version
|
|
31
45
|
|
|
32
46
|
Examples:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
bwcli README.md Convert README.md to README.html
|
|
48
|
+
bwcli README.md -o index.html Specify output file
|
|
49
|
+
bwcli README.md -o out.html --theme ocean Apply ocean theme
|
|
50
|
+
bwcli README.md -o out.html --standalone Self-contained offline HTML
|
|
51
|
+
bwcli README.md -o out.html --highlight With syntax highlighting
|
|
52
|
+
bwcli doc.md --theme "#336699,#cc6633" Custom theme colors
|
|
53
|
+
bwcli serve Serve current directory on port 7902
|
|
54
|
+
bwcli serve ./site --port 8080 Serve ./site on port 8080
|
|
39
55
|
`.trim();
|
|
40
56
|
|
|
41
57
|
/**
|
|
@@ -43,6 +59,11 @@ Examples:
|
|
|
43
59
|
* @param {string[]} argv - process.argv.slice(2)
|
|
44
60
|
*/
|
|
45
61
|
export function run(argv) {
|
|
62
|
+
// Check for subcommand before parseArgs (subcommands have different options)
|
|
63
|
+
if (argv.length > 0 && argv[0] === 'serve') {
|
|
64
|
+
return runServe(argv.slice(1));
|
|
65
|
+
}
|
|
66
|
+
|
|
46
67
|
let values, positionals;
|
|
47
68
|
|
|
48
69
|
try {
|
|
@@ -69,13 +90,13 @@ export function run(argv) {
|
|
|
69
90
|
positionals = result.positionals;
|
|
70
91
|
} catch (err) {
|
|
71
92
|
console.error(`Error: ${err.message}`);
|
|
72
|
-
console.error('Run "
|
|
93
|
+
console.error('Run "bwcli --help" for usage.');
|
|
73
94
|
process.exit(1);
|
|
74
95
|
}
|
|
75
96
|
|
|
76
97
|
// --version
|
|
77
98
|
if (values.version) {
|
|
78
|
-
console.log(`
|
|
99
|
+
console.log(`bwcli v${VERSION}`);
|
|
79
100
|
return;
|
|
80
101
|
}
|
|
81
102
|
|
|
@@ -88,7 +109,7 @@ export function run(argv) {
|
|
|
88
109
|
// No positional args → error
|
|
89
110
|
if (positionals.length === 0) {
|
|
90
111
|
console.error('Error: No input file specified.');
|
|
91
|
-
console.error('Run "
|
|
112
|
+
console.error('Run "bwcli --help" for usage.');
|
|
92
113
|
process.exit(1);
|
|
93
114
|
}
|
|
94
115
|
|