buninu-browser 0.2.2 → 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 (118) hide show
  1. package/lib/audio/playback-buffer.js +27 -0
  2. package/lib/audio/pulse-client.js +18 -8
  3. package/lib/cdp-server.js +53 -14
  4. package/lib/happy-dom/document-base.js +33 -0
  5. package/lib/happy-dom/parser.js +20 -6
  6. package/lib/happy-dom/selector-matcher.js +2 -2
  7. package/lib/happy-dom/selector-pseudo-adapter.js +33 -0
  8. package/lib/happy-dom/svg-attribute-adapter.js +50 -0
  9. package/lib/headless-shell.js +12 -6
  10. package/lib/input/hit-test.js +67 -19
  11. package/lib/layout/text-layout.js +289 -131
  12. package/lib/media/byte-pipe.js +2 -2
  13. package/lib/media/position-audio.js +10 -1
  14. package/lib/media/source-snapshots.js +126 -0
  15. package/lib/media/video-keyframe.js +42 -0
  16. package/lib/media/webm-stream.js +106 -0
  17. package/lib/network/network-service.js +268 -120
  18. package/lib/paint/display-list.js +22 -27
  19. package/lib/paint/frame-cache.js +25 -0
  20. package/lib/paint/frame.js +42 -13
  21. package/lib/paint/paragraph-text.js +24 -0
  22. package/lib/paint/raster-readback.js +41 -0
  23. package/lib/paint/rounded-rect.js +82 -0
  24. package/lib/paint/shared-frame-pool.js +105 -0
  25. package/lib/render-tree/index.js +40 -6
  26. package/lib/render-tree/svg.js +15 -7
  27. package/lib/renderer/bindings.js +244 -82
  28. package/lib/renderer/broadcast-channel.js +76 -0
  29. package/lib/renderer/canvas-2d.js +257 -14
  30. package/lib/renderer/canvas-text.js +120 -0
  31. package/lib/renderer/compositor.js +27 -10
  32. package/lib/renderer/custom-element-reactions.js +42 -0
  33. package/lib/renderer/font-loading.js +227 -0
  34. package/lib/renderer/history.js +42 -0
  35. package/lib/renderer/host.js +36 -5
  36. package/lib/renderer/image-loading.js +27 -0
  37. package/lib/renderer/image-resources.js +65 -0
  38. package/lib/renderer/interfaces.js +28 -0
  39. package/lib/renderer/keyboard-input.js +12 -0
  40. package/lib/renderer/layout-observers.js +236 -0
  41. package/lib/renderer/main.js +33 -5
  42. package/lib/renderer/media-playback-events.js +13 -0
  43. package/lib/renderer/media-source.js +126 -55
  44. package/lib/renderer/page-realm.js +278 -147
  45. package/lib/renderer/page-renderer.js +840 -301
  46. package/lib/renderer/piano-input.js +22 -0
  47. package/lib/renderer/pointer-move.js +29 -0
  48. package/lib/renderer/prepared-script-cache.js +50 -0
  49. package/lib/renderer/rendered-text.js +108 -0
  50. package/lib/renderer/rewrite-worker.js +4 -3
  51. package/lib/renderer/script-rewrite.js +13 -2
  52. package/lib/renderer/scroll-into-view.js +28 -0
  53. package/lib/renderer/slot-assignment.js +60 -0
  54. package/lib/renderer/video-presentation.js +145 -0
  55. package/lib/renderer/workers.js +2 -2
  56. package/lib/style/computed-style.js +632 -108
  57. package/lib/style/selector-traits.js +49 -5
  58. package/package.json +1 -1
  59. package/test/broadcast-channel.test.js +26 -0
  60. package/test/canvas-2d.test.js +83 -2
  61. package/test/canvas-paint.test.js +9 -0
  62. package/test/canvas-text.test.js +41 -0
  63. package/test/cdp-stream.test.js +56 -0
  64. package/test/compositor.test.js +38 -0
  65. package/test/computed-style-binding.test.js +151 -0
  66. package/test/computed-style.test.js +279 -1
  67. package/test/display-list.test.js +26 -0
  68. package/test/document-base.test.js +87 -0
  69. package/test/dom-platform.test.js +461 -0
  70. package/test/download.test.js +8 -0
  71. package/test/fetch-lifecycle.test.js +161 -0
  72. package/test/fixtures/canvas-paint.js +117 -0
  73. package/test/fixtures/canvas-text.js +42 -0
  74. package/test/fixtures/image-lifetime.js +90 -0
  75. package/test/fixtures/media-seek-snapshot.js +77 -0
  76. package/test/fixtures/mse-audio-chirp.webm +0 -0
  77. package/test/fixtures/mse-switch-large.mp4 +0 -0
  78. package/test/fixtures/mse-switch-small.mp4 +0 -0
  79. package/test/fixtures/renderer-memory.js +110 -0
  80. package/test/fixtures/summary-style.js +28 -0
  81. package/test/font-loading.test.js +97 -0
  82. package/test/grid.test.js +58 -0
  83. package/test/headless-shell.test.js +22 -0
  84. package/test/hit-test.test.js +21 -1
  85. package/test/image-lifetime.test.js +12 -0
  86. package/test/image-resources.test.js +94 -0
  87. package/test/local-media.test.js +84 -0
  88. package/test/media-playback-events.test.js +22 -0
  89. package/test/media-seek-snapshot.test.js +11 -0
  90. package/test/media-source-config.test.js +98 -0
  91. package/test/media-source-webm-accounting.test.js +98 -0
  92. package/test/media-source-webm.test.js +108 -0
  93. package/test/navigation.test.js +1 -0
  94. package/test/network-service.test.js +388 -0
  95. package/test/page-scripts.test.js +160 -2
  96. package/test/paint-frame.test.js +103 -0
  97. package/test/paint-paragraph-cache.test.js +87 -0
  98. package/test/piano-input.test.js +44 -0
  99. package/test/playback-buffer.test.js +31 -0
  100. package/test/position-audio.test.js +24 -0
  101. package/test/positioning.test.js +22 -0
  102. package/test/prepared-script-cache.test.js +45 -0
  103. package/test/pulse-client.test.js +27 -5
  104. package/test/raster-readback.test.js +40 -0
  105. package/test/render-tree.test.js +28 -0
  106. package/test/renderer-memory.test.js +11 -0
  107. package/test/selector-traits.test.js +24 -0
  108. package/test/shared-frame-pool.test.js +63 -0
  109. package/test/style-sheets.test.js +51 -0
  110. package/test/svg.test.js +29 -0
  111. package/test/text-layout.test.js +168 -0
  112. package/test/video-keyframe.test.js +61 -0
  113. package/test/video-presentation.test.js +136 -0
  114. package/tools/browser-lab.js +78 -0
  115. package/tools/browser-soak.js +75 -0
  116. package/tools/network-reference.js +49 -0
  117. package/tools/process-memory.js +38 -0
  118. package/tools/script-benchmark.js +25 -0
@@ -20,15 +20,22 @@ 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";
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";
37
+ import { historyBinding } from "./history.js";
38
+ import { installImageLoadingAttributes } from "./image-loading.js";
32
39
  import { AsyncLocalStorage } from "node:async_hooks";
33
40
 
34
41
  // URL is one shared intrinsic, hardened before any page sees it. Blob URL
@@ -61,6 +68,9 @@ const hostCrypto = globalThis.crypto;
61
68
  const hostStructuredClone = globalThis.structuredClone;
62
69
  const hostAtob = globalThis.atob;
63
70
  const hostBtoa = globalThis.btoa;
71
+ const HostHeaders = globalThis.Headers;
72
+ const HostRequest = globalThis.Request;
73
+ const HostResponse = globalThis.Response;
64
74
 
65
75
  export const CLASSIC_SCRIPT_TYPES = new Set([
66
76
  "", "text/javascript", "application/javascript", "text/ecmascript", "application/ecmascript",
@@ -150,7 +160,11 @@ const PLAIN_HTML_ELEMENTS = new Set(["ABBR", "ADDRESS", "ARTICLE", "ASIDE", "B",
150
160
  */
151
161
  const FORM_CONTROLS = ["HTMLInputElement", "HTMLSelectElement", "HTMLTextAreaElement", "HTMLButtonElement"];
152
162
  const URL_ELEMENTS = ["HTMLAnchorElement", "HTMLAreaElement"];
163
+ const VALIDATED_ELEMENTS = [...FORM_CONTROLS, "HTMLFieldSetElement", "HTMLObjectElement", "HTMLOutputElement"];
153
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"],
154
168
  contentWindow: ["HTMLIFrameElement", "HTMLObjectElement", "HTMLFrameElement"],
155
169
  contentDocument: ["HTMLIFrameElement", "HTMLObjectElement", "HTMLFrameElement"],
156
170
  content: ["HTMLTemplateElement"],
@@ -199,6 +213,9 @@ const WINDOW_HANDLER_EVENTS = [
199
213
  "popstate", "beforeunload", "unload", "focus", "blur", "online", "offline", "unhandledrejection", "rejectionhandled",
200
214
  ];
201
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;
202
219
 
203
220
  function cssEscape(value) {
204
221
  const input = String(value);
@@ -262,6 +279,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
262
279
  const consoleEntries = [];
263
280
  let nextTimerId = 1;
264
281
  let disposed = false;
282
+ const pendingPageFetches = new Set();
265
283
  let readyState = "loading";
266
284
  // A document gets a fresh high-resolution time origin. Bun's
267
285
  // performance.now() is relative to the renderer process, which is commonly
@@ -292,8 +310,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
292
310
  let inlineModuleCount = 0;
293
311
 
294
312
  const reportError = (error, source = currentScript?.src) => {
295
- // SES "safe" error taming blanks .stack, so fall back to name and message.
296
- const detail = error?.stack || (error?.name ? `${error.name}: ${error.message}` : String(error?.message ?? error));
313
+ const detail = diagnosticError(error);
297
314
  const message = source ? `${source}: ${detail}` : detail;
298
315
  consoleEntries.push({ level: "error", text: message });
299
316
  hooks.reportError(message);
@@ -329,7 +346,9 @@ export function createPageRealm({ window, document, mode, hooks }) {
329
346
  };
330
347
 
331
348
  const realm = new BindingRealm(window, {
332
- touch: hooks.touch,
349
+ touch: (node, features) => {
350
+ if (!node || node === document || node.ownerDocument === document) hooks.touch(node, features);
351
+ },
333
352
  boundsOf: hooks.boundsOf,
334
353
  computedStyle: hooks.computedStyle,
335
354
  // The engine clock's reading when this document's timeline started (see pageNow).
@@ -373,6 +392,14 @@ export function createPageRealm({ window, document, mode, hooks }) {
373
392
  (value) => value === pageGlobal || Boolean(nodeOf(value)),
374
393
  (value) => value === pageGlobal ? window : nodeOf(value));
375
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;
376
403
  const NodeInterface = function Node() { throw new TypeError("Illegal constructor"); };
377
404
  // Node.prototype inherits EventTarget.prototype (DOM §4.4), as Window.prototype does.
378
405
  NodeInterface.prototype = mutableBindingPrototype(INTERFACES.Node.prototype, eventTargets.EventTarget.prototype);
@@ -554,7 +581,27 @@ export function createPageRealm({ window, document, mode, hooks }) {
554
581
  enumerable: true,
555
582
  writable: true,
556
583
  value(type) {
557
- 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);
558
605
  },
559
606
  });
560
607
  // HTMLMediaElement. HAVE_* / NETWORK_* / MEDIA_ERR_* live on the prototype
@@ -631,6 +678,33 @@ export function createPageRealm({ window, document, mode, hooks }) {
631
678
  DocumentInterface.prototype = withParentNodeMembers(mutableBindingPrototype(INTERFACES.Document.prototype, parentPrototype));
632
679
  Object.defineProperty(DocumentInterface.prototype, "constructor", { value: DocumentInterface, writable: true, configurable: true });
633
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
+ }
634
708
 
635
709
  /** In an iframe's document, parent and top are the embedding frames' windows. */
636
710
  const defineFrameWindows = (global) => {
@@ -715,12 +789,12 @@ export function createPageRealm({ window, document, mode, hooks }) {
715
789
  };
716
790
  Object.defineProperty(pageConsole, Symbol.toStringTag, { value: "console", configurable: true });
717
791
 
718
- /**
719
- * MutationObserver wraps Happy DOM's implementation (records carry node
720
- * wrappers); Resize- and IntersectionObserver report the current layout
721
- * once per observed element, which is enough for common lazy-loading and
722
- * sizing code in a headless viewport.
723
- */
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. */
724
798
  function observerConstructors() {
725
799
  function MutationObserver(callback) {
726
800
  if (!new.target) throw new TypeError("Constructor MutationObserver requires 'new'");
@@ -756,48 +830,8 @@ export function createPageRealm({ window, document, mode, hooks }) {
756
830
  return self;
757
831
  }
758
832
 
759
- const layoutObserver = (name, entryFor) => function Observer(callback) {
760
- if (!new.target) throw new TypeError(`Constructor ${name} requires 'new'`);
761
- const self = {};
762
- const observed = new Set();
763
- self.observe = (target) => {
764
- const node = nodeOf(target);
765
- if (!node || observed.has(node)) return;
766
- observed.add(node);
767
- schedule(() => {
768
- if (!observed.has(node)) return;
769
- const rect = hooks.boundsOf(node) ?? { left: 0, top: 0, width: 0, height: 0 };
770
- callPage(callback, self, [[entryFor(target, rect)], self]);
771
- }, 0, [], false);
772
- };
773
- self.unobserve = (target) => observed.delete(nodeOf(target));
774
- self.disconnect = () => observed.clear();
775
- self.takeRecords = () => [];
776
- return self;
777
- };
778
- const rectangle = ({ left, top, width, height }) => ({
779
- x: left, y: top, left, top, width, height, right: left + width, bottom: top + height,
780
- });
781
- const ResizeObserver = layoutObserver("ResizeObserver", (target, rect) => ({
782
- target,
783
- contentRect: rectangle({ ...rect, left: 0, top: 0 }),
784
- borderBoxSize: [{ inlineSize: rect.width, blockSize: rect.height }],
785
- contentBoxSize: [{ inlineSize: rect.width, blockSize: rect.height }],
786
- }));
787
- const IntersectionObserver = layoutObserver("IntersectionObserver", (target, rect) => {
788
- const { width, height } = hooks.viewport();
789
- const visible = rect.top < height && rect.top + rect.height > 0 && rect.left < width && rect.left + rect.width > 0;
790
- return {
791
- target,
792
- time: pageNow(),
793
- isIntersecting: visible,
794
- intersectionRatio: visible ? 1 : 0,
795
- boundingClientRect: rectangle(rect),
796
- intersectionRect: rectangle(visible ? rect : { left: 0, top: 0, width: 0, height: 0 }),
797
- rootBounds: rectangle({ left: 0, top: 0, width, height }),
798
- };
799
- });
800
- return { MutationObserver, ResizeObserver, IntersectionObserver };
833
+ return { MutationObserver, ResizeObserver: layoutObservers.ResizeObserver,
834
+ IntersectionObserver: layoutObservers.IntersectionObserver };
801
835
  }
802
836
 
803
837
  /**
@@ -917,33 +951,62 @@ export function createPageRealm({ window, document, mode, hooks }) {
917
951
  }
918
952
 
919
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;
920
975
  const abortError = () => new DOMException("The operation was aborted.", "AbortError");
921
976
  const baseUrl = () => document.baseURI || document.URL;
922
977
 
923
978
  // new Request("/api") resolves against the document's base URL (Fetch
924
979
  // §5.4), which the host's Request has no document to do.
925
- const PageRequest = class Request extends globalThis.Request {
980
+ const PageRequest = class Request extends HostRequest {
926
981
  constructor(input, init = undefined) {
927
- 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);
928
983
  // Fetch §5.4 step 36: a string body gives the request its Content-Type
929
984
  // (Bun's Request only does this for the other body kinds).
930
- if (typeof init?.body === "string" && !this.headers.has("content-type")) {
931
- 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
+ }
932
990
  }
933
991
  }
992
+ clone() { return Object.setPrototypeOf(HostRequest.prototype.clone.call(this), PageRequest.prototype); }
934
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
+ });
935
998
 
936
999
  // fetch(request) reads the Request's internal state, as Chromium's
937
1000
  // Request::CreateRequestWithRequestOrString does with GetRequest(). Page
938
1001
  // script may shadow url, method or bodyUsed on the instance; that changes
939
1002
  // what script reads, not what is fetched.
940
1003
  const requestSlot = (name) => {
941
- const get = Object.getOwnPropertyDescriptor(globalThis.Request.prototype, name).get;
1004
+ const get = Object.getOwnPropertyDescriptor(HostRequest.prototype, name).get;
942
1005
  return (request) => get.call(request);
943
1006
  };
944
1007
  const slots = Object.fromEntries(["url", "method", "headers", "mode", "credentials", "redirect", "signal", "bodyUsed"]
945
1008
  .map((name) => [name, requestSlot(name)]));
946
- const cloneRequest = globalThis.Request.prototype.clone;
1009
+ const cloneRequest = HostRequest.prototype.clone;
947
1010
  /** The input when it is a platform Request (a brand check, not instanceof). */
948
1011
  const platformRequest = (input) => {
949
1012
  if (input === null || typeof input !== "object") return null;
@@ -989,10 +1052,13 @@ export function createPageRealm({ window, document, mode, hooks }) {
989
1052
  }
990
1053
 
991
1054
  function makeResponse(data) {
992
- const headers = new Headers(data.headers ?? []);
1055
+ const headers = new PageHeaders(data.headers ?? []);
993
1056
  const body = data.body instanceof ReadableStream ? data.body
994
1057
  : data.body instanceof Uint8Array ? data.body : new Uint8Array(0);
995
- 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 } });
996
1062
  const response = {
997
1063
  type: data.type ?? "basic",
998
1064
  url: data.url ?? "",
@@ -1024,7 +1090,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1024
1090
  async function pageFetch(input, init = {}) {
1025
1091
  const request = await describeRequest(input, init ?? {});
1026
1092
  const signal = request.signal;
1027
- if (signal?.aborted) throw abortError();
1093
+ if (disposed || signal?.aborted) throw signal?.reason ?? abortError();
1028
1094
  const scheme = new URL(request.url).protocol;
1029
1095
  let pending;
1030
1096
  let networkPending = null;
@@ -1047,21 +1113,38 @@ export function createPageRealm({ window, document, mode, hooks }) {
1047
1113
  } else {
1048
1114
  const { signal: _signal, ...plain } = request;
1049
1115
  networkPending = hooks.pageFetch(plain);
1116
+ pendingPageFetches.add(networkPending);
1050
1117
  // Network and CORS failures reach the page as a bare TypeError, as in
1051
1118
  // browsers; the reason is only logged to the console.
1052
1119
  pending = networkPending.catch((error) => {
1053
1120
  const text = `fetch ${plain.url}: ${error?.message ?? error}`;
1054
- consoleEntries.push({ level: "error", text });
1055
- 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();
1056
1126
  throw new TypeError("Failed to fetch");
1057
1127
  });
1058
1128
  }
1129
+ let abort;
1059
1130
  const aborted = signal
1060
- ? new Promise((_, reject) => signal.addEventListener("abort", () => {
1061
- networkPending?.abort?.();
1062
- reject(abortError());
1063
- }, { 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
+ })
1064
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);
1065
1148
  const data = await (aborted ? Promise.race([pending, aborted]) : pending);
1066
1149
  hooks.afterTask();
1067
1150
  return makeResponse(data);
@@ -1075,7 +1158,6 @@ export function createPageRealm({ window, document, mode, hooks }) {
1075
1158
  let url = null;
1076
1159
  let response = null;
1077
1160
  let controller = null;
1078
- let timer = null;
1079
1161
  const xhr = {
1080
1162
  UNSENT: 0, OPENED: 1, HEADERS_RECEIVED: 2, LOADING: 3, DONE: 4,
1081
1163
  readyState: 0,
@@ -1101,7 +1183,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1101
1183
  requestHeaders.append(String(name), String(value));
1102
1184
  },
1103
1185
  getResponseHeader(name) {
1104
- return response?.headers.get(String(name)) ?? null;
1186
+ return response ? HostHeaders.prototype.get.call(response.headers, String(name)) : null;
1105
1187
  },
1106
1188
  getAllResponseHeaders() {
1107
1189
  if (!response) return "";
@@ -1113,15 +1195,19 @@ export function createPageRealm({ window, document, mode, hooks }) {
1113
1195
  },
1114
1196
  send(body = null) {
1115
1197
  if (xhr.readyState !== 1) throw new DOMException("The object's state must be OPENED.", "InvalidStateError");
1116
- controller = new AbortController();
1117
- 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;
1118
1204
  fire("loadstart");
1119
1205
  pageFetch(url, {
1120
1206
  method,
1121
1207
  headers: requestHeaders,
1122
1208
  body: ["GET", "HEAD"].includes(method) ? null : body,
1123
1209
  credentials: xhr.withCredentials ? "include" : "same-origin",
1124
- signal: controller.signal,
1210
+ signal: requestController.signal,
1125
1211
  }).then(async (result) => {
1126
1212
  response = result;
1127
1213
  xhr.status = result.status;
@@ -1130,19 +1216,27 @@ export function createPageRealm({ window, document, mode, hooks }) {
1130
1216
  setState(2);
1131
1217
  setState(3);
1132
1218
  const bytes = await result.arrayBuffer();
1133
- const text = new TextDecoder().decode(bytes);
1219
+ const text = xhr.responseType === "arraybuffer" || xhr.responseType === "blob"
1220
+ ? "" : new TextDecoder().decode(bytes);
1134
1221
  xhr.responseText = ["", "text"].includes(xhr.responseType) ? text : "";
1135
1222
  xhr.response = xhr.responseType === "json" ? safeJson(text)
1136
1223
  : xhr.responseType === "arraybuffer" ? bytes
1137
- : 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") ?? "" })
1138
1225
  : text;
1139
1226
  setState(4);
1140
1227
  fire("load");
1141
1228
  fire("loadend");
1142
- }, (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;
1143
1233
  xhr.status = 0;
1234
+ xhr.statusText = "";
1235
+ xhr.responseURL = "";
1236
+ xhr.response = null;
1237
+ xhr.responseText = "";
1144
1238
  setState(4);
1145
- fire(error?.name === "AbortError" ? (timer !== null && xhr.timeout > 0 ? "timeout" : "abort") : "error");
1239
+ fire(timedOut ? "timeout" : requestController.signal.aborted ? "abort" : "error");
1146
1240
  fire("loadend");
1147
1241
  }).finally(() => {
1148
1242
  if (timer !== null) cancel(timer);
@@ -1308,9 +1402,9 @@ export function createPageRealm({ window, document, mode, hooks }) {
1308
1402
  fetch: function fetch(input, init = undefined) { return pageFetch(input, init); },
1309
1403
  XMLHttpRequest,
1310
1404
  WebSocket,
1311
- Headers,
1405
+ Headers: PageHeaders,
1312
1406
  Request: PageRequest,
1313
- Response,
1407
+ Response: PageResponse,
1314
1408
  AbortController,
1315
1409
  AbortSignal,
1316
1410
  FormData,
@@ -1476,6 +1570,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1476
1570
  return Promise.resolve(values);
1477
1571
  },
1478
1572
  };
1573
+ installImageLoadingAttributes(elementInterfacePrototypes.get("HTMLImageElement"), nodeOf);
1479
1574
  const { location, setHref } = locationBinding(document.URL, hooks.navigate);
1480
1575
  const { History, history, pushEntry } = historyBinding(location, setHref);
1481
1576
  const { Event, CustomEvent, ...uiEvents } = eventConstructors(realm);
@@ -1487,6 +1582,10 @@ export function createPageRealm({ window, document, mode, hooks }) {
1487
1582
  const messaging = createMessaging({
1488
1583
  realm, window, pageGlobal: () => pageGlobal, runTask, callPage, DOMException, frames: hooks.frames ?? null,
1489
1584
  });
1585
+ const broadcasts = createBroadcastChannels({
1586
+ origin: hooks.frames?.self.origin() ?? new URL(document.URL).origin,
1587
+ EventTarget: eventTargets.EventTarget, realm, runTask, DOMException,
1588
+ });
1490
1589
 
1491
1590
  const platform = platformObjects({
1492
1591
  eventTargets, userAgent, userAgentDataMembers: chromeVersion ? userAgentDataMembers : null, mobile: mobileUserAgent, viewport: hooks.viewport,
@@ -1533,12 +1632,13 @@ export function createPageRealm({ window, document, mode, hooks }) {
1533
1632
  // SES rewrites dynamic import() in evaluated classic scripts to this
1534
1633
  // hook. Resolve it through the same module loader and network boundary as
1535
1634
  // parser-inserted modules.
1536
- __import__: (specifier) => importModule(String(specifier), currentScript?.src || document.URL),
1635
+ __import__: (specifier) => importModule(String(specifier), currentScript?.src || document.baseURI || document.URL),
1537
1636
  performance: platform.performance,
1538
1637
  PerformanceObserver: userTiming.PerformanceObserver,
1539
1638
  crypto: platform.crypto,
1540
1639
  ...platform.interfaces,
1541
1640
  CSSStyleDeclaration: cssStyleDeclarationInterface(realm),
1641
+ ValidityState: validity.Interface,
1542
1642
  ...trustedTypes.interfaces,
1543
1643
  trustedTypes: trustedTypes.trustedTypes,
1544
1644
  Event,
@@ -1591,15 +1691,19 @@ export function createPageRealm({ window, document, mode, hooks }) {
1591
1691
  const tag = String(name).toLowerCase();
1592
1692
  if (typeof constructor !== "function") throw new TypeError("Custom element constructor must be a function");
1593
1693
  if (customElementConstructors.has(tag)) throw new DOMException("Custom element already defined", "NotSupportedError");
1694
+ let captureInitialAttributes;
1594
1695
  const Adapter = class extends window.HTMLElement {
1595
1696
  constructor() {
1596
1697
  super();
1698
+ const initialAttributes = captureInitialAttributes?.(this);
1597
1699
  const wrapper = realm.wrap(this);
1598
1700
  Object.setPrototypeOf(wrapper, constructor.prototype);
1599
1701
  const previous = constructingElement;
1600
1702
  constructingElement = this;
1601
1703
  try { Reflect.construct(constructor, []); }
1704
+ catch (error) { initialAttributes?.cancel(); throw error; }
1602
1705
  finally { constructingElement = previous; }
1706
+ initialAttributes?.finish();
1603
1707
  }
1604
1708
  connectedCallback() {
1605
1709
  const wrapper = realm.wrap(this);
@@ -1610,8 +1714,16 @@ export function createPageRealm({ window, document, mode, hooks }) {
1610
1714
  if (typeof wrapper.disconnectedCallback === "function") callPage(wrapper.disconnectedCallback, wrapper, []);
1611
1715
  }
1612
1716
  };
1613
- window.customElements.define(tag, Adapter);
1717
+ captureInitialAttributes = installAttributeReactions(Adapter, constructor, realm, callPage);
1614
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
+ }
1615
1727
  // Upgrades change :defined without a DOM mutation.
1616
1728
  hooks.definedChanged?.(tag);
1617
1729
  },
@@ -1626,6 +1738,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1626
1738
  },
1627
1739
  },
1628
1740
  ...INTERFACES,
1741
+ DOMStringMap,
1629
1742
  Node: NodeInterface,
1630
1743
  Text: TextInterface,
1631
1744
  CharacterData: CharacterDataInterface,
@@ -1634,6 +1747,8 @@ export function createPageRealm({ window, document, mode, hooks }) {
1634
1747
  Document: DocumentInterface,
1635
1748
  HTMLDocument: DocumentInterface,
1636
1749
  DocumentFragment: DocumentFragmentInterface,
1750
+ DOMParser,
1751
+ XMLSerializer,
1637
1752
  ...typeCheckInterfaces(),
1638
1753
  addEventListener: windowEvents.addEventListener,
1639
1754
  removeEventListener: windowEvents.removeEventListener,
@@ -1682,6 +1797,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1682
1797
  postMessage: messaging.postMessage,
1683
1798
  MessageChannel: messaging.MessageChannel,
1684
1799
  MessagePort: messaging.MessagePort,
1800
+ BroadcastChannel: broadcasts.BroadcastChannel,
1685
1801
  };
1686
1802
  const accessors = {
1687
1803
  innerWidth: viewportValue("width"),
@@ -1702,7 +1818,8 @@ export function createPageRealm({ window, document, mode, hooks }) {
1702
1818
  hardenBindings();
1703
1819
  // Dedicated workers get a compartment each, with these of the page's utilities.
1704
1820
  workers = createWorkers({
1705
- 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,
1706
1823
  constructorHelper,
1707
1824
  pagePorts: messaging.ports,
1708
1825
  globals: Object.fromEntries(["console", "atob", "btoa", "TextEncoder", "TextDecoder", "URL", "URLSearchParams",
@@ -1758,6 +1875,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1758
1875
  });
1759
1876
  Object.defineProperty(PagePromise, "length", { value: 1, configurable: true });
1760
1877
  pageGlobal.Promise = PagePromise;
1878
+ pageGlobal.Error = errorInterface(pageGlobal.Error, hostErrorIsError);
1761
1879
  // Namespace objects that libraries extend (reflect-metadata adds to
1762
1880
  // Reflect, polyfills to JSON and Math): each page gets its own mutable
1763
1881
  // copy with the same functions; the shared frozen intrinsics stay as they are.
@@ -1809,8 +1927,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1809
1927
  __rejectSomeDirectEvalExpressions__: false,
1810
1928
  });
1811
1929
  evaluate = (source) => {
1812
- const classic = rewriteClassicScriptSource(source);
1813
- return evaluatePrepared({ code: escapeSesHtmlComments(classic.code), parsed: classic.parsed, strict: classic.strict });
1930
+ return evaluatePrepared(prepareClassicScript(source));
1814
1931
  };
1815
1932
  // Module code is already strict with its own scope; no global lifting.
1816
1933
  evaluateModuleCode = (code) => compartment.evaluate(escapeSesHtmlComments(code, "module"), {
@@ -1877,7 +1994,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1877
1994
  if (node.nodeType !== 1) continue;
1878
1995
  for (const script of [node, ...node.querySelectorAll("script")]) {
1879
1996
  if (script.tagName !== "SCRIPT" || !createdScripts.has(script) || startedScripts.has(script)) continue;
1880
- if (!script.isConnected) continue;
1997
+ if (!script.isConnected || script.ownerDocument !== document) continue;
1881
1998
  runInsertedScript(script);
1882
1999
  }
1883
2000
  }
@@ -1903,7 +2020,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1903
2020
  runClassic(script, script.textContent);
1904
2021
  return;
1905
2022
  }
1906
- 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(
1907
2024
  async (source) => {
1908
2025
  runClassic(script, source, await preparedFor(prepareOffThread(source), source));
1909
2026
  runTask(() => script.dispatchEvent(new window.Event("load")));
@@ -1928,7 +2045,9 @@ export function createPageRealm({ window, document, mode, hooks }) {
1928
2045
  // phases, see module-transform.js.
1929
2046
 
1930
2047
  function ensureFetched(url, type, sourceOverride = null) {
1931
- 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}`;
1932
2051
  let record = modules.get(key);
1933
2052
  if (record) return record.fetched;
1934
2053
  record = { url, type, state: "fetching", namespace: Object.create(null), deps: [], error: null };
@@ -2048,11 +2167,12 @@ export function createPageRealm({ window, document, mode, hooks }) {
2048
2167
  async function runModuleScript(script) {
2049
2168
  const src = script.getAttribute("src");
2050
2169
  try {
2051
- if (src) await importModule(new URL(src, document.URL).href, document.URL);
2052
- 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 ?? "");
2053
2173
  if (src) runTask(() => script.dispatchEvent(new window.Event("load")));
2054
2174
  } catch (error) {
2055
- 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);
2056
2176
  if (src) runTask(() => script.dispatchEvent(new window.Event("error")));
2057
2177
  }
2058
2178
  }
@@ -2178,7 +2298,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
2178
2298
  continue;
2179
2299
  }
2180
2300
  try {
2181
- const url = new URL(src, document.URL).href;
2301
+ const url = new URL(src, script.baseURI || document.URL).href;
2182
2302
  if ((script.getAttribute("type") ?? "").trim().toLowerCase() === "module") {
2183
2303
  ensureFetched(resolveModuleSpecifier(url, document.URL, importMap), null).then(loadGraph).catch(() => {});
2184
2304
  } else {
@@ -2208,7 +2328,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
2208
2328
  let source;
2209
2329
  try {
2210
2330
  source = src
2211
- ? 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))
2212
2332
  : script.textContent ?? "";
2213
2333
  } catch (error) {
2214
2334
  reportError(error);
@@ -2284,6 +2404,12 @@ export function createPageRealm({ window, document, mode, hooks }) {
2284
2404
  messaging.receive(message);
2285
2405
  },
2286
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
+
2287
2413
  /** A MediaSource object, or the one registered at a blob URL. */
2288
2414
  isMediaSource(value) {
2289
2415
  return mediaScope.isMediaSource(value);
@@ -2297,9 +2423,15 @@ export function createPageRealm({ window, document, mode, hooks }) {
2297
2423
 
2298
2424
  dispose() {
2299
2425
  disposed = true;
2426
+ for (const pending of pendingPageFetches) pending.abort?.();
2427
+ pendingPageFetches.clear();
2428
+ disposeDocumentCanvases(document);
2300
2429
  stopRejectionEvents();
2430
+ layoutObservers.dispose();
2431
+ parserWindow?.happyDOM?.close?.();
2301
2432
  if (blobUrlFallback === mediaScope) blobUrlFallback = null;
2302
2433
  messaging.dispose();
2434
+ broadcasts.dispose();
2303
2435
  workers?.dispose();
2304
2436
  // Leaving the document closes its WebSockets (1001 "going away").
2305
2437
  for (const close of openSockets) close();
@@ -2478,56 +2610,6 @@ function directEvalHelper(compartment, pageGlobal, scriptText) {
2478
2610
  };
2479
2611
  }
2480
2612
 
2481
- /**
2482
- * window.History and window.history for one document. The methods live on a
2483
- * per-document History.prototype that pages may wrap (dev servers do, to see
2484
- * client-side routing). pushState/replaceState change the URL and state
2485
- * without navigating or firing events; traversal is not modelled.
2486
- */
2487
- function historyBinding(location, setHref) {
2488
- let state = null;
2489
- let length = 1;
2490
- const change = (data, url) => {
2491
- if (url === undefined || url === null) return;
2492
- const target = new URL(String(url), location.href);
2493
- if (target.origin !== new URL(location.href).origin) {
2494
- throw new DOMException(`A history state object with URL '${target.href}' cannot be created in a document with origin '${new URL(location.href).origin}'.`, "SecurityError");
2495
- }
2496
- setHref(target.href);
2497
- };
2498
- function History() {
2499
- throw new TypeError("Illegal constructor");
2500
- }
2501
- History.prototype = {
2502
- constructor: History,
2503
- get length() {
2504
- return length;
2505
- },
2506
- get state() {
2507
- return state;
2508
- },
2509
- scrollRestoration: "auto",
2510
- back() {},
2511
- forward() {},
2512
- go() {},
2513
- pushState(data, _title, url = undefined) {
2514
- change(data, url);
2515
- state = data ?? null;
2516
- length++;
2517
- },
2518
- replaceState(data, _title, url = undefined) {
2519
- change(data, url);
2520
- state = data ?? null;
2521
- },
2522
- };
2523
- // A fragment navigation adds an entry with no state (a same-URL one replaces it).
2524
- const pushEntry = (replace) => {
2525
- state = null;
2526
- if (!replace) length++;
2527
- };
2528
- return { History, history: Object.create(History.prototype), pushEntry };
2529
- }
2530
-
2531
2613
  /** window.location; `setHref` moves it for same-document (fragment) navigations. */
2532
2614
  function locationBinding(href, navigate) {
2533
2615
  const url = () => new URL(href);
@@ -2773,7 +2855,18 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
2773
2855
  const horizontal = (style.padding[1] ?? 0) + (style.padding[3] ?? 0) + (style.borderWidths[1] ?? 0) + (style.borderWidths[3] ?? 0);
2774
2856
  const vertical = (style.padding[0] ?? 0) + (style.padding[2] ?? 0) + (style.borderWidths[0] ?? 0) + (style.borderWidths[2] ?? 0);
2775
2857
  const transform = style.transform;
2776
- 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
+ };
2777
2870
  Object.assign(values, {
2778
2871
  display: style.display,
2779
2872
  position: style.position,
@@ -2783,10 +2876,16 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
2783
2876
  backgroundImage: style.backgroundImage == null ? "none" : String(style.backgroundImage),
2784
2877
  fontSize: px(style.fontSize),
2785
2878
  fontWeight: String(style.fontWeight),
2879
+ fontStyle: style.fontStyle ?? "normal",
2880
+ fontStretch: `${style.fontStretch ?? 100}%`,
2786
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",
2787
2884
  lineHeight: style.lineHeight == null ? "normal" : px(style.lineHeight),
2788
2885
  textAlign: style.textAlign,
2789
2886
  whiteSpace: style.whiteSpace,
2887
+ WebkitLineClamp: style.webkitLineClamp == null ? "none" : String(style.webkitLineClamp),
2888
+ WebkitBoxOrient: style.webkitBoxOrient,
2790
2889
  verticalAlign: typeof style.verticalAlign === "number" ? px(style.verticalAlign) : String(style.verticalAlign),
2791
2890
  textOverflow: style.textOverflow,
2792
2891
  boxSizing: style.boxSizing,
@@ -2811,9 +2910,8 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
2811
2910
  alignContent: style.alignContent,
2812
2911
  alignItems: style.alignItems,
2813
2912
  alignSelf: style.alignSelf,
2814
- rowGap: style.rowGap ? length(style.rowGap) : "normal",
2815
- columnGap: style.columnGap ? length(style.columnGap) : "normal",
2816
- borderRadius: length(style.borderRadius),
2913
+ rowGap: style.rowGapNormal ? "normal" : length(style.rowGap),
2914
+ columnGap: style.columnGapNormal ? "normal" : length(style.columnGap),
2817
2915
  transform: !transform ? "none"
2818
2916
  : `matrix(${transform.scaleX ?? 1}, 0, 0, ${transform.scaleY ?? 1}, ${translate(transform.x, bounds?.width ?? 0)}, ${translate(transform.y, bounds?.height ?? 0)})`,
2819
2917
  visibility: style.visibility ?? "visible",
@@ -2823,6 +2921,23 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
2823
2921
  overflowX: style.overflowScrollX ?? (style.overflowX === "clip" ? "clip" : "visible"),
2824
2922
  overflowY: style.overflowScrollY ?? (style.overflowY === "clip" ? "clip" : "visible"),
2825
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"];
2826
2941
  values.overflow = values.overflowX === values.overflowY ? values.overflowX : `${values.overflowX} ${values.overflowY}`;
2827
2942
  sides.forEach((side, index) => {
2828
2943
  values[`margin${side}`] = length(style.margin[index]);
@@ -2843,6 +2958,8 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
2843
2958
  };
2844
2959
  return {
2845
2960
  ...values,
2961
+ webkitLineClamp: values.WebkitLineClamp,
2962
+ webkitBoxOrient: values.WebkitBoxOrient,
2846
2963
  length: names.length,
2847
2964
  item: (index) => names[Number(index)] ?? "",
2848
2965
  getPropertyValue: (name) => {
@@ -2858,6 +2975,7 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
2858
2975
 
2859
2976
  function formatConsoleValue(value) {
2860
2977
  if (typeof value === "string") return value;
2978
+ if (hostErrorIsError?.(value)) return diagnosticError(value);
2861
2979
  try {
2862
2980
  return JSON.stringify(value) ?? String(value);
2863
2981
  } catch {
@@ -2865,6 +2983,19 @@ function formatConsoleValue(value) {
2865
2983
  }
2866
2984
  }
2867
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
+
2868
2999
  function safeJson(text) {
2869
3000
  try {
2870
3001
  return JSON.parse(text);