bunite-core 0.8.1 → 0.9.0

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 (57) hide show
  1. package/package.json +7 -6
  2. package/src/{bun → host}/core/App.ts +45 -81
  3. package/src/{bun → host}/core/BrowserView.ts +64 -64
  4. package/src/{bun → host}/core/BrowserWindow.ts +14 -14
  5. package/src/host/core/Socket.ts +98 -0
  6. package/src/host/core/SurfaceBrowserIPC.ts +7 -0
  7. package/src/host/core/SurfaceManager.ts +154 -0
  8. package/src/host/encryptedPipe.ts +62 -0
  9. package/src/{bun → host}/events/appEvents.ts +0 -1
  10. package/src/host/index.ts +29 -0
  11. package/src/{bun/proc → host}/native.ts +38 -52
  12. package/src/{shared → host}/paths.ts +20 -26
  13. package/src/{bun/preload/inline.ts → host/preloadBundle.ts} +2 -2
  14. package/src/host/serveWeb.ts +81 -0
  15. package/src/native/linux/bunite_linux_runtime.cpp +2 -2
  16. package/src/native/mac/bunite_mac_ffi.mm +2 -2
  17. package/src/native/shared/ffi_exports.h +1 -1
  18. package/src/native/win/native_host_ffi.cpp +2 -2
  19. package/src/preload/runtime.built.js +1 -1
  20. package/src/preload/runtime.ts +54 -219
  21. package/src/preload/tsconfig.json +3 -10
  22. package/src/rpc/encrypt.ts +74 -0
  23. package/src/rpc/error.ts +58 -0
  24. package/src/rpc/framework.ts +132 -0
  25. package/src/rpc/hash.ts +142 -0
  26. package/src/rpc/index.ts +129 -0
  27. package/src/rpc/peer.ts +1055 -0
  28. package/src/rpc/renderer.ts +82 -0
  29. package/src/rpc/schema.ts +246 -0
  30. package/src/rpc/stream.ts +72 -0
  31. package/src/rpc/transport.ts +81 -0
  32. package/src/rpc/wire.ts +150 -0
  33. package/src/{preload/webviewElement.ts → webview/native.ts} +68 -48
  34. package/src/{shared/webviewPolyfill.ts → webview/polyfill.ts} +4 -7
  35. package/src/bun/core/Socket.ts +0 -187
  36. package/src/bun/core/SurfaceBrowserIPC.ts +0 -65
  37. package/src/bun/core/SurfaceManager.ts +0 -201
  38. package/src/bun/index.ts +0 -53
  39. package/src/bun/preload/index.ts +0 -73
  40. package/src/preload/tsconfig.tsbuildinfo +0 -1
  41. package/src/shared/rpc.ts +0 -424
  42. package/src/shared/rpcDemux.ts +0 -219
  43. package/src/shared/rpcWire.ts +0 -54
  44. package/src/shared/rpcWireConstants.ts +0 -3
  45. package/src/shared/webRpcHandler.ts +0 -77
  46. package/src/shared/webSocketTransport.ts +0 -26
  47. package/src/view/index.ts +0 -196
  48. /package/src/{shared → host}/cefVersion.ts +0 -0
  49. /package/src/{bun → host}/core/SurfaceRegistry.ts +0 -0
  50. /package/src/{bun → host}/core/singleInstanceLock.ts +0 -0
  51. /package/src/{bun → host}/core/windowIds.ts +0 -0
  52. /package/src/{bun → host}/events/event.ts +0 -0
  53. /package/src/{bun → host}/events/eventEmitter.ts +0 -0
  54. /package/src/{bun → host}/events/webviewEvents.ts +0 -0
  55. /package/src/{bun → host}/events/windowEvents.ts +0 -0
  56. /package/src/{shared → host}/log.ts +0 -0
  57. /package/src/{shared → host}/platform.ts +0 -0
@@ -0,0 +1,98 @@
1
+ import type { Server, WebSocketHandler } from "bun";
2
+ import type { BrowserView } from "./BrowserView";
3
+ import { log } from "../log";
4
+ import { DEFAULT_MAX_BYTES } from "../../rpc/wire";
5
+
6
+ type ViewRegistry = {
7
+ getById(id: number): BrowserView | undefined;
8
+ };
9
+
10
+ type WebSocketData = {
11
+ webviewId: number;
12
+ pipe?: { deliver(bytes: Uint8Array): void };
13
+ };
14
+
15
+ let rpcServer: Server<WebSocketData> | null = null;
16
+ let rpcPort = 0;
17
+ let registry: ViewRegistry | null = null;
18
+
19
+ export function attachBrowserViewRegistry(nextRegistry: ViewRegistry) {
20
+ registry = nextRegistry;
21
+ }
22
+
23
+ function asBytes(message: unknown): Uint8Array | null {
24
+ if (typeof message === "string") return null;
25
+ if (message instanceof Uint8Array) return message;
26
+ if (message instanceof ArrayBuffer) return new Uint8Array(message);
27
+ if (ArrayBuffer.isView(message)) return new Uint8Array(message.buffer, message.byteOffset, message.byteLength);
28
+ return null;
29
+ }
30
+
31
+ const websocket: WebSocketHandler<WebSocketData> = {
32
+ open(ws) {
33
+ const view = registry?.getById(ws.data.webviewId);
34
+ if (!view) { ws.close(); return; }
35
+ let handler: ((bytes: Uint8Array) => void) | undefined;
36
+ const pending: Uint8Array[] = [];
37
+ const pipe = {
38
+ send: (bytes: Uint8Array) => { ws.send(bytes); },
39
+ setReceive: (h: (bytes: Uint8Array) => void) => {
40
+ handler = h;
41
+ for (const b of pending) h(b);
42
+ pending.length = 0;
43
+ },
44
+ close: () => { try { ws.close(); } catch { /* swallow */ } },
45
+ deliver: (bytes: Uint8Array) => {
46
+ if (handler) handler(bytes);
47
+ else pending.push(bytes);
48
+ },
49
+ };
50
+ ws.data.pipe = pipe;
51
+ void view.attachNewConnection(pipe);
52
+ },
53
+ close(ws) {
54
+ const view = registry?.getById(ws.data.webviewId);
55
+ view?.detachNewConnection();
56
+ ws.data.pipe = undefined;
57
+ },
58
+ message(ws, message) {
59
+ const bytes = asBytes(message);
60
+ if (bytes) ws.data.pipe?.deliver(bytes);
61
+ },
62
+ };
63
+
64
+ export function ensureRpcServer() {
65
+ if (rpcServer) return { rpcServer, rpcPort };
66
+
67
+ let port = 45000;
68
+ while (port <= 65535) {
69
+ try {
70
+ rpcServer = Bun.serve<WebSocketData>({
71
+ hostname: "127.0.0.1",
72
+ port,
73
+ fetch(req, server) {
74
+ const url = new URL(req.url);
75
+ if (url.pathname !== "/rpc") return new Response("Not found", { status: 404 });
76
+ const webviewId = Number(url.searchParams.get("webviewId"));
77
+ if (!Number.isFinite(webviewId)) return new Response("Missing webviewId", { status: 400 });
78
+ if (!registry?.getById(webviewId)) return new Response("Unknown webviewId", { status: 403 });
79
+ const upgraded = server.upgrade(req, { data: { webviewId } });
80
+ return upgraded ? undefined : new Response("Upgrade failed", { status: 500 });
81
+ },
82
+ websocket: { ...websocket, maxPayloadLength: DEFAULT_MAX_BYTES },
83
+ });
84
+ rpcPort = port;
85
+ break;
86
+ } catch (error: any) {
87
+ if (error?.code === "EADDRINUSE") { port += 1; continue; }
88
+ throw error;
89
+ }
90
+ }
91
+ if (!rpcServer) throw new Error("Could not start bunite RPC server.");
92
+ log.debug(`bunite RPC server listening on 127.0.0.1:${rpcPort}`);
93
+ return { rpcServer, rpcPort };
94
+ }
95
+
96
+ export function getRpcPort(): number {
97
+ return rpcPort;
98
+ }
@@ -0,0 +1,7 @@
1
+ import { onSurfaceInit, emitDidNavigate } from "./SurfaceManager";
2
+
3
+ onSurfaceInit((surfaceId, hostViewId, view) => {
4
+ view.on("did-navigate", (event: any) => {
5
+ emitDidNavigate(hostViewId, surfaceId, event.data.detail);
6
+ });
7
+ });
@@ -0,0 +1,154 @@
1
+ import { BrowserView } from "./BrowserView";
2
+ import {
3
+ trackSurface, untrackSurface, getOwnedSurface,
4
+ getHostSurfaceIds, getSurfaceRecord,
5
+ MAX_SURFACES_PER_HOST
6
+ } from "./SurfaceRegistry";
7
+ import { SurfaceCap, type ImplOf, IpcError } from "../../rpc/index";
8
+ import { Stream } from "../../rpc/stream";
9
+
10
+ function applyHostOffset(hostView: BrowserView, x: number, y: number) {
11
+ return { x: x + hostView.frame.x, y: y + hostView.frame.y };
12
+ }
13
+
14
+ type SurfaceInitCallback = (surfaceId: number, hostViewId: number, view: BrowserView) => void;
15
+ const initCallbacks: SurfaceInitCallback[] = [];
16
+
17
+ export function onSurfaceInit(cb: SurfaceInitCallback) {
18
+ initCallbacks.push(cb);
19
+ }
20
+
21
+ type DidNavigateEmit = (event: { surfaceId: number; url: string }) => void;
22
+ const didNavigateSubs = new Map<number, Set<DidNavigateEmit>>();
23
+
24
+ export function emitDidNavigate(hostViewId: number, surfaceId: number, url: string) {
25
+ const subs = didNavigateSubs.get(hostViewId);
26
+ if (!subs) return;
27
+ for (const emit of subs) emit({ surfaceId, url });
28
+ }
29
+
30
+ export function createSurfaceCapImpl(hostViewId: number): ImplOf<typeof SurfaceCap> {
31
+ function ownedSurface(surfaceId: number) {
32
+ const record = getOwnedSurface(surfaceId, { viewId: hostViewId });
33
+ return record;
34
+ }
35
+
36
+ return {
37
+ init: async ({ src, x, y, width, height, hidden = false }) => {
38
+ const hostView = BrowserView.getById(hostViewId);
39
+ if (!hostView) throw new IpcError({ code: "not_found", message: `Host view not found: ${hostViewId}` });
40
+ if (!hostView.windowId) throw new IpcError({ code: "failed_precondition", message: `Host window not found` });
41
+
42
+ const hostIds = getHostSurfaceIds(hostViewId);
43
+ if (hostIds && hostIds.size >= MAX_SURFACES_PER_HOST) {
44
+ throw new IpcError({ code: "resource_exhausted", message: `Surface limit reached (${MAX_SURFACES_PER_HOST})` });
45
+ }
46
+
47
+ const offset = applyHostOffset(hostView, x, y);
48
+ const view = new BrowserView({
49
+ url: src,
50
+ windowId: hostView.windowId,
51
+ appresRoot: hostView.appresRoot,
52
+ frame: { x: offset.x, y: offset.y, width, height },
53
+ autoResize: false,
54
+ });
55
+ trackSurface(view.id, { view, hostViewId, hidden });
56
+ try {
57
+ await view.whenReady();
58
+ } catch {
59
+ untrackSurface(view.id);
60
+ view.remove();
61
+ throw new IpcError({ code: "unavailable", message: "Surface browser creation failed or timed out" });
62
+ }
63
+ for (const cb of initCallbacks) cb(view.id, hostViewId, view);
64
+ if (hidden) view.setVisible(false); else view.bringToFront();
65
+ return { surfaceId: view.id };
66
+ },
67
+
68
+ resize: ({ surfaceId, x, y, w, h }) => {
69
+ const record = ownedSurface(surfaceId);
70
+ if (!record) return;
71
+ const hostView = BrowserView.getById(hostViewId);
72
+ if (!hostView) return;
73
+ const offset = applyHostOffset(hostView, x, y);
74
+ record.view.setBoundsAsync(offset.x, offset.y, w, h);
75
+ },
76
+
77
+ remove: ({ surfaceId }) => {
78
+ const record = ownedSurface(surfaceId);
79
+ if (!record) return;
80
+ untrackSurface(surfaceId);
81
+ record.view.remove();
82
+ },
83
+
84
+ setHidden: ({ surfaceId, hidden }) => {
85
+ const record = ownedSurface(surfaceId);
86
+ if (!record) return;
87
+ record.hidden = hidden;
88
+ record.view.setVisible(!hidden);
89
+ if (!hidden) record.view.bringToFront();
90
+ },
91
+
92
+ setMasks: ({ surfaceId, masks }) => {
93
+ const record = ownedSurface(surfaceId);
94
+ if (!record) return;
95
+ const hostView = BrowserView.getById(hostViewId);
96
+ if (!hostView) return;
97
+ const offset = applyHostOffset(hostView, 0, 0);
98
+ record.view.setMaskRegion(masks.map((m) => ({
99
+ x: m.x + offset.x,
100
+ y: m.y + offset.y,
101
+ w: m.w,
102
+ h: m.h,
103
+ })));
104
+ },
105
+
106
+ setAllPassthrough: ({ passthrough }) => {
107
+ const ids = getHostSurfaceIds(hostViewId);
108
+ if (!ids) return;
109
+ for (const surfaceId of ids) {
110
+ const record = getSurfaceRecord(surfaceId);
111
+ record?.view.setInputPassthrough(passthrough);
112
+ }
113
+ },
114
+
115
+ bringAllVisiblesToFront: () => {
116
+ const ids = getHostSurfaceIds(hostViewId);
117
+ if (!ids) return;
118
+ for (const surfaceId of ids) {
119
+ const record = getSurfaceRecord(surfaceId);
120
+ if (record && !record.hidden) record.view.bringToFront();
121
+ }
122
+ },
123
+
124
+ navigate: ({ surfaceId, url }) => {
125
+ const record = ownedSurface(surfaceId);
126
+ record?.view.loadURL(url);
127
+ },
128
+
129
+ goBack: ({ surfaceId }) => {
130
+ const record = ownedSurface(surfaceId);
131
+ record?.view.goBack();
132
+ },
133
+
134
+ reload: ({ surfaceId }) => {
135
+ const record = ownedSurface(surfaceId);
136
+ record?.view.reload();
137
+ },
138
+
139
+ didNavigate: () => Stream.from<{ surfaceId: number; url: string }>((emit, signal) => {
140
+ let subs = didNavigateSubs.get(hostViewId);
141
+ if (!subs) {
142
+ subs = new Set();
143
+ didNavigateSubs.set(hostViewId, subs);
144
+ }
145
+ subs.add(emit);
146
+ signal.addEventListener("abort", () => {
147
+ const set = didNavigateSubs.get(hostViewId);
148
+ if (!set) return;
149
+ set.delete(emit);
150
+ if (set.size === 0) didNavigateSubs.delete(hostViewId);
151
+ });
152
+ }),
153
+ };
154
+ }
@@ -0,0 +1,62 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
2
+ import type { BytesPipe } from "../rpc/transport";
3
+
4
+ const VERSION = 1;
5
+ const IV_LENGTH = 12;
6
+ const TAG_LENGTH = 16;
7
+ const HEADER_LENGTH = 1 + IV_LENGTH;
8
+
9
+ // node:crypto AES-256-GCM. wire layout matches WebCrypto's: version | iv(12) | ciphertext | authTag(16).
10
+ export async function createEncryptedPipe(base: BytesPipe, rawKey: Uint8Array): Promise<BytesPipe> {
11
+ let downstream: ((bytes: Uint8Array) => void) | undefined;
12
+ let closed = false;
13
+ const closeOnce = () => { if (!closed) { closed = true; base.close(); } };
14
+
15
+ base.setReceive((frame) => {
16
+ if (closed) return;
17
+ if (frame.length < HEADER_LENGTH + TAG_LENGTH || frame[0] !== VERSION) {
18
+ closeOnce();
19
+ return;
20
+ }
21
+ try {
22
+ const iv = frame.subarray(1, HEADER_LENGTH);
23
+ const body = frame.subarray(HEADER_LENGTH);
24
+ const ciphertext = body.subarray(0, body.length - TAG_LENGTH);
25
+ const authTag = body.subarray(body.length - TAG_LENGTH);
26
+ const decipher = createDecipheriv("aes-256-gcm", rawKey, iv);
27
+ decipher.setAuthTag(authTag);
28
+ const head = decipher.update(ciphertext);
29
+ const tail = decipher.final();
30
+ const plaintext = tail.length === 0 ? head : Buffer.concat([head, tail]);
31
+ // Normalize Buffer → plain Uint8Array so downstream prototype checks (msgpackr fast paths,
32
+ // structured clone, etc.) see the same type as the WebCrypto path.
33
+ downstream?.(new Uint8Array(plaintext.buffer, plaintext.byteOffset, plaintext.byteLength));
34
+ } catch {
35
+ closeOnce();
36
+ }
37
+ });
38
+
39
+ return {
40
+ send(bytes) {
41
+ if (closed) return;
42
+ try {
43
+ const iv = randomBytes(IV_LENGTH);
44
+ const cipher = createCipheriv("aes-256-gcm", rawKey, iv);
45
+ const head = cipher.update(bytes);
46
+ const tail = cipher.final();
47
+ const authTag = cipher.getAuthTag();
48
+ const out = new Uint8Array(HEADER_LENGTH + head.length + tail.length + authTag.length);
49
+ out[0] = VERSION;
50
+ out.set(iv, 1);
51
+ out.set(head, HEADER_LENGTH);
52
+ if (tail.length > 0) out.set(tail, HEADER_LENGTH + head.length);
53
+ out.set(authTag, HEADER_LENGTH + head.length + tail.length);
54
+ base.send(out);
55
+ } catch {
56
+ closeOnce();
57
+ }
58
+ },
59
+ setReceive(handler) { downstream = handler; },
60
+ close() { closeOnce(); },
61
+ };
62
+ }
@@ -1,7 +1,6 @@
1
1
  import { BuniteEvent } from "./event";
2
2
 
3
3
  export default {
4
- ready: (data: Record<string, unknown>) => new BuniteEvent("ready", data),
5
4
  beforeQuit: (data: Record<string, unknown>) =>
6
5
  new BuniteEvent<Record<string, unknown>, { allow?: boolean }>("before-quit", data),
7
6
  allWindowsClosed: () => new BuniteEvent("all-windows-closed", {})
@@ -0,0 +1,29 @@
1
+ import { AppRuntime } from "./core/App";
2
+ import { BrowserWindow, type WindowOptionsType } from "./core/BrowserWindow";
3
+ import { BrowserView, type BrowserViewOptions } from "./core/BrowserView";
4
+ import { buniteEventEmitter } from "./events/eventEmitter";
5
+ import { BuniteEvent } from "./events/event";
6
+ import { completePermissionRequest } from "./native";
7
+ import { acquireSingleInstanceLock, type SingleInstanceLock } from "./core/singleInstanceLock";
8
+ import { log, type LogLevel } from "./log";
9
+
10
+ export { serveWeb } from "./serveWeb";
11
+ export type { WebRpcMount } from "./serveWeb";
12
+
13
+ export {
14
+ acquireSingleInstanceLock,
15
+ AppRuntime,
16
+ BrowserWindow,
17
+ BrowserView,
18
+ buniteEventEmitter,
19
+ completePermissionRequest,
20
+ log
21
+ };
22
+
23
+ export type {
24
+ LogLevel,
25
+ BuniteEvent,
26
+ BrowserViewOptions,
27
+ SingleInstanceLock,
28
+ WindowOptionsType
29
+ };
@@ -1,12 +1,11 @@
1
1
  import { CString, dlopen, FFIType, JSCallback, ptr, type Pointer } from "bun:ffi";
2
2
  import { existsSync } from "node:fs";
3
3
  import { delimiter, join } from "node:path";
4
- import { buniteEventEmitter } from "../events/eventEmitter";
5
- import { resolveNativeArtifacts, type ResolvedNativeArtifacts } from "../../shared/paths";
6
- import { log } from "../../shared/log";
4
+ import { buniteEventEmitter } from "./events/eventEmitter";
5
+ import { resolveNativeArtifacts, type ResolvedNativeArtifacts } from "./paths";
6
+ import { log } from "./log";
7
7
 
8
8
  export type NativeBootstrapOptions = {
9
- allowStub?: boolean;
10
9
  hideConsole?: boolean;
11
10
  popupBlocking?: boolean;
12
11
  /**
@@ -19,8 +18,6 @@ export type NativeBootstrapOptions = {
19
18
 
20
19
  export type NativeRuntimeState = {
21
20
  initialized: boolean;
22
- usingStub: boolean;
23
- nativeLoaded: boolean;
24
21
  artifacts: ResolvedNativeArtifacts;
25
22
  };
26
23
 
@@ -32,7 +29,7 @@ type NativeSymbols = {
32
29
  bunite_engine_version: () => CString;
33
30
  bunite_set_log_level: (level: number) => void;
34
31
  bunite_init: (
35
- engineDir: CStringPointer,
32
+ cefDir: CStringPointer,
36
33
  hideConsole: boolean,
37
34
  popupBlocking: boolean,
38
35
  engineConfigJson: CStringPointer
@@ -97,7 +94,6 @@ type NativeSymbols = {
97
94
  bunite_view_bring_to_front: (viewId: number) => void;
98
95
  bunite_view_set_bounds: (viewId: number, x: number, y: number, width: number, height: number) => void;
99
96
  bunite_view_set_bounds_async: (viewId: number, x: number, y: number, width: number, height: number) => void;
100
- bunite_view_set_anchor: (viewId: number, mode: number, inset: number) => void;
101
97
  bunite_view_go_back: (viewId: number) => void;
102
98
  bunite_view_reload: (viewId: number) => void;
103
99
  bunite_view_execute_javascript: (viewId: number, script: CStringPointer) => void;
@@ -272,10 +268,6 @@ const nativeSymbolDefinitions = {
272
268
  args: [FFIType.u32, FFIType.f64, FFIType.f64, FFIType.f64, FFIType.f64],
273
269
  returns: FFIType.void
274
270
  },
275
- bunite_view_set_anchor: {
276
- args: [FFIType.u32, FFIType.i32, FFIType.f64],
277
- returns: FFIType.void
278
- },
279
271
  bunite_view_go_back: {
280
272
  args: [FFIType.u32],
281
273
  returns: FFIType.void
@@ -365,12 +357,12 @@ export function toCString(value: string): CStringPointer {
365
357
 
366
358
  function applyEnvironment(artifacts: ResolvedNativeArtifacts) {
367
359
  // CEF needs engine dir on PATH (libcef.dll) and ICU_DATA pointing at resources. Null for mac/linux.
368
- const engineBinaryDir = artifacts.engineDir && existsSync(join(artifacts.engineDir, "Release", "libcef.dll"))
369
- ? join(artifacts.engineDir, "Release")
370
- : artifacts.engineDir;
371
- const engineResourceDir = artifacts.engineDir && existsSync(join(artifacts.engineDir, "Resources", "resources.pak"))
372
- ? join(artifacts.engineDir, "Resources")
373
- : artifacts.engineDir;
360
+ const engineBinaryDir = artifacts.cefDir && existsSync(join(artifacts.cefDir, "Release", "libcef.dll"))
361
+ ? join(artifacts.cefDir, "Release")
362
+ : artifacts.cefDir;
363
+ const engineResourceDir = artifacts.cefDir && existsSync(join(artifacts.cefDir, "Resources", "resources.pak"))
364
+ ? join(artifacts.cefDir, "Resources")
365
+ : artifacts.cefDir;
374
366
 
375
367
  if (engineResourceDir && !process.env.ICU_DATA) {
376
368
  process.env.ICU_DATA = engineResourceDir;
@@ -583,7 +575,6 @@ export async function initNativeRuntime(
583
575
  return state;
584
576
  }
585
577
 
586
- const allowStub = options.allowStub ?? true;
587
578
  const artifacts = resolveNativeArtifacts();
588
579
  const hasNativeArtifacts = Boolean(
589
580
  artifacts.nativeLibPath && existsSync(artifacts.nativeLibPath)
@@ -591,50 +582,45 @@ export async function initNativeRuntime(
591
582
 
592
583
  applyEnvironment(artifacts);
593
584
 
594
- if (!hasNativeArtifacts && !allowStub) {
585
+ if (!hasNativeArtifacts) {
595
586
  throw new Error(
596
- "bunite native runtime packages are missing. Install platform packages or allow stub mode."
587
+ "bunite: native runtime not found. Install the platform package " +
588
+ `(bunite-native-${process.platform === "win32" ? "win" : process.platform === "darwin" ? "mac" : "linux"}-<arch>) ` +
589
+ "or set BUNITE_CEF_DIR to a CEF runtime directory."
597
590
  );
598
591
  }
599
592
 
600
- nativeLibrary = hasNativeArtifacts ? tryLoadNativeLibrary(artifacts) : null;
593
+ nativeLibrary = tryLoadNativeLibrary(artifacts);
594
+ if (!nativeLibrary) {
595
+ throw new Error(`bunite: failed to load native library at ${artifacts.nativeLibPath}.`);
596
+ }
601
597
 
602
- if (nativeLibrary) {
603
- const EXPECTED_ABI = 4;
604
- const nativeAbi = nativeLibrary.symbols.bunite_abi_version();
605
- if (nativeAbi !== EXPECTED_ABI) {
606
- throw new Error(
607
- `bunite native ABI mismatch: JS expects ${EXPECTED_ABI}, native reports ${nativeAbi}. ` +
608
- `Rebuild native binaries with 'bun run build:native:win'.`
609
- );
610
- }
611
- registerNativeCallbacks(nativeLibrary);
612
- const engineConfigJson = options.engineFlags
613
- ? JSON.stringify(options.engineFlags)
614
- : "";
615
- const initOk = nativeLibrary.symbols.bunite_init(
616
- toCString(artifacts.engineDir ?? ""),
617
- options.hideConsole ?? false,
618
- options.popupBlocking ?? false,
619
- toCString(engineConfigJson)
598
+ const EXPECTED_ABI = 4;
599
+ const nativeAbi = nativeLibrary.symbols.bunite_abi_version();
600
+ if (nativeAbi !== EXPECTED_ABI) {
601
+ throw new Error(
602
+ `bunite native ABI mismatch: JS expects ${EXPECTED_ABI}, native reports ${nativeAbi}. ` +
603
+ `Rebuild native binaries with 'bun run build:native:win'.`
620
604
  );
621
-
622
- if (!initOk) {
623
- nativeLibrary = null;
624
- if (!allowStub) {
625
- throw new Error("bunite native runtime failed to initialize.");
626
- }
627
- }
628
605
  }
629
-
630
- if (!nativeLibrary) {
631
- log.warn("Native runtime packages were not found or could not be loaded. Initializing in stub mode.");
606
+ registerNativeCallbacks(nativeLibrary);
607
+ const engineConfigJson = options.engineFlags ? JSON.stringify(options.engineFlags) : "";
608
+ const initOk = nativeLibrary.symbols.bunite_init(
609
+ toCString(artifacts.cefDir ?? ""),
610
+ options.hideConsole ?? false,
611
+ options.popupBlocking ?? false,
612
+ toCString(engineConfigJson)
613
+ );
614
+ if (!initOk) {
615
+ throw new Error(
616
+ "bunite: native runtime failed to initialize " +
617
+ `(engine dir: ${artifacts.cefDir || "<unset>"}). ` +
618
+ "Verify CEF binaries are available, or set BUNITE_CEF_DIR."
619
+ );
632
620
  }
633
621
 
634
622
  state = {
635
623
  initialized: true,
636
- usingStub: !nativeLibrary,
637
- nativeLoaded: Boolean(nativeLibrary),
638
624
  artifacts
639
625
  };
640
626
  return state;
@@ -12,12 +12,8 @@ export type ResolvedNativeArtifacts = {
12
12
  nativePackageName: string | null;
13
13
  enginePackageName: string | null;
14
14
  nativeLibPath: string | null;
15
- /**
16
- * Engine runtime directory. Engine-specific meaning:
17
- * - CEF (Windows): CEF framework dir containing libcef.dll. Resolved via env, package, or vendors/cef.
18
- * - WKWebView (macOS), WebKitGTK (Linux): null. Engine is the system framework.
19
- */
20
- engineDir: string | null;
15
+ /** CEF framework dir containing libcef.dll. Null on macOS/Linux (system framework). */
16
+ cefDir: string | null;
21
17
  };
22
18
 
23
19
  export function resolvePackageRoot(packageName: string): string | null {
@@ -47,12 +43,12 @@ function parseCefVersion(name: string): number[] | null {
47
43
  return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
48
44
  }
49
45
 
50
- function resolveEngineDir(searchDirs: string[]): string | null {
46
+ function resolveCefDir(searchDirs: string[]): string | null {
51
47
  // CEF-only (Win). mac/linux use system frameworks.
52
48
  if (PLATFORM_TAG !== "win") return null;
53
49
 
54
- // 0. Explicit override (BUNITE_ENGINE_DIR; BUNITE_CEF_DIR as legacy fallback).
55
- const forceDir = process.env.BUNITE_ENGINE_DIR ?? process.env.BUNITE_CEF_DIR;
50
+ // 0. Explicit override.
51
+ const forceDir = process.env.BUNITE_CEF_DIR;
56
52
  if (forceDir && hasCefRuntime(forceDir)) {
57
53
  return forceDir;
58
54
  }
@@ -65,6 +61,12 @@ function resolveEngineDir(searchDirs: string[]): string | null {
65
61
  }
66
62
  }
67
63
 
64
+ // 1b. App's own dist/cef (dev mode reuses `bunite-build`-produced binaries).
65
+ const cwdDist = join(process.cwd(), "dist", "cef");
66
+ if (hasCefRuntime(cwdDist)) {
67
+ return cwdDist;
68
+ }
69
+
68
70
  // 2. Shared CEF root: BUNITE_CEF_ROOTDIR/cef-<version>/
69
71
  const rootDir = process.env.BUNITE_CEF_ROOTDIR;
70
72
  if (rootDir && existsSync(rootDir)) {
@@ -89,15 +91,6 @@ function resolveEngineDir(searchDirs: string[]): string | null {
89
91
  } catch {}
90
92
  }
91
93
 
92
- // 3. vendors/cef inside bunite-core package (monorepo dev)
93
- const packageRoot = resolveBunitePackageRoot();
94
- if (packageRoot) {
95
- const vendorPath = join(packageRoot, "vendors", "cef");
96
- if (hasCefRuntime(vendorPath)) {
97
- return vendorPath;
98
- }
99
- }
100
-
101
94
  return null;
102
95
  }
103
96
 
@@ -114,9 +107,10 @@ export function resolveDefaultAppResRoot(): string | null {
114
107
  }
115
108
 
116
109
  export function resolveNativeArtifacts(): ResolvedNativeArtifacts {
117
- const exeDir = dirname(process.execPath);
110
+ const exeDir = getBaseDir();
118
111
 
119
- // 1. Executable-relative (compiled standalone binary)
112
+ // 1. Entry-script-dir / executable-relative covers both `bun dist/main.js`
113
+ // and a compiled standalone binary, where artifacts ship alongside the entry.
120
114
  const exeNativeLib = join(exeDir, `libBuniteNative${NATIVE_LIB_EXT}`);
121
115
  if (existsSync(exeNativeLib)) {
122
116
  return {
@@ -125,7 +119,7 @@ export function resolveNativeArtifacts(): ResolvedNativeArtifacts {
125
119
  nativePackageName: null,
126
120
  enginePackageName: null,
127
121
  nativeLibPath: exeNativeLib,
128
- engineDir: resolveEngineDir([exeDir])
122
+ cefDir: resolveCefDir([exeDir])
129
123
  };
130
124
  }
131
125
 
@@ -149,9 +143,9 @@ export function resolveNativeArtifacts(): ResolvedNativeArtifacts {
149
143
  nativePackageName,
150
144
  enginePackageName: packagedEngineDir && existsSync(packagedEngineDir) ? enginePackageName : null,
151
145
  nativeLibPath: packagedNativeLibPath,
152
- engineDir: (packagedEngineDir && existsSync(packagedEngineDir))
146
+ cefDir: (packagedEngineDir && existsSync(packagedEngineDir))
153
147
  ? packagedEngineDir
154
- : resolveEngineDir([nativePackageRoot, packageRoot].filter(Boolean) as string[])
148
+ : resolveCefDir([nativePackageRoot, packageRoot].filter(Boolean) as string[])
155
149
  };
156
150
  }
157
151
 
@@ -167,7 +161,7 @@ export function resolveNativeArtifacts(): ResolvedNativeArtifacts {
167
161
  nativePackageName: null,
168
162
  enginePackageName: null,
169
163
  nativeLibPath: directLib,
170
- engineDir: resolveEngineDir([localBuildRoot])
164
+ cefDir: resolveCefDir([localBuildRoot])
171
165
  };
172
166
  }
173
167
 
@@ -180,7 +174,7 @@ export function resolveNativeArtifacts(): ResolvedNativeArtifacts {
180
174
  nativePackageName: null,
181
175
  enginePackageName: null,
182
176
  nativeLibPath: releaseLib,
183
- engineDir: resolveEngineDir([localBuildRoot])
177
+ cefDir: resolveCefDir([localBuildRoot])
184
178
  };
185
179
  }
186
180
  }
@@ -191,6 +185,6 @@ export function resolveNativeArtifacts(): ResolvedNativeArtifacts {
191
185
  nativePackageName: nativePackageRoot ? nativePackageName : null,
192
186
  enginePackageName: enginePackageRoot ? enginePackageName : null,
193
187
  nativeLibPath: null,
194
- engineDir: null
188
+ cefDir: null
195
189
  };
196
190
  }
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { isAbsolute, resolve, sep } from "node:path";
3
- import { log } from "../../shared/log";
3
+ import { log } from "./log";
4
4
 
5
5
  function escapeRootForComparison(path: string) {
6
6
  return process.platform === "win32" ? path.toLowerCase() : path;
@@ -55,7 +55,7 @@ function readCustomPreload(preload: string | null, appresRoot: string | null) {
55
55
 
56
56
  // Bundled at build time so bun --compile works without filesystem access.
57
57
  // @ts-ignore — text import attribute
58
- import embeddedPreloadRuntime from "../../preload/runtime.built.js" with { type: "text" };
58
+ import embeddedPreloadRuntime from "../preload/runtime.built.js" with { type: "text" };
59
59
 
60
60
  function getPreloadRuntime(): string {
61
61
  return embeddedPreloadRuntime;