bunite-core 0.0.1
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 +24 -0
- package/src/bun/core/App.ts +142 -0
- package/src/bun/core/BrowserView.ts +262 -0
- package/src/bun/core/BrowserWindow.ts +322 -0
- package/src/bun/core/Socket.ts +186 -0
- package/src/bun/core/Utils.ts +72 -0
- package/src/bun/core/windowIds.ts +7 -0
- package/src/bun/events/appEvents.ts +7 -0
- package/src/bun/events/event.ts +20 -0
- package/src/bun/events/eventEmitter.ts +28 -0
- package/src/bun/events/webviewEvents.ts +13 -0
- package/src/bun/events/windowEvents.ts +19 -0
- package/src/bun/index.ts +41 -0
- package/src/bun/preload/index.ts +73 -0
- package/src/bun/preload/inline.ts +87 -0
- package/src/bun/proc/native.ts +666 -0
- package/src/native/shared/callbacks.h +6 -0
- package/src/native/shared/cef_response_filter.h +116 -0
- package/src/native/shared/ffi_exports.h +119 -0
- package/src/native/shared/webview_storage.h +89 -0
- package/src/native/win/native_host.cpp +2453 -0
- package/src/native/win/process_helper_win.cpp +26 -0
- package/src/preload/runtime.built.js +1 -0
- package/src/preload/runtime.ts +215 -0
- package/src/preload/tsconfig.json +13 -0
- package/src/shared/paths.ts +133 -0
- package/src/shared/platform.ts +9 -0
- package/src/shared/rpc.ts +399 -0
- package/src/shared/rpcWire.ts +54 -0
- package/src/shared/rpcWireConstants.ts +3 -0
- package/src/types/config.ts +29 -0
- package/src/view/index.ts +159 -0
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bunite-core",
|
|
3
|
+
"description": "Uniting UI and Bun",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build:native:win": "cmake -S . -B build/win -DBUNITE_TARGET_ARCH=x64 && cmake --build build/win --config Release",
|
|
8
|
+
"build:preload": "bun build src/preload/runtime.ts --outfile src/preload/runtime.built.js --target browser --minify"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/bun/index.ts",
|
|
12
|
+
"./bun": "./src/bun/index.ts",
|
|
13
|
+
"./view": "./src/view/index.ts",
|
|
14
|
+
"./config": "./src/types/config.ts",
|
|
15
|
+
"./shared/rpc": "./src/shared/rpc.ts",
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"msgpackr": "^1.11.9"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { BuniteEvent } from "../events/event";
|
|
3
|
+
import { buniteEventEmitter } from "../events/eventEmitter";
|
|
4
|
+
import {
|
|
5
|
+
getNativeLibrary,
|
|
6
|
+
initNativeRuntime,
|
|
7
|
+
getNativeRuntimeState,
|
|
8
|
+
setRouteRequestHandler,
|
|
9
|
+
toCString,
|
|
10
|
+
type NativeBootstrapOptions
|
|
11
|
+
} from "../proc/native";
|
|
12
|
+
import { attachGlobalIPCResolver, ensureRPCServer } from "./Socket";
|
|
13
|
+
|
|
14
|
+
type AppInitOptions = NativeBootstrapOptions & {
|
|
15
|
+
userDataDir?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type GlobalIPCHandler = (params: unknown, ctx: { viewId: number }) => unknown | Promise<unknown>;
|
|
19
|
+
|
|
20
|
+
class AppRuntime {
|
|
21
|
+
private initPromise: Promise<void> | null = null;
|
|
22
|
+
private stubKeepAliveTimer: ReturnType<typeof setInterval> | null = null;
|
|
23
|
+
private readonly globalIPCHandlers = new Map<string, GlobalIPCHandler>();
|
|
24
|
+
|
|
25
|
+
async init(options: AppInitOptions = {}) {
|
|
26
|
+
if (!this.initPromise) {
|
|
27
|
+
this.initPromise = (async () => {
|
|
28
|
+
if (options.userDataDir) {
|
|
29
|
+
process.env.BUNITE_USER_DATA_DIR = options.userDataDir;
|
|
30
|
+
} else if (!process.env.BUNITE_USER_DATA_DIR) {
|
|
31
|
+
process.env.BUNITE_USER_DATA_DIR = join(process.cwd(), ".bunite");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const runtime = await initNativeRuntime({
|
|
35
|
+
allowStub: options.allowStub,
|
|
36
|
+
hideConsole: options.hideConsole,
|
|
37
|
+
popupBlocking: options.popupBlocking,
|
|
38
|
+
chromiumFlags: options.chromiumFlags
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
attachGlobalIPCResolver((channel) => this.getGlobalIPCHandler(channel));
|
|
42
|
+
setRouteRequestHandler((requestId, path) => this.handleRouteRequest(requestId, path));
|
|
43
|
+
|
|
44
|
+
// Replay view routes registered before init
|
|
45
|
+
for (const path of this.viewHandlers.keys()) {
|
|
46
|
+
getNativeLibrary()?.symbols.bunite_register_view_route(toCString(path));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ensureRPCServer();
|
|
50
|
+
buniteEventEmitter.emitEvent(
|
|
51
|
+
new BuniteEvent("ready", {
|
|
52
|
+
usingStub: runtime.usingStub,
|
|
53
|
+
artifacts: runtime.artifacts
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
})();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await this.initPromise;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
on(name: string, handler: (payload: unknown) => void) {
|
|
63
|
+
const wrapped = (event: { data: unknown }) => handler(event.data);
|
|
64
|
+
buniteEventEmitter.on(name, wrapped);
|
|
65
|
+
return () => buniteEventEmitter.off(name, wrapped);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
run() {
|
|
69
|
+
const runtime = getNativeRuntimeState();
|
|
70
|
+
if (runtime?.nativeLoaded) {
|
|
71
|
+
getNativeLibrary()?.symbols.bunite_run_loop();
|
|
72
|
+
if (!this.stubKeepAliveTimer) {
|
|
73
|
+
this.stubKeepAliveTimer = setInterval(() => {}, 60_000);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!this.stubKeepAliveTimer) {
|
|
79
|
+
console.warn("[bunite] Running without a native event loop. Keeping the process alive in stub mode.");
|
|
80
|
+
this.stubKeepAliveTimer = setInterval(() => {}, 60_000);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
quit(code = 0) {
|
|
85
|
+
if (this.stubKeepAliveTimer) {
|
|
86
|
+
clearInterval(this.stubKeepAliveTimer);
|
|
87
|
+
this.stubKeepAliveTimer = null;
|
|
88
|
+
}
|
|
89
|
+
getNativeLibrary()?.symbols.bunite_quit();
|
|
90
|
+
setTimeout(() => process.exit(code), 0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
handle(channel: string, handler: GlobalIPCHandler) {
|
|
94
|
+
if (channel.startsWith("__bunite:")) {
|
|
95
|
+
throw new Error(`Channel prefix "__bunite:" is reserved: ${channel}`);
|
|
96
|
+
}
|
|
97
|
+
if (this.globalIPCHandlers.has(channel)) {
|
|
98
|
+
throw new Error(`Global IPC handler already registered for: ${channel}`);
|
|
99
|
+
}
|
|
100
|
+
this.globalIPCHandlers.set(channel, handler);
|
|
101
|
+
return () => this.globalIPCHandlers.delete(channel);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
removeHandler(channel: string) {
|
|
105
|
+
this.globalIPCHandlers.delete(channel);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @internal */
|
|
109
|
+
getGlobalIPCHandler(channel: string): GlobalIPCHandler | undefined {
|
|
110
|
+
return this.globalIPCHandlers.get(channel);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private readonly viewHandlers = new Map<string, () => string>();
|
|
114
|
+
|
|
115
|
+
getView(path: string, handler: () => string) {
|
|
116
|
+
this.viewHandlers.set(path, handler);
|
|
117
|
+
getNativeLibrary()?.symbols.bunite_register_view_route(toCString(path));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
removeView(path: string) {
|
|
121
|
+
this.viewHandlers.delete(path);
|
|
122
|
+
getNativeLibrary()?.symbols.bunite_unregister_view_route(toCString(path));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** @internal */
|
|
126
|
+
handleRouteRequest(requestId: number, path: string) {
|
|
127
|
+
let html: string;
|
|
128
|
+
try {
|
|
129
|
+
const handler = this.viewHandlers.get(path);
|
|
130
|
+
html = handler ? handler() : "<html><body>No handler for: " + path + "</body></html>";
|
|
131
|
+
} catch (error) {
|
|
132
|
+
html = "<html><body>Route handler error: " + (error instanceof Error ? error.message : String(error)) + "</body></html>";
|
|
133
|
+
}
|
|
134
|
+
getNativeLibrary()?.symbols.bunite_complete_route_request(requestId, toCString(html));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get runtime() {
|
|
138
|
+
return getNativeRuntimeState();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const app = new AppRuntime();
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { buildViewPreloadScript } from "../preload/inline";
|
|
2
|
+
import type { Pointer } from "bun:ffi";
|
|
3
|
+
import { buniteEventEmitter } from "../events/eventEmitter";
|
|
4
|
+
import { defineBuniteRPC, type BuniteRPCConfig, type BuniteRPCSchema, type RPCWithTransport } from "../../shared/rpc";
|
|
5
|
+
import { ensureNativeRuntime, getNativeLibrary, toCString } from "../proc/native";
|
|
6
|
+
import { attachBrowserViewRegistry, getRPCPort, sendMessageToView } from "./Socket";
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
8
|
+
import { resolveDefaultViewsRoot } from "../../shared/paths";
|
|
9
|
+
|
|
10
|
+
const BrowserViewMap: Record<number, BrowserView<any>> = {};
|
|
11
|
+
let nextWebviewId = 1;
|
|
12
|
+
|
|
13
|
+
export type BrowserViewOptions<T = undefined> = {
|
|
14
|
+
url: string | null;
|
|
15
|
+
html: string | null;
|
|
16
|
+
preload: string | null;
|
|
17
|
+
viewsRoot: string | null;
|
|
18
|
+
partition: string | null;
|
|
19
|
+
windowPtr?: Pointer | null;
|
|
20
|
+
frame: {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
};
|
|
26
|
+
rpc?: T;
|
|
27
|
+
windowId: number;
|
|
28
|
+
autoResize: boolean;
|
|
29
|
+
navigationRules: string[] | null;
|
|
30
|
+
sandbox: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const defaultOptions: BrowserViewOptions = {
|
|
34
|
+
url: null,
|
|
35
|
+
html: null,
|
|
36
|
+
preload: null,
|
|
37
|
+
viewsRoot: null,
|
|
38
|
+
partition: null,
|
|
39
|
+
windowPtr: null,
|
|
40
|
+
frame: {
|
|
41
|
+
x: 0,
|
|
42
|
+
y: 0,
|
|
43
|
+
width: 800,
|
|
44
|
+
height: 600
|
|
45
|
+
},
|
|
46
|
+
windowId: 0,
|
|
47
|
+
autoResize: true,
|
|
48
|
+
navigationRules: null,
|
|
49
|
+
sandbox: false
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
53
|
+
id = nextWebviewId++;
|
|
54
|
+
ptr: Pointer | null = null;
|
|
55
|
+
windowId: number;
|
|
56
|
+
url: string | null;
|
|
57
|
+
html: string | null;
|
|
58
|
+
preload: string | null;
|
|
59
|
+
viewsRoot: string | null;
|
|
60
|
+
partition: string | null;
|
|
61
|
+
frame: BrowserViewOptions["frame"];
|
|
62
|
+
rpc?: T;
|
|
63
|
+
rpcHandler?: (message: unknown) => void;
|
|
64
|
+
autoResize: boolean;
|
|
65
|
+
navigationRules: string[] | null;
|
|
66
|
+
sandbox: boolean;
|
|
67
|
+
secretKey: Uint8Array;
|
|
68
|
+
|
|
69
|
+
constructor(options: Partial<BrowserViewOptions<T>>) {
|
|
70
|
+
ensureNativeRuntime();
|
|
71
|
+
|
|
72
|
+
this.windowId = options.windowId ?? defaultOptions.windowId;
|
|
73
|
+
this.url = options.url ?? defaultOptions.url;
|
|
74
|
+
this.html = options.html ?? defaultOptions.html;
|
|
75
|
+
this.preload = options.preload ?? defaultOptions.preload;
|
|
76
|
+
this.viewsRoot = options.viewsRoot ?? defaultOptions.viewsRoot ?? resolveDefaultViewsRoot();
|
|
77
|
+
this.partition = options.partition ?? defaultOptions.partition;
|
|
78
|
+
this.frame = options.frame ?? defaultOptions.frame;
|
|
79
|
+
this.rpc = options.rpc;
|
|
80
|
+
this.autoResize = options.autoResize ?? defaultOptions.autoResize;
|
|
81
|
+
this.navigationRules = options.navigationRules ?? defaultOptions.navigationRules;
|
|
82
|
+
this.sandbox = options.sandbox ?? defaultOptions.sandbox;
|
|
83
|
+
this.secretKey = new Uint8Array(randomBytes(32));
|
|
84
|
+
|
|
85
|
+
if (this.sandbox) {
|
|
86
|
+
throw new Error("sandboxed BrowserView is not implemented in Bunite Windows Phase 1 yet.");
|
|
87
|
+
}
|
|
88
|
+
if (this.partition) {
|
|
89
|
+
console.warn("[bunite] BrowserView.partition is not implemented in Bunite Windows Phase 1 yet.");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const preloadScript = buildViewPreloadScript({
|
|
93
|
+
preload: this.preload,
|
|
94
|
+
viewsRoot: this.viewsRoot,
|
|
95
|
+
webviewId: this.id,
|
|
96
|
+
rpcSocketPort: getRPCPort(),
|
|
97
|
+
secretKey: this.secretKey
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
BrowserViewMap[this.id] = this;
|
|
101
|
+
this.rpc?.setTransport(this.createTransport());
|
|
102
|
+
this.ptr =
|
|
103
|
+
getNativeLibrary()?.symbols.bunite_view_create(
|
|
104
|
+
this.id,
|
|
105
|
+
options.windowPtr ?? null,
|
|
106
|
+
toCString(this.url ?? ""),
|
|
107
|
+
toCString(this.html ?? ""),
|
|
108
|
+
toCString(preloadScript),
|
|
109
|
+
toCString(this.viewsRoot ?? ""),
|
|
110
|
+
toCString(this.navigationRules ? JSON.stringify(this.navigationRules) : ""),
|
|
111
|
+
this.frame.x,
|
|
112
|
+
this.frame.y,
|
|
113
|
+
this.frame.width,
|
|
114
|
+
this.frame.height,
|
|
115
|
+
this.autoResize,
|
|
116
|
+
this.sandbox
|
|
117
|
+
) ?? null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
static getById(id: number) {
|
|
121
|
+
return BrowserViewMap[id];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static getAll() {
|
|
125
|
+
return Object.values(BrowserViewMap);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
static defineRPC<Schema extends BuniteRPCSchema>(
|
|
129
|
+
config: BuniteRPCConfig<Schema, "bun">
|
|
130
|
+
) {
|
|
131
|
+
return defineBuniteRPC("bun", config);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
handleIncomingRPC(message: unknown) {
|
|
135
|
+
this.rpcHandler?.(message);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
createTransport() {
|
|
139
|
+
return {
|
|
140
|
+
send: (message: any) => {
|
|
141
|
+
sendMessageToView(this.id, message);
|
|
142
|
+
},
|
|
143
|
+
registerHandler: (handler: (message: any) => void) => {
|
|
144
|
+
this.rpcHandler = handler;
|
|
145
|
+
},
|
|
146
|
+
unregisterHandler: () => {
|
|
147
|
+
this.rpcHandler = undefined;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get rpcPort() {
|
|
153
|
+
return getRPCPort();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
setAnchor(mode: "none" | "fill" | "top" | "below-top", inset = 0) {
|
|
157
|
+
const modeInt = { none: 0, fill: 1, top: 2, "below-top": 3 }[mode];
|
|
158
|
+
if (this.ptr) {
|
|
159
|
+
getNativeLibrary()?.symbols.bunite_view_set_anchor(this.ptr, modeInt, inset);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
goBack() {
|
|
164
|
+
if (this.ptr) {
|
|
165
|
+
getNativeLibrary()?.symbols.bunite_view_go_back(this.ptr);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
reload() {
|
|
170
|
+
if (this.ptr) {
|
|
171
|
+
getNativeLibrary()?.symbols.bunite_view_reload(this.ptr);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setVisible(visible: boolean) {
|
|
176
|
+
if (this.ptr) {
|
|
177
|
+
getNativeLibrary()?.symbols.bunite_view_set_visible(this.ptr, visible);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
setBounds(x: number, y: number, width: number, height: number) {
|
|
182
|
+
this.frame = { x, y, width, height };
|
|
183
|
+
if (this.ptr) {
|
|
184
|
+
getNativeLibrary()?.symbols.bunite_view_set_bounds(this.ptr, x, y, width, height);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
loadURL(url: string) {
|
|
189
|
+
this.url = url;
|
|
190
|
+
if (this.ptr) {
|
|
191
|
+
getNativeLibrary()?.symbols.bunite_view_load_url(this.ptr, toCString(url));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
loadHTML(html: string) {
|
|
196
|
+
this.html = html;
|
|
197
|
+
if (this.ptr) {
|
|
198
|
+
getNativeLibrary()?.symbols.bunite_view_load_html(this.ptr, toCString(html));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
remove() {
|
|
203
|
+
if (this.ptr) {
|
|
204
|
+
getNativeLibrary()?.symbols.bunite_view_remove(this.ptr);
|
|
205
|
+
}
|
|
206
|
+
this.detachFromNative();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
openDevTools() {
|
|
210
|
+
if (this.ptr) {
|
|
211
|
+
getNativeLibrary()?.symbols.bunite_view_open_devtools(this.ptr);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
closeDevTools() {
|
|
216
|
+
if (this.ptr) {
|
|
217
|
+
getNativeLibrary()?.symbols.bunite_view_close_devtools(this.ptr);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
toggleDevTools() {
|
|
222
|
+
if (this.ptr) {
|
|
223
|
+
getNativeLibrary()?.symbols.bunite_view_toggle_devtools(this.ptr);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
detachFromNative() {
|
|
228
|
+
this.ptr = null;
|
|
229
|
+
for (const eventName of [
|
|
230
|
+
"will-navigate",
|
|
231
|
+
"did-navigate",
|
|
232
|
+
"dom-ready",
|
|
233
|
+
"new-window-open",
|
|
234
|
+
"permission-requested",
|
|
235
|
+
"message-box-response"
|
|
236
|
+
]) {
|
|
237
|
+
buniteEventEmitter.removeAllListeners(`${eventName}-${this.id}`);
|
|
238
|
+
}
|
|
239
|
+
delete BrowserViewMap[this.id];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
on(
|
|
243
|
+
name:
|
|
244
|
+
| "will-navigate"
|
|
245
|
+
| "did-navigate"
|
|
246
|
+
| "dom-ready"
|
|
247
|
+
| "new-window-open"
|
|
248
|
+
| "permission-requested"
|
|
249
|
+
| "message-box-response",
|
|
250
|
+
handler: (event: unknown) => void
|
|
251
|
+
) {
|
|
252
|
+
const specificName = `${name}-${this.id}`;
|
|
253
|
+
buniteEventEmitter.on(specificName, handler);
|
|
254
|
+
return () => buniteEventEmitter.off(specificName, handler);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
attachBrowserViewRegistry({
|
|
259
|
+
getById(id) {
|
|
260
|
+
return BrowserView.getById(id);
|
|
261
|
+
}
|
|
262
|
+
});
|