wasm-quickjs-dom 0.0.0-stage → 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SoraKumo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the \"Software\"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,223 @@
1
- # Temporary Holding Version
1
+ # wasm-quickjs-dom
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Run a web page's JavaScript **without a browser and without `eval`** — and read the resulting DOM.
4
+
5
+ It is a small JSDOM for places where JSDOM cannot go: Cloudflare Workers, Vercel / Next.js Edge, Deno, Bun, browsers, and plain Node.js. The page's scripts run in [QuickJS-ng](https://github.com/quickjs-ng/quickjs) compiled to WebAssembly, on a [linkedom](https://github.com/WebReflection/linkedom) DOM, so nothing needs `eval`, native modules or a headless browser.
6
+
7
+ ```ts
8
+ import { createDOM } from "wasm-quickjs-dom";
9
+
10
+ const dom = await createDOM({ url: "https://example.com/" });
11
+ console.log(await dom.eval("document.title"));
12
+ console.log(dom.serialize()); // the HTML after the page's scripts ran
13
+ dom.close();
14
+ ```
15
+
16
+ **Good for:** pre-rendering single-page apps to HTML (SEO, OGP, link previews), reading data that a page builds with JavaScript, running DOM code in tests on edge runtimes, and handing a scripted page to an image renderer such as [wasm-html-to-image](https://www.npmjs.com/package/wasm-html-to-image).
17
+
18
+ **Not a browser:** there is no layout of its own, no canvas, no `WebSocket` or `Worker`, and QuickJS is an interpreter, so very heavy pages are slow. See [What works](#what-works-and-what-does-not).
19
+
20
+ ## Install
21
+
22
+ ```sh
23
+ npm install wasm-quickjs-dom
24
+ ```
25
+
26
+ ESM only. Node.js 20 or later; the other runtimes below need nothing extra.
27
+
28
+ ## Quick start
29
+
30
+ ### Get the HTML a page has after its scripts ran
31
+
32
+ ```ts
33
+ import { createDOM } from "wasm-quickjs-dom";
34
+
35
+ const dom = await createDOM(
36
+ { url: "https://example.com/" }, // or { html: "<p>…</p>", baseUrl: "https://example.com/" }
37
+ { waitFor: "#app" }, // wait until this selector matches (default: let timers and requests settle)
38
+ );
39
+ try {
40
+ const html = dom.serialize();
41
+ } finally {
42
+ dom.close(); // always close: it frees the page
43
+ }
44
+ ```
45
+
46
+ ### Read values out of the page
47
+
48
+ `eval` runs code in the page and gives you the result as JSON. It takes a string, or a function and JSON arguments:
49
+
50
+ ```ts
51
+ const title = await dom.eval("document.title");
52
+ const count = await dom.eval((selector: string) => document.querySelectorAll(selector).length, "a.item");
53
+ const rows = await dom.eval(() => [...document.querySelectorAll("tr")].map((tr) => tr.textContent));
54
+ ```
55
+
56
+ A DOM node cannot cross the boundary: return what you read from it. A function is sent as source text, so it cannot use variables from your own scope.
57
+
58
+ ### Drive the page
59
+
60
+ ```ts
61
+ await dom.type("#search", "wasm"); // sets the value, fires input and change
62
+ await dom.click("button[type=submit]"); // dispatches a bubbling click
63
+ await dom.waitForSelector(".results"); // resolves true, or false if it never appears
64
+ dom.expose("saveRow", (row) => db.save(row)); // the page can call: await window.saveRow({...})
65
+ ```
66
+
67
+ ### Inside a Worker
68
+
69
+ ```ts
70
+ import { createDOM } from "wasm-quickjs-dom";
71
+
72
+ export default {
73
+ async fetch(request: Request) {
74
+ const target = new URL(request.url).searchParams.get("url")!;
75
+ const dom = await createDOM(
76
+ { url: target },
77
+ { limits: { allowedProtocols: ["https:"], blockPrivateHosts: true, maxResourceBytes: 5_000_000 } },
78
+ );
79
+ try {
80
+ return new Response(dom.serialize(), { headers: { "content-type": "text/html; charset=utf-8" } });
81
+ } finally {
82
+ dom.close();
83
+ }
84
+ },
85
+ };
86
+ ```
87
+
88
+ There is nothing to configure for the WebAssembly: the right way to load it is picked for the runtime. A complete Worker is in the repository: [`packages/cloudflare-prerender`](../cloudflare-prerender).
89
+
90
+ ### From the command line
91
+
92
+ ```sh
93
+ npx wasm-quickjs-dom https://example.com/ --wait "#app" > rendered.html
94
+ npx wasm-quickjs-dom https://example.com/ -e "document.title"
95
+ npx wasm-quickjs-dom page.html --base-url https://example.com/ # a file; "-" reads standard input
96
+ ```
97
+
98
+ See [Command line](#command-line) below.
99
+
100
+ ## Pages you do not control
101
+
102
+ A page's scripts can `fetch`, and so can the scripts and modules it loads. **Without `limits`, a page can reach every host your runtime can reach, internal ones included.** For anything you did not write, set them:
103
+
104
+ ```ts
105
+ const dom = await createDOM({ url }, {
106
+ limits: {
107
+ allowedProtocols: ["https:"],
108
+ allowedHosts: ["example.com", "cdn.example.com"], // exact hostnames
109
+ blockPrivateHosts: true, // localhost, private, loopback, link-local
110
+ maxResourceCount: 50,
111
+ maxResourceBytes: 5_000_000,
112
+ maxTotalResourceBytes: 20_000_000,
113
+ },
114
+ });
115
+ ```
116
+
117
+ - The limits cover the page itself (`url`), the page's own `fetch` / `XMLHttpRequest`, and every script and module it loads. With host or protocol limits, each redirect is checked before it is followed.
118
+ - `blockPrivateHosts` looks at hostnames and IP literals. It cannot see where a hostname resolves through DNS, so use `allowedHosts` as well when the target is not trusted.
119
+ - The scripts run inside the WebAssembly sandbox: they have no file system, no `process`, and reach the outside only through `fetch` (the limits above) and the functions you `expose`.
120
+ - Also bounded for you: each call is limited by `timeout`, and memory and stack by fixed caps.
121
+
122
+ ## API
123
+
124
+ ### `createDOM(source, options?)` → `Promise<QuickJSDOM>`
125
+
126
+ `source` is `{ url }`, or `{ html, baseUrl? }` for markup you already have (`baseUrl` is where relative URLs resolve and what `location` says).
127
+
128
+ | Option | Default | |
129
+ | ------ | ------- | --- |
130
+ | `timeout` | `10000` | Upper limit in ms for **each call** into the page (the initial run, each `eval`, `settle`, …). |
131
+ | `settleMs` | `1000` | After load, run timers and requests for at most this long. It is an upper bound: the page finishes as soon as nothing is pending. |
132
+ | `waitFor` | | A CSS selector to wait for after load instead of `settleMs`. |
133
+ | `width` | `800` | `window.innerWidth`; also what `matchMedia` answers for. |
134
+ | `limits` | none | What the page may fetch (see above). |
135
+ | `skip` | | `(url) => boolean`: return `true` for a URL the page should not get. The request fails without going to the network (a `<script>` gets its `error` event, a `fetch` rejects). Analytics and ads are most of a real site's script time, and a render rarely needs them. The page itself is never skipped. |
136
+ | `onLog` | | Called with each log entry as it happens (see `logs`). |
137
+ | `quickjsWasm` | | A compiled `WebAssembly.Module` of your own, instead of the packaged one. |
138
+
139
+ ### The page: `QuickJSDOM`
140
+
141
+ | Member | |
142
+ | ------ | --- |
143
+ | `baseUrl` | The document's URL (after redirects, when it was fetched). |
144
+ | `eval(code)` / `eval(fn, ...args)` | Run code in the page's global scope; get the result as JSON. Promises are awaited. A throw becomes a rejected promise. Calls run one after another. |
145
+ | `waitForSelector(selector)` | Wait up to `timeout` for a match; `false` if there is none (or can no longer be). |
146
+ | `click(selector)` | Dispatch a bubbling `click` on the first match; `false` if nothing matched. |
147
+ | `type(selector, text)` | Set the first match's `value` (or text), then fire `input` and `change`; `false` if nothing matched. |
148
+ | `expose(name, fn)` | Make a function of yours callable from the page as `await window[name](...args)`. Arguments and result are JSON; a throw rejects with the same message. |
149
+ | `settle(ms?)` | Let timers and requests run for up to `ms` (default `settleMs`). |
150
+ | `serialize()` | The document as HTML, as it is now. |
151
+ | `logs` | What happened so far (the last 1000): `console` output, script errors, unhandled promise rejections, and requests with their status or failure. The first place to look when a page comes back empty. |
152
+ | `close()` | Free the page. It cannot be used afterwards. |
153
+ | `toOptions(options)` | Fill `value` (the serialized DOM) and `baseUrl` of an options object — for example the render options of wasm-html-to-image. |
154
+ | `markForLayout()` / `setLayout(boxes)` | Give the page a layout (see below). |
155
+
156
+ **The page's clock runs only while one of these calls is waiting.** Timers and requests progress during `eval`, `settle`, `waitForSelector` and so on, and stand still in between.
157
+
158
+ Log entries look like `{ type: "console", level, message }`, `{ type: "error", message, source }`, `{ type: "rejection", message }` and `{ type: "request", method, url, status?, error? }`.
159
+
160
+ ### `runPageScriptsQuickJS(source, options?)` → `Promise<{ html, baseUrl }>`
161
+
162
+ For when you only want the HTML: runs the page with the same options as `createDOM` and closes it for you.
163
+
164
+ ### Layout
165
+
166
+ There is no layout inside QuickJS, so `getBoundingClientRect()`, `offsetWidth` and the like return 0 — until you give the page boxes with `setLayout({ [elementNumber]: [x, y, width, height] })`. With `wasm-html-to-image` this is one call, `measureLayout(dom, render)`, which lays the page out with its engine and fills the boxes in (inline elements included). Use it for pages whose scripts size things from the real layout.
167
+
168
+ ## What works, and what does not
169
+
170
+ **Works:** inline, external, dynamically added and `type="module"` scripts (static `import`s and `import("literal")` are fetched up front); `fetch` and `XMLHttpRequest` (binary safe); timers, `requestAnimationFrame`, promises; `history.pushState`, `localStorage` / `sessionStorage` (in memory), `customElements`, `MutationObserver`, `crypto.getRandomValues` / `randomUUID`; `matchMedia` for the viewport width; `getComputedStyle` (the element's own style plus initial values); `IntersectionObserver` and `ResizeObserver` (each reports once on `observe()`, so lazy content loads). React and Next.js clients hydrate and run their effects.
171
+
172
+ **Does not:** layout (see above), `canvas`, `WebSocket`, `EventSource`, `Worker`, `iframe` content, CSS cascade computation, and anything that needs a real browser engine. A DOM smaller than JSDOM's. If a script throws, the page carries on with what it had; `dom.logs` tells you what went wrong.
173
+
174
+ **Speed:** a page starts in about 3 ms (the first in a process pays about 120 ms). A typical page, including its network, takes a few hundred ms; parsing runs at roughly 0.3 MB/s, so very large pages take seconds.
175
+
176
+ ## Where it runs
177
+
178
+ | Runtime | How the WebAssembly is loaded |
179
+ | ------- | ----------------------------- |
180
+ | Cloudflare Workers | imported as a pre-compiled module (`workerd` condition) |
181
+ | Vercel / Next.js Edge | imported with `?module` (`edge-light` condition) |
182
+ | Node.js, Bun | read from the package and compiled once |
183
+ | Deno, browsers | fetched from beside the module and compiled once |
184
+
185
+ All of that is automatic. If your setup differs, pass your own module: `createDOM(source, { quickjsWasm })` (`wasm-quickjs-dom/quickjs.wasm` is the file).
186
+
187
+ ## Command line
188
+
189
+ ```sh
190
+ wasm-quickjs-dom [options] <url | file | ->
191
+ ```
192
+
193
+ Prints the HTML after the page's scripts ran.
194
+
195
+ | Option | |
196
+ | ------ | --- |
197
+ | `-o, --output <file>` | Write to a file instead of standard output. |
198
+ | `-e, --eval <code>` | Print the value of this code instead (a string as it is, anything else as JSON). |
199
+ | `--json` | Print `{ baseUrl, html, logs }` as JSON. |
200
+ | `--logs` | Also write the page's log (console, errors, rejections, requests) to standard error. |
201
+ | `--wait <selector>`, `--settle <ms>`, `--timeout <ms>`, `--width <px>` | Shape the run (same as the options above). |
202
+ | `--base-url <url>` | The page's URL, for a file or standard input. |
203
+ | `--protocol <p>`, `--allow-host <h>`, `--block-host <h>`, `--block-private`, `--max-bytes <n>`, `--max-requests <n>` | The limits (repeat `--protocol`, `--allow-host` and `--block-host`). Set them for pages you do not control. |
204
+ | `--skip <text>` | Do not fetch URLs containing this text (repeatable). |
205
+
206
+ Exit code `0` is done, `1` the page could not be loaded (or `-e` threw), `2` wrong usage. `--help` lists everything.
207
+
208
+ ## How it works
209
+
210
+ Each page gets its own WebAssembly instance of QuickJS-ng. The browser APIs a bare QuickJS lacks, and linkedom, are compiled to QuickJS bytecode when the package is built; the first page of a process starts that program, and every later page is a copy of that started instance, which is why a page starts in about 3 ms. Nothing is shared between pages. Each gets its own clock origin and `Math.random` seed.
211
+
212
+ ## For contributors
213
+
214
+ The package builds its own QuickJS: `vendor/quickjs` (QuickJS-ng, unmodified) and `csrc/qjsdom.c` are compiled to a standalone `dist/quickjs.wasm` with Emscripten (`emcc` on `PATH`), and the guest program is compiled to bytecode with that very wasm.
215
+
216
+ ```sh
217
+ pnpm --filter wasm-quickjs-dom build
218
+ pnpm --filter wasm-quickjs-dom test
219
+ ```
220
+
221
+ ## License
222
+
223
+ MIT. QuickJS-ng, linkedom and web-streams-polyfill are included under their own licenses: see `THIRD-PARTY-NOTICES.md`.
@@ -0,0 +1,32 @@
1
+ # Third-party notices
2
+
3
+ `wasm-quickjs-dom` ships these works inside `dist/`.
4
+
5
+ - **QuickJS-ng** (v0.18.0, <https://github.com/quickjs-ng/quickjs>), compiled to `dist/quickjs.wasm`. Source in `vendor/quickjs/`. MIT:
6
+
7
+ The MIT License (MIT)
8
+
9
+ Copyright (c) 2017-2026 Fabrice Bellard
10
+ Copyright (c) 2017-2024 Charlie Gordon
11
+ Copyright (c) 2023-2026 Ben Noordhuis
12
+ Copyright (c) 2023-2026 Saúl Ibarra Corretgé
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in
22
+ all copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
27
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
30
+ THE SOFTWARE.
31
+
32
+ - **linkedom** (<https://github.com/WebReflection/linkedom>), ISC, and **web-streams-polyfill** (<https://github.com/MattiasBuelens/web-streams-polyfill>), MIT, are bundled into the guest program `dist/edge-js-payload.js`.
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { run } from "./cli.js";
4
+ const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
5
+ const code = await run(process.argv.slice(2), {
6
+ stdin: async () => {
7
+ const chunks = [];
8
+ for await (const chunk of process.stdin)
9
+ chunks.push(chunk);
10
+ return Buffer.concat(chunks).toString("utf8");
11
+ },
12
+ // a pipe closed early (`| head`) is not an error worth a stack trace
13
+ stdout: (text) => void process.stdout.write(text),
14
+ stderr: (text) => void process.stderr.write(text),
15
+ }, version);
16
+ process.exitCode = code;
package/dist/cli.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export interface CliIO {
2
+ /** Everything of standard input (read only when the input is `-`). */
3
+ stdin(): Promise<string>;
4
+ stdout(text: string): void;
5
+ stderr(text: string): void;
6
+ }
7
+ /** Run the command. Returns the exit code: 0 done, 1 the page could not be loaded, 2 wrong usage. */
8
+ export declare function run(argv: string[], io: CliIO, version?: string): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,197 @@
1
+ /**
2
+ * The `wasm-quickjs-dom` command: load a page, let its scripts run, print the result.
3
+ *
4
+ * wasm-quickjs-dom https://example.com/ --wait "#app" > rendered.html
5
+ * wasm-quickjs-dom page.html --base-url https://example.com/ -e "document.title"
6
+ * curl -s https://example.com/ | wasm-quickjs-dom - --base-url https://example.com/
7
+ *
8
+ * `run` takes its streams as arguments so it can be tested; `bin.ts` wires it to the process.
9
+ */
10
+ import { readFileSync, writeFileSync } from "node:fs";
11
+ import { parseArgs } from "node:util";
12
+ import { createDOM } from "./index.js";
13
+ const HELP = `wasm-quickjs-dom - run a page's scripts in QuickJS (WebAssembly) and print the result
14
+
15
+ Usage: wasm-quickjs-dom [options] <url | file | ->
16
+
17
+ <url> an http(s) page to fetch and run
18
+ <file> an HTML file ("-" reads standard input); use --base-url to say where it belongs,
19
+ which is also where its relative scripts are fetched from
20
+
21
+ Output (default: the HTML as it is after the scripts ran)
22
+ -o, --output <file> write to a file instead of standard output
23
+ -e, --eval <code> print the value of this code, run in the page (strings as they are, the rest as JSON)
24
+ --json print { baseUrl, html, logs } as JSON
25
+ --logs also print what happened in the page (console, errors, rejections, requests) to standard error
26
+
27
+ Running
28
+ --wait <selector> wait until a CSS selector matches (up to --timeout)
29
+ --settle <ms> at most this long for timers and requests after load (default 1000; ends early when idle)
30
+ --timeout <ms> upper limit for the scripts (default 10000)
31
+ --width <px> window width of the page (default 800)
32
+ --base-url <url> the page's URL, for file and standard input
33
+
34
+ Limits (set these for pages you do not control)
35
+ --protocol <p> allowed protocol such as https: (repeatable)
36
+ --allow-host <host> only these hosts may be fetched (repeatable)
37
+ --block-host <host> these hosts may not be fetched (repeatable)
38
+ --block-private refuse localhost and private networks
39
+ --max-bytes <n> refuse a response over n bytes
40
+ --max-requests <n> at most n requests
41
+ --skip <text> do not fetch URLs containing text (repeatable; analytics, ads, ...)
42
+
43
+ -h, --help show this help
44
+ -v, --version show the version
45
+ `;
46
+ const fail = (io, message) => {
47
+ io.stderr(`wasm-quickjs-dom: ${message}\n`);
48
+ return 2;
49
+ };
50
+ /** An error's message with the reason under it (Node's `fetch failed` hides the real one in `cause`). */
51
+ function describe(error) {
52
+ const e = error;
53
+ const why = e.cause?.message ?? e.cause?.code;
54
+ return why && !e.message.includes(why) ? `${e.message}: ${why}` : e.message;
55
+ }
56
+ function number(name, value) {
57
+ if (value === undefined)
58
+ return undefined;
59
+ const n = Number(value);
60
+ return Number.isFinite(n) && n >= 0 ? n : new Error(`--${name} needs a number, got "${value}"`);
61
+ }
62
+ function formatLog(entry) {
63
+ switch (entry.type) {
64
+ case "console":
65
+ return `console.${entry.level}: ${entry.message}`;
66
+ case "error":
67
+ return `error in ${entry.source}: ${entry.message.trim()}`;
68
+ case "rejection":
69
+ return `unhandled rejection: ${entry.message.trim()}`;
70
+ case "request":
71
+ return `${entry.method} ${entry.url} -> ${entry.error ?? entry.status}`;
72
+ }
73
+ }
74
+ /** Run the command. Returns the exit code: 0 done, 1 the page could not be loaded, 2 wrong usage. */
75
+ export async function run(argv, io, version = "0.0.0") {
76
+ let parsed;
77
+ try {
78
+ parsed = parseArgs({
79
+ args: argv,
80
+ allowPositionals: true,
81
+ options: {
82
+ output: { type: "string", short: "o" },
83
+ eval: { type: "string", short: "e" },
84
+ json: { type: "boolean" },
85
+ logs: { type: "boolean" },
86
+ wait: { type: "string" },
87
+ settle: { type: "string" },
88
+ timeout: { type: "string" },
89
+ width: { type: "string" },
90
+ "base-url": { type: "string" },
91
+ protocol: { type: "string", multiple: true },
92
+ "allow-host": { type: "string", multiple: true },
93
+ "block-host": { type: "string", multiple: true },
94
+ "block-private": { type: "boolean" },
95
+ "max-bytes": { type: "string" },
96
+ "max-requests": { type: "string" },
97
+ skip: { type: "string", multiple: true },
98
+ help: { type: "boolean", short: "h" },
99
+ version: { type: "boolean", short: "v" },
100
+ },
101
+ });
102
+ }
103
+ catch (error) {
104
+ return fail(io, `${error.message}\nTry --help.`);
105
+ }
106
+ const { values, positionals } = parsed;
107
+ if (values.help) {
108
+ io.stdout(HELP);
109
+ return 0;
110
+ }
111
+ if (values.version) {
112
+ io.stdout(`${version}\n`);
113
+ return 0;
114
+ }
115
+ if (positionals.length !== 1)
116
+ return fail(io, `give exactly one page (a URL, a file, or - for standard input)\nTry --help.`);
117
+ if (values.json && values.eval !== undefined)
118
+ return fail(io, "--json and --eval cannot be used together");
119
+ const numbers = {
120
+ settle: number("settle", values.settle),
121
+ timeout: number("timeout", values.timeout),
122
+ width: number("width", values.width),
123
+ maxBytes: number("max-bytes", values["max-bytes"]),
124
+ maxRequests: number("max-requests", values["max-requests"]),
125
+ };
126
+ for (const v of Object.values(numbers))
127
+ if (v instanceof Error)
128
+ return fail(io, v.message);
129
+ const n = numbers;
130
+ const limits = {
131
+ allowedProtocols: values.protocol,
132
+ allowedHosts: values["allow-host"],
133
+ blockedHosts: values["block-host"],
134
+ blockPrivateHosts: values["block-private"] || undefined,
135
+ maxResourceBytes: n.maxBytes,
136
+ maxResourceCount: n.maxRequests,
137
+ };
138
+ const skips = values.skip ?? [];
139
+ const options = {
140
+ settleMs: n.settle,
141
+ timeout: n.timeout,
142
+ waitFor: values.wait,
143
+ width: n.width,
144
+ limits,
145
+ skip: skips.length ? (url) => skips.some((s) => url.includes(s)) : undefined,
146
+ };
147
+ const input = positionals[0];
148
+ let source;
149
+ try {
150
+ if (/^https?:\/\//i.test(input))
151
+ source = { url: input };
152
+ else
153
+ source = { html: input === "-" ? await io.stdin() : readFileSync(input, "utf8"), baseUrl: values["base-url"] };
154
+ }
155
+ catch (error) {
156
+ return fail(io, `cannot read ${input}: ${error.message}`);
157
+ }
158
+ let dom;
159
+ try {
160
+ dom = await createDOM(source, options);
161
+ }
162
+ catch (error) {
163
+ io.stderr(`wasm-quickjs-dom: ${describe(error)}\n`);
164
+ return 1;
165
+ }
166
+ try {
167
+ let out;
168
+ if (values.eval !== undefined) {
169
+ const value = await dom.eval(values.eval);
170
+ out = typeof value === "string" ? value : JSON.stringify(value, null, 2) ?? "undefined";
171
+ }
172
+ else if (values.json) {
173
+ out = JSON.stringify({ baseUrl: dom.baseUrl, html: dom.serialize(), logs: dom.logs }, null, 2);
174
+ }
175
+ else {
176
+ out = dom.serialize();
177
+ }
178
+ if (values.logs)
179
+ for (const entry of dom.logs)
180
+ io.stderr(`${formatLog(entry)}\n`);
181
+ if (values.output)
182
+ writeFileSync(values.output, out.endsWith("\n") ? out : `${out}\n`);
183
+ else
184
+ io.stdout(out.endsWith("\n") ? out : `${out}\n`);
185
+ return 0;
186
+ }
187
+ catch (error) {
188
+ if (values.logs)
189
+ for (const entry of dom.logs)
190
+ io.stderr(`${formatLog(entry)}\n`);
191
+ io.stderr(`wasm-quickjs-dom: ${error.message}\n`);
192
+ return 1;
193
+ }
194
+ finally {
195
+ dom.close();
196
+ }
197
+ }