electrobun 0.0.13 → 0.0.14

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.
@@ -0,0 +1,42 @@
1
+ import electobunEventEmmitter from "./events/eventEmitter";
2
+ import { BrowserWindow } from "./core/BrowserWindow";
3
+ import { BrowserView } from "./core/BrowserView";
4
+ import { Tray } from "./core/Tray";
5
+ import * as ApplicationMenu from "./core/ApplicationMenu";
6
+ import * as ContextMenu from "./core/ContextMenu";
7
+ import { Updater } from "./core/Updater";
8
+ import * as Utils from "./core/Utils";
9
+ import { type RPCSchema, createRPC } from "rpc-anywhere";
10
+ import type ElectrobunEvent from "./events/event";
11
+ import * as PATHS from "./core/Paths";
12
+
13
+ // Named Exports
14
+ export {
15
+ type RPCSchema,
16
+ type ElectrobunEvent,
17
+ createRPC,
18
+ BrowserWindow,
19
+ BrowserView,
20
+ Tray,
21
+ Updater,
22
+ Utils,
23
+ ApplicationMenu,
24
+ ContextMenu,
25
+ PATHS,
26
+ };
27
+
28
+ // Default Export
29
+ const Electrobun = {
30
+ BrowserWindow,
31
+ BrowserView,
32
+ Tray,
33
+ Updater,
34
+ Utils,
35
+ ApplicationMenu,
36
+ ContextMenu,
37
+ events: electobunEventEmmitter,
38
+ PATHS,
39
+ };
40
+
41
+ // Electrobun
42
+ export default Electrobun;
@@ -0,0 +1,618 @@
1
+ import { join, resolve } from "path";
2
+ import { type RPCSchema, type RPCTransport, createRPC } from "rpc-anywhere";
3
+ import { execSync } from "child_process";
4
+ import * as fs from "fs";
5
+ import electrobunEventEmitter from "../events/eventEmitter";
6
+ import { BrowserView } from "../core/BrowserView";
7
+ import { Updater } from "../core/Updater";
8
+ import { Tray } from "../core/Tray";
9
+ const CHUNK_SIZE = 1024 * 4; // 4KB
10
+ // todo (yoav): webviewBinaryPath and ELECTROBUN_VIEWS_FOLDER should be passed in as cli/env args by the launcher binary
11
+ // will likely be different on different platforms. Right now these are hardcoded for relative paths inside the mac app bundle.
12
+ const webviewBinaryPath = join("native", "webview");
13
+
14
+ const hash = await Updater.localInfo.hash();
15
+ // Note: we use the build's hash to separate from different apps and different builds
16
+ // but we also want a randomId to separate different instances of the same app
17
+ // todo (yoav): since collisions can crash the app add a function that checks if the
18
+ // file exists first
19
+ const randomId = Math.random().toString(36).substring(7);
20
+ const mainPipe = `/private/tmp/electrobun_ipc_pipe_${hash}_${randomId}_main_in`;
21
+
22
+ try {
23
+ execSync("mkfifo " + mainPipe);
24
+ } catch (e) {
25
+ console.log("pipe out already exists");
26
+ }
27
+
28
+ const zigProc = Bun.spawn([webviewBinaryPath], {
29
+ stdin: "pipe",
30
+ stdout: "pipe",
31
+ env: {
32
+ ...process.env,
33
+ ELECTROBUN_VIEWS_FOLDER: resolve("../Resources/app/views"),
34
+ MAIN_PIPE_IN: mainPipe,
35
+ },
36
+ onExit: (_zigProc) => {
37
+ // right now just exit the whole app if the webview process dies.
38
+ // in the future we probably want to try spin it back up aagain
39
+ process.exit(0);
40
+ },
41
+ });
42
+
43
+ process.on("SIGINT", (code) => {
44
+ // todo (yoav): maybe send a friendly signal to the webviews to let them know
45
+ // we're shutting down
46
+ // clean up the webview process when the bun process dies.
47
+ zigProc.kill();
48
+ // fs.unlinkSync(mainPipe);
49
+ process.exit();
50
+ });
51
+
52
+ process.on("exit", (code) => {
53
+ // Note: this can happen when the bun process crashes
54
+ // make sure that zigProc is killed so it doesn't linger around
55
+ zigProc.kill();
56
+ });
57
+
58
+ const inStream = fs.createWriteStream(mainPipe, {
59
+ flags: "r+",
60
+ });
61
+
62
+ function createStdioTransport(proc): RPCTransport {
63
+ return {
64
+ send(message) {
65
+ try {
66
+ // TODO: this is the same chunking code as browserview pipes,
67
+ // should dedupe
68
+ const messageString = JSON.stringify(message) + "\n";
69
+
70
+ let offset = 0;
71
+ while (offset < messageString.length) {
72
+ const chunk = messageString.slice(offset, offset + CHUNK_SIZE);
73
+ inStream.write(chunk);
74
+ offset += CHUNK_SIZE;
75
+ }
76
+ } catch (error) {
77
+ console.error("bun: failed to serialize message to zig", error);
78
+ }
79
+ },
80
+ registerHandler(handler) {
81
+ async function readStream(stream) {
82
+ const reader = stream.getReader();
83
+ let buffer = "";
84
+
85
+ try {
86
+ while (true) {
87
+ const { done, value } = await reader.read();
88
+ if (done) break;
89
+ buffer += new TextDecoder().decode(value);
90
+ let eolIndex;
91
+ // Process each line contained in the buffer
92
+ while ((eolIndex = buffer.indexOf("\n")) >= 0) {
93
+ const line = buffer.slice(0, eolIndex).trim();
94
+ buffer = buffer.slice(eolIndex + 1);
95
+ if (line) {
96
+ try {
97
+ const event = JSON.parse(line);
98
+ handler(event);
99
+ } catch (error) {
100
+ // Non-json things are just bubbled up to the console.
101
+ console.error("zig: ", line);
102
+ }
103
+ }
104
+ }
105
+ }
106
+ } catch (error) {
107
+ console.error("Error reading from stream:", error);
108
+ } finally {
109
+ reader.releaseLock();
110
+ }
111
+ }
112
+
113
+ readStream(proc.stdout);
114
+ },
115
+ };
116
+ }
117
+
118
+ // todo: consider renaming to TrayMenuItemConfig
119
+ export type MenuItemConfig =
120
+ | { type: "divider" | "separator" }
121
+ | {
122
+ type: "normal";
123
+ label: string;
124
+ tooltip?: string;
125
+ action?: string;
126
+ submenu?: Array<MenuItemConfig>;
127
+ enabled?: boolean;
128
+ checked?: boolean;
129
+ hidden?: boolean;
130
+ };
131
+
132
+ export type ApplicationMenuItemConfig =
133
+ | { type: "divider" | "separator" }
134
+ | {
135
+ type?: "normal";
136
+ label: string;
137
+ tooltip?: string;
138
+ action?: string;
139
+ submenu?: Array<ApplicationMenuItemConfig>;
140
+ enabled?: boolean;
141
+ checked?: boolean;
142
+ hidden?: boolean;
143
+ }
144
+ | {
145
+ type?: "normal";
146
+ label?: string;
147
+ tooltip?: string;
148
+ role?: string;
149
+ submenu?: Array<ApplicationMenuItemConfig>;
150
+ enabled?: boolean;
151
+ checked?: boolean;
152
+ hidden?: boolean;
153
+ };
154
+
155
+ // todo (yoav): move this stuff to bun/rpc/zig.ts
156
+ type ZigHandlers = RPCSchema<{
157
+ requests: {
158
+ createWindow: {
159
+ params: {
160
+ id: number;
161
+ url: string | null;
162
+ html: string | null;
163
+ title: string;
164
+ frame: {
165
+ width: number;
166
+ height: number;
167
+ x: number;
168
+ y: number;
169
+ };
170
+ styleMask: {
171
+ Borderless: boolean;
172
+ Titled: boolean;
173
+ Closable: boolean;
174
+ Miniaturizable: boolean;
175
+ Resizable: boolean;
176
+ UnifiedTitleAndToolbar: boolean;
177
+ FullScreen: boolean;
178
+ FullSizeContentView: boolean;
179
+ UtilityWindow: boolean;
180
+ DocModalWindow: boolean;
181
+ NonactivatingPanel: boolean;
182
+ HUDWindow: boolean;
183
+ };
184
+ titleBarStyle: string;
185
+ };
186
+ response: void;
187
+ };
188
+ createWebview: {
189
+ params: {
190
+ id: number;
191
+ hostWebviewId: number | null;
192
+ pipePrefix: string;
193
+ url: string | null;
194
+ html: string | null;
195
+ partition: string | null;
196
+ preload: string | null;
197
+ frame: {
198
+ x: number;
199
+ y: number;
200
+ width: number;
201
+ height: number;
202
+ };
203
+ autoResize: boolean;
204
+ };
205
+ response: void;
206
+ };
207
+
208
+ addWebviewToWindow: {
209
+ params: {
210
+ windowId: number;
211
+ webviewId: number;
212
+ };
213
+ response: void;
214
+ };
215
+
216
+ loadURL: {
217
+ params: {
218
+ webviewId: number;
219
+ url: string;
220
+ };
221
+ response: void;
222
+ };
223
+ loadHTML: {
224
+ params: {
225
+ webviewId: number;
226
+ html: string;
227
+ };
228
+ response: void;
229
+ };
230
+
231
+ setTitle: {
232
+ params: {
233
+ winId: number;
234
+ title: string;
235
+ };
236
+ response: void;
237
+ };
238
+
239
+ closeWindow: {
240
+ params: {
241
+ winId: number;
242
+ };
243
+ response: void;
244
+ };
245
+
246
+ // fs
247
+ moveToTrash: {
248
+ params: {
249
+ path: string;
250
+ };
251
+ response: boolean;
252
+ };
253
+ showItemInFolder: {
254
+ params: {
255
+ path: string;
256
+ };
257
+ response: boolean;
258
+ };
259
+ openFileDialog: {
260
+ params: {
261
+ startingFolder: string | null;
262
+ allowedFileTypes: string | null;
263
+ canChooseFiles: boolean;
264
+ canChooseDirectory: boolean;
265
+ allowsMultipleSelection: boolean;
266
+ };
267
+ response: { openFileDialogResponse: string };
268
+ };
269
+
270
+ // tray and menu
271
+ createTray: {
272
+ params: {
273
+ id: number;
274
+ title: string;
275
+ image: string;
276
+ template: boolean;
277
+ width: number;
278
+ height: number;
279
+ };
280
+ response: void;
281
+ };
282
+ setTrayTitle: {
283
+ params: {
284
+ id: number;
285
+ title: string;
286
+ };
287
+ response: void;
288
+ };
289
+ setTrayImage: {
290
+ params: {
291
+ id: number;
292
+ image: string;
293
+ };
294
+ response: void;
295
+ };
296
+ setTrayMenu: {
297
+ params: {
298
+ id: number;
299
+ // json string of config
300
+ menuConfig: string;
301
+ };
302
+ response: void;
303
+ };
304
+ setApplicationMenu: {
305
+ params: {
306
+ // json string of config
307
+ menuConfig: string;
308
+ };
309
+ response: void;
310
+ };
311
+ showContextMenu: {
312
+ params: {
313
+ // json string of config
314
+ menuConfig: string;
315
+ };
316
+ response: void;
317
+ };
318
+ };
319
+ }>;
320
+
321
+ type BunHandlers = RPCSchema<{
322
+ requests: {
323
+ decideNavigation: {
324
+ params: {
325
+ webviewId: number;
326
+ url: string;
327
+ };
328
+ response: {
329
+ allow: boolean;
330
+ };
331
+ };
332
+ syncRequest: {
333
+ params: {
334
+ webviewId: number;
335
+ request: string;
336
+ };
337
+ response: {
338
+ payload: string;
339
+ };
340
+ };
341
+ // todo: make these messages instead of requests
342
+ log: {
343
+ params: {
344
+ msg: string;
345
+ };
346
+ response: {
347
+ success: boolean;
348
+ };
349
+ };
350
+ trayEvent: {
351
+ params: {
352
+ id: number;
353
+ action: string;
354
+ };
355
+ response: {
356
+ success: boolean;
357
+ };
358
+ };
359
+ applicationMenuEvent: {
360
+ params: {
361
+ id: number;
362
+ action: string;
363
+ };
364
+ response: {
365
+ success: boolean;
366
+ };
367
+ };
368
+ contextMenuEvent: {
369
+ params: {
370
+ action: string;
371
+ };
372
+ response: {
373
+ success: boolean;
374
+ };
375
+ };
376
+ webviewEvent: {
377
+ params: {
378
+ id: number;
379
+ eventName: string;
380
+ detail: string;
381
+ };
382
+ response: {
383
+ success: boolean;
384
+ };
385
+ };
386
+ windowClose: {
387
+ params: {
388
+ id: number;
389
+ };
390
+ response: {
391
+ success: boolean;
392
+ };
393
+ };
394
+ windowMove: {
395
+ params: {
396
+ id: number;
397
+ x: number;
398
+ y: number;
399
+ };
400
+ response: {
401
+ success: boolean;
402
+ };
403
+ };
404
+ windowResize: {
405
+ params: {
406
+ id: number;
407
+ x: number;
408
+ y: number;
409
+ width: number;
410
+ height: number;
411
+ };
412
+ response: {
413
+ success: boolean;
414
+ };
415
+ };
416
+ };
417
+ }>;
418
+
419
+ const zigRPC = createRPC<BunHandlers, ZigHandlers>({
420
+ transport: createStdioTransport(zigProc),
421
+ requestHandler: {
422
+ decideNavigation: ({ webviewId, url }) => {
423
+ const willNavigate = electrobunEventEmitter.events.webview.willNavigate({
424
+ url,
425
+ webviewId,
426
+ });
427
+
428
+ let result;
429
+ // global will-navigate event
430
+ result = electrobunEventEmitter.emitEvent(willNavigate);
431
+
432
+ result = electrobunEventEmitter.emitEvent(willNavigate, webviewId);
433
+
434
+ if (willNavigate.responseWasSet) {
435
+ return willNavigate.response || { allow: true };
436
+ } else {
437
+ return { allow: true };
438
+ }
439
+ },
440
+ syncRequest: ({ webviewId, request: requestStr }) => {
441
+ const webview = BrowserView.getById(webviewId);
442
+ const { method, params } = JSON.parse(requestStr);
443
+
444
+ if (!webview) {
445
+ const err = `error: could not find webview with id ${webviewId}`;
446
+ console.log(err);
447
+ return { payload: err };
448
+ }
449
+
450
+ if (!method) {
451
+ const err = `error: request missing a method`;
452
+ console.log(err);
453
+ return { payload: err };
454
+ }
455
+
456
+ if (!webview.syncRpc || !webview.syncRpc[method]) {
457
+ const err = `error: webview does not have a handler for method ${method}`;
458
+ console.log(err);
459
+ return { payload: err };
460
+ }
461
+
462
+ const handler = webview.syncRpc[method];
463
+ var response;
464
+ try {
465
+ response = handler(params);
466
+ // Note: Stringify(undefined) returns undefined,
467
+ // if we send undefined as the payload it'll crash
468
+ // so send an empty string which is a better analog for
469
+ // undefined json string
470
+ if (response === undefined) {
471
+ response = "";
472
+ }
473
+ } catch (err) {
474
+ console.log(err);
475
+ console.log("syncRPC failed with", { method, params });
476
+ return { payload: String(err) };
477
+ }
478
+
479
+ const payload = JSON.stringify(response);
480
+ return { payload };
481
+ },
482
+ log: ({ msg }) => {
483
+ console.log("zig: ", msg);
484
+ return { success: true };
485
+ },
486
+ trayEvent: ({ id, action }) => {
487
+ const tray = Tray.getById(id);
488
+ if (!tray) {
489
+ return { success: true };
490
+ }
491
+
492
+ const event = electrobunEventEmitter.events.tray.trayClicked({
493
+ id,
494
+ action,
495
+ });
496
+
497
+ let result;
498
+ // global event
499
+ result = electrobunEventEmitter.emitEvent(event);
500
+
501
+ result = electrobunEventEmitter.emitEvent(event, id);
502
+ // Note: we don't care about the result right now
503
+
504
+ return { success: true };
505
+ },
506
+ applicationMenuEvent: ({ id, action }) => {
507
+ const event = electrobunEventEmitter.events.app.applicationMenuClicked({
508
+ id,
509
+ action,
510
+ });
511
+
512
+ let result;
513
+ // global event
514
+ result = electrobunEventEmitter.emitEvent(event);
515
+
516
+ return { success: true };
517
+ },
518
+ contextMenuEvent: ({ action }) => {
519
+ const event = electrobunEventEmitter.events.app.contextMenuClicked({
520
+ action,
521
+ });
522
+
523
+ let result;
524
+ // global event
525
+ result = electrobunEventEmitter.emitEvent(event);
526
+
527
+ return { success: true };
528
+ },
529
+ webviewEvent: ({ id, eventName, detail }) => {
530
+ const eventMap = {
531
+ "did-navigate": "didNavigate",
532
+ "did-navigate-in-page": "didNavigateInPage",
533
+ "did-commit-navigation": "didCommitNavigation",
534
+ "dom-ready": "domReady",
535
+ "new-window-open": "newWindowOpen",
536
+ };
537
+
538
+ // todo: the events map should use the same hyphenated names instead of camelCase
539
+ const handler =
540
+ electrobunEventEmitter.events.webview[eventMap[eventName]];
541
+
542
+ if (!handler) {
543
+ console.log(`!!!no handler for webview event ${eventName}`);
544
+ return { success: false };
545
+ }
546
+
547
+ const event = handler({
548
+ id,
549
+ detail,
550
+ });
551
+
552
+ let result;
553
+ // global event
554
+ result = electrobunEventEmitter.emitEvent(event);
555
+
556
+ result = electrobunEventEmitter.emitEvent(event, id);
557
+ // Note: we don't care about the result right now
558
+ return { success: true };
559
+ },
560
+ windowClose: ({ id }) => {
561
+ const handler = electrobunEventEmitter.events.window.close;
562
+
563
+ const event = handler({
564
+ id,
565
+ });
566
+
567
+ let result;
568
+ // global event
569
+ result = electrobunEventEmitter.emitEvent(event);
570
+
571
+ result = electrobunEventEmitter.emitEvent(event, id);
572
+ // Note: we don't care about the result right now
573
+
574
+ return { success: false };
575
+ },
576
+ windowMove: ({ id, x, y }) => {
577
+ const handler = electrobunEventEmitter.events.window.move;
578
+
579
+ const event = handler({
580
+ id,
581
+ x,
582
+ y,
583
+ });
584
+
585
+ let result;
586
+ // global event
587
+ result = electrobunEventEmitter.emitEvent(event);
588
+
589
+ result = electrobunEventEmitter.emitEvent(event, id);
590
+ // Note: we don't care about the result right now
591
+
592
+ return { success: false };
593
+ },
594
+ windowResize: ({ id, x, y, width, height }) => {
595
+ const handler = electrobunEventEmitter.events.window.resize;
596
+
597
+ const event = handler({
598
+ id,
599
+ x,
600
+ y,
601
+ width,
602
+ height,
603
+ });
604
+
605
+ let result;
606
+ // global event
607
+ result = electrobunEventEmitter.emitEvent(event);
608
+
609
+ result = electrobunEventEmitter.emitEvent(event, id);
610
+ // Note: we don't care about the result right now
611
+
612
+ return { success: false };
613
+ },
614
+ },
615
+ maxRequestTime: 25000,
616
+ });
617
+
618
+ export { zigRPC, zigProc };
package/dist/electrobun CHANGED
Binary file
package/dist/webview CHANGED
Binary file
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "electrobun",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "Build ultra fast, tiny, and cross-platform desktop apps with Typescript.",
5
5
  "license": "MIT",
6
6
  "author": "Blackboard Technologies Inc.",
7
7
  "keywords": ["bun", "desktop", "app", "cross-platform", "typescript"],
8
8
  "exports": {
9
- ".": "./dist/api/bun/index.js",
10
- "./bun": "./dist/api/bun/index.js",
11
- "./view": "./dist/api/browser/index.js"
9
+ ".": "./dist/api/bun/index.ts",
10
+ "./bun": "./dist/api/bun/index.ts",
11
+ "./view": "./dist/api/browser/index.ts"
12
12
  },
13
13
  "bin": {
14
14
  "electrobun": "dist/electrobun"
@@ -32,17 +32,13 @@
32
32
  "build:zig:release": "cd src/zig && ../../vendors/zig/zig build -Doptimize=ReleaseFast",
33
33
  "build:zig:trdiff:release": "cd src/bsdiff && ../../vendors/zig/zig build -Doptimize=ReleaseFast",
34
34
  "build:launcher:release": "cd src/launcher && ../../vendors/zig/zig build -Doptimize=ReleaseSmall",
35
- "build:extractor:release": "cd src/extractor && ../../vendors/zig/zig build -Doptimize=ReleaseSmall",
36
- "build:api:browser": "rm -r src/browser/build && bun build src/browser/index.ts --target=browser --sourcemap=external --outdir src/browser/build/",
37
- "build:api:bun": "rm -r src/bun/build && bun build src/bun/index.ts --target=bun --sourcemap=external --outdir src/bun/build/",
38
- "build:api:browser:release": "rm -r src/browser/build && bun build src/browser/index.ts --sourcemap=none --target=browser --outdir src/browser/build/",
39
- "build:api:bun:release": "rm -r src/bun/build && bun build src/bun/index.ts --sourcemap=none --target=bun --outdir src/bun/build/",
35
+ "build:extractor:release": "cd src/extractor && ../../vendors/zig/zig build -Doptimize=ReleaseSmall",
40
36
  "build:cli": "bun build src/cli/index.ts --compile --outfile src/cli/build/electrobun",
41
- "build:debug": "npm install && bun build:zig:trdiff && bun build:objc && bun build:api:browser && bun build:api:bun && bun build:zig && bun build:launcher && bun build:extractor && bun build:cli",
42
- "build:release": "bun build:objc && bun build:zig:trdiff:release && bun build:api:browser:release && bun build:api:bun:release && bun build:zig:release && bun build:launcher:release && bun build:extractor:release && bun build:cli",
37
+ "build:debug": "npm install && bun build:zig:trdiff && bun build:objc && bun build:zig && bun build:launcher && bun build:extractor && bun build:cli",
38
+ "build:release": "bun build:objc && bun build:zig:trdiff:release && bun build:zig:release && bun build:launcher:release && bun build:extractor:release && bun build:cli",
43
39
  "build:package": "bun build:release && bun ./scripts/copy-to-dist.ts",
44
40
  "build:dev": "bun build:debug && bun ./scripts/copy-to-dist.ts",
45
- "build:electrobun": "bun build:objc && bun build:browser && bun build:zig && bun build:bun",
41
+ "build:electrobun": "bun build:objc && bun build:zig && bun build:bun",
46
42
  "dev:playground": "bun build:dev && cd playground && npm install && bun build:dev && bun start",
47
43
  "dev:playground:rerun": "cd playground && bun start",
48
44
  "dev:playground:canary": "bun build:package && cd playground && npm install && bun build:canary && bun start:canary",