bunite-core 0.12.1 → 0.16.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 +4 -4
- package/src/host/core/App.ts +19 -2
- package/src/host/core/BrowserView.ts +515 -38
- package/src/host/core/SurfaceBrowserIPC.ts +53 -3
- package/src/host/core/SurfaceManager.ts +603 -30
- package/src/host/core/SurfaceRegistry.ts +9 -1
- package/src/host/core/inputDispatch.ts +147 -0
- package/src/host/events/webviewEvents.ts +25 -1
- package/src/host/log.ts +6 -1
- package/src/host/native.ts +263 -1
- package/src/host/preloadBundle.ts +7 -2
- package/src/native/linux/bunite_linux_ffi.cpp +427 -6
- package/src/native/linux/bunite_linux_internal.h +18 -0
- package/src/native/linux/bunite_linux_runtime.cpp +6 -1
- package/src/native/linux/bunite_linux_utils.cpp +2 -2
- package/src/native/linux/bunite_linux_view.cpp +296 -5
- package/src/native/mac/bunite_mac_ffi.mm +630 -8
- package/src/native/mac/bunite_mac_internal.h +19 -0
- package/src/native/mac/bunite_mac_utils.mm +2 -2
- package/src/native/mac/bunite_mac_view.mm +371 -9
- package/src/native/shared/ffi_exports.h +200 -2
- package/src/native/win/native_host_cef.cpp +186 -11
- package/src/native/win/native_host_ffi.cpp +1194 -1
- package/src/native/win/native_host_internal.h +35 -0
- package/src/native/win/native_host_utils.cpp +2 -1
- package/src/native/win/process_helper_win.cpp +54 -27
- package/src/native/win-webview2/bunite_webview2_ffi.cpp +1023 -12
- package/src/native/win-webview2/webview2_internal.h +25 -0
- package/src/native/win-webview2/webview2_runtime.cpp +403 -34
- package/src/native/win-webview2/webview2_utils.cpp +30 -12
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +97 -0
- package/src/rpc/framework.ts +340 -8
- package/src/rpc/index.ts +32 -0
- package/src/webview/native.ts +253 -51
- package/src/webview/polyfill.ts +283 -22
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunite-core",
|
|
3
3
|
"description": "Uniting UI and Bun",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.16.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"setup:cef": "bun ../tools/bunite-dev/scripts/setup-cef.ts",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"msgpackr": "^1.11.9"
|
|
25
25
|
},
|
|
26
26
|
"optionalDependencies": {
|
|
27
|
-
"bunite-native-win-x64": "0.0.
|
|
28
|
-
"bunite-native-mac-arm64": "0.0.
|
|
29
|
-
"bunite-native-linux-x64": "0.0.
|
|
27
|
+
"bunite-native-win-x64": "0.0.14",
|
|
28
|
+
"bunite-native-mac-arm64": "0.0.4",
|
|
29
|
+
"bunite-native-linux-x64": "0.0.4"
|
|
30
30
|
}
|
|
31
31
|
}
|
package/src/host/core/App.ts
CHANGED
|
@@ -15,10 +15,10 @@ import {
|
|
|
15
15
|
} from "../native";
|
|
16
16
|
import { ensureRpcServer } from "./Socket";
|
|
17
17
|
import { BrowserWindow } from "./BrowserWindow";
|
|
18
|
-
import { createSurfaceCapImpl } from "./SurfaceManager";
|
|
18
|
+
import { createSurfaceCapImpl, getPopupMetricsSnapshot } from "./SurfaceManager";
|
|
19
19
|
import "./SurfaceBrowserIPC";
|
|
20
20
|
import { log, logLevelToInt } from "../log";
|
|
21
|
-
import { RuntimeCap, SurfaceCap, IpcError, type ImplOf } from "../../rpc/index";
|
|
21
|
+
import { RuntimeCap, SurfaceCap, PageReportingCap, IpcError, type ImplOf } from "../../rpc/index";
|
|
22
22
|
|
|
23
23
|
import type { LogLevel } from "../log";
|
|
24
24
|
|
|
@@ -189,6 +189,23 @@ export class AppRuntime {
|
|
|
189
189
|
themeWatch: () => notImpl("themeWatch"),
|
|
190
190
|
surface: (_: void, ctx: Parameters<ImplOf<typeof RuntimeCap>["surface"]>[1]) =>
|
|
191
191
|
ctx.exportCap(SurfaceCap, createSurfaceCapImpl(viewId)),
|
|
192
|
+
reporting: (_: void, ctx: Parameters<ImplOf<typeof RuntimeCap>["reporting"]>[1]) =>
|
|
193
|
+
ctx.exportCap(PageReportingCap, {
|
|
194
|
+
reportConsoleBatch: ({ entries }) => {
|
|
195
|
+
// One queueMicrotask per batch (not per entry) — a 1000-log
|
|
196
|
+
// spam still translates to 1 microtask + N synchronous emits,
|
|
197
|
+
// not N microtasks competing for the queue.
|
|
198
|
+
queueMicrotask(() => {
|
|
199
|
+
for (const entry of entries) {
|
|
200
|
+
buniteEventEmitter.emitEvent(
|
|
201
|
+
buniteEventEmitter.events.webview.consoleMessage(entry),
|
|
202
|
+
viewId
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
}),
|
|
208
|
+
popupMetrics: () => getPopupMetricsSnapshot(),
|
|
192
209
|
} satisfies ImplOf<typeof RuntimeCap>;
|
|
193
210
|
return impl;
|
|
194
211
|
}
|
|
@@ -8,13 +8,21 @@ import {
|
|
|
8
8
|
type Connection,
|
|
9
9
|
type BytesPipe,
|
|
10
10
|
} from "../../rpc/index";
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
EvaluateResult, SurfaceCapabilities, ScreenshotResult, Modifier,
|
|
13
|
+
AccessibilitySnapshotResult, AxNode, ListFramesResult,
|
|
14
|
+
ResolveAndClickArgs, ResolveAndClickResult,
|
|
15
|
+
} from "../../rpc/framework";
|
|
16
|
+
import { encodeModifiers, resolveKey } from "./inputDispatch";
|
|
12
17
|
import { createEncryptedPipe } from "../encryptedPipe";
|
|
13
18
|
import {
|
|
14
19
|
ensureNativeRuntime, getNativeLibrary, toCString, waitForViewReady, cancelWaitForViewReady,
|
|
15
|
-
setEvaluateResultHandler, type NativeEvaluateResult
|
|
20
|
+
setEvaluateResultHandler, type NativeEvaluateResult,
|
|
21
|
+
setScreenshotResultHandler, type NativeScreenshotResult,
|
|
22
|
+
setAccessibilityResultHandler, type NativeAccessibilityResult,
|
|
23
|
+
setListFramesResultHandler, type NativeListFramesResult,
|
|
24
|
+
setResolveAndClickResultHandler, type NativeResolveAndClickResult,
|
|
16
25
|
} from "../native";
|
|
17
|
-
import { getNativeEngineName } from "../native";
|
|
18
26
|
import { attachBrowserViewRegistry, getRpcPort } from "./Socket";
|
|
19
27
|
import { getAppRuntimeOrThrow } from "./App";
|
|
20
28
|
import { randomBytes } from "node:crypto";
|
|
@@ -47,6 +55,236 @@ function rejectEvaluatesForView(viewId: number) {
|
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
57
|
|
|
58
|
+
// Screenshot resolvers — parallel to evaluate. Native fires `screenshot-result`
|
|
59
|
+
// keyed by requestId; payload carries base64 data which TS decodes to Uint8Array.
|
|
60
|
+
type ScreenshotPending = { viewId: number; resolve: (result: ScreenshotResult) => void };
|
|
61
|
+
let nextScreenshotRequestId = 1;
|
|
62
|
+
const screenshotResolvers = new Map<number, ScreenshotPending>();
|
|
63
|
+
|
|
64
|
+
function registerScreenshotRequest(viewId: number, resolve: (result: ScreenshotResult) => void): number {
|
|
65
|
+
const id = nextScreenshotRequestId++;
|
|
66
|
+
screenshotResolvers.set(id, { viewId, resolve });
|
|
67
|
+
return id;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function rejectScreenshotsForView(viewId: number) {
|
|
71
|
+
for (const [reqId, entry] of screenshotResolvers) {
|
|
72
|
+
if (entry.viewId === viewId) {
|
|
73
|
+
screenshotResolvers.delete(reqId);
|
|
74
|
+
entry.resolve({ ok: false, code: "not_supported", message: "view destroyed" });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type AxPending = { viewId: number; resolve: (result: AccessibilitySnapshotResult) => void; interestingOnly: boolean };
|
|
80
|
+
let nextAxRequestId = 1;
|
|
81
|
+
const axResolvers = new Map<number, AxPending>();
|
|
82
|
+
|
|
83
|
+
function registerAxRequest(viewId: number, resolve: (result: AccessibilitySnapshotResult) => void, interestingOnly: boolean): number {
|
|
84
|
+
const id = nextAxRequestId++;
|
|
85
|
+
axResolvers.set(id, { viewId, resolve, interestingOnly });
|
|
86
|
+
return id;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function rejectAxForView(viewId: number) {
|
|
90
|
+
for (const [reqId, entry] of axResolvers) {
|
|
91
|
+
if (entry.viewId === viewId) {
|
|
92
|
+
axResolvers.delete(reqId);
|
|
93
|
+
entry.resolve({ ok: false, code: "not_supported", message: "view destroyed" });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// CDP `Accessibility.getFullAXTree` returns `{nodes: [flat]}` with `childIds`
|
|
99
|
+
// references — build a nested tree from the first node. When `interestingOnly`
|
|
100
|
+
// is true, ignored nodes are dropped and their children reparent up.
|
|
101
|
+
function convertAxTree(cdpResult: { nodes?: any[] } | undefined, interestingOnly: boolean): AxNode {
|
|
102
|
+
const flat = cdpResult?.nodes ?? [];
|
|
103
|
+
if (flat.length === 0) return { nodeId: "", role: "", name: "" };
|
|
104
|
+
const byId = new Map<string, any>();
|
|
105
|
+
for (const n of flat) if (n?.nodeId != null) byId.set(String(n.nodeId), n);
|
|
106
|
+
// Root = node without a parentId (rather than relying on flat[0] ordering).
|
|
107
|
+
const root = flat.find((n) => n?.parentId == null) ?? flat[0];
|
|
108
|
+
// Walk childIds skipping ignored nodes (when filtering) and produce a flat list
|
|
109
|
+
// of "interesting" descendants for the given node.
|
|
110
|
+
const seen = new Set<string>();
|
|
111
|
+
const interestingDescendants = (cdpNode: any): any[] => {
|
|
112
|
+
const out: any[] = [];
|
|
113
|
+
if (!Array.isArray(cdpNode?.childIds)) return out;
|
|
114
|
+
for (const cid of cdpNode.childIds) {
|
|
115
|
+
const child = byId.get(String(cid));
|
|
116
|
+
if (!child) continue;
|
|
117
|
+
if (interestingOnly && child.ignored === true) {
|
|
118
|
+
out.push(...interestingDescendants(child));
|
|
119
|
+
} else {
|
|
120
|
+
out.push(child);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
};
|
|
125
|
+
const build = (n: any): AxNode => {
|
|
126
|
+
const id = String(n?.nodeId ?? "");
|
|
127
|
+
if (id && seen.has(id)) return { nodeId: id, role: "", name: "" }; // cycle guard
|
|
128
|
+
if (id) seen.add(id);
|
|
129
|
+
const props = new Map<string, unknown>();
|
|
130
|
+
if (Array.isArray(n?.properties)) {
|
|
131
|
+
for (const p of n.properties) {
|
|
132
|
+
if (p?.name && p.value && "value" in p.value) props.set(p.name, p.value.value);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const out: AxNode = {
|
|
136
|
+
nodeId: id,
|
|
137
|
+
role: String(n?.role?.value ?? ""),
|
|
138
|
+
name: String(n?.name?.value ?? ""),
|
|
139
|
+
};
|
|
140
|
+
if (n?.value?.value !== undefined) out.value = String(n.value.value);
|
|
141
|
+
if (n?.description?.value !== undefined) out.description = String(n.description.value);
|
|
142
|
+
const level = props.get("level"); if (typeof level === "number") out.level = level;
|
|
143
|
+
const checked = props.get("checked"); if (checked === true || checked === false || checked === "mixed") out.checked = checked;
|
|
144
|
+
const pressed = props.get("pressed"); if (pressed === true || pressed === false || pressed === "mixed") out.pressed = pressed;
|
|
145
|
+
const expanded = props.get("expanded"); if (typeof expanded === "boolean") out.expanded = expanded;
|
|
146
|
+
const disabled = props.get("disabled"); if (typeof disabled === "boolean") out.disabled = disabled;
|
|
147
|
+
const focused = props.get("focused"); if (typeof focused === "boolean") out.focused = focused;
|
|
148
|
+
const invalid = props.get("invalid"); if (typeof invalid === "boolean") out.invalid = invalid;
|
|
149
|
+
const required = props.get("required"); if (typeof required === "boolean") out.required = required;
|
|
150
|
+
const selected = props.get("selected"); if (typeof selected === "boolean") out.selected = selected;
|
|
151
|
+
const kids = interestingDescendants(n).map(build);
|
|
152
|
+
if (kids.length > 0) out.children = kids;
|
|
153
|
+
return out;
|
|
154
|
+
};
|
|
155
|
+
return build(root);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
type FramesPending = { viewId: number; resolve: (result: ListFramesResult) => void };
|
|
159
|
+
let nextFramesRequestId = 1;
|
|
160
|
+
const framesResolvers = new Map<number, FramesPending>();
|
|
161
|
+
function registerFramesRequest(viewId: number, resolve: (result: ListFramesResult) => void): number {
|
|
162
|
+
const id = nextFramesRequestId++;
|
|
163
|
+
framesResolvers.set(id, { viewId, resolve });
|
|
164
|
+
return id;
|
|
165
|
+
}
|
|
166
|
+
function rejectFramesForView(viewId: number) {
|
|
167
|
+
for (const [reqId, entry] of framesResolvers) {
|
|
168
|
+
if (entry.viewId === viewId) {
|
|
169
|
+
framesResolvers.delete(reqId);
|
|
170
|
+
entry.resolve({ ok: false, code: "runtime_error", message: "view destroyed" });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
type ResolveAndClickPending = { viewId: number; resolve: (result: ResolveAndClickResult) => void };
|
|
176
|
+
let nextResolveAndClickRequestId = 1;
|
|
177
|
+
const resolveAndClickResolvers = new Map<number, ResolveAndClickPending>();
|
|
178
|
+
function registerResolveAndClickRequest(viewId: number, resolve: (result: ResolveAndClickResult) => void): number {
|
|
179
|
+
const id = nextResolveAndClickRequestId++;
|
|
180
|
+
resolveAndClickResolvers.set(id, { viewId, resolve });
|
|
181
|
+
return id;
|
|
182
|
+
}
|
|
183
|
+
function rejectResolveAndClickForView(viewId: number) {
|
|
184
|
+
for (const [reqId, entry] of resolveAndClickResolvers) {
|
|
185
|
+
if (entry.viewId === viewId) {
|
|
186
|
+
resolveAndClickResolvers.delete(reqId);
|
|
187
|
+
entry.resolve({ ok: false, code: "runtime_error", message: "view destroyed" });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function flattenFrameTree(raw: any): { frameId: string; parentFrameId: string | null; origin: string; url: string; name?: string }[] {
|
|
193
|
+
const out: { frameId: string; parentFrameId: string | null; origin: string; url: string; name?: string }[] = [];
|
|
194
|
+
const walk = (node: any, parent: string | null) => {
|
|
195
|
+
const f = node?.frame;
|
|
196
|
+
if (!f) return;
|
|
197
|
+
const entry: { frameId: string; parentFrameId: string | null; origin: string; url: string; name?: string } = {
|
|
198
|
+
frameId: String(f.id ?? ""),
|
|
199
|
+
parentFrameId: parent,
|
|
200
|
+
origin: String(f.securityOrigin ?? ""),
|
|
201
|
+
url: String(f.url ?? ""),
|
|
202
|
+
};
|
|
203
|
+
if (typeof f.name === "string" && f.name.length > 0) entry.name = f.name;
|
|
204
|
+
out.push(entry);
|
|
205
|
+
if (Array.isArray(node.childFrames)) for (const c of node.childFrames) walk(c, entry.frameId);
|
|
206
|
+
};
|
|
207
|
+
const root = raw?.frameTree;
|
|
208
|
+
if (root) walk(root, null);
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
setListFramesResultHandler((viewId, raw: NativeListFramesResult) => {
|
|
213
|
+
const entry = framesResolvers.get(raw.requestId);
|
|
214
|
+
if (!entry || entry.viewId !== viewId) return;
|
|
215
|
+
framesResolvers.delete(raw.requestId);
|
|
216
|
+
if (raw.ok && raw.raw) {
|
|
217
|
+
try {
|
|
218
|
+
const frames = flattenFrameTree(raw.raw);
|
|
219
|
+
entry.resolve({ ok: true, frames });
|
|
220
|
+
} catch (e) {
|
|
221
|
+
entry.resolve({ ok: false, code: "runtime_error", message: `frame tree flatten failed: ${(e as Error).message}` });
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
entry.resolve({
|
|
225
|
+
ok: false,
|
|
226
|
+
code: (raw.code as "not_supported" | "runtime_error") ?? "runtime_error",
|
|
227
|
+
message: raw.message ?? "list frames failed",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
setAccessibilityResultHandler((viewId, raw: NativeAccessibilityResult) => {
|
|
233
|
+
const entry = axResolvers.get(raw.requestId);
|
|
234
|
+
if (!entry || entry.viewId !== viewId) return;
|
|
235
|
+
axResolvers.delete(raw.requestId);
|
|
236
|
+
if (raw.ok && raw.tree) {
|
|
237
|
+
try { entry.resolve({ ok: true, tree: convertAxTree(raw.tree as any, entry.interestingOnly) }); }
|
|
238
|
+
catch (e) { entry.resolve({ ok: false, code: "runtime_error", message: `ax tree convert failed: ${(e as Error).message}` }); }
|
|
239
|
+
} else {
|
|
240
|
+
entry.resolve({
|
|
241
|
+
ok: false,
|
|
242
|
+
code: (raw.code as "not_supported" | "runtime_error" | "timeout") ?? "runtime_error",
|
|
243
|
+
message: raw.message ?? "accessibility snapshot failed",
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
function decodeBase64(b64: string): Uint8Array {
|
|
249
|
+
const bin = atob(b64);
|
|
250
|
+
const out = new Uint8Array(bin.length);
|
|
251
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
setScreenshotResultHandler((viewId, raw: NativeScreenshotResult) => {
|
|
256
|
+
const entry = screenshotResolvers.get(raw.requestId);
|
|
257
|
+
if (!entry) return;
|
|
258
|
+
if (entry.viewId !== viewId) return;
|
|
259
|
+
screenshotResolvers.delete(raw.requestId);
|
|
260
|
+
if (raw.ok && raw.dataBase64 && raw.format && raw.mime) {
|
|
261
|
+
try {
|
|
262
|
+
entry.resolve({ ok: true, data: decodeBase64(raw.dataBase64), mime: raw.mime, format: raw.format });
|
|
263
|
+
} catch (e) {
|
|
264
|
+
entry.resolve({ ok: false, code: "runtime_error", message: `base64 decode failed: ${(e as Error).message}` });
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
entry.resolve({
|
|
268
|
+
ok: false,
|
|
269
|
+
code: (raw.code as "not_supported" | "runtime_error" | "timeout") ?? "runtime_error",
|
|
270
|
+
message: raw.message ?? "screenshot failed",
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
setResolveAndClickResultHandler((viewId, raw: NativeResolveAndClickResult) => {
|
|
276
|
+
const entry = resolveAndClickResolvers.get(raw.requestId);
|
|
277
|
+
if (!entry || entry.viewId !== viewId) return;
|
|
278
|
+
resolveAndClickResolvers.delete(raw.requestId);
|
|
279
|
+
if (raw.ok && raw.rect) {
|
|
280
|
+
entry.resolve({ ok: true, rect: raw.rect, isTrustedEvent: !!raw.isTrustedEvent });
|
|
281
|
+
} else {
|
|
282
|
+
type FailCode = "not_found" | "not_visible" | "runtime_error" | "cross_origin" | "not_supported";
|
|
283
|
+
const code = (raw.code as FailCode | undefined) ?? "runtime_error";
|
|
284
|
+
entry.resolve({ ok: false, code, message: raw.message ?? "resolveAndClick failed" });
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
50
288
|
setEvaluateResultHandler((viewId, raw: NativeEvaluateResult) => {
|
|
51
289
|
const entry = evaluateResolvers.get(raw.requestId);
|
|
52
290
|
if (!entry) return;
|
|
@@ -67,18 +305,52 @@ setEvaluateResultHandler((viewId, raw: NativeEvaluateResult) => {
|
|
|
67
305
|
}
|
|
68
306
|
});
|
|
69
307
|
|
|
70
|
-
|
|
71
|
-
|
|
308
|
+
// Bit positions match the native enum in `ffi_exports.h` (BuniteCapBit).
|
|
309
|
+
const CAP_EVALUATE = 1 << 0;
|
|
310
|
+
const CAP_CROSS_ORIGIN_EVAL = 1 << 1;
|
|
311
|
+
const CAP_SURFACE_EVENTS = 1 << 2;
|
|
312
|
+
const CAP_NATIVE_INPUT_TRUSTED = 1 << 3;
|
|
313
|
+
const CAP_CLICK = 1 << 4;
|
|
314
|
+
const CAP_TYPE = 1 << 5;
|
|
315
|
+
const CAP_PRESS = 1 << 6;
|
|
316
|
+
const CAP_SCROLL = 1 << 7;
|
|
317
|
+
const CAP_SCREENSHOT = 1 << 8;
|
|
318
|
+
const CAP_FORMAT_PNG = 1 << 9;
|
|
319
|
+
const CAP_FORMAT_JPEG = 1 << 10;
|
|
320
|
+
const CAP_MOUSE = 1 << 11;
|
|
321
|
+
const CAP_DIALOGS = 1 << 12;
|
|
322
|
+
const CAP_CONSOLE = 1 << 13;
|
|
323
|
+
const CAP_AX = 1 << 15;
|
|
324
|
+
const CAP_BOUNDING_RECT = 1 << 16;
|
|
325
|
+
const CAP_FRAMES = 1 << 17;
|
|
326
|
+
const CAP_DOWNLOADS = 1 << 18;
|
|
327
|
+
const CAP_POPUPS = 1 << 19;
|
|
328
|
+
const CAP_RESOLVE_AND_CLICK = 1 << 20;
|
|
329
|
+
|
|
330
|
+
function decodeCapabilityBits(bits: number): SurfaceCapabilities {
|
|
331
|
+
const formats: ("png" | "jpeg")[] = [];
|
|
332
|
+
if (bits & CAP_FORMAT_PNG) formats.push("png");
|
|
333
|
+
if (bits & CAP_FORMAT_JPEG) formats.push("jpeg");
|
|
72
334
|
return {
|
|
73
|
-
evaluate:
|
|
74
|
-
crossOriginEval:
|
|
75
|
-
|
|
76
|
-
nativeInputTrusted:
|
|
77
|
-
click:
|
|
78
|
-
type:
|
|
79
|
-
press:
|
|
80
|
-
scroll:
|
|
81
|
-
|
|
335
|
+
evaluate: !!(bits & CAP_EVALUATE),
|
|
336
|
+
crossOriginEval: !!(bits & CAP_CROSS_ORIGIN_EVAL),
|
|
337
|
+
surfaceEvents: !!(bits & CAP_SURFACE_EVENTS),
|
|
338
|
+
nativeInputTrusted: !!(bits & CAP_NATIVE_INPUT_TRUSTED),
|
|
339
|
+
click: !!(bits & CAP_CLICK),
|
|
340
|
+
type: !!(bits & CAP_TYPE),
|
|
341
|
+
press: !!(bits & CAP_PRESS),
|
|
342
|
+
scroll: !!(bits & CAP_SCROLL),
|
|
343
|
+
mouse: !!(bits & CAP_MOUSE),
|
|
344
|
+
dialogs: !!(bits & CAP_DIALOGS),
|
|
345
|
+
console: !!(bits & CAP_CONSOLE),
|
|
346
|
+
screenshot: !!(bits & CAP_SCREENSHOT),
|
|
347
|
+
accessibilitySnapshot: !!(bits & CAP_AX),
|
|
348
|
+
getBoundingRect: !!(bits & CAP_BOUNDING_RECT),
|
|
349
|
+
frames: !!(bits & CAP_FRAMES),
|
|
350
|
+
downloads: !!(bits & CAP_DOWNLOADS),
|
|
351
|
+
popups: !!(bits & CAP_POPUPS),
|
|
352
|
+
resolveAndClick: !!(bits & CAP_RESOLVE_AND_CLICK),
|
|
353
|
+
...(formats.length > 0 ? { formats } : {}),
|
|
82
354
|
};
|
|
83
355
|
}
|
|
84
356
|
|
|
@@ -137,9 +409,36 @@ export class BrowserView {
|
|
|
137
409
|
sandbox: boolean;
|
|
138
410
|
secretKey: Uint8Array;
|
|
139
411
|
|
|
140
|
-
|
|
412
|
+
/** Wrap a pre-existing native view (popup mint). Skips `bunite_view_create`;
|
|
413
|
+
* the new view is then attached to the host window via `bunite_view_popup_accept`. */
|
|
414
|
+
static adopt(args: {
|
|
415
|
+
nativeViewId: number;
|
|
416
|
+
hostWindowId: number;
|
|
417
|
+
bounds: { x: number; y: number; width: number; height: number };
|
|
418
|
+
appresRoot: string | null;
|
|
419
|
+
}): BrowserView {
|
|
420
|
+
return new BrowserView({
|
|
421
|
+
adoptNativeViewId: args.nativeViewId,
|
|
422
|
+
windowId: args.hostWindowId,
|
|
423
|
+
frame: args.bounds,
|
|
424
|
+
appresRoot: args.appresRoot,
|
|
425
|
+
autoResize: false,
|
|
426
|
+
} as Partial<BrowserViewOptions> & { adoptNativeViewId: number });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
static dismissPopupById(newSurfaceId: number) {
|
|
430
|
+
getNativeLibrary()?.symbols.bunite_view_popup_dismiss(newSurfaceId);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
constructor(options: Partial<BrowserViewOptions> & { adoptNativeViewId?: number }) {
|
|
141
434
|
ensureNativeRuntime();
|
|
142
435
|
|
|
436
|
+
const adopting = options.adoptNativeViewId != null;
|
|
437
|
+
if (adopting) {
|
|
438
|
+
this.id = options.adoptNativeViewId!;
|
|
439
|
+
// Adopted IDs live in the upper u32 half (popup namespace). Keep TS's
|
|
440
|
+
// sequential allocator untouched so normal creates stay below 0x80000000.
|
|
441
|
+
}
|
|
143
442
|
this.windowId = options.windowId ?? defaultOptions.windowId;
|
|
144
443
|
this.url = options.url ?? defaultOptions.url;
|
|
145
444
|
this.html = options.html ?? defaultOptions.html;
|
|
@@ -171,23 +470,33 @@ export class BrowserView {
|
|
|
171
470
|
|
|
172
471
|
BrowserViewMap[this.id] = this;
|
|
173
472
|
this._readyPromise = waitForViewReady(this.id);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
473
|
+
if (adopting) {
|
|
474
|
+
// Native popup mint already created the view; bind to host window + bounds.
|
|
475
|
+
const lib = getNativeLibrary();
|
|
476
|
+
lib?.symbols.bunite_view_popup_accept(
|
|
477
|
+
this.id, this.windowId,
|
|
478
|
+
this.frame.x, this.frame.y, this.frame.width, this.frame.height,
|
|
479
|
+
);
|
|
480
|
+
this.nativeAttached = true;
|
|
481
|
+
} else {
|
|
482
|
+
this.nativeAttached =
|
|
483
|
+
getNativeLibrary()?.symbols.bunite_view_create(
|
|
484
|
+
this.id,
|
|
485
|
+
this.windowId,
|
|
486
|
+
toCString(this.url ?? ""),
|
|
487
|
+
toCString(this.html ?? ""),
|
|
488
|
+
toCString(preloadScript),
|
|
489
|
+
toCString(this.appresRoot ?? ""),
|
|
490
|
+
toCString(this.navigationRules ? JSON.stringify(this.navigationRules) : ""),
|
|
491
|
+
this.frame.x,
|
|
492
|
+
this.frame.y,
|
|
493
|
+
this.frame.width,
|
|
494
|
+
this.frame.height,
|
|
495
|
+
this.autoResize,
|
|
496
|
+
this.sandbox,
|
|
497
|
+
toCString(this.preloadOrigins ? JSON.stringify(this.preloadOrigins) : "")
|
|
498
|
+
) ?? false;
|
|
499
|
+
}
|
|
191
500
|
|
|
192
501
|
if (this.nativeAttached) {
|
|
193
502
|
this.on("did-navigate", (event: any) => {
|
|
@@ -271,18 +580,181 @@ export class BrowserView {
|
|
|
271
580
|
}
|
|
272
581
|
}
|
|
273
582
|
|
|
274
|
-
evaluate(script: string): Promise<EvaluateResult> {
|
|
583
|
+
evaluate(script: string, frameId?: string): Promise<EvaluateResult> {
|
|
275
584
|
if (!this.nativeAttached) {
|
|
276
585
|
return Promise.resolve({ ok: false, code: "not_supported", message: "native runtime unavailable" });
|
|
277
586
|
}
|
|
278
587
|
return new Promise<EvaluateResult>((resolve) => {
|
|
279
|
-
|
|
280
|
-
|
|
588
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
589
|
+
const wrappedResolve = (r: EvaluateResult) => { if (timer) clearTimeout(timer); resolve(r); };
|
|
590
|
+
const requestId = registerEvaluateRequest(this.id, wrappedResolve);
|
|
591
|
+
// 30s timeout — two-hop CDP path (createIsolatedWorld → Runtime.evaluate)
|
|
592
|
+
// can silently hang if the frame is destroyed mid-flight.
|
|
593
|
+
timer = setTimeout(() => {
|
|
594
|
+
if (evaluateResolvers.delete(requestId)) {
|
|
595
|
+
resolve({ ok: false, code: "timeout", message: "evaluate timed out after 30s" });
|
|
596
|
+
}
|
|
597
|
+
}, 30_000);
|
|
598
|
+
const lib = getNativeLibrary();
|
|
599
|
+
if (frameId) {
|
|
600
|
+
lib?.symbols.bunite_view_evaluate_in_frame(this.id, requestId, toCString(script), toCString(frameId));
|
|
601
|
+
} else {
|
|
602
|
+
lib?.symbols.bunite_view_evaluate(this.id, requestId, toCString(script));
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
setDownloadPolicy(policy: "auto" | "ask" | "block", downloadDir?: string) {
|
|
608
|
+
if (!this.nativeAttached) return;
|
|
609
|
+
const policyCode = policy === "auto" ? 0 : policy === "ask" ? 1 : 2;
|
|
610
|
+
getNativeLibrary()?.symbols.bunite_view_set_download_policy(
|
|
611
|
+
this.id, policyCode, toCString(downloadDir ?? "")
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
listFrames(): Promise<ListFramesResult> {
|
|
616
|
+
if (!this.nativeAttached) {
|
|
617
|
+
return Promise.resolve({ ok: false, code: "not_supported", message: "native runtime unavailable" });
|
|
618
|
+
}
|
|
619
|
+
return new Promise<ListFramesResult>((resolve) => {
|
|
620
|
+
const requestId = registerFramesRequest(this.id, resolve);
|
|
621
|
+
const timer = setTimeout(() => {
|
|
622
|
+
if (framesResolvers.delete(requestId)) {
|
|
623
|
+
resolve({ ok: false, code: "runtime_error", message: "list frames timed out after 10s" });
|
|
624
|
+
}
|
|
625
|
+
}, 10_000);
|
|
626
|
+
const wrappedResolve = (r: ListFramesResult) => { clearTimeout(timer); resolve(r); };
|
|
627
|
+
framesResolvers.set(requestId, { viewId: this.id, resolve: wrappedResolve });
|
|
628
|
+
getNativeLibrary()?.symbols.bunite_view_list_frames(this.id, requestId);
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
resolveAndClick(args: ResolveAndClickArgs): Promise<ResolveAndClickResult> {
|
|
633
|
+
if (!this.nativeAttached) {
|
|
634
|
+
return Promise.resolve({ ok: false, code: "not_supported", message: "native runtime unavailable" });
|
|
635
|
+
}
|
|
636
|
+
return new Promise<ResolveAndClickResult>((resolve) => {
|
|
637
|
+
const requestId = registerResolveAndClickRequest(this.id, resolve);
|
|
638
|
+
const timer = setTimeout(() => {
|
|
639
|
+
if (resolveAndClickResolvers.delete(requestId)) {
|
|
640
|
+
resolve({ ok: false, code: "runtime_error", message: "resolveAndClick timed out after 5s" });
|
|
641
|
+
}
|
|
642
|
+
}, 5_000);
|
|
643
|
+
const wrappedResolve = (r: ResolveAndClickResult) => { clearTimeout(timer); resolve(r); };
|
|
644
|
+
resolveAndClickResolvers.set(requestId, { viewId: this.id, resolve: wrappedResolve });
|
|
645
|
+
const button = args.button === "right" ? 2 : args.button === "middle" ? 1 : 0;
|
|
646
|
+
getNativeLibrary()?.symbols.bunite_view_resolve_and_click(
|
|
647
|
+
this.id, requestId,
|
|
648
|
+
toCString(args.selector), toCString(args.frameId ?? ""),
|
|
649
|
+
button, args.clickCount ?? 1, encodeModifiers(args.modifiers),
|
|
650
|
+
);
|
|
281
651
|
});
|
|
282
652
|
}
|
|
283
653
|
|
|
284
654
|
capabilities(): SurfaceCapabilities {
|
|
285
|
-
return
|
|
655
|
+
if (!this.nativeAttached) return decodeCapabilityBits(0);
|
|
656
|
+
const bits = getNativeLibrary()?.symbols.bunite_view_capabilities(this.id) ?? 0;
|
|
657
|
+
return decodeCapabilityBits(bits);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// High-level automation API — same shape as `SurfaceCap` RPC + element
|
|
661
|
+
// `send*` methods. Modifier translation + key resolution happen inside;
|
|
662
|
+
// callers never touch the FFI int contract.
|
|
663
|
+
click(args: {
|
|
664
|
+
x: number; y: number;
|
|
665
|
+
button?: "left" | "middle" | "right";
|
|
666
|
+
clickCount?: number;
|
|
667
|
+
modifiers?: Modifier[];
|
|
668
|
+
}) {
|
|
669
|
+
if (!this.nativeAttached) return;
|
|
670
|
+
const button = args.button === "right" ? 2 : args.button === "middle" ? 1 : 0;
|
|
671
|
+
getNativeLibrary()?.symbols.bunite_view_click(
|
|
672
|
+
this.id, args.x, args.y, button, args.clickCount ?? 1, encodeModifiers(args.modifiers)
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
type(text: string) {
|
|
677
|
+
if (!this.nativeAttached) return;
|
|
678
|
+
getNativeLibrary()?.symbols.bunite_view_type(this.id, toCString(text));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
press(key: string, modifiers?: Modifier[], action?: "down" | "up" | "both") {
|
|
682
|
+
if (!this.nativeAttached) return;
|
|
683
|
+
const r = resolveKey(key);
|
|
684
|
+
const a = action === "down" ? 0 : action === "up" ? 1 : 2;
|
|
685
|
+
getNativeLibrary()?.symbols.bunite_view_press(
|
|
686
|
+
this.id, r.windowsVkCode, r.macKeyCode,
|
|
687
|
+
toCString(r.key), toCString(r.code), toCString(r.character),
|
|
688
|
+
encodeModifiers(modifiers), a, r.extended, r.location
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
scroll(args: {
|
|
693
|
+
dx: number; dy: number; x?: number; y?: number;
|
|
694
|
+
modifiers?: Modifier[];
|
|
695
|
+
}) {
|
|
696
|
+
if (!this.nativeAttached) return;
|
|
697
|
+
getNativeLibrary()?.symbols.bunite_view_scroll(
|
|
698
|
+
this.id, args.dx, args.dy, args.x ?? 0, args.y ?? 0, encodeModifiers(args.modifiers)
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
mouse(args: {
|
|
703
|
+
action: "move" | "down" | "up";
|
|
704
|
+
x: number; y: number;
|
|
705
|
+
button?: "left" | "middle" | "right";
|
|
706
|
+
modifiers?: Modifier[];
|
|
707
|
+
}) {
|
|
708
|
+
if (!this.nativeAttached) return;
|
|
709
|
+
const action = args.action === "move" ? 0 : args.action === "down" ? 1 : 2;
|
|
710
|
+
const button = args.button === "right" ? 2 : args.button === "middle" ? 1 : 0;
|
|
711
|
+
getNativeLibrary()?.symbols.bunite_view_mouse(
|
|
712
|
+
this.id, action, args.x, args.y, button, encodeModifiers(args.modifiers)
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
respondToDialog(requestId: number, accept: boolean, text?: string) {
|
|
717
|
+
if (!this.nativeAttached) return;
|
|
718
|
+
getNativeLibrary()?.symbols.bunite_view_respond_dialog(
|
|
719
|
+
this.id, requestId, accept, toCString(text ?? "")
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
screenshot(format: "png" | "jpeg", quality: number): Promise<ScreenshotResult> {
|
|
724
|
+
if (!this.nativeAttached) {
|
|
725
|
+
return Promise.resolve({ ok: false, code: "not_supported", message: "native runtime unavailable" });
|
|
726
|
+
}
|
|
727
|
+
return new Promise<ScreenshotResult>((resolve) => {
|
|
728
|
+
const requestId = registerScreenshotRequest(this.id, resolve);
|
|
729
|
+
// Timeout — guards against silent hangs (e.g. CEF compositor never delivers).
|
|
730
|
+
const timer = setTimeout(() => {
|
|
731
|
+
if (screenshotResolvers.delete(requestId)) {
|
|
732
|
+
resolve({ ok: false, code: "timeout", message: "screenshot timed out after 30s" });
|
|
733
|
+
}
|
|
734
|
+
}, 30_000);
|
|
735
|
+
const wrappedResolve = (r: ScreenshotResult) => { clearTimeout(timer); resolve(r); };
|
|
736
|
+
// Replace the registered resolver so the timeout-clearing wrapper runs on success.
|
|
737
|
+
screenshotResolvers.set(requestId, { viewId: this.id, resolve: wrappedResolve });
|
|
738
|
+
getNativeLibrary()?.symbols.bunite_view_screenshot(this.id, requestId, toCString(format), quality);
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
accessibilitySnapshot(interestingOnly: boolean): Promise<AccessibilitySnapshotResult> {
|
|
743
|
+
if (!this.nativeAttached) {
|
|
744
|
+
return Promise.resolve({ ok: false, code: "not_supported", message: "native runtime unavailable" });
|
|
745
|
+
}
|
|
746
|
+
return new Promise<AccessibilitySnapshotResult>((resolve) => {
|
|
747
|
+
const requestId = registerAxRequest(this.id, resolve, interestingOnly);
|
|
748
|
+
const timer = setTimeout(() => {
|
|
749
|
+
if (axResolvers.delete(requestId)) {
|
|
750
|
+
resolve({ ok: false, code: "timeout", message: "accessibility snapshot timed out after 30s" });
|
|
751
|
+
}
|
|
752
|
+
}, 30_000);
|
|
753
|
+
const wrappedResolve = (r: AccessibilitySnapshotResult) => { clearTimeout(timer); resolve(r); };
|
|
754
|
+
axResolvers.set(requestId, { viewId: this.id, resolve: wrappedResolve, interestingOnly });
|
|
755
|
+
// Native flag is currently unused (filter is TS-side); kept for ABI shape stability.
|
|
756
|
+
getNativeLibrary()?.symbols.bunite_view_accessibility_snapshot(this.id, requestId, interestingOnly ? 1 : 0);
|
|
757
|
+
});
|
|
286
758
|
}
|
|
287
759
|
|
|
288
760
|
goBack() {
|
|
@@ -390,9 +862,14 @@ export class BrowserView {
|
|
|
390
862
|
removeSurfacesForHostView(this.id);
|
|
391
863
|
cancelWaitForViewReady(this.id);
|
|
392
864
|
rejectEvaluatesForView(this.id);
|
|
865
|
+
rejectScreenshotsForView(this.id);
|
|
866
|
+
rejectAxForView(this.id);
|
|
867
|
+
rejectFramesForView(this.id);
|
|
868
|
+
rejectResolveAndClickForView(this.id);
|
|
393
869
|
this.nativeAttached = false;
|
|
394
870
|
for (const eventName of [
|
|
395
|
-
"will-navigate", "did-navigate", "dom-ready", "new-window-open", "permission-requested", "title-changed"
|
|
871
|
+
"will-navigate", "did-navigate", "dom-ready", "new-window-open", "permission-requested", "title-changed",
|
|
872
|
+
"load-start", "load-finish", "load-fail", "dialog", "console-message", "download-event", "popup-requested",
|
|
396
873
|
]) {
|
|
397
874
|
buniteEventEmitter.removeAllListeners(`${eventName}-${this.id}`);
|
|
398
875
|
}
|
|
@@ -400,7 +877,7 @@ export class BrowserView {
|
|
|
400
877
|
}
|
|
401
878
|
|
|
402
879
|
on(
|
|
403
|
-
name: "will-navigate" | "did-navigate" | "dom-ready" | "new-window-open" | "permission-requested" | "title-changed",
|
|
880
|
+
name: "will-navigate" | "did-navigate" | "dom-ready" | "new-window-open" | "permission-requested" | "title-changed" | "load-start" | "load-finish" | "load-fail" | "dialog" | "console-message" | "download-event" | "popup-requested",
|
|
404
881
|
handler: (event: unknown) => void
|
|
405
882
|
) {
|
|
406
883
|
const specificName = `${name}-${this.id}`;
|