@miaskiewicz/turbo-dom 0.5.7 → 0.6.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 +3177 -394
- package/Cargo.toml +53 -3
- package/README.md +176 -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 +2794 -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.6.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.6.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. In 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.6.0 | lazy JS DOM runtime + native html5ever parser (N-API, WASM fallback) | vitest / jest users |
|
|
23
|
+
| [`turbo-dom`](crates/turbo-dom) | crates.io | 0.6.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.6.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.6.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.6.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,186 @@ 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.6"
|
|
109
|
+
turbo-dom-layout = "0.6" # bundled fonts on by default
|
|
110
|
+
turbo-dom-raster = "0.6"
|
|
111
|
+
turbo-dom-browser-env = "0.6" # 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
|
-
|
|
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
|
-
└─ lazy window (Proxy, self-replacing globals) ← JS
|
|
184
|
-
└─ lazy copy-on-write node tree (memoized identity) ← JS
|
|
185
|
-
└─ immutable Structure-of-Arrays parse buffer ← shared
|
|
186
|
-
└─ Rust: html5ever → flat typed-array buffer ← native (N-API / WASM)
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
The parser runs in Rust (compute-bound, one boundary crossing per parse). The DOM stays in
|
|
190
|
-
JS (chatty, fine-grained) but pays only for what a test touches. Full design notes:
|
|
191
|
-
[turbo-dom-spec.md](./turbo-dom-spec.md).
|
|
192
|
-
|
|
193
|
-
## Rust-native DOM runtime (`rtdom`) — for Rust consumers
|
|
194
|
-
|
|
195
|
-
Everything above is the **JS-consumer** path: the DOM is JS objects, so React/RTL touch it with
|
|
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
|
+
| | `svg` | off | routes `image/svg+xml` through the SVG rasterizer in the image pipeline (implies `svg-rasterize`) |
|
|
137
|
+
| | `svg-rasterize` | off | the shared resvg rasterizer only (`svg::rasterize`) |
|
|
138
|
+
| | `serde` | off | `Serialize` for the CSSOM-View metrics records |
|
|
139
|
+
| | `perf` | off | hot-path instrumentation counters |
|
|
140
|
+
| | `test-support` | off | font fixtures and test literals for downstream tests; never in production |
|
|
141
|
+
| `turbo-dom-raster` | `avif` | off | AVIF pixels (pure-Rust rav1d) |
|
|
142
|
+
| | `jxl` | off | JPEG XL pixels (jxl-oxide) |
|
|
143
|
+
| | `gpu-metal` | off | wgpu (Metal) canvas and WebGL rasterization, falling back to the CPU path on any GPU failure |
|
|
144
|
+
| | `coretext` | off | macOS only: canvas text through CoreText (inert elsewhere) |
|
|
145
|
+
| `turbo-dom-browser-env` | `host-internals` | off | doc-hidden host access to native documents (turbo-surf) |
|
|
146
|
+
| | `test-support` | off | probe, control and case constants for embedders' Chrome-parity tests |
|
|
147
|
+
| | `native-sdk-frame-read` | off | reads the interceptor's should-throw mode from the V8 frame, for native SDKs without the fork fix |
|
|
148
|
+
| `turbo-dom-parser` (npm) | `napi-bind` / `wasm-bind` / `wasm-runtime` | `napi-bind` | the native addon, the WASM parser and the WASM DOM-runtime spike |
|
|
149
|
+
|
|
150
|
+
`turbo-dom` and `turbo-dom-v8` have no features of their own. `turbo-dom-v8` forwards
|
|
151
|
+
upstream rusty_v8's features (for example `simdutf`).
|
|
152
|
+
|
|
153
|
+
## Status
|
|
154
|
+
|
|
155
|
+
- **Parser:** html5lib-tests conformance is 99.94% through the npm parser (happy-dom
|
|
156
|
+
37.35%, jsdom 97.03%). The remaining misses are `<select>`-family proposals that
|
|
157
|
+
upstream html5ever has not adopted yet. The Rust `rtdom` tree passes its direct
|
|
158
|
+
html5lib-tests gate at 100% (`npm run conformance:rtdom`).
|
|
159
|
+
- **Chrome parity:** behaviour fixes are backed by Chrome 156 controls. These are
|
|
160
|
+
fixtures recorded from headless Chrome and asserted in tests. At the 0.6.0 parity
|
|
161
|
+
remeasure (2026-10-29), turbo-surf's API inventory against Chrome 156 has 2 missing
|
|
162
|
+
members, down from 142. Every corpus output change is declared in
|
|
163
|
+
[`tools/parity/expected/`](tools/parity/expected) (checkpoint `b-0.6.0`).
|
|
164
|
+
- **Open gaps (tracked as issues):**
|
|
165
|
+
- XPath fragment contexts (turbo-dom#185)
|
|
166
|
+
- XML parsing (#186)
|
|
167
|
+
- CSS animations in `getAnimations` (#187)
|
|
168
|
+
- focus / dialog details (#188)
|
|
169
|
+
- FileList and selector error text (#189)
|
|
170
|
+
- RTL scroll and vertical writing (#190)
|
|
171
|
+
- shaping and font metrics (#191)
|
|
172
|
+
- inset `box-shadow` (#192)
|
|
173
|
+
- block and table layout residuals (#53–#102, #128, #178)
|
|
174
|
+
- **The npm runtime has no layout:** `getBoundingClientRect()` returns zeros, and
|
|
175
|
+
`getComputedStyle` is a partial cascade. It resolves real `<style>` rules and inline
|
|
176
|
+
styles, applies inheritance, and serializes colours as `rgb()`. It never invents a
|
|
177
|
+
value. `@media`, state pseudo-classes, pseudo-elements and unit conversion return `''`.
|
|
178
|
+
Shadow DOM is supported: slots, retargeting, `:host` / `::slotted`, and declarative
|
|
179
|
+
shadow roots. Layout belongs to `turbo-dom-layout`, and through it to turbo-html2pdf and
|
|
180
|
+
turbo-surf.
|
|
181
|
+
|
|
182
|
+
## Benchmarks — pending re-run
|
|
183
|
+
|
|
184
|
+
0.6.0 has not been benchmarked yet. All four repositories will be re-run together on a
|
|
185
|
+
quiet machine after the release. Until then, the last measured numbers (v0.1.55,
|
|
186
|
+
2026-06-07, darwin-arm64, JS runtime vs happy-dom and jsdom) are kept unchanged and
|
|
187
|
+
labelled in [docs/BENCHMARKS-history.md](docs/BENCHMARKS-history.md). They predate the
|
|
188
|
+
consolidation and say nothing about 0.6.0.
|
|
200
189
|
|
|
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`.
|
|
190
|
+
## Development
|
|
206
191
|
|
|
207
|
-
|
|
208
|
-
|
|
192
|
+
Requires Node ≥ 24 and Rust 1.98.0 (CI's toolchain; `mise.toml` pins Node and Rust).
|
|
193
|
+
`npm install` also points git at `.githooks`, whose pre-commit hook runs `npm run test:cov`
|
|
194
|
+
whenever `src/`, `test/` or `harness/` changes.
|
|
209
195
|
|
|
210
196
|
```bash
|
|
211
|
-
|
|
197
|
+
npm run build # native addon + both WASM packages
|
|
198
|
+
npm test # JS suite (unit, conformance, differential, gauntlets)
|
|
199
|
+
npm run test:cov # same + coverage gate (lines 100 / functions 100 / branches 97.3)
|
|
200
|
+
npm run test:vitest && npm run test:jest
|
|
201
|
+
npm run test:rust # cargo test -p turbo-dom
|
|
202
|
+
npm run conformance # html5lib-tests report (parser)
|
|
203
|
+
npm run conformance:rtdom # html5lib-tests through the rtdom tree
|
|
204
|
+
cargo test -p turbo-dom-layout # + --features svg / perf / serde,test-support
|
|
205
|
+
cargo test -p turbo-dom-raster # + --features avif / jxl
|
|
206
|
+
python3 crates/turbo-dom-v8/scripts/generate.py --download --write # generate the V8 fork
|
|
207
|
+
cargo test -p turbo-dom-browser-env --features host-internals,test-support # needs RUSTY_V8_ARCHIVE
|
|
208
|
+
npm run complexity:rust # cc-check --max 15 (install: CLAUDE.md)
|
|
209
|
+
npm run complexity:js # oxlint complexity <= 15 over src/ + crates/turbo-dom-browser-env/src
|
|
212
210
|
```
|
|
213
211
|
|
|
214
|
-
|
|
215
|
-
use turbo_dom::{Dom, DocumentExt};
|
|
216
|
-
use turbo_dom::rtdom::cascade;
|
|
212
|
+
[CLAUDE.md](CLAUDE.md) has the complete command list and the design rules.
|
|
217
213
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
214
|
+
**Test fonts come from fixtures.** Text-geometry tests load the repository's own font
|
|
215
|
+
files (`turbo_dom_layout::test_support::fixture_fonts()`, from
|
|
216
|
+
`crates/turbo-dom-layout/assets/fonts`). Their expected values come from Chrome 156 with
|
|
217
|
+
the same files loaded through `@font-face` (`scripts/fixture-font-chrome-controls.mjs`).
|
|
218
|
+
Results therefore do not depend on the fonts installed on the host. Only the tests of
|
|
219
|
+
the host-font policy itself read system fonts.
|
|
223
220
|
|
|
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.
|
|
221
|
+
### Gates
|
|
263
222
|
|
|
264
|
-
|
|
223
|
+
| gate | where | status |
|
|
224
|
+
|---|---|---|
|
|
225
|
+
| 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 |
|
|
226
|
+
| JS runtime coverage | `npm run test:cov` (pre-commit hook) | lines 100 / functions 100 / branches 97.3 |
|
|
227
|
+
| 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)) |
|
|
228
|
+
| clippy pedantic `-D warnings`, rustfmt, MSRV 1.89, version agreement, churn allocation gate, verify-copied reviews | `ci.yml` | blocking |
|
|
265
229
|
|
|
266
|
-
|
|
230
|
+
**CI runners.** Pull requests and pushes to `main` run on Linux (x64 and arm64) and on
|
|
231
|
+
Windows x64: the napi build, and the V8 / browser-env matrix with the default and
|
|
232
|
+
simdutf archives. macOS arm64 builds and jobs run only in the release workflow.
|
|
233
|
+
`publish.yml` calls `ci.yml` with `darwin: true`.
|
|
267
234
|
|
|
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
|
-
```
|
|
235
|
+
## Releasing
|
|
279
236
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
[
|
|
283
|
-
[
|
|
284
|
-
|
|
285
|
-
Contributions welcome — issues and PRs at
|
|
286
|
-
[github.com/miaskiewicz/turbo-dom](https://github.com/miaskiewicz/turbo-dom).
|
|
237
|
+
One annotated `vX.Y.Z` tag on the release merge commit publishes everything in dependency
|
|
238
|
+
order. The order is turbo-dom-v8 → turbo-dom → turbo-dom-layout → turbo-dom-raster →
|
|
239
|
+
turbo-dom-browser-env → npm. See [PUBLISHING.md](PUBLISHING.md) and the history in
|
|
240
|
+
[CHANGELOG.md](CHANGELOG.md).
|
|
287
241
|
|
|
288
242
|
## License
|
|
289
243
|
|
|
290
|
-
[MIT](
|
|
291
|
-
|
|
292
|
-
Release preparation, merge-commit tagging and registry verification are documented in [PUBLISHING.md](./PUBLISHING.md).
|
|
244
|
+
[MIT](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miaskiewicz/turbo-dom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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
|
package/pkg-web/package.json
CHANGED
|
Binary file
|