bunite-core 0.0.1 → 0.0.4

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 (40) hide show
  1. package/package.json +3 -2
  2. package/src/bun/core/App.ts +155 -15
  3. package/src/bun/core/BrowserView.ts +124 -44
  4. package/src/bun/core/BrowserWindow.ts +94 -47
  5. package/src/bun/core/Socket.ts +2 -1
  6. package/src/bun/core/SurfaceBrowserIPC.ts +65 -0
  7. package/src/bun/core/SurfaceManager.ts +201 -0
  8. package/src/bun/core/SurfaceRegistry.ts +60 -0
  9. package/src/bun/core/Utils.ts +275 -46
  10. package/src/bun/events/appEvents.ts +2 -1
  11. package/src/bun/events/webviewEvents.ts +1 -3
  12. package/src/bun/events/windowEvents.ts +2 -0
  13. package/src/bun/index.ts +4 -3
  14. package/src/bun/preload/inline.ts +19 -25
  15. package/src/bun/proc/native.ts +158 -122
  16. package/src/native/shared/callbacks.h +6 -6
  17. package/src/native/shared/ffi_exports.h +123 -119
  18. package/src/native/shared/log.h +24 -0
  19. package/src/native/shared/webview_storage.h +5 -5
  20. package/src/native/win/native_host_appres.cpp +258 -0
  21. package/src/native/win/native_host_cef.cpp +834 -0
  22. package/src/native/win/native_host_ffi.cpp +935 -0
  23. package/src/native/win/native_host_internal.h +285 -0
  24. package/src/native/win/native_host_runtime.cpp +286 -0
  25. package/src/native/win/native_host_utils.cpp +314 -0
  26. package/src/native/win/process_helper_win.cpp +126 -26
  27. package/src/preload/runtime.built.js +1 -1
  28. package/src/preload/runtime.ts +65 -42
  29. package/src/preload/tsconfig.json +2 -1
  30. package/src/preload/tsconfig.tsbuildinfo +1 -0
  31. package/src/preload/webviewElement.ts +307 -0
  32. package/src/shared/cefVersion.ts +2 -0
  33. package/src/shared/log.ts +40 -0
  34. package/src/shared/paths.ts +122 -52
  35. package/src/shared/rpc.ts +7 -1
  36. package/src/shared/webviewPolyfill.ts +80 -0
  37. package/src/view/index.ts +8 -5
  38. package/src/native/shared/cef_response_filter.h +0 -116
  39. package/src/native/win/native_host.cpp +0 -2453
  40. package/src/types/config.ts +0 -29
@@ -1,13 +1,10 @@
1
- import { buniteEventEmitter } from "../events/eventEmitter";
2
- import {
3
- cancelBrowserMessageBoxRequest,
4
- ensureNativeRuntime,
5
- getNativeLibrary,
6
- requestBrowserMessageBox,
7
- showNativeMessageBox
8
- } from "../proc/native";
1
+ import { log } from "../../shared/log";
2
+ import { ensureNativeRuntime, showNativeMessageBox } from "../proc/native";
3
+ import { BrowserView } from "./BrowserView";
4
+ import { BrowserWindow, getLastFocusedWindowId } from "./BrowserWindow";
9
5
 
10
6
  export type MessageBoxOptions = {
7
+ windowId?: number;
11
8
  type?: "none" | "info" | "warning" | "error" | "question";
12
9
  title?: string;
13
10
  message?: string;
@@ -15,58 +12,290 @@ export type MessageBoxOptions = {
15
12
  buttons?: string[];
16
13
  defaultId?: number;
17
14
  cancelId?: number;
15
+ browser?: boolean;
18
16
  };
19
17
 
20
18
  export type MessageBoxResponse = {
21
19
  response: number;
22
20
  };
23
21
 
22
+ // ---------------------------------------------------------------------------
23
+ // Pending request tracking
24
+ // ---------------------------------------------------------------------------
25
+
26
+ let nextRequestId = 1;
27
+
28
+ type PendingMessageBox = {
29
+ viewId: number;
30
+ fallbackResponse: number;
31
+ resolve: (response: number) => void;
32
+ timeoutId: ReturnType<typeof setTimeout>;
33
+ };
34
+
35
+ const pendingMessageBoxes = new Map<number, PendingMessageBox>();
36
+
37
+ export function handleMessageBoxResponse(requestId: number, response: number): boolean {
38
+ const pending = pendingMessageBoxes.get(requestId);
39
+ if (!pending) {
40
+ return false;
41
+ }
42
+ clearTimeout(pending.timeoutId);
43
+ pendingMessageBoxes.delete(requestId);
44
+ pending.resolve(typeof response === "number" && response >= 0 ? response : pending.fallbackResponse);
45
+ return true;
46
+ }
47
+
48
+ export function cancelPendingMessageBoxesForView(viewId: number): void {
49
+ for (const [requestId, pending] of pendingMessageBoxes) {
50
+ if (pending.viewId === viewId) {
51
+ clearTimeout(pending.timeoutId);
52
+ pendingMessageBoxes.delete(requestId);
53
+ pending.resolve(pending.fallbackResponse);
54
+ }
55
+ }
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Preferred view selection
60
+ // ---------------------------------------------------------------------------
61
+
62
+ function getPreferredMessageBoxView(): BrowserView | null {
63
+ const allViews = BrowserView.getAll();
64
+ if (allViews.length === 0) {
65
+ return null;
66
+ }
67
+
68
+ const focusedWindowId = getLastFocusedWindowId();
69
+ if (focusedWindowId != null) {
70
+ const view = allViews.find(v => v.windowId === focusedWindowId);
71
+ if (view) return view;
72
+ }
73
+
74
+ const allWindows = BrowserWindow.getAll();
75
+ for (const win of allWindows) {
76
+ const view = allViews.find(v => v.windowId === win.id);
77
+ if (view) return view;
78
+ }
79
+
80
+ return allViews[0] ?? null;
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Dialog script builder
85
+ // ---------------------------------------------------------------------------
86
+
87
+ function escapeJs(value: string): string {
88
+ return value
89
+ .replace(/\\/g, "\\\\")
90
+ .replace(/"/g, '\\"')
91
+ .replace(/\n/g, "\\n")
92
+ .replace(/\r/g, "\\r")
93
+ .replace(/\t/g, "\\t");
94
+ }
95
+
96
+ function buildBrowserMessageBoxScript(
97
+ requestId: number,
98
+ options: Required<Pick<MessageBoxOptions, "type">> & MessageBoxOptions
99
+ ): string {
100
+ const buttons = options.buttons && options.buttons.length > 0 ? options.buttons : ["OK"];
101
+ const defaultId = Math.max(0, Math.min(options.defaultId ?? 0, buttons.length - 1));
102
+ const cancelId = options.cancelId != null && options.cancelId >= 0
103
+ ? Math.max(0, Math.min(options.cancelId, buttons.length - 1))
104
+ : options.cancelId ?? -1;
105
+
106
+ const buttonsJson = JSON.stringify(buttons);
107
+
108
+ return `(() => {
109
+ const spec = {
110
+ requestId: ${requestId},
111
+ type: "${escapeJs(options.type ?? "info")}",
112
+ title: "${escapeJs(options.title ?? "")}",
113
+ message: "${escapeJs(options.message ?? "")}",
114
+ detail: "${escapeJs(options.detail ?? "")}",
115
+ buttons: ${buttonsJson},
116
+ defaultId: ${defaultId},
117
+ cancelId: ${cancelId}
118
+ };
119
+ const rootId = \`__bunite_message_box_\${spec.requestId}\`;
120
+ if (document.getElementById(rootId)) {
121
+ return;
122
+ }
123
+
124
+ const submit = (response) => {
125
+ void (typeof bunite !== "undefined" && bunite.invoke
126
+ ? bunite.invoke("__bunite:messageBoxResponse", { requestId: spec.requestId, response }).catch(() => {})
127
+ : Promise.resolve());
128
+ };
129
+
130
+ const mount = () => {
131
+ const host = document.body ?? document.documentElement;
132
+ if (!host) {
133
+ return;
134
+ }
135
+
136
+ const overlay = document.createElement("div");
137
+ overlay.id = rootId;
138
+ overlay.dataset.buniteMessageBox = "true";
139
+ overlay.dataset.buniteMessageBoxRequestId = String(spec.requestId);
140
+ overlay.tabIndex = -1;
141
+ overlay.style.cssText = [
142
+ "position:fixed",
143
+ "inset:0",
144
+ "display:flex",
145
+ "align-items:center",
146
+ "justify-content:center",
147
+ "padding:24px",
148
+ "background:rgba(15,23,42,0.42)",
149
+ "backdrop-filter:blur(6px)",
150
+ "z-index:2147483647",
151
+ "font-family:Segoe UI, Arial, sans-serif"
152
+ ].join(";");
153
+
154
+ const panel = document.createElement("div");
155
+ panel.style.cssText = [
156
+ "width:min(480px, calc(100vw - 48px))",
157
+ "border-radius:16px",
158
+ "border:1px solid rgba(15,23,42,0.10)",
159
+ "background:#ffffff",
160
+ "box-shadow:0 24px 80px rgba(15,23,42,0.28)",
161
+ "padding:20px 20px 18px",
162
+ "color:#0f172a"
163
+ ].join(";");
164
+
165
+ const accent = document.createElement("div");
166
+ const accentColor =
167
+ spec.type === "error" ? "#dc2626" :
168
+ spec.type === "warning" ? "#d97706" :
169
+ spec.type === "question" ? "#2563eb" :
170
+ "#0f766e";
171
+ accent.style.cssText = \`width:48px;height:4px;border-radius:999px;background:\${accentColor};margin-bottom:14px;\`;
172
+ panel.appendChild(accent);
173
+
174
+ if (spec.title) {
175
+ const heading = document.createElement("h1");
176
+ heading.textContent = spec.title;
177
+ heading.style.cssText = "margin:0 0 8px;font-size:20px;line-height:1.25;font-weight:700;";
178
+ panel.appendChild(heading);
179
+ }
180
+
181
+ if (spec.message) {
182
+ const body = document.createElement("p");
183
+ body.textContent = spec.message;
184
+ body.style.cssText = "margin:0;font-size:14px;line-height:1.55;white-space:pre-wrap;";
185
+ panel.appendChild(body);
186
+ }
187
+
188
+ if (spec.detail) {
189
+ const detail = document.createElement("p");
190
+ detail.textContent = spec.detail;
191
+ detail.style.cssText = "margin:10px 0 0;font-size:12px;line-height:1.55;color:#475569;white-space:pre-wrap;";
192
+ panel.appendChild(detail);
193
+ }
194
+
195
+ const buttonRow = document.createElement("div");
196
+ buttonRow.style.cssText = "display:flex;justify-content:flex-end;gap:10px;flex-wrap:wrap;margin-top:18px;";
197
+ spec.buttons.forEach((label, index) => {
198
+ const button = document.createElement("button");
199
+ button.type = "button";
200
+ button.textContent = label;
201
+ button.dataset.buniteMessageBoxButtonIndex = String(index);
202
+ button.style.cssText =
203
+ index === spec.defaultId
204
+ ? "appearance:none;border:0;border-radius:999px;background:#111827;color:#ffffff;padding:10px 16px;font:600 13px Segoe UI, Arial, sans-serif;cursor:pointer;"
205
+ : "appearance:none;border:1px solid rgba(15,23,42,0.14);border-radius:999px;background:#f8fafc;color:#0f172a;padding:10px 16px;font:600 13px Segoe UI, Arial, sans-serif;cursor:pointer;";
206
+ button.addEventListener("click", () => {
207
+ overlay.remove();
208
+ submit(index);
209
+ });
210
+ buttonRow.appendChild(button);
211
+ });
212
+ panel.appendChild(buttonRow);
213
+ overlay.appendChild(panel);
214
+ host.appendChild(overlay);
215
+
216
+ overlay.addEventListener("click", (event) => {
217
+ if (event.target !== overlay) {
218
+ return;
219
+ }
220
+ overlay.remove();
221
+ submit(spec.cancelId >= 0 ? spec.cancelId : spec.defaultId);
222
+ });
223
+
224
+ overlay.addEventListener("keydown", (event) => {
225
+ if (event.key !== "Escape") {
226
+ return;
227
+ }
228
+ event.preventDefault();
229
+ overlay.remove();
230
+ submit(spec.cancelId >= 0 ? spec.cancelId : spec.defaultId);
231
+ });
232
+
233
+ requestAnimationFrame(() => {
234
+ overlay.focus();
235
+ const defaultButton = overlay.querySelector(\`[data-bunite-message-box-button-index="\${spec.defaultId}"]\`);
236
+ if (defaultButton instanceof HTMLButtonElement) {
237
+ defaultButton.focus();
238
+ }
239
+ });
240
+ };
241
+
242
+ if (document.readyState === "loading") {
243
+ document.addEventListener("DOMContentLoaded", mount, { once: true });
244
+ } else {
245
+ mount();
246
+ }
247
+ })();`;
248
+ }
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // Public API
252
+ // ---------------------------------------------------------------------------
253
+
24
254
  export async function showMessageBox(
25
255
  options: MessageBoxOptions = {}
26
256
  ): Promise<MessageBoxResponse> {
27
257
  ensureNativeRuntime();
28
258
 
29
- if (!getNativeLibrary()) {
30
- console.warn(
31
- "[bunite] Utils.showMessageBox() requires the native runtime. Returning a stub response."
32
- );
33
- return {
34
- response: options.cancelId ?? options.defaultId ?? 0
35
- };
36
- }
259
+ const windowId = options.windowId ?? 0;
37
260
 
38
- const requestId = requestBrowserMessageBox(options);
39
- if (requestId > 0) {
40
- const response = await new Promise<number>((resolve) => {
261
+ if (options.browser) {
262
+ const view = getPreferredMessageBoxView();
263
+ if (view) {
264
+ const requestId = nextRequestId++;
41
265
  const fallbackResponse = options.cancelId ?? options.defaultId ?? 0;
42
- const handleResponse = (event: unknown) => {
43
- const data = (event as { data?: { requestId?: number; response?: number } }).data;
44
- if (!data || data.requestId !== requestId) {
45
- return;
46
- }
47
-
48
- clearTimeout(timeoutId);
49
- buniteEventEmitter.off("message-box-response", handleResponse);
50
- resolve(
51
- typeof data.response === "number" && data.response >= 0
52
- ? data.response
53
- : fallbackResponse
54
- );
55
- };
56
-
57
- const timeoutId = setTimeout(() => {
58
- buniteEventEmitter.off("message-box-response", handleResponse);
59
- cancelBrowserMessageBoxRequest(requestId);
60
- resolve(fallbackResponse);
61
- }, 15_000);
62
-
63
- buniteEventEmitter.on("message-box-response", handleResponse);
64
- });
266
+ const script = buildBrowserMessageBoxScript(requestId, {
267
+ type: options.type ?? "info",
268
+ ...options
269
+ });
270
+
271
+ view.bringToFront();
272
+
273
+ const response = await new Promise<number>((resolve) => {
274
+ const timeoutId = setTimeout(() => {
275
+ pendingMessageBoxes.delete(requestId);
276
+ resolve(fallbackResponse);
277
+ }, 15_000);
65
278
 
66
- return { response };
279
+ pendingMessageBoxes.set(requestId, {
280
+ viewId: view.id,
281
+ fallbackResponse,
282
+ resolve,
283
+ timeoutId
284
+ });
285
+
286
+ view.executeJavaScript(script);
287
+ });
288
+
289
+ return { response };
290
+ }
67
291
  }
68
292
 
69
- return {
70
- response: showNativeMessageBox(options)
71
- };
293
+ return { response: showNativeMessageBox(windowId, options) };
294
+ }
295
+
296
+ export function showMessageBoxSync(
297
+ options: MessageBoxOptions = {}
298
+ ): MessageBoxResponse {
299
+ ensureNativeRuntime();
300
+ return { response: showNativeMessageBox(options.windowId ?? 0, options) };
72
301
  }
@@ -3,5 +3,6 @@ import { BuniteEvent } from "./event";
3
3
  export default {
4
4
  ready: (data: Record<string, unknown>) => new BuniteEvent("ready", data),
5
5
  beforeQuit: (data: Record<string, unknown>) =>
6
- new BuniteEvent<Record<string, unknown>, { allow?: boolean }>("before-quit", data)
6
+ new BuniteEvent<Record<string, unknown>, { allow?: boolean }>("before-quit", data),
7
+ allWindowsClosed: () => new BuniteEvent("all-windows-closed", {})
7
8
  };
@@ -7,7 +7,5 @@ export default {
7
7
  newWindowOpen: (data: { detail: string | { url: string } }) =>
8
8
  new BuniteEvent("new-window-open", data),
9
9
  permissionRequested: (data: { requestId: number; kind: number; url?: string }) =>
10
- new BuniteEvent("permission-requested", data),
11
- messageBoxResponse: (data: { requestId: number; response: number }) =>
12
- new BuniteEvent("message-box-response", data)
10
+ new BuniteEvent("permission-requested", data)
13
11
  };
@@ -1,6 +1,8 @@
1
1
  import { BuniteEvent } from "./event";
2
2
 
3
3
  export default {
4
+ closeRequested: (data: { id: number }) =>
5
+ new BuniteEvent<{ id: number }, { allow?: boolean }>("close-requested", data),
4
6
  close: (data: { id: number }) => new BuniteEvent("close", data),
5
7
  focus: (data: { id: number }) => new BuniteEvent("focus", data),
6
8
  blur: (data: { id: number }) => new BuniteEvent("blur", data),
package/src/bun/index.ts CHANGED
@@ -13,8 +13,8 @@ import {
13
13
  type RPCSchema,
14
14
  type RPCWithTransport
15
15
  } from "../shared/rpc";
16
- import type { BuniteConfig } from "../types/config";
17
16
  import type { MessageBoxOptions, MessageBoxResponse } from "./core/Utils";
17
+ import { log, type LogLevel } from "../shared/log";
18
18
 
19
19
  export {
20
20
  app,
@@ -24,12 +24,13 @@ export {
24
24
  buniteEventEmitter,
25
25
  completePermissionRequest,
26
26
  createRPC,
27
- defineBuniteRPC
27
+ defineBuniteRPC,
28
+ log
28
29
  };
29
30
 
30
31
  export type {
32
+ LogLevel,
31
33
  BuniteEvent,
32
- BuniteConfig,
33
34
  BuniteRPCConfig,
34
35
  BuniteRPCSchema,
35
36
  BrowserViewOptions,
@@ -1,13 +1,14 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { isAbsolute, resolve, sep } from "node:path";
3
+ import { log } from "../../shared/log";
3
4
 
4
5
  function escapeRootForComparison(path: string) {
5
6
  return process.platform === "win32" ? path.toLowerCase() : path;
6
7
  }
7
8
 
8
- function resolveViewsFile(viewsRoot: string, url: string) {
9
- const relativePath = url.replace(/^views:\/\//, "").replace(/^[\\/]+/, "");
10
- const normalizedRoot = resolve(viewsRoot);
9
+ function resolveAppResFile(appresRoot: string, url: string) {
10
+ const relativePath = url.replace(/^appres:\/\/app\.internal\//, "").replace(/^[\\/]+/, "");
11
+ const normalizedRoot = resolve(appresRoot);
11
12
  const candidate = resolve(normalizedRoot, relativePath.split("/").join(sep));
12
13
  const comparableRoot = escapeRootForComparison(normalizedRoot);
13
14
  const comparableCandidate = escapeRootForComparison(candidate);
@@ -16,61 +17,54 @@ function resolveViewsFile(viewsRoot: string, url: string) {
16
17
  comparableCandidate !== comparableRoot &&
17
18
  !comparableCandidate.startsWith(`${comparableRoot}${sep}`)
18
19
  ) {
19
- throw new Error(`preload path escapes viewsRoot: ${url}`);
20
+ throw new Error(`preload path escapes appresRoot: ${url}`);
20
21
  }
21
22
 
22
23
  return candidate;
23
24
  }
24
25
 
25
- function readCustomPreload(preload: string | null, viewsRoot: string | null) {
26
+ function readCustomPreload(preload: string | null, appresRoot: string | null) {
26
27
  if (!preload) {
27
28
  return "";
28
29
  }
29
30
 
30
31
  try {
31
- const resolvedPath = preload.startsWith("views://")
32
- ? viewsRoot
33
- ? resolveViewsFile(viewsRoot, preload)
32
+ const resolvedPath = preload.startsWith("appres://app.internal/")
33
+ ? appresRoot
34
+ ? resolveAppResFile(appresRoot, preload)
34
35
  : null
35
36
  : isAbsolute(preload)
36
37
  ? preload
37
38
  : resolve(preload);
38
39
 
39
40
  if (!resolvedPath) {
40
- console.warn(`[bunite] Cannot resolve preload without viewsRoot: ${preload}`);
41
+ log.warn(`Cannot resolve preload without appresRoot: ${preload}`);
41
42
  return "";
42
43
  }
43
44
  if (!existsSync(resolvedPath)) {
44
- console.warn(`[bunite] Preload file was not found: ${resolvedPath}`);
45
+ log.warn(`Preload file was not found: ${resolvedPath}`);
45
46
  return "";
46
47
  }
47
48
 
48
49
  return readFileSync(resolvedPath, "utf8");
49
50
  } catch (error) {
50
- console.warn("[bunite] Failed to resolve preload script.", error);
51
+ log.warn("Failed to resolve preload script.", error);
51
52
  return "";
52
53
  }
53
54
  }
54
55
 
55
- // Pre-built preload runtime (built via `bun run build:preload` in package/)
56
- const runtimePath = resolve(import.meta.dirname, "../../preload/runtime.built.js");
57
- let cachedRuntime: string | null = null;
56
+ // Pre-built preload runtime (built via `bun run build:preload` in package/).
57
+ // Embedded at bundle time so bun --compile includes it without filesystem access.
58
+ // @ts-ignore text import attribute
59
+ import embeddedPreloadRuntime from "../../preload/runtime.built.js" with { type: "text" };
58
60
 
59
61
  function getPreloadRuntime(): string {
60
- if (cachedRuntime === null) {
61
- if (!existsSync(runtimePath)) {
62
- throw new Error(
63
- `Preload runtime not found at ${runtimePath}. Run "bun run build:preload" in the package directory.`
64
- );
65
- }
66
- cachedRuntime = readFileSync(runtimePath, "utf8");
67
- }
68
- return cachedRuntime;
62
+ return embeddedPreloadRuntime;
69
63
  }
70
64
 
71
65
  export function buildViewPreloadScript(options: {
72
66
  preload: string | null;
73
- viewsRoot: string | null;
67
+ appresRoot: string | null;
74
68
  webviewId: number;
75
69
  rpcSocketPort: number;
76
70
  secretKey: Uint8Array;
@@ -81,7 +75,7 @@ export function buildViewPreloadScript(options: {
81
75
  const config = `var __buniteWebviewId=${options.webviewId},__buniteRpcSocketPort=${options.rpcSocketPort},__buniteSecretKeyBase64=${JSON.stringify(secretKeyBase64)};`;
82
76
 
83
77
  const runtime = getPreloadRuntime();
84
- const customPreload = readCustomPreload(options.preload, options.viewsRoot).trim();
78
+ const customPreload = readCustomPreload(options.preload, options.appresRoot).trim();
85
79
 
86
80
  return [config, runtime, customPreload].filter(Boolean).join("\n");
87
81
  }