buninu-browser 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -1
- package/lib/cdp-server.js +47 -14
- package/lib/clipboard.js +22 -0
- package/lib/happy-dom/document-base.js +33 -0
- package/lib/happy-dom/parser.js +15 -6
- package/lib/happy-dom/selector-matcher.js +2 -2
- package/lib/happy-dom/selector-pseudo-adapter.js +19 -0
- package/lib/happy-dom/svg-attribute-adapter.js +50 -0
- package/lib/headless-shell.js +40 -11
- package/lib/input/hit-test.js +67 -19
- package/lib/layout/text-layout.js +213 -91
- package/lib/media/byte-pipe.js +2 -2
- package/lib/media/position-audio.js +10 -1
- package/lib/media/source-snapshots.js +126 -0
- package/lib/media/video-keyframe.js +17 -4
- package/lib/media/webm-stream.js +106 -0
- package/lib/network/network-service.js +268 -120
- package/lib/paint/display-list.js +8 -28
- package/lib/paint/frame-cache.js +25 -0
- package/lib/paint/frame.js +77 -16
- package/lib/paint/paragraph-text.js +24 -0
- package/lib/paint/raster-readback.js +41 -0
- package/lib/paint/raw-frames.js +47 -0
- package/lib/paint/rounded-rect.js +82 -0
- package/lib/paint/shared-frame-pool.js +105 -0
- package/lib/render-tree/index.js +38 -6
- package/lib/render-tree/svg.js +15 -7
- package/lib/renderer/bindings.js +233 -80
- package/lib/renderer/broadcast-channel.js +76 -0
- package/lib/renderer/canvas-2d.js +257 -14
- package/lib/renderer/canvas-text.js +120 -0
- package/lib/renderer/compositor-channel.js +103 -0
- package/lib/renderer/compositor.js +34 -40
- package/lib/renderer/custom-element-reactions.js +25 -1
- package/lib/renderer/flags.js +1 -1
- package/lib/renderer/font-loading.js +227 -0
- package/lib/renderer/host.js +133 -98
- package/lib/renderer/image-resources.js +65 -0
- package/lib/renderer/interfaces.js +28 -0
- package/lib/renderer/layout-observers.js +236 -0
- package/lib/renderer/main.js +52 -5
- package/lib/renderer/media-source.js +126 -55
- package/lib/renderer/page-realm.js +287 -99
- package/lib/renderer/page-renderer.js +663 -239
- package/lib/renderer/prepared-script-cache.js +50 -0
- package/lib/renderer/rendered-text.js +108 -0
- package/lib/renderer/rewrite-worker.js +4 -3
- package/lib/renderer/script-rewrite.js +13 -2
- package/lib/renderer/scroll-into-view.js +28 -0
- package/lib/renderer/slot-assignment.js +60 -0
- package/lib/renderer/video-presentation.js +23 -4
- package/lib/renderer/workers.js +2 -2
- package/lib/style/computed-style.js +611 -104
- package/lib/style/selector-traits.js +49 -5
- package/package.json +1 -1
- package/test/broadcast-channel.test.js +26 -0
- package/test/canvas-2d.test.js +83 -2
- package/test/canvas-paint.test.js +9 -0
- package/test/canvas-text.test.js +41 -0
- package/test/cdp-stream.test.js +56 -0
- package/test/clipboard.test.js +35 -0
- package/test/computed-style-binding.test.js +151 -0
- package/test/computed-style.test.js +258 -1
- package/test/display-list.test.js +12 -0
- package/test/document-base.test.js +87 -0
- package/test/dom-platform.test.js +461 -0
- package/test/fetch-lifecycle.test.js +161 -0
- package/test/fixtures/browser-raw-demo.html +36 -0
- package/test/fixtures/canvas-paint.js +117 -0
- package/test/fixtures/canvas-text.js +42 -0
- package/test/fixtures/image-lifetime.js +90 -0
- package/test/fixtures/media-seek-snapshot.js +77 -0
- package/test/fixtures/mse-audio-chirp.webm +0 -0
- package/test/fixtures/mse-switch-large.mp4 +0 -0
- package/test/fixtures/mse-switch-small.mp4 +0 -0
- package/test/fixtures/raw-capture-benchmark.json +76 -0
- package/test/fixtures/renderer-memory.js +110 -0
- package/test/fixtures/summary-style.js +28 -0
- package/test/font-loading.test.js +97 -0
- package/test/grid.test.js +58 -0
- package/test/hit-test.test.js +21 -1
- package/test/image-lifetime.test.js +12 -0
- package/test/image-resources.test.js +94 -0
- package/test/local-media.test.js +84 -0
- package/test/media-seek-snapshot.test.js +11 -0
- package/test/media-source-config.test.js +98 -0
- package/test/media-source-webm-accounting.test.js +98 -0
- package/test/media-source-webm.test.js +108 -0
- package/test/navigation.test.js +1 -0
- package/test/network-service.test.js +388 -0
- package/test/page-scripts.test.js +1 -1
- package/test/paint-frame.test.js +103 -0
- package/test/paint-paragraph-cache.test.js +87 -0
- package/test/position-audio.test.js +24 -0
- package/test/positioning.test.js +22 -0
- package/test/prepared-script-cache.test.js +45 -0
- package/test/pulse-client.test.js +10 -3
- package/test/raster-readback.test.js +40 -0
- package/test/raw-capture.test.js +152 -0
- package/test/render-tree.test.js +28 -0
- package/test/renderer-memory.test.js +11 -0
- package/test/selector-traits.test.js +24 -0
- package/test/shared-frame-pool.test.js +63 -0
- package/test/style-sheets.test.js +51 -0
- package/test/svg.test.js +29 -0
- package/test/text-layout.test.js +106 -0
- package/test/video-keyframe.test.js +61 -0
- package/test/video-presentation.test.js +69 -0
- package/test/window-resize.test.js +36 -0
- package/tools/bench-raw-capture.js +32 -0
- package/tools/browser-lab.js +78 -0
- package/tools/browser-soak.js +75 -0
- package/tools/network-reference.js +49 -0
- package/tools/process-memory.js +38 -0
- package/tools/script-benchmark.js +25 -0
- 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
|
-
|
|
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:
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
|
|
763
|
-
|
|
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
|
|
980
|
+
const PageRequest = class Request extends HostRequest {
|
|
929
981
|
constructor(input, init = undefined) {
|
|
930
|
-
super(input instanceof
|
|
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"
|
|
934
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1058
|
-
|
|
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) =>
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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:
|
|
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
|
-
}
|
|
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(
|
|
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,10 +1582,23 @@ 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,
|
|
1497
1592
|
performance: userTiming.performance, storages: { local: hostStorage.get(origin), session: new Map() },
|
|
1593
|
+
clipboardWrite: async text => {
|
|
1594
|
+
try {
|
|
1595
|
+
const url = new URL(document.URL);
|
|
1596
|
+
const secure = url.protocol === 'https:' || (url.protocol === 'http:' && ['localhost','127.0.0.1','[::1]'].includes(url.hostname));
|
|
1597
|
+
if (!secure || !hooks.clipboardWrite) throw Error('Clipboard unavailable');
|
|
1598
|
+
await hooks.clipboardWrite(text);
|
|
1599
|
+
}
|
|
1600
|
+
catch { throw new DOMException('Clipboard write was not allowed or failed', 'NotAllowedError'); }
|
|
1601
|
+
},
|
|
1498
1602
|
sendBeacon: (url, data) => {
|
|
1499
1603
|
network.fetch(url, { method: "POST", body: data, mode: "no-cors", credentials: "include", keepalive: true })
|
|
1500
1604
|
.catch(() => {});
|
|
@@ -1537,12 +1641,13 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
1537
1641
|
// SES rewrites dynamic import() in evaluated classic scripts to this
|
|
1538
1642
|
// hook. Resolve it through the same module loader and network boundary as
|
|
1539
1643
|
// parser-inserted modules.
|
|
1540
|
-
__import__: (specifier) => importModule(String(specifier), currentScript?.src || document.URL),
|
|
1644
|
+
__import__: (specifier) => importModule(String(specifier), currentScript?.src || document.baseURI || document.URL),
|
|
1541
1645
|
performance: platform.performance,
|
|
1542
1646
|
PerformanceObserver: userTiming.PerformanceObserver,
|
|
1543
1647
|
crypto: platform.crypto,
|
|
1544
1648
|
...platform.interfaces,
|
|
1545
1649
|
CSSStyleDeclaration: cssStyleDeclarationInterface(realm),
|
|
1650
|
+
ValidityState: validity.Interface,
|
|
1546
1651
|
...trustedTypes.interfaces,
|
|
1547
1652
|
trustedTypes: trustedTypes.trustedTypes,
|
|
1548
1653
|
Event,
|
|
@@ -1595,15 +1700,19 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
1595
1700
|
const tag = String(name).toLowerCase();
|
|
1596
1701
|
if (typeof constructor !== "function") throw new TypeError("Custom element constructor must be a function");
|
|
1597
1702
|
if (customElementConstructors.has(tag)) throw new DOMException("Custom element already defined", "NotSupportedError");
|
|
1703
|
+
let captureInitialAttributes;
|
|
1598
1704
|
const Adapter = class extends window.HTMLElement {
|
|
1599
1705
|
constructor() {
|
|
1600
1706
|
super();
|
|
1707
|
+
const initialAttributes = captureInitialAttributes?.(this);
|
|
1601
1708
|
const wrapper = realm.wrap(this);
|
|
1602
1709
|
Object.setPrototypeOf(wrapper, constructor.prototype);
|
|
1603
1710
|
const previous = constructingElement;
|
|
1604
1711
|
constructingElement = this;
|
|
1605
1712
|
try { Reflect.construct(constructor, []); }
|
|
1713
|
+
catch (error) { initialAttributes?.cancel(); throw error; }
|
|
1606
1714
|
finally { constructingElement = previous; }
|
|
1715
|
+
initialAttributes?.finish();
|
|
1607
1716
|
}
|
|
1608
1717
|
connectedCallback() {
|
|
1609
1718
|
const wrapper = realm.wrap(this);
|
|
@@ -1614,9 +1723,16 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
1614
1723
|
if (typeof wrapper.disconnectedCallback === "function") callPage(wrapper.disconnectedCallback, wrapper, []);
|
|
1615
1724
|
}
|
|
1616
1725
|
};
|
|
1617
|
-
installAttributeReactions(Adapter, constructor, realm, callPage);
|
|
1618
|
-
window.customElements.define(tag, Adapter);
|
|
1726
|
+
captureInitialAttributes = installAttributeReactions(Adapter, constructor, realm, callPage);
|
|
1619
1727
|
customElementConstructors.set(tag, constructor);
|
|
1728
|
+
try { window.customElements.define(tag, Adapter); }
|
|
1729
|
+
catch (error) {
|
|
1730
|
+
// The definition is observable while existing elements upgrade.
|
|
1731
|
+
// Validation failures happen before registration; constructor errors
|
|
1732
|
+
// can happen afterward and must not erase that registered identity.
|
|
1733
|
+
if (window.customElements.get(tag) !== Adapter) customElementConstructors.delete(tag);
|
|
1734
|
+
throw error;
|
|
1735
|
+
}
|
|
1620
1736
|
// Upgrades change :defined without a DOM mutation.
|
|
1621
1737
|
hooks.definedChanged?.(tag);
|
|
1622
1738
|
},
|
|
@@ -1631,6 +1747,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
1631
1747
|
},
|
|
1632
1748
|
},
|
|
1633
1749
|
...INTERFACES,
|
|
1750
|
+
DOMStringMap,
|
|
1634
1751
|
Node: NodeInterface,
|
|
1635
1752
|
Text: TextInterface,
|
|
1636
1753
|
CharacterData: CharacterDataInterface,
|
|
@@ -1639,6 +1756,8 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
1639
1756
|
Document: DocumentInterface,
|
|
1640
1757
|
HTMLDocument: DocumentInterface,
|
|
1641
1758
|
DocumentFragment: DocumentFragmentInterface,
|
|
1759
|
+
DOMParser,
|
|
1760
|
+
XMLSerializer,
|
|
1642
1761
|
...typeCheckInterfaces(),
|
|
1643
1762
|
addEventListener: windowEvents.addEventListener,
|
|
1644
1763
|
removeEventListener: windowEvents.removeEventListener,
|
|
@@ -1687,6 +1806,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
1687
1806
|
postMessage: messaging.postMessage,
|
|
1688
1807
|
MessageChannel: messaging.MessageChannel,
|
|
1689
1808
|
MessagePort: messaging.MessagePort,
|
|
1809
|
+
BroadcastChannel: broadcasts.BroadcastChannel,
|
|
1690
1810
|
};
|
|
1691
1811
|
const accessors = {
|
|
1692
1812
|
innerWidth: viewportValue("width"),
|
|
@@ -1707,7 +1827,8 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
1707
1827
|
hardenBindings();
|
|
1708
1828
|
// Dedicated workers get a compartment each, with these of the page's utilities.
|
|
1709
1829
|
workers = createWorkers({
|
|
1710
|
-
realm, documentUrl: () => document.URL,
|
|
1830
|
+
realm, documentUrl: () => document.URL, documentBaseUrl: () => document.baseURI || document.URL,
|
|
1831
|
+
fetchScript: hooks.fetchScript, runTask, callPage, reportError, DOMException,
|
|
1711
1832
|
constructorHelper,
|
|
1712
1833
|
pagePorts: messaging.ports,
|
|
1713
1834
|
globals: Object.fromEntries(["console", "atob", "btoa", "TextEncoder", "TextDecoder", "URL", "URLSearchParams",
|
|
@@ -1763,6 +1884,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
1763
1884
|
});
|
|
1764
1885
|
Object.defineProperty(PagePromise, "length", { value: 1, configurable: true });
|
|
1765
1886
|
pageGlobal.Promise = PagePromise;
|
|
1887
|
+
pageGlobal.Error = errorInterface(pageGlobal.Error, hostErrorIsError);
|
|
1766
1888
|
// Namespace objects that libraries extend (reflect-metadata adds to
|
|
1767
1889
|
// Reflect, polyfills to JSON and Math): each page gets its own mutable
|
|
1768
1890
|
// copy with the same functions; the shared frozen intrinsics stay as they are.
|
|
@@ -1814,8 +1936,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
1814
1936
|
__rejectSomeDirectEvalExpressions__: false,
|
|
1815
1937
|
});
|
|
1816
1938
|
evaluate = (source) => {
|
|
1817
|
-
|
|
1818
|
-
return evaluatePrepared({ code: escapeSesHtmlComments(classic.code), parsed: classic.parsed, strict: classic.strict });
|
|
1939
|
+
return evaluatePrepared(prepareClassicScript(source));
|
|
1819
1940
|
};
|
|
1820
1941
|
// Module code is already strict with its own scope; no global lifting.
|
|
1821
1942
|
evaluateModuleCode = (code) => compartment.evaluate(escapeSesHtmlComments(code, "module"), {
|
|
@@ -1882,7 +2003,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
1882
2003
|
if (node.nodeType !== 1) continue;
|
|
1883
2004
|
for (const script of [node, ...node.querySelectorAll("script")]) {
|
|
1884
2005
|
if (script.tagName !== "SCRIPT" || !createdScripts.has(script) || startedScripts.has(script)) continue;
|
|
1885
|
-
if (!script.isConnected) continue;
|
|
2006
|
+
if (!script.isConnected || script.ownerDocument !== document) continue;
|
|
1886
2007
|
runInsertedScript(script);
|
|
1887
2008
|
}
|
|
1888
2009
|
}
|
|
@@ -1908,7 +2029,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
1908
2029
|
runClassic(script, script.textContent);
|
|
1909
2030
|
return;
|
|
1910
2031
|
}
|
|
1911
|
-
const pending = hooks.fetchScript(new URL(src, document.URL).href).then(
|
|
2032
|
+
const pending = hooks.fetchScript(new URL(src, script.baseURI || document.URL).href).then(
|
|
1912
2033
|
async (source) => {
|
|
1913
2034
|
runClassic(script, source, await preparedFor(prepareOffThread(source), source));
|
|
1914
2035
|
runTask(() => script.dispatchEvent(new window.Event("load")));
|
|
@@ -1933,7 +2054,9 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
1933
2054
|
// phases, see module-transform.js.
|
|
1934
2055
|
|
|
1935
2056
|
function ensureFetched(url, type, sourceOverride = null) {
|
|
1936
|
-
|
|
2057
|
+
// Inline modules are distinct scripts sharing a real base URL. Their
|
|
2058
|
+
// identity must not leak into import.meta.url as an invented fragment.
|
|
2059
|
+
const key = sourceOverride === null ? `${type ?? "js"} ${url}` : `inline:${++inlineModuleCount}`;
|
|
1937
2060
|
let record = modules.get(key);
|
|
1938
2061
|
if (record) return record.fetched;
|
|
1939
2062
|
record = { url, type, state: "fetching", namespace: Object.create(null), deps: [], error: null };
|
|
@@ -2053,11 +2176,12 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
2053
2176
|
async function runModuleScript(script) {
|
|
2054
2177
|
const src = script.getAttribute("src");
|
|
2055
2178
|
try {
|
|
2056
|
-
|
|
2057
|
-
|
|
2179
|
+
const baseURL = script.baseURI || document.URL;
|
|
2180
|
+
if (src) await importModule(new URL(src, baseURL).href, baseURL);
|
|
2181
|
+
else await importModule(baseURL, baseURL, null, script.textContent ?? "");
|
|
2058
2182
|
if (src) runTask(() => script.dispatchEvent(new window.Event("load")));
|
|
2059
2183
|
} catch (error) {
|
|
2060
|
-
reportError(error, src ? new URL(src, document.URL).href : document.URL);
|
|
2184
|
+
reportError(error, src ? new URL(src, script.baseURI || document.URL).href : document.URL);
|
|
2061
2185
|
if (src) runTask(() => script.dispatchEvent(new window.Event("error")));
|
|
2062
2186
|
}
|
|
2063
2187
|
}
|
|
@@ -2183,7 +2307,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
2183
2307
|
continue;
|
|
2184
2308
|
}
|
|
2185
2309
|
try {
|
|
2186
|
-
const url = new URL(src, document.URL).href;
|
|
2310
|
+
const url = new URL(src, script.baseURI || document.URL).href;
|
|
2187
2311
|
if ((script.getAttribute("type") ?? "").trim().toLowerCase() === "module") {
|
|
2188
2312
|
ensureFetched(resolveModuleSpecifier(url, document.URL, importMap), null).then(loadGraph).catch(() => {});
|
|
2189
2313
|
} else {
|
|
@@ -2213,7 +2337,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
2213
2337
|
let source;
|
|
2214
2338
|
try {
|
|
2215
2339
|
source = src
|
|
2216
|
-
? await (preloaded.get(script) ?? hooks.fetchScript(new URL(src, document.URL).href))
|
|
2340
|
+
? await (preloaded.get(script) ?? hooks.fetchScript(new URL(src, script.baseURI || document.URL).href))
|
|
2217
2341
|
: script.textContent ?? "";
|
|
2218
2342
|
} catch (error) {
|
|
2219
2343
|
reportError(error);
|
|
@@ -2289,6 +2413,12 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
2289
2413
|
messaging.receive(message);
|
|
2290
2414
|
},
|
|
2291
2415
|
|
|
2416
|
+
/** The renderer completed layout or changed a scroll offset. */
|
|
2417
|
+
layoutChanged(version) {
|
|
2418
|
+
const viewport = hooks.viewport();
|
|
2419
|
+
layoutObservers.update(version, viewport.scrollX, viewport.scrollY, viewport.width, viewport.height, viewport.devicePixelRatio);
|
|
2420
|
+
},
|
|
2421
|
+
|
|
2292
2422
|
/** A MediaSource object, or the one registered at a blob URL. */
|
|
2293
2423
|
isMediaSource(value) {
|
|
2294
2424
|
return mediaScope.isMediaSource(value);
|
|
@@ -2302,9 +2432,15 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
2302
2432
|
|
|
2303
2433
|
dispose() {
|
|
2304
2434
|
disposed = true;
|
|
2435
|
+
for (const pending of pendingPageFetches) pending.abort?.();
|
|
2436
|
+
pendingPageFetches.clear();
|
|
2437
|
+
disposeDocumentCanvases(document);
|
|
2305
2438
|
stopRejectionEvents();
|
|
2439
|
+
layoutObservers.dispose();
|
|
2440
|
+
parserWindow?.happyDOM?.close?.();
|
|
2306
2441
|
if (blobUrlFallback === mediaScope) blobUrlFallback = null;
|
|
2307
2442
|
messaging.dispose();
|
|
2443
|
+
broadcasts.dispose();
|
|
2308
2444
|
workers?.dispose();
|
|
2309
2445
|
// Leaving the document closes its WebSockets (1001 "going away").
|
|
2310
2446
|
for (const close of openSockets) close();
|
|
@@ -2545,7 +2681,7 @@ function navigatorPlatform(userAgent) {
|
|
|
2545
2681
|
* global exposes for them. Desktop Chromium lists five PDF viewer plugins
|
|
2546
2682
|
* and their two MIME types whatever is installed (HTML §8.9.1.6).
|
|
2547
2683
|
*/
|
|
2548
|
-
function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile, viewport, performance, storages, sendBeacon }) {
|
|
2684
|
+
function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile, viewport, performance, storages, sendBeacon, clipboardWrite }) {
|
|
2549
2685
|
const EventTargetPrototype = eventTargets.EventTarget.prototype;
|
|
2550
2686
|
// An instance an event target's listeners can be added to.
|
|
2551
2687
|
const eventTarget = (prototype) => {
|
|
@@ -2623,6 +2759,12 @@ function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile
|
|
|
2623
2759
|
}, targetOptions);
|
|
2624
2760
|
const NavigatorUAData = userAgentDataMembers ? define("NavigatorUAData", userAgentDataMembers) : null;
|
|
2625
2761
|
const languages = Object.freeze(["en-US"]);
|
|
2762
|
+
const Clipboard = define('Clipboard', {
|
|
2763
|
+
writeText(text) {
|
|
2764
|
+
if (arguments.length===0) return Promise.reject(new TypeError('writeText requires text'));
|
|
2765
|
+
return clipboardWrite(String(text));
|
|
2766
|
+
},
|
|
2767
|
+
}, targetOptions);
|
|
2626
2768
|
const Navigator = define("Navigator", {
|
|
2627
2769
|
userAgent,
|
|
2628
2770
|
appCodeName: "Mozilla",
|
|
@@ -2648,6 +2790,7 @@ function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile
|
|
|
2648
2790
|
...(NavigatorUAData ? { userAgentData: NavigatorUAData.instance } : {}),
|
|
2649
2791
|
connection: NetworkInformation.instance,
|
|
2650
2792
|
permissions: Permissions.instance,
|
|
2793
|
+
clipboard: Clipboard.instance,
|
|
2651
2794
|
sendBeacon(url, data = null) { return sendBeacon(url, data); },
|
|
2652
2795
|
javaEnabled() { return false; },
|
|
2653
2796
|
getGamepads() { return [null, null, null, null]; },
|
|
@@ -2728,7 +2871,18 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
|
|
|
2728
2871
|
const horizontal = (style.padding[1] ?? 0) + (style.padding[3] ?? 0) + (style.borderWidths[1] ?? 0) + (style.borderWidths[3] ?? 0);
|
|
2729
2872
|
const vertical = (style.padding[0] ?? 0) + (style.padding[2] ?? 0) + (style.borderWidths[0] ?? 0) + (style.borderWidths[2] ?? 0);
|
|
2730
2873
|
const transform = style.transform;
|
|
2731
|
-
const translate = (value, size) =>
|
|
2874
|
+
const translate = (value, size) => {
|
|
2875
|
+
if (typeof value === "number") return value;
|
|
2876
|
+
if (value?.unit === "%") return value.value / 100 * size;
|
|
2877
|
+
if (value?.unit !== "math") return 0;
|
|
2878
|
+
if (value.kind === "calc") return value.px + value.percent / 100 * size;
|
|
2879
|
+
if (value.kind === "scale") return value.factor * translate(value.value, size);
|
|
2880
|
+
const parts = value.values.map(part => translate(part, size));
|
|
2881
|
+
if (value.kind === "sum") return parts.reduce((sum, part) => sum + part, 0);
|
|
2882
|
+
if (value.kind === "min") return Math.min(...parts);
|
|
2883
|
+
if (value.kind === "max") return Math.max(...parts);
|
|
2884
|
+
return Math.max(parts[0], Math.min(parts[1], parts[2]));
|
|
2885
|
+
};
|
|
2732
2886
|
Object.assign(values, {
|
|
2733
2887
|
display: style.display,
|
|
2734
2888
|
position: style.position,
|
|
@@ -2738,7 +2892,11 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
|
|
|
2738
2892
|
backgroundImage: style.backgroundImage == null ? "none" : String(style.backgroundImage),
|
|
2739
2893
|
fontSize: px(style.fontSize),
|
|
2740
2894
|
fontWeight: String(style.fontWeight),
|
|
2895
|
+
fontStyle: style.fontStyle ?? "normal",
|
|
2896
|
+
fontStretch: `${style.fontStretch ?? 100}%`,
|
|
2741
2897
|
fontFamily: (style.fontFamily ?? []).join(", "),
|
|
2898
|
+
translate: style.translate ? (style.translate.y === 0 ? length(style.translate.x)
|
|
2899
|
+
: `${length(style.translate.x)} ${length(style.translate.y)}`) : "none",
|
|
2742
2900
|
lineHeight: style.lineHeight == null ? "normal" : px(style.lineHeight),
|
|
2743
2901
|
textAlign: style.textAlign,
|
|
2744
2902
|
whiteSpace: style.whiteSpace,
|
|
@@ -2768,9 +2926,8 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
|
|
|
2768
2926
|
alignContent: style.alignContent,
|
|
2769
2927
|
alignItems: style.alignItems,
|
|
2770
2928
|
alignSelf: style.alignSelf,
|
|
2771
|
-
rowGap: style.
|
|
2772
|
-
columnGap: style.
|
|
2773
|
-
borderRadius: length(style.borderRadius),
|
|
2929
|
+
rowGap: style.rowGapNormal ? "normal" : length(style.rowGap),
|
|
2930
|
+
columnGap: style.columnGapNormal ? "normal" : length(style.columnGap),
|
|
2774
2931
|
transform: !transform ? "none"
|
|
2775
2932
|
: `matrix(${transform.scaleX ?? 1}, 0, 0, ${transform.scaleY ?? 1}, ${translate(transform.x, bounds?.width ?? 0)}, ${translate(transform.y, bounds?.height ?? 0)})`,
|
|
2776
2933
|
visibility: style.visibility ?? "visible",
|
|
@@ -2780,6 +2937,23 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
|
|
|
2780
2937
|
overflowX: style.overflowScrollX ?? (style.overflowX === "clip" ? "clip" : "visible"),
|
|
2781
2938
|
overflowY: style.overflowScrollY ?? (style.overflowY === "clip" ? "clip" : "visible"),
|
|
2782
2939
|
});
|
|
2940
|
+
const cornerNames = ["TopLeft", "TopRight", "BottomRight", "BottomLeft"];
|
|
2941
|
+
const cornerLengths = cornerNames.map(corner => (style[`border${corner}Radius`]
|
|
2942
|
+
?? [style.borderRadius, style.borderRadius]).map(length));
|
|
2943
|
+
cornerNames.forEach((corner, index) => {
|
|
2944
|
+
const [x, y] = cornerLengths[index];
|
|
2945
|
+
values[`border${corner}Radius`] = x === y ? x : `${x} ${y}`;
|
|
2946
|
+
});
|
|
2947
|
+
const compress = ([a, b, c, d]) => a === c && b === d ? a === b ? a : `${a} ${b}`
|
|
2948
|
+
: b === d ? `${a} ${b} ${c}` : `${a} ${b} ${c} ${d}`;
|
|
2949
|
+
const horizontalRadius = compress(cornerLengths.map(pair => pair[0]));
|
|
2950
|
+
const verticalRadius = compress(cornerLengths.map(pair => pair[1]));
|
|
2951
|
+
values.borderRadius = horizontalRadius === verticalRadius ? horizontalRadius : `${horizontalRadius} / ${verticalRadius}`;
|
|
2952
|
+
const rtl = style.direction === "rtl";
|
|
2953
|
+
values.borderStartStartRadius = values[rtl ? "borderTopRightRadius" : "borderTopLeftRadius"];
|
|
2954
|
+
values.borderStartEndRadius = values[rtl ? "borderTopLeftRadius" : "borderTopRightRadius"];
|
|
2955
|
+
values.borderEndStartRadius = values[rtl ? "borderBottomRightRadius" : "borderBottomLeftRadius"];
|
|
2956
|
+
values.borderEndEndRadius = values[rtl ? "borderBottomLeftRadius" : "borderBottomRightRadius"];
|
|
2783
2957
|
values.overflow = values.overflowX === values.overflowY ? values.overflowX : `${values.overflowX} ${values.overflowY}`;
|
|
2784
2958
|
sides.forEach((side, index) => {
|
|
2785
2959
|
values[`margin${side}`] = length(style.margin[index]);
|
|
@@ -2817,6 +2991,7 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
|
|
|
2817
2991
|
|
|
2818
2992
|
function formatConsoleValue(value) {
|
|
2819
2993
|
if (typeof value === "string") return value;
|
|
2994
|
+
if (hostErrorIsError?.(value)) return diagnosticError(value);
|
|
2820
2995
|
try {
|
|
2821
2996
|
return JSON.stringify(value) ?? String(value);
|
|
2822
2997
|
} catch {
|
|
@@ -2824,6 +2999,19 @@ function formatConsoleValue(value) {
|
|
|
2824
2999
|
}
|
|
2825
3000
|
}
|
|
2826
3001
|
|
|
3002
|
+
// SES retains a privileged stack reader in the host compartment. Use it only
|
|
3003
|
+
// when formatting developer diagnostics: pages still see tamed Error.stack
|
|
3004
|
+
// and never receive the reader itself.
|
|
3005
|
+
function diagnosticError(error) {
|
|
3006
|
+
const message = error?.name ? `${error.name}: ${error.message}` : String(error?.message ?? error);
|
|
3007
|
+
try {
|
|
3008
|
+
const stack = error?.stack || globalThis.getStackString?.(error);
|
|
3009
|
+
return stack ? (stack.startsWith(message) ? stack : `${message}\n${stack}`) : message;
|
|
3010
|
+
} catch {
|
|
3011
|
+
return message;
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
|
|
2827
3015
|
function safeJson(text) {
|
|
2828
3016
|
try {
|
|
2829
3017
|
return JSON.parse(text);
|