Contributing
Architecture, test suites, and how to add a new CSS feature.
This page is for people landing on the Domotion repo who want to fix a bug, add a CSS feature, or otherwise touch the source. Users of the library should start at Quick start instead.
Architecture at 30,000 ft
Domotion has three logical stages, all in src/:
| Stage | Files | What happens |
|---|---|---|
| Capture | dom-to-svg.ts (CAPTURE_SCRIPT), capture.ts |
Playwright loads the page; an in-page script walks the DOM, recording position, computed styles, text, and per-glyph offsets into a serialisable CapturedElement tree. |
| Render | dom-to-svg.ts (server side), text-renderer.ts, text-to-path.ts, form-controls.ts, gradients.ts, chrome.ts |
Pure functions take a tree and emit SVG markup — <rect> for backgrounds, <path> for glyph outlines via fontkit, <linearGradient> for CSS gradients, etc. |
| Animate | animator.ts, frame-merge.ts |
Multiple captured frames get composed into one SVG with CSS keyframe transitions. The merge pipeline de-duplicates shared elements across frames and emits per-element visibility timelines for the all-crossfade fast path. |
The CLI (src/cli/) is a thin shell over the public API
(src/index.ts). The user manual under site/ is a
separate static-site generator that imports types and helpers from the
library — it's a good source of "how is this actually used" examples.
The CAPTURE_SCRIPT discipline
The DOM-walking code lives as a string literal at the top of
src/dom-to-svg.ts and is page.evaluate'd into the
captured page. Two consequences:
- No
importstatements, no closures over module-scope vars. The script only sees the args passed toevaluateand globals available in the page. - Use
var/ function declarations or arrow functions that don't reference outer scope.
If you find yourself wanting to call a helper from CAPTURE_SCRIPT, either
inline it (small) or capture the data needed and call the helper after the
script returns (large). Anything reusable across capture and render can live
as plain TypeScript helpers in src/ and be re-imported by the
renderer side — just don't try to call them from inside CAPTURE_SCRIPT.
This is also why CAPTURE_SCRIPT is tested via the test runner (capturing
real fixtures) rather than unit tests: the script only behaves correctly
when the surrounding evaluate() injects its arguments.
Running the test suites
| Command | What it runs |
|---|---|
npm test | Vitest unit tests under src/**/*.test.ts — fast, no browser. |
npm run demos:test | The feature visual-regression suite (tests/features.ts). One fixture per CSS feature; each is rendered both via Chromium and via Domotion's pipeline and the PNG diff thresholded at 3%. |
npm run demos:test:html | The broad-coverage sweep against ~/Documents/html-test/*.html. Some failures are pre-existing and tracked separately; new code shouldn't introduce regressions here. |
npm run demos:test:all | features + showcase + html-test-suite. |
npm run demos:review | Local web UI to compare expected / actual / diff PNGs from each suite by hand. Most useful when adding or updating fixtures. |
Adding a new CSS feature
- Capture the new property in CAPTURE_SCRIPT — add it
to the styles object that gets serialised onto each
CapturedElement. - Plumb it through the
CapturedElementtype (src/dom-to-svg.ts, near the top). - Render it in the appropriate place — usually
renderElementinsrc/dom-to-svg.tsfor box properties, orsrc/text-renderer.tsfor text-related ones. - Add a fixture to
tests/features.ts: a minimal HTML snippet that exercises the feature and a single matching expected behavior. Runnpm run demos:testto record the expected PNG. - Update the support matrix in
site/pages/css/<area>.tsx(the per-area page) and the top-levelFEATURES.mdchecklist. - Document any new fallback behavior in
docs/— these are the contract with consumers about what round-trips and what doesn't.
If the feature has no faithful SVG representation (e.g. complex CSS
filters), it goes through the raster-fallback path: capture
page.screenshot() for the affected region and emit it as an
embedded <image>. Add a capture warning so users can find
it.
Where the docs live
| Location | Audience |
|---|---|
docs/ | Implementation requirements (rendering fidelity, supported CSS, known caveats). The internal contract. |
site/pages/ | Public user manual. CLI- and use-case-focused. |
FEATURES.md | Per-feature support checklist with links to fixtures. Keep in sync when fixtures land. |
CLAUDE.md | Guidance for AI assistants working in the repo. |
Quality gates
npm run buildmust pass (TypeScript strict). Noanyoutside the fontkit / bidi-js boundary types.npm testmust pass.npm run demos:testis the primary regression signal — every feature has a fixture; new features need fixtures.
Reading list before your first PR
- The capture pipeline — visual overview of the same architecture this page lists.
- The element tree — what flows from capture to render.
- Text rendering — the most subtle subsystem; understanding subpixel positioning and run-based shaping saves hours later.
- The top of
CLAUDE.md— house-style invariants that aren't enforceable by the type-checker.