bunite-core 0.14.0 → 0.17.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 (33) hide show
  1. package/package.json +4 -4
  2. package/src/host/core/App.ts +6 -3
  3. package/src/host/core/BrowserView.ts +345 -24
  4. package/src/host/core/BrowserWindow.ts +52 -6
  5. package/src/host/core/SurfaceBrowserIPC.ts +10 -1
  6. package/src/host/core/SurfaceManager.ts +357 -16
  7. package/src/host/core/windowCap.ts +69 -0
  8. package/src/host/events/webviewEvents.ts +18 -1
  9. package/src/host/log.ts +6 -1
  10. package/src/host/native.ts +145 -1
  11. package/src/host/preloadBundle.ts +7 -2
  12. package/src/native/linux/bunite_linux_ffi.cpp +225 -1
  13. package/src/native/linux/bunite_linux_internal.h +12 -0
  14. package/src/native/linux/bunite_linux_runtime.cpp +6 -1
  15. package/src/native/linux/bunite_linux_view.cpp +211 -5
  16. package/src/native/mac/bunite_mac_ffi.mm +293 -4
  17. package/src/native/mac/bunite_mac_internal.h +13 -0
  18. package/src/native/mac/bunite_mac_view.mm +227 -7
  19. package/src/native/shared/ffi_exports.h +97 -30
  20. package/src/native/win/native_host_cef.cpp +107 -13
  21. package/src/native/win/native_host_ffi.cpp +831 -2
  22. package/src/native/win/native_host_internal.h +22 -0
  23. package/src/native/win/native_host_runtime.cpp +34 -0
  24. package/src/native/win-webview2/bunite_webview2_ffi.cpp +827 -5
  25. package/src/native/win-webview2/webview2_internal.h +19 -0
  26. package/src/native/win-webview2/webview2_runtime.cpp +383 -31
  27. package/src/preload/runtime.built.js +1 -1
  28. package/src/preload/runtime.ts +39 -0
  29. package/src/rpc/framework.ts +194 -12
  30. package/src/rpc/index.ts +12 -0
  31. package/src/rpc/peer.ts +1 -1
  32. package/src/webview/native.ts +142 -32
  33. package/src/webview/polyfill.ts +91 -14
@@ -1,6 +1,6 @@
1
1
  // Iframe fallback for web (no-op if native already registered). HTMLElement deref'd lazily so module is import-safe in Node/Bun.
2
2
 
3
- import type { SurfaceEvent } from "../rpc/framework";
3
+ import type { SurfaceEvent, SurfaceEventBase } from "../rpc/framework";
4
4
 
5
5
  // Default sandbox omits allow-same-origin / allow-top-navigation / allow-modals /
6
6
  // allow-popups-to-escape-sandbox — popup escape stays opt-in so a sandboxed page
@@ -31,6 +31,12 @@ function definePolyfillClass(): CustomElementConstructor {
31
31
  private _iframe: HTMLIFrameElement | null = null;
32
32
  private _titleObserver: MutationObserver | null = null;
33
33
  private _lastTitle: string = "";
34
+ private _epoch: number = 0;
35
+ private _isLoading: boolean = false;
36
+ private _currentUrl: string = "";
37
+ // Local history stack for goBack — cross-origin contentWindow.history is
38
+ // inaccessible, so we track navigations ourselves.
39
+ private _history: string[] = [];
34
40
 
35
41
  private isReachable(): boolean {
36
42
  if (!this._iframe) return false;
@@ -50,8 +56,22 @@ function definePolyfillClass(): CustomElementConstructor {
50
56
  };
51
57
  }
52
58
 
53
- private emit(event: SurfaceEvent) {
54
- this.dispatchEvent(new CustomEvent<SurfaceEvent>("surface-event", { detail: event }));
59
+ private emit(event: SurfaceEventBase) {
60
+ if (event.type === "navigate") {
61
+ this._epoch++;
62
+ // Avoid pushing duplicate / same-as-top entries (reload doesn't grow history).
63
+ if (this._currentUrl && this._currentUrl !== event.url &&
64
+ this._history[this._history.length - 1] !== this._currentUrl) {
65
+ this._history.push(this._currentUrl);
66
+ }
67
+ this._currentUrl = event.url;
68
+ } else if (event.type === "load-start") {
69
+ this._isLoading = true;
70
+ } else if (event.type === "load-finish" || event.type === "load-fail") {
71
+ this._isLoading = false;
72
+ }
73
+ const stamped: SurfaceEvent = { ...event, epoch: this._epoch };
74
+ this.dispatchEvent(new CustomEvent<SurfaceEvent>("surface-event", { detail: stamped }));
55
75
  }
56
76
 
57
77
  private setupTitleObserver() {
@@ -100,6 +120,7 @@ function definePolyfillClass(): CustomElementConstructor {
100
120
  this.dispatchBlocked(src);
101
121
  } else {
102
122
  iframe.src = src;
123
+ this._currentUrl = src;
103
124
  this.emit({ type: "load-start", url: src });
104
125
  }
105
126
  }
@@ -112,8 +133,9 @@ function definePolyfillClass(): CustomElementConstructor {
112
133
  // Suppress the spurious about:blank load that fires after a blocked
113
134
  // navigation (or before any explicit navigate).
114
135
  if (isBlockedSrc(url)) return;
115
- this.emit({ type: "load-finish", url });
136
+ // navigate first so load-finish carries the bumped epoch.
116
137
  this.emit({ type: "navigate", url });
138
+ this.emit({ type: "load-finish", url });
117
139
  this.setupTitleObserver();
118
140
  });
119
141
 
@@ -148,19 +170,22 @@ function definePolyfillClass(): CustomElementConstructor {
148
170
  }
149
171
 
150
172
  goBack() {
151
- try {
152
- this._iframe?.contentWindow?.history.back();
153
- } catch {}
173
+ // Same-origin path uses native history. Cross-origin throws → fall back
174
+ // to our tracked stack (push on every navigate; pop here).
175
+ try { this._iframe?.contentWindow?.history.back(); return; } catch {}
176
+ const prev = this._history.pop();
177
+ if (prev && this._iframe) this._iframe.src = prev;
154
178
  }
155
179
 
156
180
  reload() {
157
- try {
158
- this._iframe?.contentWindow?.location.reload();
159
- } catch {
160
- if (this._iframe) {
161
- this._iframe.src = this._iframe.src;
162
- }
163
- }
181
+ try { this._iframe?.contentWindow?.location.reload(); return; } catch {}
182
+ // Cross-origin fallback: reassigning the same src is a no-op in WHATWG;
183
+ // cycle via about:blank to force a fresh navigation.
184
+ const iframe = this._iframe;
185
+ if (!iframe) return;
186
+ const url = this._currentUrl || iframe.src;
187
+ iframe.src = "about:blank";
188
+ requestAnimationFrame(() => { if (this._iframe) this._iframe.src = url; });
164
189
  }
165
190
 
166
191
  setHidden(hidden: boolean) {
@@ -219,6 +244,54 @@ function definePolyfillClass(): CustomElementConstructor {
219
244
  target.dispatchEvent(new MouseEvent("click", init));
220
245
  }
221
246
 
247
+ async resolveAndClick(_selector: string, _opts?: unknown) {
248
+ return { ok: false as const, code: "not_supported" as const, message: "polyfill iframe: atomic resolveAndClick not supported" };
249
+ }
250
+
251
+ async getBoundingRect(selector: string, _opts?: unknown) {
252
+ if (!this.isReachable()) return { ok: false as const, code: "not_supported" as const, message: "iframe not reachable" };
253
+ try {
254
+ const el = this._iframe!.contentDocument!.querySelector(selector) as Element | null;
255
+ if (!el) return { ok: false as const, code: "not_found" as const, message: `selector ${selector} not found` };
256
+ const r = el.getBoundingClientRect();
257
+ const win = this._iframe!.contentWindow!;
258
+ const visible = r.width > 0 && r.height > 0 && r.bottom > 0 && r.right > 0
259
+ && r.top < win.innerHeight && r.left < win.innerWidth;
260
+ return { ok: true as const, rect: { x: r.x, y: r.y, width: r.width, height: r.height }, visible };
261
+ } catch (e: any) {
262
+ const code = e?.name === "SecurityError" ? "cross_origin" as const : "runtime_error" as const;
263
+ return { ok: false as const, code, message: e?.message ?? String(e) };
264
+ }
265
+ }
266
+
267
+ async listFrames() {
268
+ return { ok: false as const, code: "not_supported" as const, message: "polyfill: not implemented" };
269
+ }
270
+
271
+ async accessibilitySnapshot(_opts?: unknown) {
272
+ return { ok: false as const, code: "not_supported" as const, message: "polyfill: not implemented" };
273
+ }
274
+
275
+ async setDownloadPolicy(_policy: unknown, _dir?: unknown) {
276
+ // No-op — iframe has no download lifecycle hook.
277
+ }
278
+
279
+ async waitForDownload(_opts?: unknown) {
280
+ return { ok: false as const, code: "not_supported" as const, message: "polyfill: not implemented" };
281
+ }
282
+
283
+ async acceptPopup(_opts?: unknown) {
284
+ return { ok: false as const, code: "not_found" as const, message: "polyfill: no popup orchestration" };
285
+ }
286
+
287
+ async dismissPopup(_id?: unknown) {
288
+ // No-op.
289
+ }
290
+
291
+ async extendAdoptionTimeout(_id?: unknown, _ms?: unknown) {
292
+ return { ok: false as const, code: "not_found" as const, message: "polyfill: no popup orchestration" };
293
+ }
294
+
222
295
  async sendType(text: string) {
223
296
  if (!this.isReachable()) return;
224
297
  const doc = this._iframe!.contentDocument!;
@@ -313,6 +386,10 @@ function definePolyfillClass(): CustomElementConstructor {
313
386
  return [] as { level: string; args: string[]; ts: number }[];
314
387
  }
315
388
 
389
+ async getNavigationState() {
390
+ return { lastLoadEpoch: this._epoch, isLoading: this._isLoading, currentUrl: this._currentUrl };
391
+ }
392
+
316
393
  async screenshot(_args?: { format?: "png" | "jpeg"; quality?: number }) {
317
394
  return { ok: false as const, code: "not_supported" as const, message: "iframe polyfill does not support screenshot" };
318
395
  }