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/src/webview/native.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
// <bunite-webview> custom element — registered in every appres:// page via preload.
|
|
2
2
|
|
|
3
3
|
import type { ClientOf } from "../rpc/index";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
SurfaceCap, EvaluateResult, SurfaceCapabilities, ScreenshotResult,
|
|
6
|
+
SurfaceEvent, ConsoleEntry, WaitResult, NavigationState,
|
|
7
|
+
AccessibilitySnapshotResult, BoundingRectResult, ListFramesResult,
|
|
8
|
+
DownloadEvent, DownloadPolicy, WaitForDownloadResult,
|
|
9
|
+
ResolveAndClickResult,
|
|
10
|
+
} from "../rpc/framework";
|
|
11
|
+
|
|
12
|
+
declare const __buniteWebviewId: number;
|
|
5
13
|
|
|
6
14
|
declare const host: {
|
|
7
15
|
runtime(): Promise<ClientOf<typeof import("../rpc/framework").RuntimeCap>>;
|
|
@@ -136,40 +144,51 @@ class BuniteWebviewElement extends HTMLElement {
|
|
|
136
144
|
void (async () => {
|
|
137
145
|
try {
|
|
138
146
|
const s = await getSurfaceCap();
|
|
139
|
-
|
|
140
|
-
this.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
const stream = s.titleChanged();
|
|
157
|
-
this._activeStreams.push(stream as { cancel?: () => void });
|
|
158
|
-
for await (const ev of stream) {
|
|
147
|
+
if (ctrl.signal.aborted) return;
|
|
148
|
+
await this._waitForSurfaceId(ctrl.signal);
|
|
149
|
+
const sid = this._surfaceId;
|
|
150
|
+
if (ctrl.signal.aborted || sid == null) return;
|
|
151
|
+
const surfStream = s.surfaceEvents({ surfaceId: sid });
|
|
152
|
+
this._activeStreams.push(surfStream as { cancel?: () => void });
|
|
153
|
+
const dlStream = s.downloadEvents({ surfaceId: sid });
|
|
154
|
+
this._activeStreams.push(dlStream as { cancel?: () => void });
|
|
155
|
+
(async () => {
|
|
156
|
+
try {
|
|
157
|
+
for await (const event of dlStream) {
|
|
158
|
+
if (ctrl.signal.aborted) break;
|
|
159
|
+
this.dispatchEvent(new CustomEvent<DownloadEvent>("download-event", { detail: event }));
|
|
160
|
+
}
|
|
161
|
+
} catch { /* stream torn down */ }
|
|
162
|
+
})();
|
|
163
|
+
for await (const event of surfStream) {
|
|
159
164
|
if (ctrl.signal.aborted) break;
|
|
160
|
-
|
|
161
|
-
this.dispatchEvent(new CustomEvent("title-changed", { detail: { title: ev.title } }));
|
|
162
|
-
}
|
|
165
|
+
this.dispatchEvent(new CustomEvent<SurfaceEvent>("surface-event", { detail: event }));
|
|
163
166
|
}
|
|
164
167
|
} catch (err) {
|
|
165
168
|
if ((globalThis as { __BUNITE_DEBUG__?: boolean }).__BUNITE_DEBUG__) {
|
|
166
|
-
console.warn("[bunite]
|
|
169
|
+
console.warn("[bunite] surfaceEvents stream failed", err);
|
|
167
170
|
}
|
|
168
171
|
}
|
|
169
172
|
})();
|
|
170
173
|
this._waitForLayout();
|
|
171
174
|
}
|
|
172
175
|
|
|
176
|
+
private async _waitForSurfaceId(signal: AbortSignal): Promise<void> {
|
|
177
|
+
while (this._surfaceId == null && !signal.aborted) {
|
|
178
|
+
const pending = this._initPromise;
|
|
179
|
+
if (pending) {
|
|
180
|
+
// Await this exact init attempt; if it rejects, bail (init failed —
|
|
181
|
+
// we'd otherwise spin forever waiting for a surfaceId that never lands).
|
|
182
|
+
try { await pending; } catch { return; }
|
|
183
|
+
// After resolve, _surfaceId may still be null if disconnect raced —
|
|
184
|
+
// the next loop iteration checks signal.aborted to exit cleanly.
|
|
185
|
+
} else {
|
|
186
|
+
// No init in flight yet (waiting for layout). Yield then re-check.
|
|
187
|
+
await new Promise((r) => setTimeout(r, 16));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
173
192
|
private _waitForLayout() {
|
|
174
193
|
if (this._layoutObserver) return; // already waiting
|
|
175
194
|
|
|
@@ -257,24 +276,168 @@ class BuniteWebviewElement extends HTMLElement {
|
|
|
257
276
|
this.setAttribute("src", url);
|
|
258
277
|
}
|
|
259
278
|
|
|
260
|
-
async evaluate(script: string): Promise<EvaluateResult> {
|
|
279
|
+
async evaluate(script: string, opts?: { frameId?: string }): Promise<EvaluateResult> {
|
|
261
280
|
const sid = this._surfaceId;
|
|
262
281
|
if (sid == null) return { ok: false, code: "not_supported", message: "surface not ready" };
|
|
263
|
-
return callSurfaceTyped((s) => s.evaluate({ surfaceId: sid, script }));
|
|
282
|
+
return callSurfaceTyped((s) => s.evaluate({ surfaceId: sid, script, frameId: opts?.frameId }));
|
|
264
283
|
}
|
|
265
284
|
|
|
266
285
|
async capabilities(): Promise<SurfaceCapabilities> {
|
|
267
286
|
const sid = this._surfaceId;
|
|
268
287
|
if (sid == null) {
|
|
269
288
|
return {
|
|
270
|
-
evaluate: false, crossOriginEval: false,
|
|
289
|
+
evaluate: false, crossOriginEval: false, surfaceEvents: false,
|
|
271
290
|
nativeInputTrusted: false, click: false, type: false, press: false,
|
|
272
|
-
scroll: false,
|
|
291
|
+
scroll: false, mouse: false, dialogs: false, console: false,
|
|
292
|
+
screenshot: false, accessibilitySnapshot: false, getBoundingRect: false,
|
|
293
|
+
frames: false, downloads: false, popups: false, resolveAndClick: false,
|
|
273
294
|
};
|
|
274
295
|
}
|
|
275
296
|
return callSurfaceTyped((s) => s.capabilities({ surfaceId: sid }));
|
|
276
297
|
}
|
|
277
298
|
|
|
299
|
+
// Automation input — `send*` prefix avoids clashing with HTMLElement.click() / .scroll().
|
|
300
|
+
async sendClick(args: {
|
|
301
|
+
x: number; y: number;
|
|
302
|
+
button?: "left" | "middle" | "right";
|
|
303
|
+
clickCount?: number;
|
|
304
|
+
modifiers?: Array<"alt" | "ctrl" | "meta" | "shift">;
|
|
305
|
+
}): Promise<void> {
|
|
306
|
+
const sid = this._surfaceId;
|
|
307
|
+
if (sid == null) return;
|
|
308
|
+
await callSurfaceTyped((s) => s.click({ surfaceId: sid, ...args }));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async sendType(text: string): Promise<void> {
|
|
312
|
+
const sid = this._surfaceId;
|
|
313
|
+
if (sid == null) return;
|
|
314
|
+
await callSurfaceTyped((s) => s.type({ surfaceId: sid, text }));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async sendPress(
|
|
318
|
+
key: string,
|
|
319
|
+
modifiers?: Array<"alt" | "ctrl" | "meta" | "shift">,
|
|
320
|
+
action?: "down" | "up" | "both"
|
|
321
|
+
): Promise<void> {
|
|
322
|
+
const sid = this._surfaceId;
|
|
323
|
+
if (sid == null) return;
|
|
324
|
+
await callSurfaceTyped((s) => s.press({ surfaceId: sid, key, modifiers, action }));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async sendScroll(args: {
|
|
328
|
+
dx: number; dy: number; x?: number; y?: number;
|
|
329
|
+
modifiers?: Array<"alt" | "ctrl" | "meta" | "shift">;
|
|
330
|
+
}): Promise<void> {
|
|
331
|
+
const sid = this._surfaceId;
|
|
332
|
+
if (sid == null) return;
|
|
333
|
+
await callSurfaceTyped((s) => s.scroll({ surfaceId: sid, ...args }));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async sendMouse(args: {
|
|
337
|
+
action: "move" | "down" | "up";
|
|
338
|
+
x: number; y: number;
|
|
339
|
+
button?: "left" | "middle" | "right";
|
|
340
|
+
modifiers?: Array<"alt" | "ctrl" | "meta" | "shift">;
|
|
341
|
+
}): Promise<void> {
|
|
342
|
+
const sid = this._surfaceId;
|
|
343
|
+
if (sid == null) return;
|
|
344
|
+
await callSurfaceTyped((s) => s.mouse({ surfaceId: sid, ...args }));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async respondToDialog(requestId: number, accept: boolean, text?: string): Promise<void> {
|
|
348
|
+
const sid = this._surfaceId;
|
|
349
|
+
if (sid == null) return;
|
|
350
|
+
await callSurfaceTyped((s) => s.respondToDialog({ surfaceId: sid, requestId, accept, text }));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async setDialogTimeout(ms: number | null): Promise<void> {
|
|
354
|
+
const sid = this._surfaceId;
|
|
355
|
+
if (sid == null) return;
|
|
356
|
+
await callSurfaceTyped((s) => s.setDialogTimeout({ surfaceId: sid, ms }));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async waitForSelector(selector: string, timeoutMs?: number): Promise<WaitResult> {
|
|
360
|
+
const sid = this._surfaceId;
|
|
361
|
+
if (sid == null) return { ok: false, code: "runtime_error", message: "surface not ready" };
|
|
362
|
+
return callSurfaceTyped((s) => s.waitForSelector({ surfaceId: sid, selector, timeoutMs }));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async waitForFunction(expression: string, opts?: { timeoutMs?: number; pollIntervalMs?: number }): Promise<WaitResult> {
|
|
366
|
+
const sid = this._surfaceId;
|
|
367
|
+
if (sid == null) return { ok: false, code: "runtime_error", message: "surface not ready" };
|
|
368
|
+
return callSurfaceTyped((s) => s.waitForFunction({ surfaceId: sid, expression, ...opts }));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async getConsoleBuffer(opts?: { clear?: boolean }): Promise<ConsoleEntry[]> {
|
|
372
|
+
const sid = this._surfaceId;
|
|
373
|
+
if (sid == null) return [];
|
|
374
|
+
return (await callSurfaceTyped((s) => s.getConsoleBuffer({ surfaceId: sid, clear: opts?.clear }))) ?? [];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async getNavigationState(): Promise<NavigationState> {
|
|
378
|
+
const sid = this._surfaceId;
|
|
379
|
+
if (sid == null) return { lastLoadEpoch: 0, isLoading: false, currentUrl: "" };
|
|
380
|
+
return callSurfaceTyped((s) => s.getNavigationState({ surfaceId: sid }));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async accessibilitySnapshot(opts?: { interestingOnly?: boolean }): Promise<AccessibilitySnapshotResult> {
|
|
384
|
+
const sid = this._surfaceId;
|
|
385
|
+
if (sid == null) return { ok: false, code: "not_supported", message: "surface not ready" };
|
|
386
|
+
return callSurfaceTyped((s) => s.accessibilitySnapshot({ surfaceId: sid, interestingOnly: opts?.interestingOnly }));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async getBoundingRect(selector: string, opts?: { frameId?: string }): Promise<BoundingRectResult> {
|
|
390
|
+
const sid = this._surfaceId;
|
|
391
|
+
if (sid == null) return { ok: false, code: "runtime_error", message: "surface not ready" };
|
|
392
|
+
return callSurfaceTyped((s) => s.getBoundingRect({ surfaceId: sid, selector, frameId: opts?.frameId }));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async listFrames(): Promise<ListFramesResult> {
|
|
396
|
+
const sid = this._surfaceId;
|
|
397
|
+
if (sid == null) return { ok: false, code: "not_supported", message: "surface not ready" };
|
|
398
|
+
return callSurfaceTyped((s) => s.listFrames({ surfaceId: sid }));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async resolveAndClick(
|
|
402
|
+
selector: string,
|
|
403
|
+
opts?: {
|
|
404
|
+
frameId?: string;
|
|
405
|
+
button?: "left" | "middle" | "right";
|
|
406
|
+
clickCount?: number;
|
|
407
|
+
modifiers?: Array<"alt" | "ctrl" | "meta" | "shift">;
|
|
408
|
+
}
|
|
409
|
+
): Promise<ResolveAndClickResult> {
|
|
410
|
+
const sid = this._surfaceId;
|
|
411
|
+
if (sid == null) return { ok: false, code: "runtime_error", message: "surface not ready" };
|
|
412
|
+
return callSurfaceTyped((s) => s.resolveAndClick({ surfaceId: sid, selector, ...opts }));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async setDownloadPolicy(policy: DownloadPolicy, downloadDir?: string): Promise<void> {
|
|
416
|
+
const sid = this._surfaceId;
|
|
417
|
+
if (sid == null) return;
|
|
418
|
+
await callSurface((s) => s.setDownloadPolicy({ surfaceId: sid, policy, downloadDir }));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async waitForDownload(opts?: { timeoutMs?: number }): Promise<WaitForDownloadResult> {
|
|
422
|
+
const sid = this._surfaceId;
|
|
423
|
+
if (sid == null) return { ok: false, code: "not_supported", message: "surface not ready" };
|
|
424
|
+
return callSurfaceTyped((s) => s.waitForDownload({ surfaceId: sid, timeoutMs: opts?.timeoutMs }));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async dismissPopup(newSurfaceId: number): Promise<void> {
|
|
428
|
+
await callSurface((s) => s.dismissPopup({ newSurfaceId }));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async extendAdoptionTimeout(newSurfaceId: number, gracePeriodMs: number) {
|
|
432
|
+
return callSurfaceTyped((s) => s.extendPopupTimeout({ newSurfaceId, gracePeriodMs }));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async screenshot(args?: { format?: "png" | "jpeg"; quality?: number }): Promise<ScreenshotResult> {
|
|
436
|
+
const sid = this._surfaceId;
|
|
437
|
+
if (sid == null) return { ok: false, code: "not_supported", message: "surface not ready" };
|
|
438
|
+
return callSurfaceTyped((s) => s.screenshot({ surfaceId: sid, ...args }));
|
|
439
|
+
}
|
|
440
|
+
|
|
278
441
|
private _applySurfaceHidden() {
|
|
279
442
|
const sid = this._surfaceId;
|
|
280
443
|
if (sid == null) return;
|
|
@@ -282,11 +445,71 @@ class BuniteWebviewElement extends HTMLElement {
|
|
|
282
445
|
void callSurface((s) => s.setHidden({ surfaceId: sid, hidden }));
|
|
283
446
|
}
|
|
284
447
|
|
|
448
|
+
private _setupSyncCtrl() {
|
|
449
|
+
this._syncCtrl = new OverlaySyncController(this, (rect) => {
|
|
450
|
+
const sid = this._surfaceId;
|
|
451
|
+
if (sid == null) return;
|
|
452
|
+
const isZero = rect.width === 0 && rect.height === 0;
|
|
453
|
+
if (isZero) {
|
|
454
|
+
if (!this._syncHidden) {
|
|
455
|
+
this._syncHidden = true;
|
|
456
|
+
this._applySurfaceHidden();
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (this._syncHidden) {
|
|
461
|
+
this._syncHidden = false;
|
|
462
|
+
this._applySurfaceHidden();
|
|
463
|
+
}
|
|
464
|
+
void callSurface((s) => s.resize({
|
|
465
|
+
surfaceId: sid, x: rect.x, y: rect.y, w: rect.width, h: rect.height,
|
|
466
|
+
}));
|
|
467
|
+
});
|
|
468
|
+
this._syncCtrl.start();
|
|
469
|
+
}
|
|
470
|
+
|
|
285
471
|
private initSurface() {
|
|
286
472
|
if (this._surfaceId != null || this._initPromise != null) return;
|
|
287
473
|
|
|
288
474
|
const dpr = window.devicePixelRatio || 1;
|
|
289
475
|
const r = this.getBoundingClientRect();
|
|
476
|
+
const adoptAttr = this.getAttribute("adopt-popup-id");
|
|
477
|
+
const adoptId = adoptAttr ? Number(adoptAttr) : NaN;
|
|
478
|
+
if (Number.isFinite(adoptId)) {
|
|
479
|
+
// Popup adoption — bind a backend-minted surface (received via the parent
|
|
480
|
+
// page's `surface-event` `popup` arm) to this element.
|
|
481
|
+
const initPromise = getSurfaceCap().then(async (s) => {
|
|
482
|
+
const res = await s.acceptPopup({
|
|
483
|
+
newSurfaceId: adoptId,
|
|
484
|
+
hostViewId: __buniteWebviewId,
|
|
485
|
+
bounds: {
|
|
486
|
+
x: Math.round(r.x * dpr),
|
|
487
|
+
y: Math.round(r.y * dpr),
|
|
488
|
+
width: Math.round(r.width * dpr),
|
|
489
|
+
height: Math.round(r.height * dpr),
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
if (!res.ok) throw new Error(`acceptPopup failed: ${res.code}: ${res.message}`);
|
|
493
|
+
return { surfaceId: adoptId };
|
|
494
|
+
}) as Promise<SurfaceInitResponse>;
|
|
495
|
+
this._initPromise = initPromise;
|
|
496
|
+
initPromise.then((response) => {
|
|
497
|
+
if (this._initPromise !== initPromise) return;
|
|
498
|
+
if (this._aborted) {
|
|
499
|
+
void callSurface((s) => s.remove({ surfaceId: response.surfaceId }));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
this._surfaceId = response.surfaceId;
|
|
503
|
+
this._setupSyncCtrl();
|
|
504
|
+
}).catch((err) => {
|
|
505
|
+
if ((globalThis as { __BUNITE_DEBUG__?: boolean }).__BUNITE_DEBUG__) {
|
|
506
|
+
console.warn("[bunite] adopt-popup-id init failed", err);
|
|
507
|
+
}
|
|
508
|
+
this._initPromise = null;
|
|
509
|
+
});
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
290
513
|
const src = this._pendingSrc || this.getAttribute("src") || "";
|
|
291
514
|
this._pendingSrc = null;
|
|
292
515
|
|
|
@@ -323,28 +546,7 @@ class BuniteWebviewElement extends HTMLElement {
|
|
|
323
546
|
}
|
|
324
547
|
}
|
|
325
548
|
|
|
326
|
-
this.
|
|
327
|
-
const sid = this._surfaceId;
|
|
328
|
-
if (sid == null) return;
|
|
329
|
-
|
|
330
|
-
const isZero = rect.width === 0 && rect.height === 0;
|
|
331
|
-
if (isZero) {
|
|
332
|
-
if (!this._syncHidden) {
|
|
333
|
-
this._syncHidden = true;
|
|
334
|
-
this._applySurfaceHidden();
|
|
335
|
-
}
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
if (this._syncHidden) {
|
|
339
|
-
this._syncHidden = false;
|
|
340
|
-
this._applySurfaceHidden();
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
void callSurface((s) => s.resize({
|
|
344
|
-
surfaceId: sid, x: rect.x, y: rect.y, w: rect.width, h: rect.height,
|
|
345
|
-
}));
|
|
346
|
-
});
|
|
347
|
-
this._syncCtrl.start();
|
|
549
|
+
this._setupSyncCtrl();
|
|
348
550
|
})
|
|
349
551
|
.catch(() => {})
|
|
350
552
|
.finally(() => {
|