buninu-browser 0.2.3 → 0.2.4

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 (105) hide show
  1. package/lib/cdp-server.js +47 -14
  2. package/lib/happy-dom/document-base.js +33 -0
  3. package/lib/happy-dom/parser.js +15 -6
  4. package/lib/happy-dom/selector-matcher.js +2 -2
  5. package/lib/happy-dom/selector-pseudo-adapter.js +19 -0
  6. package/lib/happy-dom/svg-attribute-adapter.js +50 -0
  7. package/lib/headless-shell.js +9 -6
  8. package/lib/input/hit-test.js +67 -19
  9. package/lib/layout/text-layout.js +213 -91
  10. package/lib/media/byte-pipe.js +2 -2
  11. package/lib/media/position-audio.js +10 -1
  12. package/lib/media/source-snapshots.js +126 -0
  13. package/lib/media/video-keyframe.js +17 -4
  14. package/lib/media/webm-stream.js +106 -0
  15. package/lib/network/network-service.js +268 -120
  16. package/lib/paint/display-list.js +8 -28
  17. package/lib/paint/frame-cache.js +25 -0
  18. package/lib/paint/frame.js +42 -13
  19. package/lib/paint/paragraph-text.js +24 -0
  20. package/lib/paint/raster-readback.js +41 -0
  21. package/lib/paint/rounded-rect.js +82 -0
  22. package/lib/paint/shared-frame-pool.js +105 -0
  23. package/lib/render-tree/index.js +38 -6
  24. package/lib/render-tree/svg.js +15 -7
  25. package/lib/renderer/bindings.js +233 -80
  26. package/lib/renderer/broadcast-channel.js +76 -0
  27. package/lib/renderer/canvas-2d.js +257 -14
  28. package/lib/renderer/canvas-text.js +120 -0
  29. package/lib/renderer/compositor.js +13 -11
  30. package/lib/renderer/custom-element-reactions.js +25 -1
  31. package/lib/renderer/font-loading.js +227 -0
  32. package/lib/renderer/host.js +28 -4
  33. package/lib/renderer/image-resources.js +65 -0
  34. package/lib/renderer/interfaces.js +28 -0
  35. package/lib/renderer/layout-observers.js +236 -0
  36. package/lib/renderer/main.js +30 -4
  37. package/lib/renderer/media-source.js +126 -55
  38. package/lib/renderer/page-realm.js +270 -98
  39. package/lib/renderer/page-renderer.js +647 -235
  40. package/lib/renderer/prepared-script-cache.js +50 -0
  41. package/lib/renderer/rendered-text.js +108 -0
  42. package/lib/renderer/rewrite-worker.js +4 -3
  43. package/lib/renderer/script-rewrite.js +13 -2
  44. package/lib/renderer/scroll-into-view.js +28 -0
  45. package/lib/renderer/slot-assignment.js +60 -0
  46. package/lib/renderer/video-presentation.js +23 -4
  47. package/lib/renderer/workers.js +2 -2
  48. package/lib/style/computed-style.js +611 -104
  49. package/lib/style/selector-traits.js +49 -5
  50. package/package.json +1 -1
  51. package/test/broadcast-channel.test.js +26 -0
  52. package/test/canvas-2d.test.js +83 -2
  53. package/test/canvas-paint.test.js +9 -0
  54. package/test/canvas-text.test.js +41 -0
  55. package/test/cdp-stream.test.js +56 -0
  56. package/test/computed-style-binding.test.js +151 -0
  57. package/test/computed-style.test.js +258 -1
  58. package/test/display-list.test.js +12 -0
  59. package/test/document-base.test.js +87 -0
  60. package/test/dom-platform.test.js +461 -0
  61. package/test/fetch-lifecycle.test.js +161 -0
  62. package/test/fixtures/canvas-paint.js +117 -0
  63. package/test/fixtures/canvas-text.js +42 -0
  64. package/test/fixtures/image-lifetime.js +90 -0
  65. package/test/fixtures/media-seek-snapshot.js +77 -0
  66. package/test/fixtures/mse-audio-chirp.webm +0 -0
  67. package/test/fixtures/mse-switch-large.mp4 +0 -0
  68. package/test/fixtures/mse-switch-small.mp4 +0 -0
  69. package/test/fixtures/renderer-memory.js +110 -0
  70. package/test/fixtures/summary-style.js +28 -0
  71. package/test/font-loading.test.js +97 -0
  72. package/test/grid.test.js +58 -0
  73. package/test/hit-test.test.js +21 -1
  74. package/test/image-lifetime.test.js +12 -0
  75. package/test/image-resources.test.js +94 -0
  76. package/test/local-media.test.js +84 -0
  77. package/test/media-seek-snapshot.test.js +11 -0
  78. package/test/media-source-config.test.js +98 -0
  79. package/test/media-source-webm-accounting.test.js +98 -0
  80. package/test/media-source-webm.test.js +108 -0
  81. package/test/navigation.test.js +1 -0
  82. package/test/network-service.test.js +388 -0
  83. package/test/page-scripts.test.js +1 -1
  84. package/test/paint-frame.test.js +103 -0
  85. package/test/paint-paragraph-cache.test.js +87 -0
  86. package/test/position-audio.test.js +24 -0
  87. package/test/positioning.test.js +22 -0
  88. package/test/prepared-script-cache.test.js +45 -0
  89. package/test/pulse-client.test.js +10 -3
  90. package/test/raster-readback.test.js +40 -0
  91. package/test/render-tree.test.js +28 -0
  92. package/test/renderer-memory.test.js +11 -0
  93. package/test/selector-traits.test.js +24 -0
  94. package/test/shared-frame-pool.test.js +63 -0
  95. package/test/style-sheets.test.js +51 -0
  96. package/test/svg.test.js +29 -0
  97. package/test/text-layout.test.js +106 -0
  98. package/test/video-keyframe.test.js +61 -0
  99. package/test/video-presentation.test.js +69 -0
  100. package/tools/browser-lab.js +78 -0
  101. package/tools/browser-soak.js +75 -0
  102. package/tools/network-reference.js +49 -0
  103. package/tools/process-memory.js +38 -0
  104. package/tools/script-benchmark.js +25 -0
  105. package/ai-debug.log +0 -5537
@@ -20,16 +20,20 @@ import {
20
20
  DIRECT_EVAL_SCOPE_HELPER, REFERENCE_HELPER, referenceHelper, restoreSourceText, THIS_HELPER,
21
21
  rewriteClassicScriptSource, rewriteConstructorReads, rewriteModuleSource,
22
22
  } from "./script-rewrite.js";
23
+ import { prepareClassicScript } from "./prepared-script-cache.js";
23
24
  import { mediaQueryMatches, resolveVariables } from "../style/computed-style.js";
24
25
  import { asIncumbent, createMessaging } from "./messaging.js";
25
- import { canvasContext, resetCanvas } from "./canvas-2d.js";
26
+ import { canvasContext, resetCanvas, disposeDocumentCanvases, encodeCanvas } from "./canvas-2d.js";
26
27
  import { createWorkers } from "./workers.js";
27
28
  import { createMediaSourceScope } from "./media-source.js";
28
29
  import { markNative, markNativeApi, setFunctionSourceTextTransform } from "./native-functions.js";
29
- import { applyGlobalShape, platformObject, storageInterface } from "./interfaces.js";
30
+ import { applyGlobalShape, errorInterface, platformObject, storageInterface } from "./interfaces.js";
30
31
  import { trustedTypesApi } from "./trusted-types.js";
31
32
  import { onPromiseRejection } from "./rejections.js";
32
33
  import { installAttributeReactions } from "./custom-element-reactions.js";
34
+ import { createLayoutObservers } from "./layout-observers.js";
35
+ import { createBroadcastChannels } from "./broadcast-channel.js";
36
+ import { createInertWindow } from "../happy-dom/parser.js";
33
37
  import { historyBinding } from "./history.js";
34
38
  import { installImageLoadingAttributes } from "./image-loading.js";
35
39
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -64,6 +68,9 @@ const hostCrypto = globalThis.crypto;
64
68
  const hostStructuredClone = globalThis.structuredClone;
65
69
  const hostAtob = globalThis.atob;
66
70
  const hostBtoa = globalThis.btoa;
71
+ const HostHeaders = globalThis.Headers;
72
+ const HostRequest = globalThis.Request;
73
+ const HostResponse = globalThis.Response;
67
74
 
68
75
  export const CLASSIC_SCRIPT_TYPES = new Set([
69
76
  "", "text/javascript", "application/javascript", "text/ecmascript", "application/ecmascript",
@@ -153,7 +160,11 @@ const PLAIN_HTML_ELEMENTS = new Set(["ABBR", "ADDRESS", "ARTICLE", "ASIDE", "B",
153
160
  */
154
161
  const FORM_CONTROLS = ["HTMLInputElement", "HTMLSelectElement", "HTMLTextAreaElement", "HTMLButtonElement"];
155
162
  const URL_ELEMENTS = ["HTMLAnchorElement", "HTMLAreaElement"];
163
+ const VALIDATED_ELEMENTS = [...FORM_CONTROLS, "HTMLFieldSetElement", "HTMLObjectElement", "HTMLOutputElement"];
156
164
  const INTERFACE_MEMBERS = {
165
+ assignedNodes: ["HTMLSlotElement"], assignedElements: ["HTMLSlotElement"], assign: ["HTMLSlotElement"],
166
+ validity: VALIDATED_ELEMENTS, setCustomValidity: VALIDATED_ELEMENTS,
167
+ checkValidity: [...VALIDATED_ELEMENTS, "HTMLFormElement"], reportValidity: [...VALIDATED_ELEMENTS, "HTMLFormElement"],
157
168
  contentWindow: ["HTMLIFrameElement", "HTMLObjectElement", "HTMLFrameElement"],
158
169
  contentDocument: ["HTMLIFrameElement", "HTMLObjectElement", "HTMLFrameElement"],
159
170
  content: ["HTMLTemplateElement"],
@@ -202,6 +213,9 @@ const WINDOW_HANDLER_EVENTS = [
202
213
  "popstate", "beforeunload", "unload", "focus", "blur", "online", "offline", "unhandledrejection", "rejectionhandled",
203
214
  ];
204
215
  const hostStorage = new Map();
216
+ // SES's older intrinsic allow-list removes this standard method during
217
+ // lockdown; retain Bun's native brand check without replacing tamed errors.
218
+ const hostErrorIsError = Error.isError;
205
219
 
206
220
  function cssEscape(value) {
207
221
  const input = String(value);
@@ -265,6 +279,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
265
279
  const consoleEntries = [];
266
280
  let nextTimerId = 1;
267
281
  let disposed = false;
282
+ const pendingPageFetches = new Set();
268
283
  let readyState = "loading";
269
284
  // A document gets a fresh high-resolution time origin. Bun's
270
285
  // performance.now() is relative to the renderer process, which is commonly
@@ -295,8 +310,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
295
310
  let inlineModuleCount = 0;
296
311
 
297
312
  const reportError = (error, source = currentScript?.src) => {
298
- // SES "safe" error taming blanks .stack, so fall back to name and message.
299
- const detail = error?.stack || (error?.name ? `${error.name}: ${error.message}` : String(error?.message ?? error));
313
+ const detail = diagnosticError(error);
300
314
  const message = source ? `${source}: ${detail}` : detail;
301
315
  consoleEntries.push({ level: "error", text: message });
302
316
  hooks.reportError(message);
@@ -332,7 +346,9 @@ export function createPageRealm({ window, document, mode, hooks }) {
332
346
  };
333
347
 
334
348
  const realm = new BindingRealm(window, {
335
- touch: hooks.touch,
349
+ touch: (node, features) => {
350
+ if (!node || node === document || node.ownerDocument === document) hooks.touch(node, features);
351
+ },
336
352
  boundsOf: hooks.boundsOf,
337
353
  computedStyle: hooks.computedStyle,
338
354
  // The engine clock's reading when this document's timeline started (see pageNow).
@@ -376,6 +392,14 @@ export function createPageRealm({ window, document, mode, hooks }) {
376
392
  (value) => value === pageGlobal || Boolean(nodeOf(value)),
377
393
  (value) => value === pageGlobal ? window : nodeOf(value));
378
394
  const documentBinding = realm.wrap(document);
395
+ const DOMStringMap = function DOMStringMap() { throw new TypeError("Illegal constructor"); };
396
+ realm.datasetPrototype = DOMStringMap.prototype;
397
+ const validityMembers = {};
398
+ for (const key of ["badInput", "customError", "patternMismatch", "rangeOverflow", "rangeUnderflow", "stepMismatch", "tooLong", "tooShort", "typeMismatch", "valid", "valueMissing"]) {
399
+ Object.defineProperty(validityMembers, key, { get() { return Boolean(this[key]); }, enumerable: true });
400
+ }
401
+ const validity = platformObject("ValidityState", validityMembers);
402
+ realm.wrapValidity = validity.create;
379
403
  const NodeInterface = function Node() { throw new TypeError("Illegal constructor"); };
380
404
  // Node.prototype inherits EventTarget.prototype (DOM §4.4), as Window.prototype does.
381
405
  NodeInterface.prototype = mutableBindingPrototype(INTERFACES.Node.prototype, eventTargets.EventTarget.prototype);
@@ -557,7 +581,27 @@ export function createPageRealm({ window, document, mode, hooks }) {
557
581
  enumerable: true,
558
582
  writable: true,
559
583
  value(type) {
560
- return canvasContext(nodeOf(this), String(type));
584
+ return canvasContext(nodeOf(this), String(type), {
585
+ canvas: this, resolveImage: source => hooks.canvasImage?.(nodeOf(source)),
586
+ parseFont: value => hooks.canvasFont?.(nodeOf(this), value),
587
+ text: request => hooks.canvasText?.(nodeOf(this), request),
588
+ });
589
+ },
590
+ });
591
+ Object.defineProperty(canvasPrototype, "toDataURL", {
592
+ configurable: true, enumerable: true, writable: true,
593
+ value(type, quality) {
594
+ const encoded = encodeCanvas(nodeOf(this), type, quality);
595
+ return encoded ? `data:${encoded.type};base64,${encoded.bytes.toBase64()}` : "data:,";
596
+ },
597
+ });
598
+ Object.defineProperty(canvasPrototype, "toBlob", {
599
+ configurable: true, enumerable: true, writable: true,
600
+ value(callback, type, quality) {
601
+ if (typeof callback !== "function") throw new TypeError("Canvas toBlob requires a callback");
602
+ const encoded = encodeCanvas(nodeOf(this), type, quality);
603
+ const blob = encoded ? new Blob([encoded.bytes], { type: encoded.type }) : null;
604
+ hostSetTimeout(() => { if (!disposed) runTask(() => callPage(callback, pageGlobal, [blob])); }, 0);
561
605
  },
562
606
  });
563
607
  // HTMLMediaElement. HAVE_* / NETWORK_* / MEDIA_ERR_* live on the prototype
@@ -634,6 +678,33 @@ export function createPageRealm({ window, document, mode, hooks }) {
634
678
  DocumentInterface.prototype = withParentNodeMembers(mutableBindingPrototype(INTERFACES.Document.prototype, parentPrototype));
635
679
  Object.defineProperty(DocumentInterface.prototype, "constructor", { value: DocumentInterface, writable: true, configurable: true });
636
680
  Object.setPrototypeOf(documentBinding, DocumentInterface.prototype);
681
+ realm.documentPrototype = DocumentInterface.prototype;
682
+ let parserWindow = null;
683
+ const parsers = new WeakSet();
684
+ class DOMParser {
685
+ constructor() { parsers.add(this); }
686
+ parseFromString(source, type) {
687
+ if (!parsers.has(this)) throw new TypeError("Illegal invocation");
688
+ const mime = String(type);
689
+ if (!["text/html", "text/xml", "application/xml", "application/xhtml+xml", "image/svg+xml"].includes(mime)) {
690
+ throw new TypeError("Invalid DOMParser MIME type.");
691
+ }
692
+ parserWindow ??= createInertWindow(document.URL);
693
+ const parsed = new parserWindow.DOMParser().parseFromString(String(source), mime);
694
+ Object.defineProperty(parsed, "contentType", { value: mime, configurable: true });
695
+ return realm.wrap(parsed);
696
+ }
697
+ }
698
+ const serializers = new WeakSet();
699
+ class XMLSerializer {
700
+ constructor() { serializers.add(this); }
701
+ serializeToString(root) {
702
+ if (!serializers.has(this)) throw new TypeError("Illegal invocation");
703
+ const node = nodeOf(root);
704
+ if (!node) throw new TypeError("XMLSerializer requires a Node.");
705
+ return new window.XMLSerializer().serializeToString(node);
706
+ }
707
+ }
637
708
 
638
709
  /** In an iframe's document, parent and top are the embedding frames' windows. */
639
710
  const defineFrameWindows = (global) => {
@@ -718,12 +789,12 @@ export function createPageRealm({ window, document, mode, hooks }) {
718
789
  };
719
790
  Object.defineProperty(pageConsole, Symbol.toStringTag, { value: "console", configurable: true });
720
791
 
721
- /**
722
- * MutationObserver wraps Happy DOM's implementation (records carry node
723
- * wrappers); Resize- and IntersectionObserver report the current layout
724
- * once per observed element, which is enough for common lazy-loading and
725
- * sizing code in a headless viewport.
726
- */
792
+ const layoutObservers = createLayoutObservers({
793
+ nodeOf, boundsOf: hooks.boundsOf, computedStyle: hooks.computedStyle, viewport: hooks.viewport,
794
+ schedule: (callback) => schedule(callback, 0, [], false), cancel, call: callPage, now: pageNow,
795
+ });
796
+
797
+ /** MutationObserver wraps Happy DOM records; layout observers use renderer geometry. */
727
798
  function observerConstructors() {
728
799
  function MutationObserver(callback) {
729
800
  if (!new.target) throw new TypeError("Constructor MutationObserver requires 'new'");
@@ -759,48 +830,8 @@ export function createPageRealm({ window, document, mode, hooks }) {
759
830
  return self;
760
831
  }
761
832
 
762
- const layoutObserver = (name, entryFor) => function Observer(callback) {
763
- if (!new.target) throw new TypeError(`Constructor ${name} requires 'new'`);
764
- const self = {};
765
- const observed = new Set();
766
- self.observe = (target) => {
767
- const node = nodeOf(target);
768
- if (!node || observed.has(node)) return;
769
- observed.add(node);
770
- schedule(() => {
771
- if (!observed.has(node)) return;
772
- const rect = hooks.boundsOf(node) ?? { left: 0, top: 0, width: 0, height: 0 };
773
- callPage(callback, self, [[entryFor(target, rect)], self]);
774
- }, 0, [], false);
775
- };
776
- self.unobserve = (target) => observed.delete(nodeOf(target));
777
- self.disconnect = () => observed.clear();
778
- self.takeRecords = () => [];
779
- return self;
780
- };
781
- const rectangle = ({ left, top, width, height }) => ({
782
- x: left, y: top, left, top, width, height, right: left + width, bottom: top + height,
783
- });
784
- const ResizeObserver = layoutObserver("ResizeObserver", (target, rect) => ({
785
- target,
786
- contentRect: rectangle({ ...rect, left: 0, top: 0 }),
787
- borderBoxSize: [{ inlineSize: rect.width, blockSize: rect.height }],
788
- contentBoxSize: [{ inlineSize: rect.width, blockSize: rect.height }],
789
- }));
790
- const IntersectionObserver = layoutObserver("IntersectionObserver", (target, rect) => {
791
- const { width, height } = hooks.viewport();
792
- const visible = rect.top < height && rect.top + rect.height > 0 && rect.left < width && rect.left + rect.width > 0;
793
- return {
794
- target,
795
- time: pageNow(),
796
- isIntersecting: visible,
797
- intersectionRatio: visible ? 1 : 0,
798
- boundingClientRect: rectangle(rect),
799
- intersectionRect: rectangle(visible ? rect : { left: 0, top: 0, width: 0, height: 0 }),
800
- rootBounds: rectangle({ left: 0, top: 0, width, height }),
801
- };
802
- });
803
- return { MutationObserver, ResizeObserver, IntersectionObserver };
833
+ return { MutationObserver, ResizeObserver: layoutObservers.ResizeObserver,
834
+ IntersectionObserver: layoutObservers.IntersectionObserver };
804
835
  }
805
836
 
806
837
  /**
@@ -920,33 +951,62 @@ export function createPageRealm({ window, document, mode, hooks }) {
920
951
  }
921
952
 
922
953
  function networkApi() {
954
+ // Instrumentation libraries wrap Headers.prototype.set/append. Keep
955
+ // those overrides local to this document: the renderer's own Headers
956
+ // intrinsic remains frozen and is never modified by a page.
957
+ const PageHeaders = class Headers extends HostHeaders {};
958
+ for (const key of Reflect.ownKeys(HostHeaders.prototype)) {
959
+ if (key === "constructor") continue;
960
+ const descriptor = Object.getOwnPropertyDescriptor(HostHeaders.prototype, key);
961
+ Object.defineProperty(PageHeaders.prototype, key, {
962
+ ...descriptor, configurable: true, ...("value" in descriptor ? { writable: true } : {}),
963
+ });
964
+ }
965
+ const exposeHeaders = (headers) => Object.getPrototypeOf(headers) === PageHeaders.prototype ? headers
966
+ : Object.setPrototypeOf(headers, PageHeaders.prototype);
967
+ const PageResponse = class Response extends HostResponse {
968
+ get headers() { return exposeHeaders(responseHeaders.call(this)); }
969
+ clone() { return Object.setPrototypeOf(HostResponse.prototype.clone.call(this), PageResponse.prototype); }
970
+ static error() { return Object.setPrototypeOf(HostResponse.error(), PageResponse.prototype); }
971
+ static redirect(url, status = 302) { return Object.setPrototypeOf(HostResponse.redirect(url, status), PageResponse.prototype); }
972
+ static json(data, init = undefined) { return Object.setPrototypeOf(HostResponse.json(data, init), PageResponse.prototype); }
973
+ };
974
+ const responseHeaders = Object.getOwnPropertyDescriptor(HostResponse.prototype, "headers").get;
923
975
  const abortError = () => new DOMException("The operation was aborted.", "AbortError");
924
976
  const baseUrl = () => document.baseURI || document.URL;
925
977
 
926
978
  // new Request("/api") resolves against the document's base URL (Fetch
927
979
  // §5.4), which the host's Request has no document to do.
928
- const PageRequest = class Request extends globalThis.Request {
980
+ const PageRequest = class Request extends HostRequest {
929
981
  constructor(input, init = undefined) {
930
- super(input instanceof globalThis.Request ? input : new URL(String(input), baseUrl()).href, init);
982
+ super(input instanceof HostRequest ? input : new URL(String(input), baseUrl()).href, init);
931
983
  // Fetch §5.4 step 36: a string body gives the request its Content-Type
932
984
  // (Bun's Request only does this for the other body kinds).
933
- if (typeof init?.body === "string" && !this.headers.has("content-type")) {
934
- this.headers.set("content-type", "text/plain;charset=UTF-8");
985
+ if (typeof init?.body === "string") {
986
+ const headers = requestHeaders.call(this);
987
+ if (!HostHeaders.prototype.has.call(headers, "content-type")) {
988
+ HostHeaders.prototype.set.call(headers, "content-type", "text/plain;charset=UTF-8");
989
+ }
935
990
  }
936
991
  }
992
+ clone() { return Object.setPrototypeOf(HostRequest.prototype.clone.call(this), PageRequest.prototype); }
937
993
  };
994
+ const requestHeaders = Object.getOwnPropertyDescriptor(HostRequest.prototype, "headers").get;
995
+ Object.defineProperty(PageRequest.prototype, "headers", {
996
+ get() { return exposeHeaders(requestHeaders.call(this)); }, enumerable: true, configurable: true,
997
+ });
938
998
 
939
999
  // fetch(request) reads the Request's internal state, as Chromium's
940
1000
  // Request::CreateRequestWithRequestOrString does with GetRequest(). Page
941
1001
  // script may shadow url, method or bodyUsed on the instance; that changes
942
1002
  // what script reads, not what is fetched.
943
1003
  const requestSlot = (name) => {
944
- const get = Object.getOwnPropertyDescriptor(globalThis.Request.prototype, name).get;
1004
+ const get = Object.getOwnPropertyDescriptor(HostRequest.prototype, name).get;
945
1005
  return (request) => get.call(request);
946
1006
  };
947
1007
  const slots = Object.fromEntries(["url", "method", "headers", "mode", "credentials", "redirect", "signal", "bodyUsed"]
948
1008
  .map((name) => [name, requestSlot(name)]));
949
- const cloneRequest = globalThis.Request.prototype.clone;
1009
+ const cloneRequest = HostRequest.prototype.clone;
950
1010
  /** The input when it is a platform Request (a brand check, not instanceof). */
951
1011
  const platformRequest = (input) => {
952
1012
  if (input === null || typeof input !== "object") return null;
@@ -992,10 +1052,13 @@ export function createPageRealm({ window, document, mode, hooks }) {
992
1052
  }
993
1053
 
994
1054
  function makeResponse(data) {
995
- const headers = new Headers(data.headers ?? []);
1055
+ const headers = new PageHeaders(data.headers ?? []);
996
1056
  const body = data.body instanceof ReadableStream ? data.body
997
1057
  : data.body instanceof Uint8Array ? data.body : new Uint8Array(0);
998
- const native = new Response(body, { headers: { "content-type": headers.get("content-type") ?? "" } });
1058
+ // Fetch reads the header list internally; page instrumentation of the
1059
+ // public Headers prototype must not affect response construction.
1060
+ const contentType = HostHeaders.prototype.get.call(headers, "content-type") ?? "";
1061
+ const native = new Response(body, { headers: { "content-type": contentType } });
999
1062
  const response = {
1000
1063
  type: data.type ?? "basic",
1001
1064
  url: data.url ?? "",
@@ -1027,7 +1090,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1027
1090
  async function pageFetch(input, init = {}) {
1028
1091
  const request = await describeRequest(input, init ?? {});
1029
1092
  const signal = request.signal;
1030
- if (signal?.aborted) throw abortError();
1093
+ if (disposed || signal?.aborted) throw signal?.reason ?? abortError();
1031
1094
  const scheme = new URL(request.url).protocol;
1032
1095
  let pending;
1033
1096
  let networkPending = null;
@@ -1050,21 +1113,38 @@ export function createPageRealm({ window, document, mode, hooks }) {
1050
1113
  } else {
1051
1114
  const { signal: _signal, ...plain } = request;
1052
1115
  networkPending = hooks.pageFetch(plain);
1116
+ pendingPageFetches.add(networkPending);
1053
1117
  // Network and CORS failures reach the page as a bare TypeError, as in
1054
1118
  // browsers; the reason is only logged to the console.
1055
1119
  pending = networkPending.catch((error) => {
1056
1120
  const text = `fetch ${plain.url}: ${error?.message ?? error}`;
1057
- consoleEntries.push({ level: "error", text });
1058
- hooks.consoleMessage?.("error", text);
1121
+ if (!disposed && !signal?.aborted) {
1122
+ consoleEntries.push({ level: "error", text });
1123
+ hooks.consoleMessage?.("error", text);
1124
+ }
1125
+ if (disposed || signal?.aborted) throw signal?.reason ?? abortError();
1059
1126
  throw new TypeError("Failed to fetch");
1060
1127
  });
1061
1128
  }
1129
+ let abort;
1062
1130
  const aborted = signal
1063
- ? new Promise((_, reject) => signal.addEventListener("abort", () => {
1064
- networkPending?.abort?.();
1065
- reject(abortError());
1066
- }, { once: true }))
1131
+ ? new Promise((_, reject) => {
1132
+ abort = () => {
1133
+ const reason = signal.reason ?? abortError();
1134
+ networkPending?.abort?.(reason);
1135
+ reject(reason);
1136
+ };
1137
+ signal.addEventListener("abort", abort, { once: true });
1138
+ if (signal.aborted) abort();
1139
+ })
1067
1140
  : null;
1141
+ const cleanup = () => {
1142
+ pendingPageFetches.delete(networkPending);
1143
+ if (abort) signal.removeEventListener("abort", abort);
1144
+ };
1145
+ // The fetch promise resolves at headers. Keep cancellation attached
1146
+ // until the streaming body finishes, including across navigation.
1147
+ (networkPending?.finished ?? pending).then(cleanup, cleanup);
1068
1148
  const data = await (aborted ? Promise.race([pending, aborted]) : pending);
1069
1149
  hooks.afterTask();
1070
1150
  return makeResponse(data);
@@ -1078,7 +1158,6 @@ export function createPageRealm({ window, document, mode, hooks }) {
1078
1158
  let url = null;
1079
1159
  let response = null;
1080
1160
  let controller = null;
1081
- let timer = null;
1082
1161
  const xhr = {
1083
1162
  UNSENT: 0, OPENED: 1, HEADERS_RECEIVED: 2, LOADING: 3, DONE: 4,
1084
1163
  readyState: 0,
@@ -1104,7 +1183,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1104
1183
  requestHeaders.append(String(name), String(value));
1105
1184
  },
1106
1185
  getResponseHeader(name) {
1107
- return response?.headers.get(String(name)) ?? null;
1186
+ return response ? HostHeaders.prototype.get.call(response.headers, String(name)) : null;
1108
1187
  },
1109
1188
  getAllResponseHeaders() {
1110
1189
  if (!response) return "";
@@ -1116,15 +1195,19 @@ export function createPageRealm({ window, document, mode, hooks }) {
1116
1195
  },
1117
1196
  send(body = null) {
1118
1197
  if (xhr.readyState !== 1) throw new DOMException("The object's state must be OPENED.", "InvalidStateError");
1119
- controller = new AbortController();
1120
- if (xhr.timeout > 0) timer = schedule(() => controller.abort(), xhr.timeout, [], false);
1198
+ const requestController = controller = new AbortController();
1199
+ let timedOut = false;
1200
+ const timer = xhr.timeout > 0 ? schedule(() => {
1201
+ timedOut = true;
1202
+ requestController.abort();
1203
+ }, xhr.timeout, [], false) : null;
1121
1204
  fire("loadstart");
1122
1205
  pageFetch(url, {
1123
1206
  method,
1124
1207
  headers: requestHeaders,
1125
1208
  body: ["GET", "HEAD"].includes(method) ? null : body,
1126
1209
  credentials: xhr.withCredentials ? "include" : "same-origin",
1127
- signal: controller.signal,
1210
+ signal: requestController.signal,
1128
1211
  }).then(async (result) => {
1129
1212
  response = result;
1130
1213
  xhr.status = result.status;
@@ -1133,19 +1216,27 @@ export function createPageRealm({ window, document, mode, hooks }) {
1133
1216
  setState(2);
1134
1217
  setState(3);
1135
1218
  const bytes = await result.arrayBuffer();
1136
- const text = new TextDecoder().decode(bytes);
1219
+ const text = xhr.responseType === "arraybuffer" || xhr.responseType === "blob"
1220
+ ? "" : new TextDecoder().decode(bytes);
1137
1221
  xhr.responseText = ["", "text"].includes(xhr.responseType) ? text : "";
1138
1222
  xhr.response = xhr.responseType === "json" ? safeJson(text)
1139
1223
  : xhr.responseType === "arraybuffer" ? bytes
1140
- : xhr.responseType === "blob" ? new Blob([bytes], { type: result.headers.get("content-type") ?? "" })
1224
+ : xhr.responseType === "blob" ? new Blob([bytes], { type: HostHeaders.prototype.get.call(result.headers, "content-type") ?? "" })
1141
1225
  : text;
1142
1226
  setState(4);
1143
1227
  fire("load");
1144
1228
  fire("loadend");
1145
- }, (error) => {
1229
+ }).catch(() => {
1230
+ // The response promise resolves at headers; body failures still
1231
+ // run XHR's request-error steps instead of leaving it LOADING.
1232
+ response = null;
1146
1233
  xhr.status = 0;
1234
+ xhr.statusText = "";
1235
+ xhr.responseURL = "";
1236
+ xhr.response = null;
1237
+ xhr.responseText = "";
1147
1238
  setState(4);
1148
- fire(error?.name === "AbortError" ? (timer !== null && xhr.timeout > 0 ? "timeout" : "abort") : "error");
1239
+ fire(timedOut ? "timeout" : requestController.signal.aborted ? "abort" : "error");
1149
1240
  fire("loadend");
1150
1241
  }).finally(() => {
1151
1242
  if (timer !== null) cancel(timer);
@@ -1311,9 +1402,9 @@ export function createPageRealm({ window, document, mode, hooks }) {
1311
1402
  fetch: function fetch(input, init = undefined) { return pageFetch(input, init); },
1312
1403
  XMLHttpRequest,
1313
1404
  WebSocket,
1314
- Headers,
1405
+ Headers: PageHeaders,
1315
1406
  Request: PageRequest,
1316
- Response,
1407
+ Response: PageResponse,
1317
1408
  AbortController,
1318
1409
  AbortSignal,
1319
1410
  FormData,
@@ -1491,6 +1582,10 @@ export function createPageRealm({ window, document, mode, hooks }) {
1491
1582
  const messaging = createMessaging({
1492
1583
  realm, window, pageGlobal: () => pageGlobal, runTask, callPage, DOMException, frames: hooks.frames ?? null,
1493
1584
  });
1585
+ const broadcasts = createBroadcastChannels({
1586
+ origin: hooks.frames?.self.origin() ?? new URL(document.URL).origin,
1587
+ EventTarget: eventTargets.EventTarget, realm, runTask, DOMException,
1588
+ });
1494
1589
 
1495
1590
  const platform = platformObjects({
1496
1591
  eventTargets, userAgent, userAgentDataMembers: chromeVersion ? userAgentDataMembers : null, mobile: mobileUserAgent, viewport: hooks.viewport,
@@ -1537,12 +1632,13 @@ export function createPageRealm({ window, document, mode, hooks }) {
1537
1632
  // SES rewrites dynamic import() in evaluated classic scripts to this
1538
1633
  // hook. Resolve it through the same module loader and network boundary as
1539
1634
  // parser-inserted modules.
1540
- __import__: (specifier) => importModule(String(specifier), currentScript?.src || document.URL),
1635
+ __import__: (specifier) => importModule(String(specifier), currentScript?.src || document.baseURI || document.URL),
1541
1636
  performance: platform.performance,
1542
1637
  PerformanceObserver: userTiming.PerformanceObserver,
1543
1638
  crypto: platform.crypto,
1544
1639
  ...platform.interfaces,
1545
1640
  CSSStyleDeclaration: cssStyleDeclarationInterface(realm),
1641
+ ValidityState: validity.Interface,
1546
1642
  ...trustedTypes.interfaces,
1547
1643
  trustedTypes: trustedTypes.trustedTypes,
1548
1644
  Event,
@@ -1595,15 +1691,19 @@ export function createPageRealm({ window, document, mode, hooks }) {
1595
1691
  const tag = String(name).toLowerCase();
1596
1692
  if (typeof constructor !== "function") throw new TypeError("Custom element constructor must be a function");
1597
1693
  if (customElementConstructors.has(tag)) throw new DOMException("Custom element already defined", "NotSupportedError");
1694
+ let captureInitialAttributes;
1598
1695
  const Adapter = class extends window.HTMLElement {
1599
1696
  constructor() {
1600
1697
  super();
1698
+ const initialAttributes = captureInitialAttributes?.(this);
1601
1699
  const wrapper = realm.wrap(this);
1602
1700
  Object.setPrototypeOf(wrapper, constructor.prototype);
1603
1701
  const previous = constructingElement;
1604
1702
  constructingElement = this;
1605
1703
  try { Reflect.construct(constructor, []); }
1704
+ catch (error) { initialAttributes?.cancel(); throw error; }
1606
1705
  finally { constructingElement = previous; }
1706
+ initialAttributes?.finish();
1607
1707
  }
1608
1708
  connectedCallback() {
1609
1709
  const wrapper = realm.wrap(this);
@@ -1614,9 +1714,16 @@ export function createPageRealm({ window, document, mode, hooks }) {
1614
1714
  if (typeof wrapper.disconnectedCallback === "function") callPage(wrapper.disconnectedCallback, wrapper, []);
1615
1715
  }
1616
1716
  };
1617
- installAttributeReactions(Adapter, constructor, realm, callPage);
1618
- window.customElements.define(tag, Adapter);
1717
+ captureInitialAttributes = installAttributeReactions(Adapter, constructor, realm, callPage);
1619
1718
  customElementConstructors.set(tag, constructor);
1719
+ try { window.customElements.define(tag, Adapter); }
1720
+ catch (error) {
1721
+ // The definition is observable while existing elements upgrade.
1722
+ // Validation failures happen before registration; constructor errors
1723
+ // can happen afterward and must not erase that registered identity.
1724
+ if (window.customElements.get(tag) !== Adapter) customElementConstructors.delete(tag);
1725
+ throw error;
1726
+ }
1620
1727
  // Upgrades change :defined without a DOM mutation.
1621
1728
  hooks.definedChanged?.(tag);
1622
1729
  },
@@ -1631,6 +1738,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1631
1738
  },
1632
1739
  },
1633
1740
  ...INTERFACES,
1741
+ DOMStringMap,
1634
1742
  Node: NodeInterface,
1635
1743
  Text: TextInterface,
1636
1744
  CharacterData: CharacterDataInterface,
@@ -1639,6 +1747,8 @@ export function createPageRealm({ window, document, mode, hooks }) {
1639
1747
  Document: DocumentInterface,
1640
1748
  HTMLDocument: DocumentInterface,
1641
1749
  DocumentFragment: DocumentFragmentInterface,
1750
+ DOMParser,
1751
+ XMLSerializer,
1642
1752
  ...typeCheckInterfaces(),
1643
1753
  addEventListener: windowEvents.addEventListener,
1644
1754
  removeEventListener: windowEvents.removeEventListener,
@@ -1687,6 +1797,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1687
1797
  postMessage: messaging.postMessage,
1688
1798
  MessageChannel: messaging.MessageChannel,
1689
1799
  MessagePort: messaging.MessagePort,
1800
+ BroadcastChannel: broadcasts.BroadcastChannel,
1690
1801
  };
1691
1802
  const accessors = {
1692
1803
  innerWidth: viewportValue("width"),
@@ -1707,7 +1818,8 @@ export function createPageRealm({ window, document, mode, hooks }) {
1707
1818
  hardenBindings();
1708
1819
  // Dedicated workers get a compartment each, with these of the page's utilities.
1709
1820
  workers = createWorkers({
1710
- realm, documentUrl: () => document.URL, fetchScript: hooks.fetchScript, runTask, callPage, reportError, DOMException,
1821
+ realm, documentUrl: () => document.URL, documentBaseUrl: () => document.baseURI || document.URL,
1822
+ fetchScript: hooks.fetchScript, runTask, callPage, reportError, DOMException,
1711
1823
  constructorHelper,
1712
1824
  pagePorts: messaging.ports,
1713
1825
  globals: Object.fromEntries(["console", "atob", "btoa", "TextEncoder", "TextDecoder", "URL", "URLSearchParams",
@@ -1763,6 +1875,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1763
1875
  });
1764
1876
  Object.defineProperty(PagePromise, "length", { value: 1, configurable: true });
1765
1877
  pageGlobal.Promise = PagePromise;
1878
+ pageGlobal.Error = errorInterface(pageGlobal.Error, hostErrorIsError);
1766
1879
  // Namespace objects that libraries extend (reflect-metadata adds to
1767
1880
  // Reflect, polyfills to JSON and Math): each page gets its own mutable
1768
1881
  // copy with the same functions; the shared frozen intrinsics stay as they are.
@@ -1814,8 +1927,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1814
1927
  __rejectSomeDirectEvalExpressions__: false,
1815
1928
  });
1816
1929
  evaluate = (source) => {
1817
- const classic = rewriteClassicScriptSource(source);
1818
- return evaluatePrepared({ code: escapeSesHtmlComments(classic.code), parsed: classic.parsed, strict: classic.strict });
1930
+ return evaluatePrepared(prepareClassicScript(source));
1819
1931
  };
1820
1932
  // Module code is already strict with its own scope; no global lifting.
1821
1933
  evaluateModuleCode = (code) => compartment.evaluate(escapeSesHtmlComments(code, "module"), {
@@ -1882,7 +1994,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1882
1994
  if (node.nodeType !== 1) continue;
1883
1995
  for (const script of [node, ...node.querySelectorAll("script")]) {
1884
1996
  if (script.tagName !== "SCRIPT" || !createdScripts.has(script) || startedScripts.has(script)) continue;
1885
- if (!script.isConnected) continue;
1997
+ if (!script.isConnected || script.ownerDocument !== document) continue;
1886
1998
  runInsertedScript(script);
1887
1999
  }
1888
2000
  }
@@ -1908,7 +2020,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1908
2020
  runClassic(script, script.textContent);
1909
2021
  return;
1910
2022
  }
1911
- const pending = hooks.fetchScript(new URL(src, document.URL).href).then(
2023
+ const pending = hooks.fetchScript(new URL(src, script.baseURI || document.URL).href).then(
1912
2024
  async (source) => {
1913
2025
  runClassic(script, source, await preparedFor(prepareOffThread(source), source));
1914
2026
  runTask(() => script.dispatchEvent(new window.Event("load")));
@@ -1933,7 +2045,9 @@ export function createPageRealm({ window, document, mode, hooks }) {
1933
2045
  // phases, see module-transform.js.
1934
2046
 
1935
2047
  function ensureFetched(url, type, sourceOverride = null) {
1936
- const key = `${type ?? "js"} ${url}`;
2048
+ // Inline modules are distinct scripts sharing a real base URL. Their
2049
+ // identity must not leak into import.meta.url as an invented fragment.
2050
+ const key = sourceOverride === null ? `${type ?? "js"} ${url}` : `inline:${++inlineModuleCount}`;
1937
2051
  let record = modules.get(key);
1938
2052
  if (record) return record.fetched;
1939
2053
  record = { url, type, state: "fetching", namespace: Object.create(null), deps: [], error: null };
@@ -2053,11 +2167,12 @@ export function createPageRealm({ window, document, mode, hooks }) {
2053
2167
  async function runModuleScript(script) {
2054
2168
  const src = script.getAttribute("src");
2055
2169
  try {
2056
- if (src) await importModule(new URL(src, document.URL).href, document.URL);
2057
- else await importModule(`${document.URL}#inline-module-${++inlineModuleCount}`, document.URL, null, script.textContent ?? "");
2170
+ const baseURL = script.baseURI || document.URL;
2171
+ if (src) await importModule(new URL(src, baseURL).href, baseURL);
2172
+ else await importModule(baseURL, baseURL, null, script.textContent ?? "");
2058
2173
  if (src) runTask(() => script.dispatchEvent(new window.Event("load")));
2059
2174
  } catch (error) {
2060
- reportError(error, src ? new URL(src, document.URL).href : document.URL);
2175
+ reportError(error, src ? new URL(src, script.baseURI || document.URL).href : document.URL);
2061
2176
  if (src) runTask(() => script.dispatchEvent(new window.Event("error")));
2062
2177
  }
2063
2178
  }
@@ -2183,7 +2298,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
2183
2298
  continue;
2184
2299
  }
2185
2300
  try {
2186
- const url = new URL(src, document.URL).href;
2301
+ const url = new URL(src, script.baseURI || document.URL).href;
2187
2302
  if ((script.getAttribute("type") ?? "").trim().toLowerCase() === "module") {
2188
2303
  ensureFetched(resolveModuleSpecifier(url, document.URL, importMap), null).then(loadGraph).catch(() => {});
2189
2304
  } else {
@@ -2213,7 +2328,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
2213
2328
  let source;
2214
2329
  try {
2215
2330
  source = src
2216
- ? await (preloaded.get(script) ?? hooks.fetchScript(new URL(src, document.URL).href))
2331
+ ? await (preloaded.get(script) ?? hooks.fetchScript(new URL(src, script.baseURI || document.URL).href))
2217
2332
  : script.textContent ?? "";
2218
2333
  } catch (error) {
2219
2334
  reportError(error);
@@ -2289,6 +2404,12 @@ export function createPageRealm({ window, document, mode, hooks }) {
2289
2404
  messaging.receive(message);
2290
2405
  },
2291
2406
 
2407
+ /** The renderer completed layout or changed a scroll offset. */
2408
+ layoutChanged(version) {
2409
+ const viewport = hooks.viewport();
2410
+ layoutObservers.update(version, viewport.scrollX, viewport.scrollY, viewport.width, viewport.height, viewport.devicePixelRatio);
2411
+ },
2412
+
2292
2413
  /** A MediaSource object, or the one registered at a blob URL. */
2293
2414
  isMediaSource(value) {
2294
2415
  return mediaScope.isMediaSource(value);
@@ -2302,9 +2423,15 @@ export function createPageRealm({ window, document, mode, hooks }) {
2302
2423
 
2303
2424
  dispose() {
2304
2425
  disposed = true;
2426
+ for (const pending of pendingPageFetches) pending.abort?.();
2427
+ pendingPageFetches.clear();
2428
+ disposeDocumentCanvases(document);
2305
2429
  stopRejectionEvents();
2430
+ layoutObservers.dispose();
2431
+ parserWindow?.happyDOM?.close?.();
2306
2432
  if (blobUrlFallback === mediaScope) blobUrlFallback = null;
2307
2433
  messaging.dispose();
2434
+ broadcasts.dispose();
2308
2435
  workers?.dispose();
2309
2436
  // Leaving the document closes its WebSockets (1001 "going away").
2310
2437
  for (const close of openSockets) close();
@@ -2728,7 +2855,18 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
2728
2855
  const horizontal = (style.padding[1] ?? 0) + (style.padding[3] ?? 0) + (style.borderWidths[1] ?? 0) + (style.borderWidths[3] ?? 0);
2729
2856
  const vertical = (style.padding[0] ?? 0) + (style.padding[2] ?? 0) + (style.borderWidths[0] ?? 0) + (style.borderWidths[2] ?? 0);
2730
2857
  const transform = style.transform;
2731
- const translate = (value, size) => typeof value === "number" ? value : value?.unit === "%" ? value.value / 100 * size : 0;
2858
+ const translate = (value, size) => {
2859
+ if (typeof value === "number") return value;
2860
+ if (value?.unit === "%") return value.value / 100 * size;
2861
+ if (value?.unit !== "math") return 0;
2862
+ if (value.kind === "calc") return value.px + value.percent / 100 * size;
2863
+ if (value.kind === "scale") return value.factor * translate(value.value, size);
2864
+ const parts = value.values.map(part => translate(part, size));
2865
+ if (value.kind === "sum") return parts.reduce((sum, part) => sum + part, 0);
2866
+ if (value.kind === "min") return Math.min(...parts);
2867
+ if (value.kind === "max") return Math.max(...parts);
2868
+ return Math.max(parts[0], Math.min(parts[1], parts[2]));
2869
+ };
2732
2870
  Object.assign(values, {
2733
2871
  display: style.display,
2734
2872
  position: style.position,
@@ -2738,7 +2876,11 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
2738
2876
  backgroundImage: style.backgroundImage == null ? "none" : String(style.backgroundImage),
2739
2877
  fontSize: px(style.fontSize),
2740
2878
  fontWeight: String(style.fontWeight),
2879
+ fontStyle: style.fontStyle ?? "normal",
2880
+ fontStretch: `${style.fontStretch ?? 100}%`,
2741
2881
  fontFamily: (style.fontFamily ?? []).join(", "),
2882
+ translate: style.translate ? (style.translate.y === 0 ? length(style.translate.x)
2883
+ : `${length(style.translate.x)} ${length(style.translate.y)}`) : "none",
2742
2884
  lineHeight: style.lineHeight == null ? "normal" : px(style.lineHeight),
2743
2885
  textAlign: style.textAlign,
2744
2886
  whiteSpace: style.whiteSpace,
@@ -2768,9 +2910,8 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
2768
2910
  alignContent: style.alignContent,
2769
2911
  alignItems: style.alignItems,
2770
2912
  alignSelf: style.alignSelf,
2771
- rowGap: style.rowGap ? length(style.rowGap) : "normal",
2772
- columnGap: style.columnGap ? length(style.columnGap) : "normal",
2773
- borderRadius: length(style.borderRadius),
2913
+ rowGap: style.rowGapNormal ? "normal" : length(style.rowGap),
2914
+ columnGap: style.columnGapNormal ? "normal" : length(style.columnGap),
2774
2915
  transform: !transform ? "none"
2775
2916
  : `matrix(${transform.scaleX ?? 1}, 0, 0, ${transform.scaleY ?? 1}, ${translate(transform.x, bounds?.width ?? 0)}, ${translate(transform.y, bounds?.height ?? 0)})`,
2776
2917
  visibility: style.visibility ?? "visible",
@@ -2780,6 +2921,23 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
2780
2921
  overflowX: style.overflowScrollX ?? (style.overflowX === "clip" ? "clip" : "visible"),
2781
2922
  overflowY: style.overflowScrollY ?? (style.overflowY === "clip" ? "clip" : "visible"),
2782
2923
  });
2924
+ const cornerNames = ["TopLeft", "TopRight", "BottomRight", "BottomLeft"];
2925
+ const cornerLengths = cornerNames.map(corner => (style[`border${corner}Radius`]
2926
+ ?? [style.borderRadius, style.borderRadius]).map(length));
2927
+ cornerNames.forEach((corner, index) => {
2928
+ const [x, y] = cornerLengths[index];
2929
+ values[`border${corner}Radius`] = x === y ? x : `${x} ${y}`;
2930
+ });
2931
+ const compress = ([a, b, c, d]) => a === c && b === d ? a === b ? a : `${a} ${b}`
2932
+ : b === d ? `${a} ${b} ${c}` : `${a} ${b} ${c} ${d}`;
2933
+ const horizontalRadius = compress(cornerLengths.map(pair => pair[0]));
2934
+ const verticalRadius = compress(cornerLengths.map(pair => pair[1]));
2935
+ values.borderRadius = horizontalRadius === verticalRadius ? horizontalRadius : `${horizontalRadius} / ${verticalRadius}`;
2936
+ const rtl = style.direction === "rtl";
2937
+ values.borderStartStartRadius = values[rtl ? "borderTopRightRadius" : "borderTopLeftRadius"];
2938
+ values.borderStartEndRadius = values[rtl ? "borderTopLeftRadius" : "borderTopRightRadius"];
2939
+ values.borderEndStartRadius = values[rtl ? "borderBottomRightRadius" : "borderBottomLeftRadius"];
2940
+ values.borderEndEndRadius = values[rtl ? "borderBottomLeftRadius" : "borderBottomRightRadius"];
2783
2941
  values.overflow = values.overflowX === values.overflowY ? values.overflowX : `${values.overflowX} ${values.overflowY}`;
2784
2942
  sides.forEach((side, index) => {
2785
2943
  values[`margin${side}`] = length(style.margin[index]);
@@ -2817,6 +2975,7 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
2817
2975
 
2818
2976
  function formatConsoleValue(value) {
2819
2977
  if (typeof value === "string") return value;
2978
+ if (hostErrorIsError?.(value)) return diagnosticError(value);
2820
2979
  try {
2821
2980
  return JSON.stringify(value) ?? String(value);
2822
2981
  } catch {
@@ -2824,6 +2983,19 @@ function formatConsoleValue(value) {
2824
2983
  }
2825
2984
  }
2826
2985
 
2986
+ // SES retains a privileged stack reader in the host compartment. Use it only
2987
+ // when formatting developer diagnostics: pages still see tamed Error.stack
2988
+ // and never receive the reader itself.
2989
+ function diagnosticError(error) {
2990
+ const message = error?.name ? `${error.name}: ${error.message}` : String(error?.message ?? error);
2991
+ try {
2992
+ const stack = error?.stack || globalThis.getStackString?.(error);
2993
+ return stack ? (stack.startsWith(message) ? stack : `${message}\n${stack}`) : message;
2994
+ } catch {
2995
+ return message;
2996
+ }
2997
+ }
2998
+
2827
2999
  function safeJson(text) {
2828
3000
  try {
2829
3001
  return JSON.parse(text);