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.
Files changed (36) hide show
  1. package/package.json +4 -4
  2. package/src/host/core/App.ts +19 -2
  3. package/src/host/core/BrowserView.ts +515 -38
  4. package/src/host/core/SurfaceBrowserIPC.ts +53 -3
  5. package/src/host/core/SurfaceManager.ts +603 -30
  6. package/src/host/core/SurfaceRegistry.ts +9 -1
  7. package/src/host/core/inputDispatch.ts +147 -0
  8. package/src/host/events/webviewEvents.ts +25 -1
  9. package/src/host/log.ts +6 -1
  10. package/src/host/native.ts +263 -1
  11. package/src/host/preloadBundle.ts +7 -2
  12. package/src/native/linux/bunite_linux_ffi.cpp +427 -6
  13. package/src/native/linux/bunite_linux_internal.h +18 -0
  14. package/src/native/linux/bunite_linux_runtime.cpp +6 -1
  15. package/src/native/linux/bunite_linux_utils.cpp +2 -2
  16. package/src/native/linux/bunite_linux_view.cpp +296 -5
  17. package/src/native/mac/bunite_mac_ffi.mm +630 -8
  18. package/src/native/mac/bunite_mac_internal.h +19 -0
  19. package/src/native/mac/bunite_mac_utils.mm +2 -2
  20. package/src/native/mac/bunite_mac_view.mm +371 -9
  21. package/src/native/shared/ffi_exports.h +200 -2
  22. package/src/native/win/native_host_cef.cpp +186 -11
  23. package/src/native/win/native_host_ffi.cpp +1194 -1
  24. package/src/native/win/native_host_internal.h +35 -0
  25. package/src/native/win/native_host_utils.cpp +2 -1
  26. package/src/native/win/process_helper_win.cpp +54 -27
  27. package/src/native/win-webview2/bunite_webview2_ffi.cpp +1023 -12
  28. package/src/native/win-webview2/webview2_internal.h +25 -0
  29. package/src/native/win-webview2/webview2_runtime.cpp +403 -34
  30. package/src/native/win-webview2/webview2_utils.cpp +30 -12
  31. package/src/preload/runtime.built.js +1 -1
  32. package/src/preload/runtime.ts +97 -0
  33. package/src/rpc/framework.ts +340 -8
  34. package/src/rpc/index.ts +32 -0
  35. package/src/webview/native.ts +253 -51
  36. package/src/webview/polyfill.ts +283 -22
@@ -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 { SurfaceCap, EvaluateResult, SurfaceCapabilities } from "../rpc/framework";
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
- const stream = s.didNavigate();
140
- this._activeStreams.push(stream as { cancel?: () => void });
141
- for await (const ev of stream) {
142
- if (ctrl.signal.aborted) break;
143
- if (ev.surfaceId === this._surfaceId) {
144
- this.dispatchEvent(new CustomEvent("did-navigate", { detail: { url: ev.url } }));
145
- }
146
- }
147
- } catch (err) {
148
- if ((globalThis as { __BUNITE_DEBUG__?: boolean }).__BUNITE_DEBUG__) {
149
- console.warn("[bunite] didNavigate stream failed", err);
150
- }
151
- }
152
- })();
153
- void (async () => {
154
- try {
155
- const s = await getSurfaceCap();
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
- if (ev.surfaceId === this._surfaceId) {
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] titleChanged stream failed", err);
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, titleChanged: false,
289
+ evaluate: false, crossOriginEval: false, surfaceEvents: false,
271
290
  nativeInputTrusted: false, click: false, type: false, press: false,
272
- scroll: false, screenshot: 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._syncCtrl = new OverlaySyncController(this, (rect) => {
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(() => {