@miaskiewicz/turbo-dom 0.5.8 → 0.7.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/Cargo.lock +3227 -389
- package/Cargo.toml +53 -3
- package/README.md +193 -224
- package/package.json +5 -3
- package/pkg/package.json +1 -1
- package/pkg/turbo_dom_parser_bg.wasm +0 -0
- package/pkg-web/package.json +1 -1
- package/pkg-web/turbo_dom_parser_bg.wasm +0 -0
- package/src/core.rs +387 -44
- package/src/lib.rs +43 -10
- package/src/runtime/buffer.mjs +0 -1
- package/src/runtime/cascade.mjs +18 -24
- package/src/runtime/color.mjs +1 -1
- package/src/runtime/css-shorthand-table.mjs +238 -0
- package/src/runtime/css-shorthand.mjs +2803 -0
- package/src/runtime/cssom.mjs +3 -4
- package/src/runtime/custom-elements.mjs +398 -0
- package/src/runtime/dom.mjs +924 -238
- package/src/runtime/html-serialize.mjs +15 -7
- package/src/runtime/index.d.ts +100 -0
- package/src/runtime/index.mjs +5 -0
- package/src/runtime/parser.mjs +2 -4
- package/src/runtime/selectors.mjs +1080 -402
- package/src/runtime/stubs.mjs +1 -19
- package/src/runtime/window.mjs +15 -7
- package/src/spike.rs +26 -6
- package/turbo-dom-parser.darwin-arm64.node +0 -0
- package/turbo-dom-parser.linux-arm64-gnu.node +0 -0
- package/turbo-dom-parser.linux-x64-gnu.node +0 -0
- package/turbo-dom-parser.linux-x64-musl.node +0 -0
- package/turbo-dom-parser.win32-x64-msvc.node +0 -0
package/Cargo.toml
CHANGED
|
@@ -1,13 +1,53 @@
|
|
|
1
1
|
[workspace]
|
|
2
2
|
# The root package (turbo-dom-parser) plus the standalone Rust-native DOM runtime
|
|
3
|
-
# crate published to crates.io as `turbo-dom
|
|
4
|
-
|
|
3
|
+
# crate published to crates.io as `turbo-dom`, plus the add-on crates:
|
|
4
|
+
# `turbo-dom-browser-env` (rtdom <-> V8 browser binding), `turbo-dom-layout` (HTML/CSS
|
|
5
|
+
# layout engine) and `turbo-dom-raster` (screenshot painter), the last two relocated
|
|
6
|
+
# from turbo-html2pdf-core / turbo-surf-raster. The parser ships to npm.
|
|
7
|
+
members = ["crates/turbo-dom", "crates/turbo-dom-browser-env", "crates/turbo-dom-layout", "crates/turbo-dom-raster"]
|
|
8
|
+
# turbo-dom-v8 is generated (crates/turbo-dom-v8/scripts/generate.py) into the
|
|
9
|
+
# gitignored crates/turbo-dom-v8/crate/ and built as its own standalone package, so a
|
|
10
|
+
# fresh checkout resolves this workspace without generating V8 first.
|
|
11
|
+
exclude = ["crates/turbo-dom-v8"]
|
|
12
|
+
|
|
13
|
+
# The pedantic clippy gate shared by the turbo-dom, turbo-dom-layout and
|
|
14
|
+
# turbo-dom-raster crates (`[lints] workspace = true`; the root parser package keeps its
|
|
15
|
+
# own copy below, which adds one FFI-specific allowance; turbo-dom-browser-env is
|
|
16
|
+
# lint-exempt by design). The allowed lints don't fit a DOM / layout / raster engine —
|
|
17
|
+
# "fixing" them would mangle correct, intentional code, so each is a documented opt-out
|
|
18
|
+
# rather than a silent suppression. Item-level `#[allow]`s in layout/raster carry their
|
|
19
|
+
# reason inline.
|
|
20
|
+
[workspace.lints.clippy]
|
|
21
|
+
pedantic = { level = "warn", priority = -1 }
|
|
22
|
+
# We bridge f64 layout math and integer indices/lengths with bounded, intentional casts.
|
|
23
|
+
cast_possible_truncation = "allow"
|
|
24
|
+
cast_precision_loss = "allow"
|
|
25
|
+
cast_sign_loss = "allow"
|
|
26
|
+
cast_possible_wrap = "allow"
|
|
27
|
+
# Exact float comparisons here test canonical/sentinel values (e.g. `0.0`), not approximations.
|
|
28
|
+
float_cmp = "allow"
|
|
29
|
+
# Single-char names are idiomatic for the tokenizer / loop indices (`i`, `j`, `n`, `c`).
|
|
30
|
+
many_single_char_names = "allow"
|
|
31
|
+
# Test-time DOM oracle: it deliberately doesn't document every panic/error path.
|
|
32
|
+
missing_panics_doc = "allow"
|
|
33
|
+
missing_errors_doc = "allow"
|
|
34
|
+
# A few config-ish structs legitimately carry several independent bool flags.
|
|
35
|
+
struct_excessive_bools = "allow"
|
|
36
|
+
# One large dispatch fn reads better intact than artificially split.
|
|
37
|
+
too_many_lines = "allow"
|
|
38
|
+
# We intentionally pin the concrete FxHashMap hasher rather than generalize over hashers.
|
|
39
|
+
implicit_hasher = "allow"
|
|
40
|
+
# `f32::midpoint` rounds through f64, so `(a + b) / 2.0` -> `a.midpoint(b)` can move the
|
|
41
|
+
# last bit of layout / paint geometry; outputs are pinned byte for byte, keep the f32 math.
|
|
42
|
+
manual_midpoint = "allow"
|
|
5
43
|
|
|
6
44
|
[package]
|
|
7
45
|
name = "turbo-dom-parser"
|
|
8
|
-
version = "0.
|
|
46
|
+
version = "0.7.0"
|
|
9
47
|
edition = "2021"
|
|
10
48
|
description = "html5ever-backed native parser for turbo-dom (Layer 1)"
|
|
49
|
+
# Ships to npm only (napi/wasm addon); never published to crates.io.
|
|
50
|
+
publish = false
|
|
11
51
|
|
|
12
52
|
[lib]
|
|
13
53
|
crate-type = ["cdylib", "rlib"]
|
|
@@ -27,6 +67,10 @@ wasm-bindgen = { version = "0.2", optional = true }
|
|
|
27
67
|
serde = { version = "1", features = ["derive"], optional = true }
|
|
28
68
|
serde-wasm-bindgen = { version = "0.6", optional = true }
|
|
29
69
|
|
|
70
|
+
[dev-dependencies]
|
|
71
|
+
# The Chrome 156 control fixture (src/core/conformance_tests.rs).
|
|
72
|
+
serde_json = "1"
|
|
73
|
+
|
|
30
74
|
[build-dependencies]
|
|
31
75
|
napi-build = { version = "2", optional = true }
|
|
32
76
|
|
|
@@ -69,3 +113,9 @@ opt-level = 3
|
|
|
69
113
|
[profile.profiling]
|
|
70
114
|
inherits = "release"
|
|
71
115
|
debug = true
|
|
116
|
+
|
|
117
|
+
# Pre-publish only: resolve the generated turbo-dom-v8 fork from its candidate branch
|
|
118
|
+
# (crates/turbo-dom-v8/scripts/push-candidate.sh). Remove once turbo-dom-v8 150.4.0 is
|
|
119
|
+
# on crates.io (see PUBLISHING.md).
|
|
120
|
+
[patch.crates-io]
|
|
121
|
+
turbo-dom-v8 = { git = "https://github.com/miaskiewicz/turbo-dom", rev = "e9b36c8733ef1cf1073f7053a8c5931381c978c5" }
|
package/README.md
CHANGED
|
@@ -1,34 +1,55 @@
|
|
|
1
1
|
# turbo-dom
|
|
2
2
|
|
|
3
|
-
Release **0.
|
|
3
|
+
Release **0.7.0**. turbo-dom is the shared browser-engine base of three projects:
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
- **[turbo-test](https://github.com/miaskiewicz/turbo-test)**, a vitest-compatible test runner. It runs tests in V8 over this repository's DOM.
|
|
6
|
+
- **[turbo-html2pdf](https://github.com/miaskiewicz/turbo-html2pdf)**, which renders HTML to PDF and uses this repository's layout engine.
|
|
7
|
+
- **[turbo-surf](https://github.com/miaskiewicz/turbo-surf)**, a headless browser that uses the DOM, the V8 binding, the layout engine and the screenshot painter.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
turbo-dom is also a standalone npm package: a fast, spec-correct DOM environment for
|
|
10
|
+
**vitest** and **jest**, usable in place of jsdom or happy-dom.
|
|
11
|
+
|
|
12
|
+
Up to 0.5.x, each consumer kept its own copy of these pieces. Since 0.6.0 each piece lives
|
|
13
|
+
in this repository once and is published from here. The consumers depend on the
|
|
14
|
+
published crates and keep no copies.
|
|
15
|
+
[docs/CONSOLIDATION.md](docs/CONSOLIDATION.md) has the architecture, the dependency graph
|
|
16
|
+
and the publish order.
|
|
17
|
+
|
|
18
|
+
## What is published
|
|
19
|
+
|
|
20
|
+
| package | registry | version | what it is | used by |
|
|
21
|
+
|---|---|---|---|---|
|
|
22
|
+
| [`@miaskiewicz/turbo-dom`](https://www.npmjs.com/package/@miaskiewicz/turbo-dom) | npm | 0.7.0 | lazy JS DOM runtime + native html5ever parser (N-API, WASM fallback) | vitest / jest users |
|
|
23
|
+
| [`turbo-dom`](crates/turbo-dom) | crates.io | 0.7.0 | pure-Rust DOM (`rtdom`), the one selector engine, CSSOM, partial cascade | all three consumers |
|
|
24
|
+
| [`turbo-dom-layout`](crates/turbo-dom-layout) | crates.io | 0.7.0 | HTML/CSS layout (block / inline / flex / grid / table), fonts, images, CSSOM-View geometry | turbo-html2pdf, turbo-surf |
|
|
25
|
+
| [`turbo-dom-raster`](crates/turbo-dom-raster) | crates.io | 0.7.0 | synthetic screenshots (PNG / SVG), canvas op-log replay, WebGL readback | turbo-surf |
|
|
26
|
+
| [`turbo-dom-browser-env`](crates/turbo-dom-browser-env) | crates.io | 0.7.0 | the `rtdom` <-> V8 browser environment (window, document, CSSOM, forms, events) | turbo-test, turbo-surf |
|
|
27
|
+
| [`turbo-dom-v8`](crates/turbo-dom-v8) | crates.io | 150.4.0 | turbo-dom's rusty_v8 fork, generated from the upstream crate plus reviewed patches | turbo-dom-browser-env, turbo-surf |
|
|
28
|
+
|
|
29
|
+
The root `turbo-dom-parser` crate is `publish = false`. It ships inside the npm package.
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
turbo-dom ──► turbo-dom-layout ──► turbo-dom-raster
|
|
33
|
+
│ │ │
|
|
34
|
+
│ ├──► turbo-html2pdf │
|
|
35
|
+
│ └────────────────────┴──► turbo-surf
|
|
36
|
+
└──► turbo-dom-browser-env ◄── turbo-dom-v8
|
|
37
|
+
├──► turbo-test
|
|
38
|
+
└──► turbo-surf
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
### npm (vitest / jest)
|
|
12
44
|
|
|
13
45
|
```bash
|
|
14
46
|
npm install -D @miaskiewicz/turbo-dom
|
|
15
47
|
```
|
|
16
48
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- ⚡ **Faster than both** — ~130× jsdom / ~45× happy-dom on a realistic suite (parse-memoized repeated shells), ~8–44× faster HTML parsing on real pages, and ~2.7× happy-dom on repeated queries while staying 99.7% spec-correct.
|
|
20
|
-
- 🎨 **Real computed style** — `getComputedStyle` runs a **partial cascade**: it resolves real
|
|
21
|
-
injected `<style>` rules (emotion/MUI `.css-HASH{…}`) + inline styles with proper specificity/source
|
|
22
|
-
order, **inherits the standard inheritable properties down the tree** (a global `body{color}` reaches
|
|
23
|
-
descendants), and **canonicalizes colors to `rgb()/rgba()`** like a browser (so
|
|
24
|
-
`toHaveStyle({color:'#fff'})` matches a `#ffffff`/`white`/`rgb(255,255,255)` rule). It only ever
|
|
25
|
-
returns values a real rule set — never an invented one.
|
|
26
|
-
- 🎯 **Honest, not lying** — no fake layout numbers; `getBoundingClientRect()` is zeros and computed
|
|
27
|
-
style never invents layout. Geometry tests belong in a real browser.
|
|
28
|
-
|
|
29
|
-
## Quick start
|
|
49
|
+
Prebuilt native binaries are bundled for linux x64 (glibc and musl), linux arm64,
|
|
50
|
+
macOS arm64 and windows x64. Other platforms use the WASM parser.
|
|
30
51
|
|
|
31
|
-
|
|
52
|
+
**vitest** (1–4):
|
|
32
53
|
|
|
33
54
|
```ts
|
|
34
55
|
// vitest.config.ts
|
|
@@ -38,255 +59,203 @@ import { createRequire } from 'node:module';
|
|
|
38
59
|
const envPath = createRequire(import.meta.url).resolve('@miaskiewicz/turbo-dom/environment/vitest');
|
|
39
60
|
|
|
40
61
|
export default defineConfig({
|
|
41
|
-
test: {
|
|
42
|
-
environment: envPath, // vitest resolves a bare name only for `vitest-environment-*`
|
|
43
|
-
// packages, so a scoped package is referenced by file path
|
|
44
|
-
},
|
|
62
|
+
test: { environment: envPath }, // a scoped package is referenced by file path
|
|
45
63
|
});
|
|
46
64
|
```
|
|
47
65
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
### jest
|
|
66
|
+
**jest**:
|
|
51
67
|
|
|
52
68
|
```js
|
|
53
69
|
// jest.config.js
|
|
54
|
-
module.exports = {
|
|
55
|
-
testEnvironment: '@miaskiewicz/turbo-dom/jest',
|
|
56
|
-
};
|
|
70
|
+
module.exports = { testEnvironment: '@miaskiewicz/turbo-dom/jest' };
|
|
57
71
|
```
|
|
58
72
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
```js
|
|
63
|
-
import { render, screen } from '@testing-library/react';
|
|
64
|
-
import userEvent from '@testing-library/user-event';
|
|
73
|
+
`document` and `window` are then global, as with jsdom. React Testing Library,
|
|
74
|
+
`user-event`, downshift, Radix UI and Headless UI run unmodified.
|
|
65
75
|
|
|
66
|
-
|
|
67
|
-
render(<Counter />);
|
|
68
|
-
await userEvent.click(screen.getByRole('button'));
|
|
69
|
-
expect(screen.getByText('count: 1')).toBeInTheDocument();
|
|
70
|
-
});
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### Without a test runner
|
|
76
|
+
**Without a test runner:**
|
|
74
77
|
|
|
75
78
|
```js
|
|
76
79
|
import { createEnvironment } from '@miaskiewicz/turbo-dom/runtime';
|
|
77
80
|
|
|
78
81
|
const env = createEnvironment('<!doctype html><body><div id="app"></div></body>');
|
|
79
82
|
env.document.querySelector('#app'); // nodes inflate lazily from the parse buffer
|
|
80
|
-
env.window.localStorage; // globals materialize on first touch
|
|
81
83
|
env.reset(); // fast per-file reset (reuses the parse buffer)
|
|
82
84
|
```
|
|
83
85
|
|
|
84
|
-
|
|
86
|
+
**Just the parser:**
|
|
85
87
|
|
|
86
88
|
```js
|
|
87
89
|
const { parse, parseBuffer, parseFragment } = require('@miaskiewicz/turbo-dom');
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
parseFragment('<rect/>', 'svg path'); // fragment in a context element
|
|
90
|
+
parse('<div id=a><span>hi</span></div>'); // nested tree
|
|
91
|
+
parseBuffer('<div id=a>...</div>'); // compact SoA typed-array buffer
|
|
92
|
+
parseFragment('<rect/>', 'svg path'); // fragment in a context element
|
|
92
93
|
```
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
**Parser backend.** The default is the native addon, with WASM as the fallback. To force
|
|
96
|
+
one backend, use `createEnvironment(html, { parser: 'wasm' | 'native' | 'auto' })`,
|
|
97
|
+
`setParserMode('wasm')` or `TURBO_DOM_PARSER=wasm`.
|
|
95
98
|
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
To run without Node builtins (bare V8): instantiate the `--target web` build from
|
|
100
|
+
`@miaskiewicz/turbo-dom/parser-wasm` yourself. Pass the binding to `setParser({ parse,
|
|
101
|
+
parseBuffer, parseFragment })` or set it as `globalThis.__TURBO_DOM_PARSER__`. Then call
|
|
102
|
+
`installGlobals(globalThis, { html })` from `@miaskiewicz/turbo-dom/install`.
|
|
98
103
|
|
|
99
|
-
|
|
100
|
-
import { createEnvironment, setParserMode } from '@miaskiewicz/turbo-dom/runtime';
|
|
104
|
+
### Rust crates
|
|
101
105
|
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
```toml
|
|
107
|
+
[dependencies]
|
|
108
|
+
turbo-dom = "0.7"
|
|
109
|
+
turbo-dom-layout = "0.7" # bundled fonts on by default
|
|
110
|
+
turbo-dom-raster = "0.7"
|
|
111
|
+
turbo-dom-browser-env = "0.7" # needs a V8 archive, see its README
|
|
104
112
|
```
|
|
105
113
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
node-free host, instantiate the `--target web` WASM yourself and inject the binding —
|
|
110
|
-
the runtime then never touches `node:module`/`fs`:
|
|
111
|
-
|
|
112
|
-
```js
|
|
113
|
-
import init, { initSync, parse, parseBuffer, parseFragment } from '@miaskiewicz/turbo-dom/parser-wasm';
|
|
114
|
-
import { setParser, createEnvironment } from '@miaskiewicz/turbo-dom/runtime';
|
|
114
|
+
```rust
|
|
115
|
+
use turbo_dom::Dom;
|
|
116
|
+
use turbo_dom::rtdom::cascade;
|
|
115
117
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
118
|
+
let dom = Dom::parse("<main class=grid><div class=card id=hero>hi</div></main>");
|
|
119
|
+
let cards = dom.tree.query_selector_all("div.card"); // version-cached
|
|
120
|
+
let id = dom.tree.get_attribute(cards[0], "id"); // Some("hero")
|
|
121
|
+
let style = cascade::computed_style(&dom.tree, cards[0]); // partial, honest cascade
|
|
119
122
|
```
|
|
120
123
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
Each crate's README covers its API: [turbo-dom](crates/turbo-dom/README.md),
|
|
125
|
+
[turbo-dom-layout](crates/turbo-dom-layout/README.md),
|
|
126
|
+
[turbo-dom-raster](crates/turbo-dom-raster/README.md),
|
|
127
|
+
[turbo-dom-browser-env](crates/turbo-dom-browser-env/README.md),
|
|
128
|
+
[turbo-dom-v8](crates/turbo-dom-v8/README.md). MSRV: Rust 1.89 for turbo-dom, -layout and
|
|
129
|
+
-raster; Rust 1.91 for turbo-dom-v8 and -browser-env.
|
|
124
130
|
|
|
125
|
-
##
|
|
131
|
+
## Feature flags
|
|
126
132
|
|
|
127
|
-
| |
|
|
133
|
+
| crate | feature | default | effect |
|
|
128
134
|
|---|---|---|---|
|
|
129
|
-
|
|
|
130
|
-
|
|
|
131
|
-
|
|
|
132
|
-
|
|
|
133
|
-
| `
|
|
134
|
-
|
|
|
135
|
-
|
|
136
|
-
turbo-dom
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
`
|
|
175
|
-
`
|
|
176
|
-
|
|
177
|
-
`
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
zero boundary — that's why it's fast for vitest/jest. A **Rust** consumer (crawler, extractor,
|
|
197
|
-
server-side scraper) wants the opposite: the DOM in-process in Rust, no JS at all. For that there's
|
|
198
|
-
**`rtdom`** — a pure-Rust port of the runtime (lazy copy-on-write tree over the same SoA buffer,
|
|
199
|
-
version-cached queries, partial `getComputedStyle`, events, shadow DOM, serialize).
|
|
135
|
+
| `turbo-dom-layout` | `bundled-fonts` | on | embeds OFL faces (Inter/Roboto, Liberation Serif/PT Serif, Fira Code/IBM Plex Mono) so a document lays out with no caller fonts |
|
|
136
|
+
| | `hyphenation` | on | `hyphens: auto` for English (`en` / `en-*`, not en-GB) with the embedded en-US TeX patterns (~90 KB); off, `auto` acts as `manual` |
|
|
137
|
+
| | `svg` | off | routes `image/svg+xml` through the SVG rasterizer in the image pipeline (implies `svg-rasterize`) |
|
|
138
|
+
| | `svg-rasterize` | off | the shared resvg rasterizer only (`svg::rasterize`) |
|
|
139
|
+
| | `serde` | off | `Serialize` for the CSSOM-View metrics records |
|
|
140
|
+
| | `perf` | off | hot-path instrumentation counters |
|
|
141
|
+
| | `test-support` | off | font fixtures and test literals for downstream tests; never in production |
|
|
142
|
+
| `turbo-dom-raster` | `avif` | off | AVIF pixels (pure-Rust rav1d) |
|
|
143
|
+
| | `jxl` | off | JPEG XL pixels (jxl-oxide) |
|
|
144
|
+
| | `gpu-metal` | off | wgpu (Metal) canvas and WebGL rasterization, falling back to the CPU path on any GPU failure |
|
|
145
|
+
| | `coretext` | off | macOS only: canvas text through CoreText (inert elsewhere) |
|
|
146
|
+
| `turbo-dom-browser-env` | `host-internals` | off | doc-hidden host access to native documents (turbo-surf) |
|
|
147
|
+
| | `test-support` | off | probe, control and case constants for embedders' Chrome-parity tests |
|
|
148
|
+
| | `native-sdk-frame-read` | off | reads the interceptor's should-throw mode from the V8 frame, for native SDKs without the fork fix |
|
|
149
|
+
| `turbo-dom-parser` (npm) | `napi-bind` / `wasm-bind` / `wasm-runtime` | `napi-bind` | the native addon, the WASM parser and the WASM DOM-runtime spike |
|
|
150
|
+
|
|
151
|
+
`turbo-dom` and `turbo-dom-v8` have no features of their own. `turbo-dom-v8` forwards
|
|
152
|
+
upstream rusty_v8's features (for example `simdutf`).
|
|
153
|
+
|
|
154
|
+
## Status
|
|
155
|
+
|
|
156
|
+
- **Parser:** html5lib-tests conformance is 99.94% through the npm parser (happy-dom
|
|
157
|
+
37.35%, jsdom 97.03%). The remaining misses are `<select>`-family proposals that
|
|
158
|
+
upstream html5ever has not adopted yet. The Rust `rtdom` tree passes its direct
|
|
159
|
+
html5lib-tests gate at 100% (`npm run conformance:rtdom`).
|
|
160
|
+
- **Chrome parity:** behaviour fixes are backed by Chrome 156 controls. These are
|
|
161
|
+
fixtures recorded from headless Chrome and asserted in tests. Every corpus output change
|
|
162
|
+
is declared in [`tools/parity/expected/`](tools/parity/expected) (checkpoint `b-0.7.0`).
|
|
163
|
+
0.7.0 adds vertical writing modes, UAX #9 bidi, Chrome's line box model on the PDF path,
|
|
164
|
+
the full `border-radius` model with `overflow` clipping, background tiling and gradient
|
|
165
|
+
length stops, native form-control appearance, size / layout containment, replaced media
|
|
166
|
+
boxes, XML documents (with DTD ATTLIST defaults), processing instructions, CSS
|
|
167
|
+
animations in `getAnimations()`, a browser-env cascade with specificity, `@layer`,
|
|
168
|
+
`@supports` and `@scope`, real canvas `measureText` and an opt-in V8 startup snapshot
|
|
169
|
+
install (see [CHANGELOG.md](CHANGELOG.md)).
|
|
170
|
+
- **Open gaps (tracked as issues):**
|
|
171
|
+
- glyph stems in raster screenshots lack Chrome's stem darkening, so glyph edges are
|
|
172
|
+
up to ~0.5px thinner per side (#266).
|
|
173
|
+
- Known difference, not tracked: `hyphens: auto` covers English only, and Chrome on
|
|
174
|
+
macOS hyphenates with the system dictionary, whose points differ for some words.
|
|
175
|
+
- **The npm runtime has no layout.** Geometry is synthetic: `getBoundingClientRect()`,
|
|
176
|
+
`offsetWidth/Height`, `clientWidth/Height` and `scrollWidth/Height` return a non-zero
|
|
177
|
+
size that is stable for a given DOM state (blocks fill the parent's width, the root
|
|
178
|
+
takes `innerWidth` or 1024; heights stack children or 18px per line), while every
|
|
179
|
+
position (`top` / `left`, `offsetTop/Left`, `scrollTop/Left`) is 0.
|
|
180
|
+
`getComputedStyle` is a partial cascade: top-level `<style>` rules (and rules injected
|
|
181
|
+
with `insertRule`) plus inline styles, ordered by Selectors 4 specificity and source
|
|
182
|
+
order, with a fixed set of inherited properties. Colours read back as `rgb()` /
|
|
183
|
+
`rgba()` and a bare `0` as `0px`; a property no rule sets reads `''` (never an initial
|
|
184
|
+
value). Not resolved: rules inside any at-rule block (`@media`, `@supports`, `@layer`,
|
|
185
|
+
`@container`, `@scope`) are skipped, rules with state pseudo-classes (`:hover`, ...)
|
|
186
|
+
never match, a pseudo-element argument is ignored (the element's own style comes
|
|
187
|
+
back) and `::before`-style rules never apply. Values come back as written: no unit
|
|
188
|
+
conversion (`2em` stays `2em`), and `var(...)` and `inherit` / `initial` / `unset` are
|
|
189
|
+
returned verbatim, not resolved. Shadow DOM is supported: `attachShadow`, declarative
|
|
190
|
+
shadow roots, slots and `assignedNodes()`, event retargeting, scoped styles with
|
|
191
|
+
`:host` / `::slotted()`, and inheritance across the host boundary. Layout belongs to
|
|
192
|
+
`turbo-dom-layout`, and through it to turbo-html2pdf and turbo-surf.
|
|
193
|
+
|
|
194
|
+
## Benchmarks — pending re-run
|
|
195
|
+
|
|
196
|
+
0.6.0 and 0.7.0 have not been benchmarked against jsdom / happy-dom yet. All four
|
|
197
|
+
repositories will be re-run together on a quiet machine. Until then, the last measured
|
|
198
|
+
numbers (v0.1.55, 2026-06-07, darwin-arm64, JS runtime vs happy-dom and jsdom) are kept
|
|
199
|
+
unchanged and labelled in [docs/BENCHMARKS-history.md](docs/BENCHMARKS-history.md). They
|
|
200
|
+
predate the consolidation. 0.7.0's browser-env performance work (measured on a real
|
|
201
|
+
React/MUI suite) is listed in [CHANGELOG.md](CHANGELOG.md).
|
|
200
202
|
|
|
201
|
-
|
|
202
|
-
called *from JS* is **~0.55×** the JS runtime (the boundary crossing dominates — exactly what the
|
|
203
|
-
[spec §3](./turbo-dom-spec.md) predicted), while `rtdom` run **in-process from Rust** is **~2.7×**
|
|
204
|
-
the JS runtime on the same chatty workload (zero boundary). So: JS consumers keep the JS runtime,
|
|
205
|
-
Rust consumers use `rtdom`.
|
|
203
|
+
## Development
|
|
206
204
|
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
Requires Node ≥ 24 and Rust 1.98.0 (CI's toolchain; `mise.toml` pins Node and Rust).
|
|
206
|
+
`npm install` also points git at `.githooks`, whose pre-commit hook runs `npm run test:cov`
|
|
207
|
+
whenever `src/`, `test/` or `harness/` changes.
|
|
209
208
|
|
|
210
209
|
```bash
|
|
211
|
-
|
|
210
|
+
npm run build # native addon + both WASM packages
|
|
211
|
+
npm test # JS suite (unit, conformance, differential, gauntlets)
|
|
212
|
+
npm run test:cov # same + coverage gate (lines 100 / functions 100 / branches 97.3)
|
|
213
|
+
npm run test:vitest && npm run test:jest
|
|
214
|
+
npm run test:rust # cargo test -p turbo-dom
|
|
215
|
+
npm run conformance # html5lib-tests report (parser)
|
|
216
|
+
npm run conformance:rtdom # html5lib-tests through the rtdom tree
|
|
217
|
+
cargo test -p turbo-dom-layout # + --features svg / perf / serde,test-support
|
|
218
|
+
cargo test -p turbo-dom-raster # + --features avif / jxl
|
|
219
|
+
python3 crates/turbo-dom-v8/scripts/generate.py --download --write # generate the V8 fork
|
|
220
|
+
cargo test -p turbo-dom-browser-env --features host-internals,test-support # needs RUSTY_V8_ARCHIVE
|
|
221
|
+
npm run complexity:rust # cc-check --max 15 (install: CLAUDE.md)
|
|
222
|
+
npm run complexity:js # oxlint complexity <= 15 over src/ + crates/turbo-dom-browser-env/src
|
|
212
223
|
```
|
|
213
224
|
|
|
214
|
-
|
|
215
|
-
use turbo_dom::{Dom, DocumentExt};
|
|
216
|
-
use turbo_dom::rtdom::cascade;
|
|
225
|
+
[CLAUDE.md](CLAUDE.md) has the complete command list and the design rules.
|
|
217
226
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
227
|
+
**Test fonts come from fixtures.** Text-geometry tests load the repository's own font
|
|
228
|
+
files (`turbo_dom_layout::test_support::fixture_fonts()`, from
|
|
229
|
+
`crates/turbo-dom-layout/assets/fonts`). Their expected values come from Chrome 156 with
|
|
230
|
+
the same files loaded through `@font-face` (`scripts/fixture-font-chrome-controls.mjs`).
|
|
231
|
+
Results therefore do not depend on the fonts installed on the host. Only the tests of
|
|
232
|
+
the host-font policy itself read system fonts.
|
|
223
233
|
|
|
224
|
-
|
|
225
|
-
here and only here (the workspace-member crate `crates/turbo-dom/`; there is no in-repo
|
|
226
|
-
`src/rtdom` copy and no `rust-runtime` cargo feature — an earlier feature gated a duplicate copy
|
|
227
|
-
that was consolidated away). Minimal deps (`html5ever` + `rustc-hash`, no napi/wasm), a runnable
|
|
228
|
-
`examples/crawl.rs`, 227 tests, 100% line coverage, and a direct html5lib-tests gate at
|
|
229
|
-
**99.75%**. From the repo: `npm run build:rtdom` (= `cargo build -p turbo-dom --release`),
|
|
230
|
-
`npm run test:rust` (= `cargo test -p turbo-dom`), `npm run conformance:rtdom`.
|
|
231
|
-
|
|
232
|
-
## Limitations (by design)
|
|
233
|
-
|
|
234
|
-
- **No layout.** `getBoundingClientRect()` returns zeros; `getClientRects()` is empty.
|
|
235
|
-
- **`getComputedStyle` is a partial cascade** — it resolves REAL injected `<style>` rules
|
|
236
|
-
(emotion/MUI `.css-HASH{…}`) plus inline `style`, applying specificity and source order
|
|
237
|
-
(inline wins). It **inherits the standard inheritable properties** (`color`, `font*`,
|
|
238
|
-
`line-height`, `text-align`, `visibility`, …) down the flattened tree — a global rule on
|
|
239
|
-
`body`/`:root` reaches descendants, and inheritance crosses the shadow host boundary. It
|
|
240
|
-
**canonicalizes `<color>` values to `rgb()/rgba()`** (`#fff`/`white`/`hsl(...)` → `rgb(255, 255, 255)`)
|
|
241
|
-
exactly as browsers serialize computed style, so `@testing-library/jest-dom`'s `toHaveStyle`
|
|
242
|
-
color assertions compare equal regardless of how the rule authored the color. It expands common
|
|
243
|
-
shorthands to longhands (`margin`/`padding`/`border`/single-token `background`), serializes bare
|
|
244
|
-
`0` as `0px` for length props, and normalizes `font-family` comma spacing. Out of scope (returns
|
|
245
|
-
`''`): `@media`/`@supports`/`@keyframes`, `:hover`/state pseudo-classes, pseudo-elements, the
|
|
246
|
-
`inherit`/`initial`/`unset` keywords, CSS custom properties, and length-unit conversion
|
|
247
|
-
(`em`/`rem`→`px`). Only ever returns values from a matched, inline, or inherited declaration —
|
|
248
|
-
never an invented initial value. Style/geometry assertions belong in a real browser
|
|
249
|
-
(Playwright/WebDriver).
|
|
250
|
-
- **`<style>.textContent` reflects rules injected via `sheet.insertRule()`** — CSS-in-JS engines in
|
|
251
|
-
"speedy" mode (emotion/styled-components) inject rules straight into the CSSOM without writing the
|
|
252
|
-
node's text; turbo-dom serializes them back into `textContent` (as browsers/jsdom do), so tests
|
|
253
|
-
that scrape `querySelectorAll('style')` text see the injected CSS.
|
|
254
|
-
- Canvas, `<select>` rendering, and similar visual APIs are honest no-op stubs.
|
|
255
|
-
- **Shadow DOM** is supported and pay-for-what-you-use — every event/query/cascade hot path
|
|
256
|
-
is unchanged until the first `attachShadow` flips a per-document flag. Covered: `attachShadow`
|
|
257
|
-
(open/closed), encapsulated `querySelector`/`getElementById`, `getRootNode({composed})`,
|
|
258
|
-
full event propagation with `target`/`relatedTarget` retargeting and `composed` boundary
|
|
259
|
-
crossing, `<slot>` `assignedNodes`/`assignedElements`/`assignedSlot`, scoped `getComputedStyle`
|
|
260
|
-
with `:host`/`:host(...)`/`::slotted(...)` and inheritance across the boundary, and declarative
|
|
261
|
-
`<template shadowrootmode>` promotion. Out of scope (honest): flattened-tree layout, `slotchange`
|
|
262
|
-
events, and the cascade caveats above (`@media`/state/pseudo-elements) inside shadow trees.
|
|
234
|
+
### Gates
|
|
263
235
|
|
|
264
|
-
|
|
236
|
+
| gate | where | status |
|
|
237
|
+
|---|---|---|
|
|
238
|
+
| line coverage (tarpaulin, LLVM) | `ci.yml` `coverage` | **blocking**. Floors: turbo-dom 100, turbo-dom-parser `src/core.rs` 100, turbo-dom-layout 99.8, turbo-dom-raster 100 (default / avif / jxl), turbo-dom-browser-env 97.6 |
|
|
239
|
+
| JS runtime coverage | `npm run test:cov` (pre-commit hook) | lines 100 / functions 100 / branches 97.3 |
|
|
240
|
+
| cyclomatic complexity ≤ 15 (Rust cc-check, JS oxlint) | `ci.yml` `complexity-rust`, `complexity-js` | **report-only** (`continue-on-error`) until the post-release refactor clears the backlog ([docs/CONSOLIDATION.md](docs/CONSOLIDATION.md#complexity)) |
|
|
241
|
+
| clippy pedantic `-D warnings`, rustfmt, MSRV 1.89, version agreement, churn allocation gate, verify-copied reviews | `ci.yml` | blocking |
|
|
265
242
|
|
|
266
|
-
|
|
243
|
+
**CI runners.** Pull requests and pushes to `main` run on Linux (x64 and arm64) and on
|
|
244
|
+
Windows x64: the napi build, and the V8 / browser-env matrix with the default and
|
|
245
|
+
simdutf archives. macOS arm64 builds and jobs run only in the release workflow.
|
|
246
|
+
`publish.yml` calls `ci.yml` with `darwin: true`. Linux binaries build on ubuntu-22.04 and a
|
|
247
|
+
guard (`scripts/check-glibc-ceiling.sh`) rejects any addon that needs glibc above 2.35. The
|
|
248
|
+
guard also fails when an expected addon is missing or unreadable. A newer push to a pull
|
|
249
|
+
request cancels its superseded CI run. `main`, tag and publish runs each get their own
|
|
250
|
+
concurrency group, so they are never cancelled or dropped while pending.
|
|
267
251
|
|
|
268
|
-
|
|
269
|
-
npm install
|
|
270
|
-
npm run build # native addon (.node) + wasm — the JS-consumer artifacts
|
|
271
|
-
npm test # JS suite (unit, conformance, differential, gauntlets)
|
|
272
|
-
npm run test:rust # Rust tests: parser core + the turbo-dom crate (cargo test -p turbo-dom)
|
|
273
|
-
npm run conformance # html5lib-tests report (parser)
|
|
274
|
-
npm run conformance:rtdom # html5lib-tests gate run through the rtdom tree
|
|
275
|
-
npm run bench:all # JS-runtime benchmarks
|
|
276
|
-
npm run build:wasm # wasm32 parser fallback
|
|
277
|
-
npm run build:rtdom # pure-Rust DOM runtime (no napi/wasm) — the Rust-consumer build
|
|
278
|
-
```
|
|
252
|
+
## Releasing
|
|
279
253
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
[
|
|
283
|
-
[
|
|
284
|
-
|
|
285
|
-
Contributions welcome — issues and PRs at
|
|
286
|
-
[github.com/miaskiewicz/turbo-dom](https://github.com/miaskiewicz/turbo-dom).
|
|
254
|
+
One annotated `vX.Y.Z` tag on the release merge commit publishes everything in dependency
|
|
255
|
+
order. The order is turbo-dom-v8 → turbo-dom → turbo-dom-layout → turbo-dom-raster →
|
|
256
|
+
turbo-dom-browser-env → npm. See [PUBLISHING.md](PUBLISHING.md) and the history in
|
|
257
|
+
[CHANGELOG.md](CHANGELOG.md).
|
|
287
258
|
|
|
288
259
|
## License
|
|
289
260
|
|
|
290
|
-
[MIT](
|
|
291
|
-
|
|
292
|
-
Release preparation, merge-commit tagging and registry verification are documented in [PUBLISHING.md](./PUBLISHING.md).
|
|
261
|
+
[MIT](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miaskiewicz/turbo-dom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Faster, more spec-correct DOM for test runners \u2014 native html5ever (Rust/WASM) parser + lazy copy-on-write DOM. A drop-in-style alternative to jsdom/happy-dom for vitest & jest.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "index.js",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"build:wasm:pkg": "wasm-pack build --target nodejs --out-dir pkg --no-default-features --features wasm-bind",
|
|
78
78
|
"build:wasm:web": "wasm-pack build --target web --out-dir pkg-web --no-default-features --features wasm-bind",
|
|
79
79
|
"test": "node --test 'test/*.mjs'",
|
|
80
|
-
"test:cov": "node --test --experimental-test-coverage --test-coverage-include='src/runtime/**' --test-coverage-lines=
|
|
80
|
+
"test:cov": "node --test --experimental-test-coverage --test-coverage-include='src/runtime/**' --test-coverage-lines=100 --test-coverage-functions=100 --test-coverage-branches=97.3 'test/*.mjs'",
|
|
81
81
|
"test:rust": "cargo test -p turbo-dom",
|
|
82
82
|
"test:all": "npm run build && npm run test:rust && npm test",
|
|
83
83
|
"prepare": "git config core.hooksPath .githooks || true",
|
|
@@ -97,7 +97,9 @@
|
|
|
97
97
|
"build:rtdom": "cargo build -p turbo-dom --release",
|
|
98
98
|
"profile:churn": "cargo build --profile profiling --example churn_profile -p turbo-dom && echo 'built \u2014 CPU-profile with: ./target/profiling/examples/churn_profile & sample $! 20 -file target/churn.sample.txt'",
|
|
99
99
|
"bench:compare": "node bench/dom-compare.mjs",
|
|
100
|
-
"bench:compare:all": "RTDOM_JSON=\"$(cargo test -p turbo-dom --release rtdom::bench::compare_all -- --ignored --nocapture 2>/dev/null | sed -n 's/.*RTDOM_JSON //p')\" node --expose-gc --max-old-space-size=8192 bench/compare-all.mjs"
|
|
100
|
+
"bench:compare:all": "RTDOM_JSON=\"$(cargo test -p turbo-dom --release rtdom::bench::compare_all -- --ignored --nocapture 2>/dev/null | sed -n 's/.*RTDOM_JSON //p')\" node --expose-gc --max-old-space-size=8192 bench/compare-all.mjs",
|
|
101
|
+
"complexity:rust": "git ls-files -z '*.rs' | xargs -0 cc-check --max 15",
|
|
102
|
+
"complexity:js": "npx --yes oxlint@1.87.0 -c .oxlintrc.complexity.json src crates/turbo-dom-browser-env/src"
|
|
101
103
|
},
|
|
102
104
|
"keywords": [
|
|
103
105
|
"dom",
|
package/pkg/package.json
CHANGED
|
Binary file
|