bunite-core 0.17.1 → 0.17.2
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 +1 -1
- package/src/host/core/App.ts +28 -18
- package/src/host/core/BrowserView.ts +333 -116
- package/src/host/core/BrowserWindow.ts +44 -22
- package/src/host/core/Socket.ts +26 -9
- package/src/host/core/SurfaceBrowserIPC.ts +15 -5
- package/src/host/core/SurfaceManager.ts +345 -158
- package/src/host/core/SurfaceRegistry.ts +1 -1
- package/src/host/core/inputDispatch.ts +71 -42
- package/src/host/core/singleInstanceLock.ts +10 -2
- package/src/host/core/windowCap.ts +42 -23
- package/src/host/encryptedPipe.ts +12 -3
- package/src/host/events/appEvents.ts +1 -1
- package/src/host/events/eventEmitter.ts +6 -6
- package/src/host/events/webviewEvents.ts +11 -4
- package/src/host/events/windowEvents.ts +1 -2
- package/src/host/index.ts +9 -17
- package/src/host/log.ts +6 -4
- package/src/host/native.ts +271 -163
- package/src/host/paths.ts +18 -15
- package/src/host/platform.ts +2 -4
- package/src/host/preloadBundle.ts +1 -1
- package/src/host/serveWeb.ts +18 -11
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +61 -24
- package/src/rpc/encrypt.ts +15 -4
- package/src/rpc/error.ts +2 -7
- package/src/rpc/framework.ts +105 -35
- package/src/rpc/index.ts +129 -140
- package/src/rpc/peer.ts +430 -159
- package/src/rpc/renderer.ts +15 -9
- package/src/rpc/schema.ts +86 -61
- package/src/rpc/stream.ts +12 -3
- package/src/rpc/transport.ts +28 -9
- package/src/rpc/wire.ts +23 -10
- package/src/webview/native.ts +150 -73
- package/src/webview/polyfill.ts +184 -48
package/src/webview/polyfill.ts
CHANGED
|
@@ -12,6 +12,7 @@ const BLOCKED_SCHEME_RE = /^(javascript|data|vbscript|file|about):/i;
|
|
|
12
12
|
// before scheme detection. Mirror that so embedded controls (e.g. `java\nscript:`)
|
|
13
13
|
// can't bypass the scheme guard.
|
|
14
14
|
function normalizeForSchemeCheck(src: string): string {
|
|
15
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional, see above.
|
|
15
16
|
return src.replace(/[\t\n\r]/g, "").replace(/^[\x00-\x20]+/, "");
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -42,17 +43,22 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
42
43
|
if (!this._iframe) return false;
|
|
43
44
|
try {
|
|
44
45
|
return this._iframe.contentDocument != null;
|
|
45
|
-
} catch {
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
private modifierBag(mods?: string[]): {
|
|
49
|
-
shiftKey: boolean;
|
|
52
|
+
shiftKey: boolean;
|
|
53
|
+
ctrlKey: boolean;
|
|
54
|
+
altKey: boolean;
|
|
55
|
+
metaKey: boolean;
|
|
50
56
|
} {
|
|
51
57
|
return {
|
|
52
58
|
shiftKey: !!mods?.includes("shift"),
|
|
53
|
-
ctrlKey:
|
|
54
|
-
altKey:
|
|
55
|
-
metaKey:
|
|
59
|
+
ctrlKey: !!mods?.includes("ctrl"),
|
|
60
|
+
altKey: !!mods?.includes("alt"),
|
|
61
|
+
metaKey: !!mods?.includes("meta"),
|
|
56
62
|
};
|
|
57
63
|
}
|
|
58
64
|
|
|
@@ -60,8 +66,11 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
60
66
|
if (event.type === "navigate") {
|
|
61
67
|
this._epoch++;
|
|
62
68
|
// Avoid pushing duplicate / same-as-top entries (reload doesn't grow history).
|
|
63
|
-
if (
|
|
64
|
-
|
|
69
|
+
if (
|
|
70
|
+
this._currentUrl &&
|
|
71
|
+
this._currentUrl !== event.url &&
|
|
72
|
+
this._history[this._history.length - 1] !== this._currentUrl
|
|
73
|
+
) {
|
|
65
74
|
this._history.push(this._currentUrl);
|
|
66
75
|
}
|
|
67
76
|
this._currentUrl = event.url;
|
|
@@ -172,20 +181,28 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
172
181
|
goBack() {
|
|
173
182
|
// Same-origin path uses native history. Cross-origin throws → fall back
|
|
174
183
|
// to our tracked stack (push on every navigate; pop here).
|
|
175
|
-
try {
|
|
184
|
+
try {
|
|
185
|
+
this._iframe?.contentWindow?.history.back();
|
|
186
|
+
return;
|
|
187
|
+
} catch {}
|
|
176
188
|
const prev = this._history.pop();
|
|
177
189
|
if (prev && this._iframe) this._iframe.src = prev;
|
|
178
190
|
}
|
|
179
191
|
|
|
180
192
|
reload() {
|
|
181
|
-
try {
|
|
193
|
+
try {
|
|
194
|
+
this._iframe?.contentWindow?.location.reload();
|
|
195
|
+
return;
|
|
196
|
+
} catch {}
|
|
182
197
|
// Cross-origin fallback: reassigning the same src is a no-op in WHATWG;
|
|
183
198
|
// cycle via about:blank to force a fresh navigation.
|
|
184
199
|
const iframe = this._iframe;
|
|
185
200
|
if (!iframe) return;
|
|
186
201
|
const url = this._currentUrl || iframe.src;
|
|
187
202
|
iframe.src = "about:blank";
|
|
188
|
-
requestAnimationFrame(() => {
|
|
203
|
+
requestAnimationFrame(() => {
|
|
204
|
+
if (this._iframe) this._iframe.src = url;
|
|
205
|
+
});
|
|
189
206
|
}
|
|
190
207
|
|
|
191
208
|
setHidden(hidden: boolean) {
|
|
@@ -200,7 +217,11 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
200
217
|
// synthesised DOM events is always false → `nativeInputTrusted` stays false.
|
|
201
218
|
async evaluate(script: string) {
|
|
202
219
|
if (!this.isReachable()) {
|
|
203
|
-
return {
|
|
220
|
+
return {
|
|
221
|
+
ok: false as const,
|
|
222
|
+
code: "cross_origin" as const,
|
|
223
|
+
message: "iframe content not same-origin",
|
|
224
|
+
};
|
|
204
225
|
}
|
|
205
226
|
try {
|
|
206
227
|
const win = this._iframe!.contentWindow as Window & { eval(s: string): unknown };
|
|
@@ -208,33 +229,55 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
208
229
|
} catch (e: unknown) {
|
|
209
230
|
const err = e as { name?: string; message?: string };
|
|
210
231
|
if (err?.name === "SecurityError") {
|
|
211
|
-
return {
|
|
232
|
+
return {
|
|
233
|
+
ok: false as const,
|
|
234
|
+
code: "cross_origin" as const,
|
|
235
|
+
message: err.message ?? "SecurityError",
|
|
236
|
+
};
|
|
212
237
|
}
|
|
213
|
-
return {
|
|
238
|
+
return {
|
|
239
|
+
ok: false as const,
|
|
240
|
+
code: "runtime_error" as const,
|
|
241
|
+
message: err?.message ?? String(e),
|
|
242
|
+
};
|
|
214
243
|
}
|
|
215
244
|
}
|
|
216
245
|
|
|
217
246
|
async capabilities() {
|
|
218
247
|
const reachable = this.isReachable();
|
|
219
248
|
return {
|
|
220
|
-
evaluate: reachable,
|
|
249
|
+
evaluate: reachable,
|
|
250
|
+
crossOriginEval: false,
|
|
251
|
+
surfaceEvents: true,
|
|
221
252
|
nativeInputTrusted: false,
|
|
222
|
-
click: reachable,
|
|
223
|
-
|
|
253
|
+
click: reachable,
|
|
254
|
+
type: reachable,
|
|
255
|
+
press: reachable,
|
|
256
|
+
scroll: reachable,
|
|
257
|
+
mouse: reachable,
|
|
258
|
+
dialogs: false,
|
|
259
|
+
console: false,
|
|
224
260
|
screenshot: false,
|
|
225
261
|
};
|
|
226
262
|
}
|
|
227
263
|
|
|
228
264
|
async sendClick(args: {
|
|
229
|
-
x: number;
|
|
265
|
+
x: number;
|
|
266
|
+
y: number;
|
|
267
|
+
button?: string;
|
|
268
|
+
clickCount?: number;
|
|
269
|
+
modifiers?: string[];
|
|
230
270
|
}) {
|
|
231
271
|
if (!this.isReachable()) return;
|
|
232
272
|
const doc = this._iframe!.contentDocument!;
|
|
233
273
|
const target = doc.elementFromPoint(args.x, args.y) ?? doc.body;
|
|
234
274
|
if (!target) return;
|
|
235
275
|
const init: MouseEventInit = {
|
|
236
|
-
bubbles: true,
|
|
237
|
-
|
|
276
|
+
bubbles: true,
|
|
277
|
+
cancelable: true,
|
|
278
|
+
view: this._iframe!.contentWindow,
|
|
279
|
+
clientX: args.x,
|
|
280
|
+
clientY: args.y,
|
|
238
281
|
button: args.button === "right" ? 2 : args.button === "middle" ? 1 : 0,
|
|
239
282
|
detail: args.clickCount ?? 1,
|
|
240
283
|
...this.modifierBag(args.modifiers),
|
|
@@ -245,31 +288,63 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
245
288
|
}
|
|
246
289
|
|
|
247
290
|
async resolveAndClick(_selector: string, _opts?: unknown) {
|
|
248
|
-
return {
|
|
291
|
+
return {
|
|
292
|
+
ok: false as const,
|
|
293
|
+
code: "not_supported" as const,
|
|
294
|
+
message: "polyfill iframe: atomic resolveAndClick not supported",
|
|
295
|
+
};
|
|
249
296
|
}
|
|
250
297
|
|
|
251
298
|
async getBoundingRect(selector: string, _opts?: unknown) {
|
|
252
|
-
if (!this.isReachable())
|
|
299
|
+
if (!this.isReachable())
|
|
300
|
+
return {
|
|
301
|
+
ok: false as const,
|
|
302
|
+
code: "not_supported" as const,
|
|
303
|
+
message: "iframe not reachable",
|
|
304
|
+
};
|
|
253
305
|
try {
|
|
254
306
|
const el = this._iframe!.contentDocument!.querySelector(selector) as Element | null;
|
|
255
|
-
if (!el)
|
|
307
|
+
if (!el)
|
|
308
|
+
return {
|
|
309
|
+
ok: false as const,
|
|
310
|
+
code: "not_found" as const,
|
|
311
|
+
message: `selector ${selector} not found`,
|
|
312
|
+
};
|
|
256
313
|
const r = el.getBoundingClientRect();
|
|
257
314
|
const win = this._iframe!.contentWindow!;
|
|
258
|
-
const visible =
|
|
259
|
-
|
|
260
|
-
|
|
315
|
+
const visible =
|
|
316
|
+
r.width > 0 &&
|
|
317
|
+
r.height > 0 &&
|
|
318
|
+
r.bottom > 0 &&
|
|
319
|
+
r.right > 0 &&
|
|
320
|
+
r.top < win.innerHeight &&
|
|
321
|
+
r.left < win.innerWidth;
|
|
322
|
+
return {
|
|
323
|
+
ok: true as const,
|
|
324
|
+
rect: { x: r.x, y: r.y, width: r.width, height: r.height },
|
|
325
|
+
visible,
|
|
326
|
+
};
|
|
261
327
|
} catch (e: any) {
|
|
262
|
-
const code =
|
|
328
|
+
const code =
|
|
329
|
+
e?.name === "SecurityError" ? ("cross_origin" as const) : ("runtime_error" as const);
|
|
263
330
|
return { ok: false as const, code, message: e?.message ?? String(e) };
|
|
264
331
|
}
|
|
265
332
|
}
|
|
266
333
|
|
|
267
334
|
async listFrames() {
|
|
268
|
-
return {
|
|
335
|
+
return {
|
|
336
|
+
ok: false as const,
|
|
337
|
+
code: "not_supported" as const,
|
|
338
|
+
message: "polyfill: not implemented",
|
|
339
|
+
};
|
|
269
340
|
}
|
|
270
341
|
|
|
271
342
|
async accessibilitySnapshot(_opts?: unknown) {
|
|
272
|
-
return {
|
|
343
|
+
return {
|
|
344
|
+
ok: false as const,
|
|
345
|
+
code: "not_supported" as const,
|
|
346
|
+
message: "polyfill: not implemented",
|
|
347
|
+
};
|
|
273
348
|
}
|
|
274
349
|
|
|
275
350
|
async setDownloadPolicy(_policy: unknown, _dir?: unknown) {
|
|
@@ -277,11 +352,19 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
277
352
|
}
|
|
278
353
|
|
|
279
354
|
async waitForDownload(_opts?: unknown) {
|
|
280
|
-
return {
|
|
355
|
+
return {
|
|
356
|
+
ok: false as const,
|
|
357
|
+
code: "not_supported" as const,
|
|
358
|
+
message: "polyfill: not implemented",
|
|
359
|
+
};
|
|
281
360
|
}
|
|
282
361
|
|
|
283
362
|
async acceptPopup(_opts?: unknown) {
|
|
284
|
-
return {
|
|
363
|
+
return {
|
|
364
|
+
ok: false as const,
|
|
365
|
+
code: "not_found" as const,
|
|
366
|
+
message: "polyfill: no popup orchestration",
|
|
367
|
+
};
|
|
285
368
|
}
|
|
286
369
|
|
|
287
370
|
async dismissPopup(_id?: unknown) {
|
|
@@ -289,20 +372,26 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
289
372
|
}
|
|
290
373
|
|
|
291
374
|
async extendAdoptionTimeout(_id?: unknown, _ms?: unknown) {
|
|
292
|
-
return {
|
|
375
|
+
return {
|
|
376
|
+
ok: false as const,
|
|
377
|
+
code: "not_found" as const,
|
|
378
|
+
message: "polyfill: no popup orchestration",
|
|
379
|
+
};
|
|
293
380
|
}
|
|
294
381
|
|
|
295
382
|
async sendType(text: string) {
|
|
296
383
|
if (!this.isReachable()) return;
|
|
297
384
|
const doc = this._iframe!.contentDocument!;
|
|
298
|
-
const target = doc.activeElement as
|
|
385
|
+
const target = doc.activeElement as HTMLInputElement | HTMLTextAreaElement | null;
|
|
299
386
|
if (!target || !("setRangeText" in target)) return;
|
|
300
387
|
// setRangeText preserves selection + caret; the `data` field on an
|
|
301
388
|
// InputEvent + bubbling lets React-style controllers detect the change.
|
|
302
389
|
const start = target.selectionStart ?? target.value.length;
|
|
303
390
|
const end = target.selectionEnd ?? target.value.length;
|
|
304
391
|
target.setRangeText(text, start, end, "end");
|
|
305
|
-
target.dispatchEvent(
|
|
392
|
+
target.dispatchEvent(
|
|
393
|
+
new InputEvent("input", { bubbles: true, data: text, inputType: "insertText" }),
|
|
394
|
+
);
|
|
306
395
|
}
|
|
307
396
|
|
|
308
397
|
async sendPress(key: string, modifiers?: string[], action?: "down" | "up" | "both") {
|
|
@@ -311,7 +400,10 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
311
400
|
const target = (doc.activeElement ?? doc.body) as Element | null;
|
|
312
401
|
if (!target) return;
|
|
313
402
|
const init: KeyboardEventInit = {
|
|
314
|
-
bubbles: true,
|
|
403
|
+
bubbles: true,
|
|
404
|
+
cancelable: true,
|
|
405
|
+
key,
|
|
406
|
+
...this.modifierBag(modifiers),
|
|
315
407
|
};
|
|
316
408
|
const a = action ?? "both";
|
|
317
409
|
if (a !== "up") target.dispatchEvent(new KeyboardEvent("keydown", init));
|
|
@@ -319,14 +411,21 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
319
411
|
if (a !== "down") target.dispatchEvent(new KeyboardEvent("keyup", init));
|
|
320
412
|
}
|
|
321
413
|
|
|
322
|
-
async sendScroll(args: {
|
|
414
|
+
async sendScroll(args: {
|
|
415
|
+
dx: number;
|
|
416
|
+
dy: number;
|
|
417
|
+
x?: number;
|
|
418
|
+
y?: number;
|
|
419
|
+
modifiers?: string[];
|
|
420
|
+
}) {
|
|
323
421
|
if (!this.isReachable()) return;
|
|
324
422
|
this._iframe!.contentWindow!.scrollBy(args.dx, args.dy);
|
|
325
423
|
}
|
|
326
424
|
|
|
327
425
|
async sendMouse(args: {
|
|
328
426
|
action: "move" | "down" | "up";
|
|
329
|
-
x: number;
|
|
427
|
+
x: number;
|
|
428
|
+
y: number;
|
|
330
429
|
button?: string;
|
|
331
430
|
modifiers?: string[];
|
|
332
431
|
}) {
|
|
@@ -334,10 +433,14 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
334
433
|
const doc = this._iframe!.contentDocument!;
|
|
335
434
|
const target = doc.elementFromPoint(args.x, args.y) ?? doc.body;
|
|
336
435
|
if (!target) return;
|
|
337
|
-
const type =
|
|
436
|
+
const type =
|
|
437
|
+
args.action === "move" ? "mousemove" : args.action === "down" ? "mousedown" : "mouseup";
|
|
338
438
|
const init: MouseEventInit = {
|
|
339
|
-
bubbles: true,
|
|
340
|
-
|
|
439
|
+
bubbles: true,
|
|
440
|
+
cancelable: true,
|
|
441
|
+
view: this._iframe!.contentWindow,
|
|
442
|
+
clientX: args.x,
|
|
443
|
+
clientY: args.y,
|
|
341
444
|
button: args.button === "right" ? 2 : args.button === "middle" ? 1 : 0,
|
|
342
445
|
...this.modifierBag(args.modifiers),
|
|
343
446
|
};
|
|
@@ -348,11 +451,17 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
348
451
|
// iframe sandbox forbids cross-frame dialog interception; nothing to do.
|
|
349
452
|
}
|
|
350
453
|
|
|
351
|
-
async setDialogTimeout(_ms: number | null) {
|
|
454
|
+
async setDialogTimeout(_ms: number | null) {
|
|
455
|
+
/* no-op */
|
|
456
|
+
}
|
|
352
457
|
|
|
353
458
|
async waitForSelector(selector: string, timeoutMs = 5000) {
|
|
354
459
|
if (!this.isReachable()) {
|
|
355
|
-
return {
|
|
460
|
+
return {
|
|
461
|
+
ok: false as const,
|
|
462
|
+
code: "runtime_error" as const,
|
|
463
|
+
message: "iframe content not same-origin",
|
|
464
|
+
};
|
|
356
465
|
}
|
|
357
466
|
const doc = this._iframe!.contentDocument!;
|
|
358
467
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -360,12 +469,23 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
360
469
|
if (doc.querySelector(selector)) return { ok: true as const };
|
|
361
470
|
await new Promise((r) => setTimeout(r, 50));
|
|
362
471
|
}
|
|
363
|
-
return {
|
|
472
|
+
return {
|
|
473
|
+
ok: false as const,
|
|
474
|
+
code: "timeout" as const,
|
|
475
|
+
message: `selector ${JSON.stringify(selector)} not found within ${timeoutMs}ms`,
|
|
476
|
+
};
|
|
364
477
|
}
|
|
365
478
|
|
|
366
|
-
async waitForFunction(
|
|
479
|
+
async waitForFunction(
|
|
480
|
+
expression: string,
|
|
481
|
+
opts?: { timeoutMs?: number; pollIntervalMs?: number },
|
|
482
|
+
) {
|
|
367
483
|
if (!this.isReachable()) {
|
|
368
|
-
return {
|
|
484
|
+
return {
|
|
485
|
+
ok: false as const,
|
|
486
|
+
code: "runtime_error" as const,
|
|
487
|
+
message: "iframe content not same-origin",
|
|
488
|
+
};
|
|
369
489
|
}
|
|
370
490
|
const win = this._iframe!.contentWindow as Window & { eval(s: string): unknown };
|
|
371
491
|
const deadline = Date.now() + (opts?.timeoutMs ?? 5000);
|
|
@@ -374,11 +494,19 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
374
494
|
try {
|
|
375
495
|
if (win.eval(expression)) return { ok: true as const };
|
|
376
496
|
} catch (e) {
|
|
377
|
-
return {
|
|
497
|
+
return {
|
|
498
|
+
ok: false as const,
|
|
499
|
+
code: "runtime_error" as const,
|
|
500
|
+
message: (e as Error).message,
|
|
501
|
+
};
|
|
378
502
|
}
|
|
379
503
|
await new Promise((r) => setTimeout(r, interval));
|
|
380
504
|
}
|
|
381
|
-
return {
|
|
505
|
+
return {
|
|
506
|
+
ok: false as const,
|
|
507
|
+
code: "timeout" as const,
|
|
508
|
+
message: `function did not satisfy within ${opts?.timeoutMs ?? 5000}ms`,
|
|
509
|
+
};
|
|
382
510
|
}
|
|
383
511
|
|
|
384
512
|
async getConsoleBuffer(_opts?: { clear?: boolean }) {
|
|
@@ -387,11 +515,19 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
387
515
|
}
|
|
388
516
|
|
|
389
517
|
async getNavigationState() {
|
|
390
|
-
return {
|
|
518
|
+
return {
|
|
519
|
+
lastLoadEpoch: this._epoch,
|
|
520
|
+
isLoading: this._isLoading,
|
|
521
|
+
currentUrl: this._currentUrl,
|
|
522
|
+
};
|
|
391
523
|
}
|
|
392
524
|
|
|
393
525
|
async screenshot(_args?: { format?: "png" | "jpeg"; quality?: number }) {
|
|
394
|
-
return {
|
|
526
|
+
return {
|
|
527
|
+
ok: false as const,
|
|
528
|
+
code: "not_supported" as const,
|
|
529
|
+
message: "iframe polyfill does not support screenshot",
|
|
530
|
+
};
|
|
395
531
|
}
|
|
396
532
|
}
|
|
397
533
|
|