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.
- package/package.json +7 -6
- package/src/{bun → host}/core/App.ts +45 -81
- package/src/{bun → host}/core/BrowserView.ts +64 -64
- package/src/{bun → host}/core/BrowserWindow.ts +14 -14
- package/src/host/core/Socket.ts +98 -0
- package/src/host/core/SurfaceBrowserIPC.ts +7 -0
- package/src/host/core/SurfaceManager.ts +154 -0
- package/src/host/encryptedPipe.ts +62 -0
- package/src/{bun → host}/events/appEvents.ts +0 -1
- package/src/host/index.ts +29 -0
- package/src/{bun/proc → host}/native.ts +38 -52
- package/src/{shared → host}/paths.ts +20 -26
- package/src/{bun/preload/inline.ts → host/preloadBundle.ts} +2 -2
- package/src/host/serveWeb.ts +81 -0
- package/src/native/linux/bunite_linux_runtime.cpp +2 -2
- package/src/native/mac/bunite_mac_ffi.mm +2 -2
- package/src/native/shared/ffi_exports.h +1 -1
- package/src/native/win/native_host_ffi.cpp +2 -2
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +54 -219
- package/src/preload/tsconfig.json +3 -10
- package/src/rpc/encrypt.ts +74 -0
- package/src/rpc/error.ts +58 -0
- package/src/rpc/framework.ts +132 -0
- package/src/rpc/hash.ts +142 -0
- package/src/rpc/index.ts +129 -0
- package/src/rpc/peer.ts +1055 -0
- package/src/rpc/renderer.ts +82 -0
- package/src/rpc/schema.ts +246 -0
- package/src/rpc/stream.ts +72 -0
- package/src/rpc/transport.ts +81 -0
- package/src/rpc/wire.ts +150 -0
- package/src/{preload/webviewElement.ts → webview/native.ts} +68 -48
- package/src/{shared/webviewPolyfill.ts → webview/polyfill.ts} +4 -7
- package/src/bun/core/Socket.ts +0 -187
- package/src/bun/core/SurfaceBrowserIPC.ts +0 -65
- package/src/bun/core/SurfaceManager.ts +0 -201
- package/src/bun/index.ts +0 -53
- package/src/bun/preload/index.ts +0 -73
- package/src/preload/tsconfig.tsbuildinfo +0 -1
- package/src/shared/rpc.ts +0 -424
- package/src/shared/rpcDemux.ts +0 -219
- package/src/shared/rpcWire.ts +0 -54
- package/src/shared/rpcWireConstants.ts +0 -3
- package/src/shared/webRpcHandler.ts +0 -77
- package/src/shared/webSocketTransport.ts +0 -26
- package/src/view/index.ts +0 -196
- /package/src/{shared → host}/cefVersion.ts +0 -0
- /package/src/{bun → host}/core/SurfaceRegistry.ts +0 -0
- /package/src/{bun → host}/core/singleInstanceLock.ts +0 -0
- /package/src/{bun → host}/core/windowIds.ts +0 -0
- /package/src/{bun → host}/events/event.ts +0 -0
- /package/src/{bun → host}/events/eventEmitter.ts +0 -0
- /package/src/{bun → host}/events/webviewEvents.ts +0 -0
- /package/src/{bun → host}/events/windowEvents.ts +0 -0
- /package/src/{shared → host}/log.ts +0 -0
- /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,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 "
|
|
5
|
-
import { resolveNativeArtifacts, type ResolvedNativeArtifacts } from "
|
|
6
|
-
import { log } from "
|
|
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
|
-
|
|
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.
|
|
369
|
-
? join(artifacts.
|
|
370
|
-
: artifacts.
|
|
371
|
-
const engineResourceDir = artifacts.
|
|
372
|
-
? join(artifacts.
|
|
373
|
-
: artifacts.
|
|
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
|
|
585
|
+
if (!hasNativeArtifacts) {
|
|
595
586
|
throw new Error(
|
|
596
|
-
"bunite native runtime
|
|
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 =
|
|
593
|
+
nativeLibrary = tryLoadNativeLibrary(artifacts);
|
|
594
|
+
if (!nativeLibrary) {
|
|
595
|
+
throw new Error(`bunite: failed to load native library at ${artifacts.nativeLibPath}.`);
|
|
596
|
+
}
|
|
601
597
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
631
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
55
|
-
const forceDir = process.env.
|
|
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 =
|
|
110
|
+
const exeDir = getBaseDir();
|
|
118
111
|
|
|
119
|
-
// 1.
|
|
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
|
-
|
|
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
|
-
|
|
146
|
+
cefDir: (packagedEngineDir && existsSync(packagedEngineDir))
|
|
153
147
|
? packagedEngineDir
|
|
154
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
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 "
|
|
58
|
+
import embeddedPreloadRuntime from "../preload/runtime.built.js" with { type: "text" };
|
|
59
59
|
|
|
60
60
|
function getPreloadRuntime(): string {
|
|
61
61
|
return embeddedPreloadRuntime;
|