buninu-browser 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/README.md +74 -9
- package/bunfig.toml +1 -0
- package/buninu-browser.js +4 -0
- package/index.js +5 -0
- package/lib/audio/piano.js +24 -1
- package/lib/audio/pulse-client.js +120 -12
- package/lib/cdp-server.js +16 -11
- package/lib/happy-dom/parser.js +24 -4
- package/lib/headless-shell.js +66 -19
- package/lib/input/hit-test.js +25 -7
- package/lib/layout/text-layout.js +196 -49
- package/lib/log.js +5 -2
- package/lib/media/byte-pipe.js +82 -0
- package/lib/media/position-audio.js +42 -0
- package/lib/network/network-service.js +161 -14
- package/lib/paint/display-list.js +300 -14
- package/lib/paint/gradient.js +11 -5
- package/lib/render-tree/index.js +17 -2
- package/lib/render-tree/svg.js +2 -1
- package/lib/renderer/bindings.js +985 -68
- package/lib/renderer/canvas-2d.js +92 -0
- package/lib/renderer/host.js +158 -8
- package/lib/renderer/interfaces.js +215 -0
- package/lib/renderer/main.js +160 -12
- package/lib/renderer/media-source.js +819 -0
- package/lib/renderer/messaging.js +411 -0
- package/lib/renderer/native-functions.js +113 -0
- package/lib/renderer/page-realm.js +1405 -198
- package/lib/renderer/page-renderer.js +1736 -193
- package/lib/renderer/rejections.js +31 -0
- package/lib/renderer/rewrite-worker.js +22 -0
- package/lib/renderer/script-rewrite.js +805 -57
- package/lib/renderer/trusted-types.js +129 -0
- package/lib/renderer/workers.js +327 -0
- package/lib/screenshot-cli.js +2 -0
- package/lib/style/computed-style.js +162 -22
- package/lib/user-agent.js +14 -2
- package/package.json +4 -1
- package/patches/happy-dom/0002-use-bun-platform-adapters.patch +139 -19
- package/patches/happy-dom/0006-parse-css-escapes-in-selectors.patch +115 -0
- package/patches/happy-dom/README.md +26 -0
- package/patches/ses/0002-embedder-native-functions-source-text-sloppy-code.patch +106 -0
- package/patches/ses/0003-cache-sloppy-evaluators.patch +47 -0
- package/patches/ses/README.md +4 -0
- package/test/canvas-2d.test.js +55 -0
- package/test/compositor.test.js +3 -1
- package/test/computed-style.test.js +38 -1
- package/test/display-list.test.js +74 -1
- package/test/download.test.js +35 -0
- package/test/fixtures/long-video.mp4 +0 -0
- package/test/fixtures/media-sync.js +57 -0
- package/test/happy-dom-platform-adapters.test.js +25 -0
- package/test/headless-shell.test.js +1 -1
- package/test/hit-test.test.js +25 -1
- package/test/media-source.test.js +156 -0
- package/test/media-sync.test.js +12 -0
- package/test/mobile.test.js +2 -2
- package/test/navigation.test.js +61 -0
- package/test/network-service.test.js +14 -0
- package/test/page-scripts.test.js +1045 -7
- package/test/position-audio.test.js +67 -0
- package/test/positioning.test.js +95 -1
- package/test/pulse-client.test.js +69 -0
- package/test/renderer-host.test.js +5 -2
- package/test/screenshot-cli.test.js +25 -1
- package/test/script-rewrite.test.js +111 -6
- package/test/setup.js +5 -0
- package/test/user-agent.test.js +60 -4
- package/vendor/happy-dom/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts +52 -22
- package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +18 -17
- package/vendor/ses/dist/ses.mjs +52 -6
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trusted Types (W3C Trusted Types §2–§3): window.trustedTypes, the
|
|
3
|
+
* TrustedTypePolicyFactory that makes named policies, and the TrustedHTML,
|
|
4
|
+
* TrustedScript and TrustedScriptURL values a policy returns.
|
|
5
|
+
*
|
|
6
|
+
* Nothing is enforced: no Content-Security-Policy turns on
|
|
7
|
+
* `require-trusted-types-for`, so every sink still accepts strings, and a
|
|
8
|
+
* trusted value reaching a sink is used through its string value (its
|
|
9
|
+
* toString). What pages see is the API, which they feature-detect and use
|
|
10
|
+
* as in Chromium, and eval() of a TrustedScript, which runs its text
|
|
11
|
+
* (HostGetCodeForEval) where any other non-string is returned unchanged.
|
|
12
|
+
*/
|
|
13
|
+
import { platformObject } from "./interfaces.js";
|
|
14
|
+
|
|
15
|
+
const TRUSTED_KINDS = [
|
|
16
|
+
["TrustedHTML", "createHTML"],
|
|
17
|
+
["TrustedScript", "createScript"],
|
|
18
|
+
["TrustedScriptURL", "createScriptURL"],
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
|
|
22
|
+
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {object} [parent] the page's Object.prototype
|
|
26
|
+
* @returns {{ interfaces: Record<string, Function>, trustedTypes: object, scriptText: (value: unknown) => string | undefined }}
|
|
27
|
+
*/
|
|
28
|
+
export function trustedTypesApi(parent = Object.prototype) {
|
|
29
|
+
/** @type {Record<string, { made: ReturnType<typeof platformObject>, values: WeakMap<object, string> }>} */
|
|
30
|
+
const kinds = {};
|
|
31
|
+
for (const [name] of TRUSTED_KINDS) {
|
|
32
|
+
const values = new WeakMap();
|
|
33
|
+
const made = platformObject(name, {
|
|
34
|
+
toString() { return this.value; },
|
|
35
|
+
toJSON() { return this.value; },
|
|
36
|
+
}, { parent });
|
|
37
|
+
kinds[name] = { made, values };
|
|
38
|
+
}
|
|
39
|
+
const trusted = (name, value) => {
|
|
40
|
+
const kind = kinds[name];
|
|
41
|
+
const object = kind.made.create({ value });
|
|
42
|
+
kind.values.set(object, value);
|
|
43
|
+
return object;
|
|
44
|
+
};
|
|
45
|
+
const isKind = (name, value) => value !== null && typeof value === "object" && kinds[name].values.has(value);
|
|
46
|
+
|
|
47
|
+
let defaultPolicy = null;
|
|
48
|
+
const policyMembers = { get name() { return this.name; } };
|
|
49
|
+
for (const [kind, method] of TRUSTED_KINDS) {
|
|
50
|
+
// Create a Trusted Type (§4.3.1): the policy's callback turns the input
|
|
51
|
+
// into the value's text; null or undefined become the empty string.
|
|
52
|
+
policyMembers[method] = function (input, ...args) {
|
|
53
|
+
const callback = this.options[method];
|
|
54
|
+
if (typeof callback !== "function") {
|
|
55
|
+
throw new TypeError(`Failed to execute '${method}' on 'TrustedTypePolicy': Policy ${this.name}'s TrustedTypePolicyOptions did not specify a '${method}' member.`);
|
|
56
|
+
}
|
|
57
|
+
const result = Reflect.apply(callback, undefined, [String(input), ...args]);
|
|
58
|
+
return trusted(kind, result === null || result === undefined ? "" : String(result));
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const Policy = platformObject("TrustedTypePolicy", policyMembers, { parent });
|
|
62
|
+
|
|
63
|
+
const Factory = platformObject("TrustedTypePolicyFactory", {
|
|
64
|
+
createPolicy(name, options = {}) {
|
|
65
|
+
const policyName = String(name);
|
|
66
|
+
const callbacks = {};
|
|
67
|
+
for (const [, method] of TRUSTED_KINDS) {
|
|
68
|
+
const callback = options?.[method];
|
|
69
|
+
if (callback !== undefined && callback !== null) {
|
|
70
|
+
if (typeof callback !== "function") {
|
|
71
|
+
throw new TypeError(`Failed to execute 'createPolicy' on 'TrustedTypePolicyFactory': The '${method}' property of the dictionary is not a function.`);
|
|
72
|
+
}
|
|
73
|
+
callbacks[method] = callback;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (policyName === "default" && defaultPolicy) {
|
|
77
|
+
throw new TypeError("Failed to execute 'createPolicy' on 'TrustedTypePolicyFactory': Policy with name \"default\" already exists.");
|
|
78
|
+
}
|
|
79
|
+
const policy = Policy.create({ name: policyName, options: callbacks });
|
|
80
|
+
if (policyName === "default") defaultPolicy = policy;
|
|
81
|
+
return policy;
|
|
82
|
+
},
|
|
83
|
+
isHTML(value) { return isKind("TrustedHTML", value); },
|
|
84
|
+
isScript(value) { return isKind("TrustedScript", value); },
|
|
85
|
+
isScriptURL(value) { return isKind("TrustedScriptURL", value); },
|
|
86
|
+
get emptyHTML() { return trusted("TrustedHTML", ""); },
|
|
87
|
+
get emptyScript() { return trusted("TrustedScript", ""); },
|
|
88
|
+
// The sinks that would require a trusted type (§4.2.4 and §4.2.5).
|
|
89
|
+
getAttributeType(tagName, attribute, elementNs = "", attrNs = "") {
|
|
90
|
+
const tag = String(tagName).toLowerCase();
|
|
91
|
+
const name = String(attribute).toLowerCase();
|
|
92
|
+
const namespace = elementNs === null || elementNs === "" ? HTML_NAMESPACE : String(elementNs);
|
|
93
|
+
if (attrNs !== null && attrNs !== "") return null;
|
|
94
|
+
if (name.startsWith("on")) return "TrustedScript";
|
|
95
|
+
if (namespace === HTML_NAMESPACE) {
|
|
96
|
+
if (tag === "iframe" && name === "srcdoc") return "TrustedHTML";
|
|
97
|
+
if (tag === "script" && name === "src") return "TrustedScriptURL";
|
|
98
|
+
}
|
|
99
|
+
if (namespace === SVG_NAMESPACE && tag === "script" && name === "href") return "TrustedScriptURL";
|
|
100
|
+
return null;
|
|
101
|
+
},
|
|
102
|
+
getPropertyType(tagName, property, elementNs = "") {
|
|
103
|
+
const tag = String(tagName).toLowerCase();
|
|
104
|
+
const name = String(property);
|
|
105
|
+
const namespace = elementNs === null || elementNs === "" ? HTML_NAMESPACE : String(elementNs);
|
|
106
|
+
if (name === "innerHTML" || name === "outerHTML") return "TrustedHTML";
|
|
107
|
+
if (namespace === HTML_NAMESPACE && tag === "iframe" && name === "srcdoc") return "TrustedHTML";
|
|
108
|
+
if (namespace === HTML_NAMESPACE && tag === "script") {
|
|
109
|
+
if (name === "src") return "TrustedScriptURL";
|
|
110
|
+
if (name === "innerText" || name === "textContent" || name === "text") return "TrustedScript";
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
},
|
|
114
|
+
get defaultPolicy() { return defaultPolicy; },
|
|
115
|
+
}, { parent });
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
interfaces: {
|
|
119
|
+
TrustedHTML: kinds.TrustedHTML.made.Interface,
|
|
120
|
+
TrustedScript: kinds.TrustedScript.made.Interface,
|
|
121
|
+
TrustedScriptURL: kinds.TrustedScriptURL.made.Interface,
|
|
122
|
+
TrustedTypePolicy: Policy.Interface,
|
|
123
|
+
TrustedTypePolicyFactory: Factory.Interface,
|
|
124
|
+
},
|
|
125
|
+
trustedTypes: Factory.instance,
|
|
126
|
+
/** The text eval() runs for `value`: a TrustedScript's, or undefined for anything else. */
|
|
127
|
+
scriptText: (value) => (isKind("TrustedScript", value) ? kinds.TrustedScript.values.get(value) : undefined),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dedicated workers (HTML §10.2) for a document's realm. A worker's script
|
|
3
|
+
* runs in a compartment of its own with a worker global scope; messages in
|
|
4
|
+
* both directions are structured clones delivered as tasks.
|
|
5
|
+
*
|
|
6
|
+
* Workers run on the renderer's one thread, interleaved with the page's
|
|
7
|
+
* tasks: a worker that computes for long holds the page up meanwhile.
|
|
8
|
+
*
|
|
9
|
+
* importScripts() loads synchronously, which the renderer's network cannot:
|
|
10
|
+
* the scripts a worker names with string literals (and those they name in
|
|
11
|
+
* turn) are fetched before it starts, and importScripts() runs those.
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
CONSTRUCTOR_HELPER, escapeSesHtmlComments, REFERENCE_HELPER, rewriteClassicScript, rewriteConstructorReads, THIS_HELPER,
|
|
15
|
+
referenceHelper,
|
|
16
|
+
} from "./script-rewrite.js";
|
|
17
|
+
import { createPorts } from "./messaging.js";
|
|
18
|
+
|
|
19
|
+
/** Constructor reads, then the classic-script rewrite, sharing one parse when the first changes nothing. */
|
|
20
|
+
function rewriteWorkerScript(source) {
|
|
21
|
+
const reads = rewriteConstructorReads(source);
|
|
22
|
+
return rewriteClassicScript(reads.code, reads.program);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Captured before lockdown.
|
|
26
|
+
// importScripts('a.js', "b.js"): the literal arguments of each call.
|
|
27
|
+
const IMPORT_CALL = /\bimportScripts\s*\(([^)]*)\)/g;
|
|
28
|
+
const STRING_LITERAL = /(["'])((?:\\.|(?!\1)[^\\\n])*)\1/g;
|
|
29
|
+
const MAX_IMPORTED_SCRIPTS = 32;
|
|
30
|
+
const MAX_IMPORT_DEPTH = 4;
|
|
31
|
+
|
|
32
|
+
/** The script URLs a worker script passes to importScripts() as string literals. */
|
|
33
|
+
function literalImports(source, base) {
|
|
34
|
+
const urls = [];
|
|
35
|
+
for (const call of String(source).matchAll(IMPORT_CALL)) {
|
|
36
|
+
for (const literal of call[1].matchAll(STRING_LITERAL)) {
|
|
37
|
+
try {
|
|
38
|
+
urls.push(new URL(literal[2].replace(/\\(.)/g, "$1"), base).href);
|
|
39
|
+
} catch {
|
|
40
|
+
// An invalid URL fails when it is imported.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return urls;
|
|
45
|
+
}
|
|
46
|
+
const hostSetTimeout = globalThis.setTimeout;
|
|
47
|
+
const hostClearTimeout = globalThis.clearTimeout;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {{
|
|
51
|
+
* realm: object, documentUrl(): string, fetchScript(url: string): Promise<string>,
|
|
52
|
+
* runTask(task: () => void): void, callPage(fn: Function, thisArg: object, args: unknown[]): unknown,
|
|
53
|
+
* reportError(error: unknown, url?: string): void, DOMException: Function,
|
|
54
|
+
* globals: object, constructorHelper(compartment: object): Function,
|
|
55
|
+
* pagePorts: ReturnType<typeof createPorts>,
|
|
56
|
+
* }} options
|
|
57
|
+
* @returns {{ Worker: Function, dispose(): void }}
|
|
58
|
+
*/
|
|
59
|
+
export function createWorkers({
|
|
60
|
+
realm, documentUrl, fetchScript, runTask, callPage, reportError, DOMException, globals, constructorHelper, pagePorts,
|
|
61
|
+
}) {
|
|
62
|
+
const live = new Set();
|
|
63
|
+
|
|
64
|
+
/** Listeners and on* handlers of a worker or its global scope. */
|
|
65
|
+
function eventTarget(owner) {
|
|
66
|
+
const listeners = [];
|
|
67
|
+
const handlers = {};
|
|
68
|
+
return {
|
|
69
|
+
add(type, listener) {
|
|
70
|
+
if (typeof listener !== "function" && typeof listener?.handleEvent !== "function") return;
|
|
71
|
+
if (!listeners.some((entry) => entry.type === type && entry.listener === listener)) listeners.push({ type, listener });
|
|
72
|
+
},
|
|
73
|
+
remove(type, listener) {
|
|
74
|
+
const index = listeners.findIndex((entry) => entry.type === type && entry.listener === listener);
|
|
75
|
+
if (index >= 0) listeners.splice(index, 1);
|
|
76
|
+
},
|
|
77
|
+
handler: (type) => handlers[type] ?? null,
|
|
78
|
+
setHandler(type, value) {
|
|
79
|
+
handlers[type] = typeof value === "function" ? value : null;
|
|
80
|
+
},
|
|
81
|
+
dispatch(event, call) {
|
|
82
|
+
const handler = handlers[event.type];
|
|
83
|
+
if (handler) call(handler, owner(), [event]);
|
|
84
|
+
for (const { type, listener } of [...listeners]) {
|
|
85
|
+
if (type !== event.type) continue;
|
|
86
|
+
if (typeof listener === "function") call(listener, owner(), [event]);
|
|
87
|
+
else call(listener.handleEvent, listener, [event]);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function Worker(scriptURL, options = undefined) {
|
|
94
|
+
if (!new.target) throw new TypeError("Constructor Worker requires 'new'");
|
|
95
|
+
let url;
|
|
96
|
+
try {
|
|
97
|
+
url = new URL(String(scriptURL), documentUrl());
|
|
98
|
+
} catch {
|
|
99
|
+
throw new DOMException(`Failed to construct 'Worker': invalid URL '${scriptURL}'`, "SyntaxError");
|
|
100
|
+
}
|
|
101
|
+
const page = new URL(documentUrl());
|
|
102
|
+
if (!["blob:", "data:"].includes(url.protocol) && url.origin !== page.origin) {
|
|
103
|
+
throw new DOMException(`Script at '${url.href}' cannot be accessed from origin '${page.origin}'`, "SecurityError");
|
|
104
|
+
}
|
|
105
|
+
if (options?.type === "module") throw new TypeError("Module workers are not supported");
|
|
106
|
+
|
|
107
|
+
const worker = Object.create(Worker.prototype);
|
|
108
|
+
const outside = eventTarget(() => worker);
|
|
109
|
+
const inside = eventTarget(() => scope);
|
|
110
|
+
const timers = new Map();
|
|
111
|
+
let nextTimer = 1;
|
|
112
|
+
let terminated = false;
|
|
113
|
+
let started = false;
|
|
114
|
+
// Messages to the worker wait until its script has run.
|
|
115
|
+
const pending = [];
|
|
116
|
+
|
|
117
|
+
const state = {
|
|
118
|
+
outside,
|
|
119
|
+
terminate() {
|
|
120
|
+
terminated = true;
|
|
121
|
+
workerPorts.dispose();
|
|
122
|
+
for (const handle of timers.values()) hostClearTimeout(handle);
|
|
123
|
+
timers.clear();
|
|
124
|
+
pending.length = 0;
|
|
125
|
+
live.delete(state);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
live.add(state);
|
|
129
|
+
states.set(worker, state);
|
|
130
|
+
|
|
131
|
+
// ---- the worker's side
|
|
132
|
+
const workerTask = (task) => {
|
|
133
|
+
if (terminated) return;
|
|
134
|
+
try {
|
|
135
|
+
task();
|
|
136
|
+
} catch (error) {
|
|
137
|
+
workerError(error);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const workerCall = (fn, thisArg, args) => {
|
|
141
|
+
if (typeof fn === "function") Reflect.apply(fn, thisArg, args);
|
|
142
|
+
};
|
|
143
|
+
const schedule = (handler, delay, args, repeat) => {
|
|
144
|
+
const id = nextTimer++;
|
|
145
|
+
const fire = () => {
|
|
146
|
+
if (!timers.has(id)) return;
|
|
147
|
+
if (repeat) timers.set(id, hostSetTimeout(fire, Math.max(4, Number(delay) || 0)));
|
|
148
|
+
else timers.delete(id);
|
|
149
|
+
workerTask(() => typeof handler === "function" ? Reflect.apply(handler, scope, args) : evaluate(String(handler)));
|
|
150
|
+
};
|
|
151
|
+
timers.set(id, hostSetTimeout(fire, Math.max(0, Number(delay) || 0)));
|
|
152
|
+
return id;
|
|
153
|
+
};
|
|
154
|
+
const cancel = (id) => {
|
|
155
|
+
hostClearTimeout(timers.get(Number(id)));
|
|
156
|
+
timers.delete(Number(id));
|
|
157
|
+
};
|
|
158
|
+
const messageFor = (target, data, ports = []) => Object.freeze({
|
|
159
|
+
type: "message", data, origin: "", lastEventId: "", source: null, ports: Object.freeze([...ports]),
|
|
160
|
+
target, currentTarget: target, bubbles: false, cancelable: false, defaultPrevented: false,
|
|
161
|
+
timeStamp: globals.performance.now(), preventDefault() {}, stopPropagation() {}, stopImmediatePropagation() {},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const scopeGlobals = {
|
|
165
|
+
...globals,
|
|
166
|
+
location: Object.freeze({
|
|
167
|
+
href: url.href, origin: url.origin, protocol: url.protocol, host: url.host, hostname: url.hostname,
|
|
168
|
+
port: url.port, pathname: url.pathname, search: url.search, hash: url.hash, toString: () => url.href,
|
|
169
|
+
}),
|
|
170
|
+
postMessage(message, transfer = undefined) {
|
|
171
|
+
const { data, ports: taken } = workerPorts.pack(message, Array.isArray(transfer) ? transfer : transfer?.transfer);
|
|
172
|
+
hostSetTimeout(() => {
|
|
173
|
+
if (terminated) return;
|
|
174
|
+
runTask(() => outside.dispatch(realm.messageEvent("message", { data, ports: taken.map(pagePorts.adopt) }), callPage));
|
|
175
|
+
}, 0);
|
|
176
|
+
},
|
|
177
|
+
MessageChannel: undefined,
|
|
178
|
+
MessagePort: undefined,
|
|
179
|
+
setTimeout: (handler, delay = 0, ...args) => schedule(handler, delay, args, false),
|
|
180
|
+
setInterval: (handler, delay = 0, ...args) => schedule(handler, delay, args, true),
|
|
181
|
+
clearTimeout: cancel,
|
|
182
|
+
clearInterval: cancel,
|
|
183
|
+
queueMicrotask: (callback) => Promise.resolve().then(() => workerTask(() => workerCall(callback, scope, []))),
|
|
184
|
+
close() {
|
|
185
|
+
state.terminate();
|
|
186
|
+
},
|
|
187
|
+
importScripts(...urls) {
|
|
188
|
+
for (const value of urls) {
|
|
189
|
+
let target;
|
|
190
|
+
try {
|
|
191
|
+
target = new URL(String(value), url).href;
|
|
192
|
+
} catch {
|
|
193
|
+
throw new DOMException(`Failed to execute 'importScripts': invalid URL '${value}'`, "SyntaxError");
|
|
194
|
+
}
|
|
195
|
+
const source = imported.get(target);
|
|
196
|
+
if (typeof source !== "string") throw new DOMException(`The script at '${target}' failed to load`, "NetworkError");
|
|
197
|
+
evaluate(source);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
addEventListener: (type, listener) => inside.add(String(type), listener),
|
|
201
|
+
removeEventListener: (type, listener) => inside.remove(String(type), listener),
|
|
202
|
+
};
|
|
203
|
+
// The worker's own message ports; ports move between it and the page.
|
|
204
|
+
const workerPorts = createPorts({
|
|
205
|
+
makeEvent: (data, adopted) => messageFor(scope, data, adopted),
|
|
206
|
+
queueTask: (task) => hostSetTimeout(() => workerTask(task), 0),
|
|
207
|
+
call: workerCall,
|
|
208
|
+
DOMException,
|
|
209
|
+
});
|
|
210
|
+
scopeGlobals.MessageChannel = workerPorts.MessageChannel;
|
|
211
|
+
scopeGlobals.MessagePort = workerPorts.MessagePort;
|
|
212
|
+
const compartment = new Compartment({ globals: scopeGlobals, __options__: true });
|
|
213
|
+
const scope = compartment.globalThis;
|
|
214
|
+
const helper = constructorHelper(compartment);
|
|
215
|
+
Object.defineProperty(scope, CONSTRUCTOR_HELPER, { value: helper });
|
|
216
|
+
// A function's missing receiver is the worker's global, never the engine's (see rewriteClassicScript).
|
|
217
|
+
const hostGlobal = globalThis;
|
|
218
|
+
Object.defineProperty(scope, THIS_HELPER, {
|
|
219
|
+
value: (value) => value === undefined || value === null || value === hostGlobal ? scope : value,
|
|
220
|
+
});
|
|
221
|
+
Object.defineProperty(scope, REFERENCE_HELPER, { value: referenceHelper(scope) });
|
|
222
|
+
Object.defineProperty(scope, "Function", { value: helper.Function, writable: true, configurable: true });
|
|
223
|
+
Object.defineProperty(scope, "self", { value: scope, writable: true, configurable: true });
|
|
224
|
+
for (const type of ["message", "messageerror", "error"]) {
|
|
225
|
+
Object.defineProperty(scope, `on${type}`, {
|
|
226
|
+
configurable: true,
|
|
227
|
+
get: () => inside.handler(type),
|
|
228
|
+
set: (value) => inside.setHandler(type, value),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
const evaluate = (source) => compartment.evaluate(escapeSesHtmlComments(
|
|
232
|
+
rewriteWorkerScript(source).code), {
|
|
233
|
+
sloppyGlobalsMode: true,
|
|
234
|
+
__evadeImportExpressionTest__: true,
|
|
235
|
+
__evadeHtmlCommentTest__: true,
|
|
236
|
+
__rejectSomeDirectEvalExpressions__: false,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
function workerError(error) {
|
|
240
|
+
reportError(error, url.href);
|
|
241
|
+
const message = String(error?.message ?? error);
|
|
242
|
+
hostSetTimeout(() => {
|
|
243
|
+
if (terminated) return;
|
|
244
|
+
runTask(() => outside.dispatch(Object.freeze({
|
|
245
|
+
type: "error", message, filename: url.href, lineno: 0, colno: 0, error: null,
|
|
246
|
+
target: worker, currentTarget: worker, preventDefault() {}, stopPropagation() {},
|
|
247
|
+
}), callPage));
|
|
248
|
+
}, 0);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const deliverInside = ({ data, ports }) => {
|
|
252
|
+
hostSetTimeout(() => workerTask(() => inside.dispatch(messageFor(scope, data, ports.map(workerPorts.adopt)), workerCall)), 0);
|
|
253
|
+
};
|
|
254
|
+
state.post = (message) => {
|
|
255
|
+
if (terminated) return;
|
|
256
|
+
if (started) deliverInside(message);
|
|
257
|
+
else pending.push(message);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Scripts it imports with literal URLs, fetched ahead (see importScripts).
|
|
261
|
+
const imported = new Map();
|
|
262
|
+
const prefetch = async (source, base, depth) => {
|
|
263
|
+
for (const target of literalImports(source, base)) {
|
|
264
|
+
if (imported.has(target) || imported.size >= MAX_IMPORTED_SCRIPTS) continue;
|
|
265
|
+
imported.set(target, null);
|
|
266
|
+
try {
|
|
267
|
+
const text = await fetchScript(target);
|
|
268
|
+
imported.set(target, text);
|
|
269
|
+
if (depth < MAX_IMPORT_DEPTH) await prefetch(text, target, depth + 1);
|
|
270
|
+
} catch {
|
|
271
|
+
imported.delete(target);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
fetchScript(url.href).then(async (source) => {
|
|
277
|
+
await prefetch(source, url.href, 0);
|
|
278
|
+
if (terminated) return;
|
|
279
|
+
workerTask(() => evaluate(source));
|
|
280
|
+
started = true;
|
|
281
|
+
for (const data of pending.splice(0)) deliverInside(data);
|
|
282
|
+
}, (error) => workerError(error));
|
|
283
|
+
return worker;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const states = new WeakMap();
|
|
287
|
+
const stateOf = (worker) => {
|
|
288
|
+
const state = states.get(worker);
|
|
289
|
+
if (!state) throw new TypeError("Illegal invocation");
|
|
290
|
+
return state;
|
|
291
|
+
};
|
|
292
|
+
Worker.prototype = {
|
|
293
|
+
constructor: Worker,
|
|
294
|
+
postMessage(message, transfer = undefined) {
|
|
295
|
+
const state = stateOf(this);
|
|
296
|
+
state.post(pagePorts.pack(message, Array.isArray(transfer) ? transfer : transfer?.transfer));
|
|
297
|
+
},
|
|
298
|
+
terminate() {
|
|
299
|
+
stateOf(this).terminate();
|
|
300
|
+
},
|
|
301
|
+
addEventListener(type, listener) {
|
|
302
|
+
stateOf(this).outside.add(String(type), listener);
|
|
303
|
+
},
|
|
304
|
+
removeEventListener(type, listener) {
|
|
305
|
+
stateOf(this).outside.remove(String(type), listener);
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
for (const type of ["message", "messageerror", "error"]) {
|
|
309
|
+
Object.defineProperty(Worker.prototype, `on${type}`, {
|
|
310
|
+
configurable: true,
|
|
311
|
+
enumerable: true,
|
|
312
|
+
get() {
|
|
313
|
+
return stateOf(this).outside.handler(type);
|
|
314
|
+
},
|
|
315
|
+
set(value) {
|
|
316
|
+
stateOf(this).outside.setHandler(type, value);
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
Worker,
|
|
323
|
+
dispose() {
|
|
324
|
+
for (const state of [...live]) state.terminate();
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
package/lib/screenshot-cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
3
|
import { createBrowser } from "./headless-shell.js";
|
|
4
|
+
import { webkitFromArgs } from "./user-agent.js";
|
|
4
5
|
|
|
5
6
|
export function screenshotTargetUrl(input, { platform = process.platform, resolvePath = resolve, fileURL = pathToFileURL } = {}) {
|
|
6
7
|
const windowsPath = platform === "win32" && (/^[a-z]:/i.test(input) || input.startsWith("\\\\") || input.startsWith("//"));
|
|
@@ -39,6 +40,7 @@ export async function runScreenshot(args) {
|
|
|
39
40
|
const browser = await createBrowser({
|
|
40
41
|
allowHostJs: args.includes("--dangerously-allow-host-js"),
|
|
41
42
|
mobile: args.includes("--mobile"),
|
|
43
|
+
webkit: webkitFromArgs(args),
|
|
42
44
|
spareRenderer: false,
|
|
43
45
|
});
|
|
44
46
|
try {
|