electrobun 1.18.1 → 1.18.4-beta.10

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 (64) hide show
  1. package/README.md +37 -14
  2. package/dash.config.ts +28 -0
  3. package/dist/api/browser/global.d.ts +6 -0
  4. package/dist/api/browser/index.ts +162 -44
  5. package/dist/api/{bun → config}/ElectrobunConfig.ts +109 -40
  6. package/dist/api/config/validate.test.ts +31 -0
  7. package/dist/api/config/validate.ts +19 -0
  8. package/dist/api/preload/.generated/compiled.ts +8 -0
  9. package/dist/api/{bun/preload → preload}/build.ts +6 -6
  10. package/dist/api/{bun/preload → preload}/encryption.ts +11 -5
  11. package/dist/api/{bun/preload → preload}/globals.d.ts +8 -0
  12. package/dist/api/{bun/preload → preload}/index.ts +18 -8
  13. package/dist/api/{bun/preload → preload}/webviewTag.ts +32 -3
  14. package/dist/api/{bun/preload → preload}/wgpuTag.ts +33 -2
  15. package/dist/api/{bun → sdks/bun}/__tests__/ffi-contract.test.ts +6 -9
  16. package/dist/api/{bun → sdks/bun}/core/BrowserView.ts +96 -55
  17. package/dist/api/{bun → sdks/bun}/core/BrowserWindow.ts +20 -30
  18. package/dist/api/{bun → sdks/bun}/core/BuildConfig.ts +32 -5
  19. package/dist/api/{bun → sdks/bun}/core/GpuWindow.ts +20 -7
  20. package/dist/api/sdks/bun/core/Socket.ts +22 -0
  21. package/dist/api/{bun → sdks/bun}/core/Tray.ts +33 -32
  22. package/dist/api/{bun → sdks/bun}/core/Updater.ts +10 -10
  23. package/dist/api/{bun → sdks/bun}/core/Utils.ts +3 -5
  24. package/dist/api/{bun → sdks/bun}/core/WGPUView.ts +43 -13
  25. package/dist/api/{bun → sdks/bun}/index.ts +39 -10
  26. package/dist/api/{bun → sdks/bun}/proc/native.ts +942 -746
  27. package/dist/api/{bun → sdks/bun}/webGPU.ts +1 -1
  28. package/dist/api/{bun → sdks/bun}/webgpuAdapter.ts +40 -33
  29. package/dist/api/shared/build-dependencies.test.ts +48 -0
  30. package/dist/api/shared/build-dependencies.ts +46 -0
  31. package/dist/api/shared/go-version.ts +3 -0
  32. package/dist/api/shared/odin-version.ts +6 -0
  33. package/dist/api/shared/rust-version.ts +3 -0
  34. package/dist/go-sdk/callbacks.go +260 -0
  35. package/dist/go-sdk/electrobun.go +2021 -0
  36. package/dist/main.js +35 -28
  37. package/dist/odin-sdk/electrobun/electrobun.odin +2337 -0
  38. package/dist/preload-full.js +948 -0
  39. package/dist/preload-sandboxed.js +111 -0
  40. package/dist/rust-sdk/electrobun.rs +2396 -0
  41. package/dist/zig-sdk/electrobun.zig +2005 -0
  42. package/package.json +5 -29
  43. package/src/cli/index.ts +868 -654
  44. package/bin/electrobun.cjs +0 -169
  45. package/dist/api/bun/core/Socket.ts +0 -205
  46. package/dist/api/bun/core/windowIds.ts +0 -5
  47. package/dist/api/bun/preload/.generated/compiled.ts +0 -8
  48. package/dist/api/shared/bun-version.ts +0 -3
  49. /package/dist/api/{bun/preload → preload}/dragRegions.ts +0 -0
  50. /package/dist/api/{bun/preload → preload}/events.ts +0 -0
  51. /package/dist/api/{bun/preload → preload}/index-sandboxed.ts +0 -0
  52. /package/dist/api/{bun/preload → preload}/internalRpc.ts +0 -0
  53. /package/dist/api/{bun/preload → preload}/overlaySync.ts +0 -0
  54. /package/dist/api/{bun → sdks/bun}/core/ApplicationMenu.ts +0 -0
  55. /package/dist/api/{bun → sdks/bun}/core/ContextMenu.ts +0 -0
  56. /package/dist/api/{bun → sdks/bun}/core/Paths.ts +0 -0
  57. /package/dist/api/{bun → sdks/bun}/core/menuRoles.ts +0 -0
  58. /package/dist/api/{bun → sdks/bun}/events/ApplicationEvents.ts +0 -0
  59. /package/dist/api/{bun → sdks/bun}/events/event.ts +0 -0
  60. /package/dist/api/{bun → sdks/bun}/events/eventEmitter.ts +0 -0
  61. /package/dist/api/{bun → sdks/bun}/events/trayEvents.ts +0 -0
  62. /package/dist/api/{bun → sdks/bun}/events/webviewEvents.ts +0 -0
  63. /package/dist/api/{bun → sdks/bun}/events/windowEvents.ts +0 -0
  64. /package/dist/api/{bun → sdks/bun}/proc/linux.md +0 -0
@@ -1,16 +1,13 @@
1
- import { native, toCString, ffi } from "../proc/native";
2
- import * as fs from "fs";
1
+ import { ffi } from "../proc/native";
3
2
  import electrobunEventEmitter from "../events/eventEmitter";
4
3
  import {
5
4
  type ElectrobunRPCSchema,
6
5
  type ElectrobunRPCConfig,
7
6
  type RPCWithTransport,
8
7
  defineElectrobunRPC,
9
- } from "../../shared/rpc.js";
10
- import { Updater } from "./Updater";
8
+ } from "../../../shared/rpc.js";
11
9
  import { BuildConfig } from "./BuildConfig";
12
10
  import {
13
- rpcPort,
14
11
  sendMessageToWebviewViaSocket,
15
12
  removeSocketForWebview,
16
13
  } from "./Socket";
@@ -20,7 +17,6 @@ import { type Pointer } from "bun:ffi";
20
17
  const BrowserViewMap: {
21
18
  [id: number]: BrowserView<any>;
22
19
  } = {};
23
- let nextWebviewId = 1;
24
20
 
25
21
  export type BrowserViewOptions<T = undefined> = {
26
22
  url: string | null;
@@ -38,7 +34,6 @@ export type BrowserViewOptions<T = undefined> = {
38
34
  rpc: T;
39
35
  hostWebviewId: number;
40
36
  autoResize: boolean;
41
-
42
37
  windowId: number;
43
38
  navigationRules: string | null;
44
39
  // Sandbox mode: when true, disables RPC and only allows event emission
@@ -52,8 +47,7 @@ export type BrowserViewOptions<T = undefined> = {
52
47
  // renderer:
53
48
  };
54
49
 
55
- const hash = await Updater.localInfo.hash();
56
- const buildConfig = await BuildConfig.get();
50
+ const buildConfig = BuildConfig.getSync();
57
51
 
58
52
  const defaultOptions: Partial<BrowserViewOptions> = {
59
53
  url: null,
@@ -68,13 +62,8 @@ const defaultOptions: Partial<BrowserViewOptions> = {
68
62
  height: 600,
69
63
  },
70
64
  };
71
- // Note: we use the build's hash to separate from different apps and different builds
72
- // but we also want a randomId to separate different instances of the same app
73
- const randomId = Math.random().toString(36).substring(7);
74
-
75
65
  export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
76
- id: number = nextWebviewId++;
77
- ptr: Pointer | null = null;
66
+ id = 0;
78
67
  hostWebviewId?: number;
79
68
  windowId!: number;
80
69
  renderer!: "cef" | "native";
@@ -95,9 +84,6 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
95
84
  width: 800,
96
85
  height: 600,
97
86
  };
98
- pipePrefix!: string;
99
- inStream!: fs.WriteStream;
100
- outStream!: ReadableStream<Uint8Array>;
101
87
  secretKey!: Uint8Array;
102
88
  rpc?: T;
103
89
  rpcHandler?: (msg: unknown) => void;
@@ -108,6 +94,13 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
108
94
  startPassthrough: boolean = false;
109
95
  isRemoved: boolean = false;
110
96
 
97
+ get ptr(): Pointer | null {
98
+ if (this.isRemoved) {
99
+ return null;
100
+ }
101
+ return ffi.request.getWebviewPointer({ id: this.id }) as Pointer | null;
102
+ }
103
+
111
104
  constructor(options: Partial<BrowserViewOptions<T>> = defaultOptions) {
112
105
  // const rpc = options.rpc;
113
106
 
@@ -124,9 +117,6 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
124
117
  this.rpc = options.rpc;
125
118
  this.secretKey = new Uint8Array(randomBytes(32));
126
119
  this.partition = options.partition || null;
127
- // todo (yoav): since collisions can crash the app add a function that checks if the
128
- // file exists first
129
- this.pipePrefix = `/private/tmp/electrobun_ipc_pipe_${hash}_${randomId}_${this.id}`;
130
120
  this.hostWebviewId = options.hostWebviewId;
131
121
  this.windowId = options.windowId ?? 0;
132
122
  this.autoResize = options.autoResize === false ? false : true;
@@ -136,8 +126,8 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
136
126
  this.startTransparent = options.startTransparent ?? false;
137
127
  this.startPassthrough = options.startPassthrough ?? false;
138
128
 
129
+ this.id = this.init() as number;
139
130
  BrowserViewMap[this.id] = this;
140
- this.ptr = this.init() as Pointer;
141
131
 
142
132
  // If HTML content was provided, load it after webview creation.
143
133
  if (this.html) {
@@ -148,21 +138,17 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
148
138
  }
149
139
 
150
140
  init() {
151
- this.createStreams();
141
+ this.initializeRpcTransport();
152
142
 
153
143
  return ffi.request.createWebview({
154
- id: this.id,
155
144
  windowId: this.windowId,
145
+ hostWebviewId: this.hostWebviewId ?? null,
156
146
  renderer: this.renderer,
157
- rpcPort: rpcPort,
158
147
  // todo: consider sending secretKey as base64
159
148
  secretKey: this.secretKey.toString(),
160
- hostWebviewId: this.hostWebviewId || null,
161
- pipePrefix: this.pipePrefix,
162
149
  partition: this.partition,
163
150
  // Only pass URL if no HTML content is provided to avoid conflicts
164
151
  url: this.html ? null : this.url,
165
- html: this.html,
166
152
  preload: this.preload,
167
153
  viewsRoot: this.viewsRoot,
168
154
  frame: {
@@ -180,7 +166,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
180
166
  });
181
167
  }
182
168
 
183
- createStreams() {
169
+ initializeRpcTransport() {
184
170
  if (!this.rpc) {
185
171
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
186
172
  this.rpc = BrowserView.defineRPC({
@@ -191,23 +177,23 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
191
177
  this.rpc!.setTransport(this.createTransport());
192
178
  }
193
179
 
194
- sendMessageToWebviewViaExecute(jsonMessage: unknown) {
180
+ sendHostMessageToWebviewViaExecute(jsonMessage: unknown) {
195
181
  const stringifiedMessage =
196
182
  typeof jsonMessage === "string"
197
183
  ? jsonMessage
198
184
  : JSON.stringify(jsonMessage);
199
185
  // todo (yoav): make this a shared const with the browser api
200
- const wrappedMessage = `window.__electrobun.receiveMessageFromBun(${stringifiedMessage})`;
186
+ const wrappedMessage = `window.__electrobun.receiveMessageFromHost(${stringifiedMessage})`;
201
187
  this.executeJavascript(wrappedMessage);
202
188
  }
203
189
 
204
- sendInternalMessageViaExecute(jsonMessage: unknown) {
190
+ sendInternalHostMessageViaExecute(jsonMessage: unknown) {
205
191
  const stringifiedMessage =
206
192
  typeof jsonMessage === "string"
207
193
  ? jsonMessage
208
194
  : JSON.stringify(jsonMessage);
209
195
  // todo (yoav): make this a shared const with the browser api
210
- const wrappedMessage = `window.__electrobun.receiveInternalMessageFromBun(${stringifiedMessage})`;
196
+ const wrappedMessage = `window.__electrobun.receiveInternalMessageFromHost(${stringifiedMessage})`;
211
197
  this.executeJavascript(wrappedMessage);
212
198
  }
213
199
 
@@ -224,7 +210,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
224
210
 
225
211
  loadURL(url: string) {
226
212
  this.url = url;
227
- native!.symbols.loadURLInWebView(this.ptr, toCString(this.url));
213
+ ffi.request.loadURLInWebView({ id: this.id, url: this.url });
228
214
  }
229
215
 
230
216
  loadHTML(html: string) {
@@ -232,18 +218,18 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
232
218
 
233
219
  if (this.renderer === "cef") {
234
220
  // For CEF, store HTML content in native map and use scheme handler
235
- native!.symbols.setWebviewHTMLContent(this.id, toCString(html));
221
+ ffi.request.setWebviewHTMLContent({ id: this.id, html });
236
222
  this.loadURL("views://internal/index.html");
237
223
  } else {
238
224
  // For WKWebView, load HTML content directly
239
- native!.symbols.loadHTMLInWebView(this.ptr, toCString(html));
225
+ ffi.request.loadHTMLInWebView({ id: this.id, html });
240
226
  }
241
227
  }
242
228
 
243
229
  setNavigationRules(rules: string[]) {
244
230
  this.navigationRules = JSON.stringify(rules);
245
231
  const rulesJson = JSON.stringify(rules);
246
- native!.symbols.setWebviewNavigationRules(this.ptr, toCString(rulesJson));
232
+ ffi.request.setWebviewNavigationRules({ id: this.id, rulesJson });
247
233
  }
248
234
 
249
235
  findInPage(
@@ -252,28 +238,28 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
252
238
  ) {
253
239
  const forward = options?.forward ?? true;
254
240
  const matchCase = options?.matchCase ?? false;
255
- native!.symbols.webviewFindInPage(
256
- this.ptr,
257
- toCString(searchText),
241
+ ffi.request.webviewFindInPage({
242
+ id: this.id,
243
+ searchText,
258
244
  forward,
259
245
  matchCase,
260
- );
246
+ });
261
247
  }
262
248
 
263
249
  stopFindInPage() {
264
- native!.symbols.webviewStopFind(this.ptr);
250
+ ffi.request.webviewStopFind({ id: this.id });
265
251
  }
266
252
 
267
253
  openDevTools() {
268
- native!.symbols.webviewOpenDevTools(this.ptr);
254
+ ffi.request.webviewOpenDevTools({ id: this.id });
269
255
  }
270
256
 
271
257
  closeDevTools() {
272
- native!.symbols.webviewCloseDevTools(this.ptr);
258
+ ffi.request.webviewCloseDevTools({ id: this.id });
273
259
  }
274
260
 
275
261
  toggleDevTools() {
276
- native!.symbols.webviewToggleDevTools(this.ptr);
262
+ ffi.request.webviewToggleDevTools({ id: this.id });
277
263
  }
278
264
 
279
265
  /**
@@ -281,7 +267,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
281
267
  * @param zoomLevel - The zoom level (1.0 = 100%, 1.5 = 150%, etc.)
282
268
  */
283
269
  setPageZoom(zoomLevel: number) {
284
- native!.symbols.webviewSetPageZoom(this.ptr, zoomLevel);
270
+ ffi.request.webviewSetPageZoom({ id: this.id, zoomLevel });
285
271
  }
286
272
 
287
273
  /**
@@ -289,7 +275,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
289
275
  * @returns The current zoom level (1.0 = 100%)
290
276
  */
291
277
  getPageZoom(): number {
292
- return native!.symbols.webviewGetPageZoom(this.ptr) as number;
278
+ return ffi.request.webviewGetPageZoom({ id: this.id }) as number;
293
279
  }
294
280
 
295
281
  // todo (yoav): move this to a class that also has off, append, prepend, etc.
@@ -326,9 +312,9 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
326
312
  if (!sentOverSocket) {
327
313
  try {
328
314
  const messageString = JSON.stringify(message);
329
- that.sendMessageToWebviewViaExecute(messageString);
315
+ that.sendHostMessageToWebviewViaExecute(messageString);
330
316
  } catch (error) {
331
- console.error("bun: failed to serialize message to webview", error);
317
+ console.error("host: failed to serialize message to webview", error);
332
318
  }
333
319
  }
334
320
  },
@@ -342,10 +328,9 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
342
328
  };
343
329
 
344
330
  remove() {
345
- if (!this.ptr || this.isRemoved) {
331
+ if (this.isRemoved) {
346
332
  return;
347
333
  }
348
- const ptr = this.ptr;
349
334
  this.isRemoved = true;
350
335
  // Drop JS-side references first so late callbacks cannot target a stale view.
351
336
  delete BrowserViewMap[this.id];
@@ -356,16 +341,72 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
356
341
  unregisterHandler() {},
357
342
  });
358
343
  this.rpcHandler = undefined;
359
-
360
- this.rpcHandler = undefined;
361
- this.ptr = null;
362
- native!.symbols.webviewRemove(ptr);
344
+ try {
345
+ ffi.request.webviewRemove({ id: this.id });
346
+ } catch (error) {
347
+ console.error(`Error removing webview ${this.id}:`, error);
348
+ }
363
349
  }
364
350
 
365
351
  static getById(id: number) {
366
352
  return BrowserViewMap[id];
367
353
  }
368
354
 
355
+ // Core can create webviews before Bun has constructed a JS wrapper for them.
356
+ // Use this in native/runtime paths that need to ensure a wrapper exists.
357
+ static ensureWrapped<T extends RPCWithTransport = RPCWithTransport>(
358
+ id: number,
359
+ options: Partial<BrowserViewOptions<T>> = {},
360
+ ) {
361
+ return (
362
+ (BrowserViewMap[id] as BrowserView<T> | undefined) ??
363
+ BrowserView.adoptExisting(id, options)
364
+ );
365
+ }
366
+
367
+ static adoptExisting<T extends RPCWithTransport = RPCWithTransport>(
368
+ id: number,
369
+ options: Partial<BrowserViewOptions<T>> = {},
370
+ ) {
371
+ const existing = BrowserViewMap[id] as BrowserView<T> | undefined;
372
+ if (existing) {
373
+ return existing;
374
+ }
375
+
376
+ const ptr = ffi.request.getWebviewPointer({ id }) as Pointer | null;
377
+ if (!ptr) {
378
+ return undefined;
379
+ }
380
+
381
+ const view = Object.create(BrowserView.prototype) as BrowserView<T>;
382
+ view.id = id;
383
+ view.hostWebviewId = options.hostWebviewId;
384
+ view.windowId = options.windowId ?? 0;
385
+ view.renderer = options.renderer ?? defaultOptions.renderer ?? "native";
386
+ view.url = options.url ?? defaultOptions.url ?? null;
387
+ view.html = options.html ?? defaultOptions.html ?? null;
388
+ view.preload = options.preload ?? defaultOptions.preload ?? null;
389
+ view.viewsRoot = options.viewsRoot ?? defaultOptions.viewsRoot ?? null;
390
+ view.partition = options.partition ?? null;
391
+ view.frame = {
392
+ x: options.frame?.x ?? defaultOptions.frame!.x,
393
+ y: options.frame?.y ?? defaultOptions.frame!.y,
394
+ width: options.frame?.width ?? defaultOptions.frame!.width,
395
+ height: options.frame?.height ?? defaultOptions.frame!.height,
396
+ };
397
+ view.secretKey = new Uint8Array(0);
398
+ view.rpc = options.rpc;
399
+ view.rpcHandler = undefined;
400
+ view.autoResize = options.autoResize === false ? false : true;
401
+ view.navigationRules = options.navigationRules ?? null;
402
+ view.sandbox = options.sandbox ?? false;
403
+ view.startTransparent = options.startTransparent ?? false;
404
+ view.startPassthrough = options.startPassthrough ?? false;
405
+ view.isRemoved = false;
406
+ BrowserViewMap[id] = view as BrowserView<any>;
407
+ return view;
408
+ }
409
+
369
410
  static getAll() {
370
411
  return Object.values(BrowserViewMap);
371
412
  }
@@ -3,13 +3,13 @@ import electrobunEventEmitter from "../events/eventEmitter";
3
3
  import { BrowserView } from "./BrowserView";
4
4
  import { type Pointer } from "bun:ffi";
5
5
  import { BuildConfig } from "./BuildConfig";
6
- import { quit } from "./Utils";
7
- import { type RPCWithTransport } from "../../shared/rpc.js";
8
- import { getNextWindowId } from "./windowIds";
9
- import { GpuWindowMap } from "./GpuWindow";
6
+ import { type RPCWithTransport } from "../../../shared/rpc.js";
10
7
  import { WGPUView } from "./WGPUView";
11
8
 
12
- const buildConfig = await BuildConfig.get();
9
+ const buildConfig = BuildConfig.getSync();
10
+ ffi.request.setExitOnLastWindowClosed({
11
+ enabled: buildConfig.runtime?.exitOnLastWindowClosed ?? true,
12
+ });
13
13
 
14
14
  export type WindowOptionsType<T = undefined> = {
15
15
  trafficLightOffset?: {
@@ -73,7 +73,7 @@ export const BrowserWindowMap: {
73
73
  [id: number]: BrowserWindow<RPCWithTransport>;
74
74
  } = {};
75
75
 
76
- // Clean up the window map when a window closes and optionally quit the app
76
+ // Clean up JS wrapper state when a window closes. Native child cleanup is core-owned.
77
77
  electrobunEventEmitter.on("close", (event: { data: { id: number } }) => {
78
78
  const windowId = event.data.id;
79
79
  delete BrowserWindowMap[windowId];
@@ -89,35 +89,16 @@ electrobunEventEmitter.on("close", (event: { data: { id: number } }) => {
89
89
  const wgpuViews = WGPUView.getAll().filter(v => v.windowId === windowId);
90
90
  for (const view of wgpuViews) {
91
91
  try {
92
- // If ptr is null, the view was already cleaned up by the renderer or native cleanup
93
- if (view.ptr === null) {
94
- // Already cleaned up, skip
95
- } else {
96
- // Programmatic close path - remove the view
97
- view.remove();
98
- }
92
+ view.remove();
99
93
  } catch (e) {
100
94
  console.error(`Error cleaning up WGPU view ${view.id}:`, e);
101
- // If remove() failed, at least mark it as cleaned up
102
- view.ptr = null as any;
103
95
  }
104
96
  }
105
97
 
106
- const exitOnLastWindowClosed =
107
- buildConfig.runtime?.exitOnLastWindowClosed ?? true;
108
-
109
- if (
110
- exitOnLastWindowClosed &&
111
- Object.keys(BrowserWindowMap).length === 0 &&
112
- Object.keys(GpuWindowMap).length === 0
113
- ) {
114
- quit();
115
- }
116
98
  });
117
99
 
118
100
  export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
119
- id: number = getNextWindowId();
120
- ptr!: Pointer;
101
+ id = 0;
121
102
  title: string = "Electrobun";
122
103
  state: "creating" | "created" = "creating";
123
104
  url: string | null = null;
@@ -146,6 +127,10 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
146
127
  // todo (yoav): make this an array of ids or something
147
128
  webviewId!: number;
148
129
 
130
+ get ptr(): Pointer | null {
131
+ return ffi.request.getWindowPointer({ winId: this.id }) as Pointer | null;
132
+ }
133
+
149
134
  constructor(options: Partial<WindowOptionsType<T>> = defaultOptions) {
150
135
  this.title = options.title || "New Window";
151
136
  this.frame = options.frame
@@ -177,8 +162,7 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
177
162
  hidden,
178
163
  activate,
179
164
  }: Partial<WindowOptionsType<T>>) {
180
- this.ptr = ffi.request.createWindow({
181
- id: this.id,
165
+ const windowId = ffi.request.createWindow({
182
166
  title: this.title,
183
167
  url: this.url || "",
184
168
  frame: {
@@ -221,7 +205,13 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
221
205
  hidden: hidden ?? false,
222
206
  activate: activate ?? true,
223
207
  trafficLightOffset: this.trafficLightOffset,
224
- }) as Pointer;
208
+ });
209
+
210
+ if (!windowId) {
211
+ throw "Failed to create window";
212
+ }
213
+
214
+ this.id = windowId as number;
225
215
 
226
216
  BrowserWindowMap[this.id] = this;
227
217
 
@@ -1,8 +1,10 @@
1
+ import { readFileSync } from "fs";
2
+
1
3
  export type BuildConfigType = {
4
+ mainProcess?: "cottontail" | "zig" | "rust" | "go" | "odin";
2
5
  defaultRenderer: "native" | "cef";
3
6
  availableRenderers: ("native" | "cef")[];
4
7
  cefVersion?: string;
5
- bunVersion?: string;
6
8
  runtime?: {
7
9
  exitOnLastWindowClosed?: boolean;
8
10
  [key: string]: unknown;
@@ -11,6 +13,13 @@ export type BuildConfigType = {
11
13
 
12
14
  let buildConfig: BuildConfigType | null = null;
13
15
 
16
+ function fallbackBuildConfig(): BuildConfigType {
17
+ return {
18
+ defaultRenderer: "native",
19
+ availableRenderers: ["native"],
20
+ };
21
+ }
22
+
14
23
  const BuildConfig = {
15
24
  /**
16
25
  * Get the build configuration. Loads from build.json on first call, then returns cached value.
@@ -26,10 +35,28 @@ const BuildConfig = {
26
35
  return buildConfig!;
27
36
  } catch (error) {
28
37
  // Fallback for dev mode or missing file
29
- buildConfig = {
30
- defaultRenderer: "native",
31
- availableRenderers: ["native"],
32
- };
38
+ buildConfig = fallbackBuildConfig();
39
+ return buildConfig;
40
+ }
41
+ },
42
+
43
+ /**
44
+ * Get the build configuration synchronously.
45
+ * Useful for modules that cannot use top-level await.
46
+ */
47
+ getSync: (): BuildConfigType => {
48
+ if (buildConfig) {
49
+ return buildConfig;
50
+ }
51
+
52
+ try {
53
+ const resourcesDir = "Resources";
54
+ buildConfig = JSON.parse(
55
+ readFileSync(`../${resourcesDir}/build.json`, "utf8"),
56
+ ) as BuildConfigType;
57
+ return buildConfig;
58
+ } catch (error) {
59
+ buildConfig = fallbackBuildConfig();
33
60
  return buildConfig;
34
61
  }
35
62
  },
@@ -2,7 +2,7 @@ import { ffi } from "../proc/native";
2
2
  import electrobunEventEmitter from "../events/eventEmitter";
3
3
  import { type Pointer } from "bun:ffi";
4
4
  import { WGPUView } from "./WGPUView";
5
- import { getNextWindowId } from "./windowIds";
5
+ import { BuildConfig } from "./BuildConfig";
6
6
 
7
7
 
8
8
  export type GpuWindowOptionsType = {
@@ -35,11 +35,16 @@ const defaultOptions: GpuWindowOptionsType = {
35
35
  transparent: false,
36
36
  };
37
37
 
38
+ const buildConfig = BuildConfig.getSync();
39
+ ffi.request.setExitOnLastWindowClosed({
40
+ enabled: buildConfig.runtime?.exitOnLastWindowClosed ?? true,
41
+ });
42
+
38
43
  export const GpuWindowMap: {
39
44
  [id: number]: GpuWindow;
40
45
  } = {};
41
46
 
42
- // Clean up the window map when a window closes and optionally quit the app
47
+ // Clean up JS wrapper state when a window closes. Native child cleanup is core-owned.
43
48
  electrobunEventEmitter.on("close", (event: { data: { id: number } }) => {
44
49
  const windowId = event.data.id;
45
50
  delete GpuWindowMap[windowId];
@@ -54,8 +59,7 @@ electrobunEventEmitter.on("close", (event: { data: { id: number } }) => {
54
59
  });
55
60
 
56
61
  export class GpuWindow {
57
- id: number = getNextWindowId();
58
- ptr!: Pointer;
62
+ id = 0;
59
63
  title: string = "Electrobun";
60
64
  state: "creating" | "created" = "creating";
61
65
  transparent: boolean = false;
@@ -73,6 +77,10 @@ export class GpuWindow {
73
77
  };
74
78
  wgpuViewId!: number;
75
79
 
80
+ get ptr(): Pointer | null {
81
+ return ffi.request.getWindowPointer({ winId: this.id }) as Pointer | null;
82
+ }
83
+
76
84
  constructor(options: Partial<GpuWindowOptionsType> = defaultOptions) {
77
85
  this.title = options.title || "New Window";
78
86
  this.frame = options.frame
@@ -93,8 +101,7 @@ export class GpuWindow {
93
101
  transparent,
94
102
  activate,
95
103
  }: Partial<GpuWindowOptionsType>) {
96
- this.ptr = ffi.request.createWindow({
97
- id: this.id,
104
+ const windowId = ffi.request.createWindow({
98
105
  title: this.title,
99
106
  url: "",
100
107
  frame: {
@@ -136,7 +143,13 @@ export class GpuWindow {
136
143
  transparent: transparent ?? false,
137
144
  activate: activate ?? true,
138
145
  trafficLightOffset: this.trafficLightOffset,
139
- }) as Pointer;
146
+ });
147
+
148
+ if (!windowId) {
149
+ throw "Failed to create window";
150
+ }
151
+
152
+ this.id = windowId as number;
140
153
 
141
154
  GpuWindowMap[this.id] = this;
142
155
 
@@ -0,0 +1,22 @@
1
+ import { ffi } from "../proc/native";
2
+
3
+ export const removeSocketForWebview = (webviewId: number) => {
4
+ ffi.request.clearWebviewHostTransport({ id: webviewId });
5
+ };
6
+
7
+ // Will return true if message was sent over the core-owned websocket transport.
8
+ // False means the caller should fall back to the native bridge / evaluateJS path.
9
+ export const sendMessageToWebviewViaSocket = (
10
+ webviewId: number,
11
+ message: unknown,
12
+ ): boolean => {
13
+ try {
14
+ return ffi.request.sendHostMessageToWebviewViaTransport({
15
+ id: webviewId,
16
+ messageJson: JSON.stringify(message),
17
+ }) as boolean;
18
+ } catch (error) {
19
+ console.error("Error sending message to webview via host transport:", error);
20
+ return false;
21
+ }
22
+ };