mizchi/crater

MoonBit

74

1,605 commits

updated Sep 20, 2026

See the code

README

Crater

A headless browser environment for MoonBit, driven by Playwright over WebDriver BiDi.

Use Playwright tests on a small, predictable target instead of spinning up a full Chromium. Crater speaks the WebDriver BiDi protocol natively, so existing Playwright suites can connect to it for layout-and-DOM-level testing without the weight of a real browser.

Why

  • Playwright + BiDi is becoming the standard surface for web automation. Crater treats that surface as a first-class contract instead of an afterthought.
  • The runtime is small enough to embed in tests, CI pipelines, and design tools where Chromium is overkill.
  • A predictable layout / paint pipeline (Taffy-derived) makes regressions easier to bisect than against a moving Chromium release train.
  • Built in MoonBit, so the codebase compiles to JS / native / wasm targets out of the same source.

Quick start

pnpm install
pnpm exec playwright install chromium  # only needed for VRT reference captures
pkf run prepare

Run a Playwright test against Crater (Crater starts on demand via the project's playwright.config.ts webServer):

import { test, expect } from "@playwright/test";

test("Crater serves a static page over BiDi", async ({ page }) => {
  await page.goto("data:text/html,<h1>hello</h1>");
  await expect(page.locator("h1")).toHaveText("hello");
});

Or drive it raw from the WebDriver BiDi protocol:

import { connectCraterBidi } from "./tests/helpers/crater-bidi";

const session = await connectCraterBidi();
await session.browsingContext.navigate({
  url: "data:text/html,<form id=login>...</form>",
  wait: "complete",
});
const count = await session.script.evaluate({
  expression: "document.forms.length",
});

Supported surface

AreaStatus
WebDriver BiDi session / browsingContext / script / input / network / storage100% of the WPT subset we gate on
HTML + CSS parsing100% local parser tests, 94% WPT CSS layout pass rate
DOM (wpt/dom/nodes)100%
Shadow DOM + custom elementsCore surface; full coverage in flight (compat.web-components-* scenarios)
Cookie jar (Set-Cookie / SameSite / partitioned storage)Spec-conformant for the http-only path
CORS preflight + Access-Control-* validationEnforced on script.evaluate fetches via Phase 1 of the auth/CORS spec
Per-origin Authorization injection (Bearer / JWT)crater.setOriginAuthorization BiDi extension command
HTTP Basic 401 challenge + network.continueWithAuthPhase 2, tracked in #147
Form-based login end-to-end (form.submit / navigate wait:"complete")Working via the cookie jar + Set-Cookie ingest path
Visual regression vs Chromium referencepkf run test-visual (paint-vrt) and pkf run test-wpt-vrt

Modules

Crater is split into focused MoonBit modules under one repository. Pick the narrow module you need:

moon add mizchi/crater-layout            # Taffy-derived layout engine
moon add mizchi/css                      # CSS parser + selector matching
moon add mizchi/crater-dom               # DOM + Shadow DOM
moon add mizchi/crater-renderer          # HTML -> layout tree -> paint tree
moon add mizchi/crater-browser           # Browser shell (cookie jar, fetch, navigation)
moon add mizchi/crater-browser-runtime   # JS runtime + DOM serializer
moon add mizchi/crater-webdriver-bidi    # WebDriver BiDi protocol surface
moon add mizchi/crater-browser-http      # http / cookie / cors / samesite / auth profile
moon add mizchi/crater-wasm              # wasm component target

Each module exposes its public contract through a generated .mbti file; CI ensures the surface doesn't drift accidentally.

Architecture highlights

  • Profile-backed HTTP state. Each BiDi session carries a Profile { cookie_jar, http_cache, auth_state, preflight_cache }. Cookies, CORS preflight cache, and per-origin Authorization headers live on the same value, scoped per browsing context; user-agent emulation stays in WebDriver state.
  • Two-stage CORS. The fetch shim runs spec-faithful classify_request -> preflight (OPTIONS) -> validate_actual_response for cross-origin requests, with a JS-side preflight cache that mirrors the MoonBit PreflightCache (same Max-Age clamp, same cache key formula).
  • Caller-wins header attach. When the runtime auto-attaches cookies or Authorization, it skips if the caller (page script) already supplied that header. Same policy for both.
  • Async / sync bridge. WebDriver BiDi handlers are synchronous MoonBit code, but navigation and fetch are async JavaScript. Bridges (js_navigate_and_send_async, js_eval_and_send_async) let the JS side send BiDi responses via socket.send once the promise resolves, instead of the MoonBit handler trying (and failing) to await.

Full design documents live under docs/superpowers/specs/:

Test compatibility

Web Platform Tests (CSS)

Pass rates measured by the layout-tree-compare runner (scripts/wpt-runner.ts):

ModulePassedTotalRate
css-flexbox28228997.6%
css-grid323397.0%
css-tables303293.8%
css-display7979100.0%
css-box3030100.0%
css-sizing839488.3%
css-align374484.1%
css-position838498.8%
css-overflow21424388.1%
css-contain28330393.4%
css-variables107107100.0%
filter-effects9810692.5%
compositing22100.0%
css-logical55100.0%
css-content1250.0%
css-multicol44100.0%
css-break262796.3%
css-color3030100.0%
css-backgrounds748092.5%
css-transforms22100.0%
css-writing-modes172763.0%
css-pseudo11100.0%
css-borders4580.0%

Module-level pass / fail counts are pinned in tests/wpt-baselines/<module>.env; CI fails if the count regresses. See pkspec spec --goals specs/crater.pkl specs/tasks.Test.pkl for per-goal coverage.

WebDriver BiDi

ProfilePassedTotal
wpt/webdriver (strict subset)277277
session130130
browsing_context10081008
script10251025
input708708
network13891389
pkf run wpt           # CSS + DOM + WebDriver BiDi
just wpt-webdriver-profile strict

Layout (Taffy compatibility)

Crater's layout engine is a MoonBit port of Taffy:

ModulePassedTotalRate
Flexbox54360989.2%
Block20422690.3%
Grid26833181.0%
Total1015116687.0%

Visual regression vs Chromium

SiteDiffStatus
example.com1.16%PASS
info.cern.ch3.30%PASS
www.google.com3.80%PASS
news.ycombinator.com12.4%WARN
en.wikipedia.org7.65%WARN
just vrt-url-native https://example.com
just vrt-url https://example.com --mask-text --mask-dynamic
just test-wpt-vrt
CRATER_PAINT_BACKEND=native just test-vrt

WPT runner setup

npm run wpt:fetch-all  # Fetch all WPT tests
npm run wpt:run-all    # Run all enabled WPT tests

WPT target selection is configured in wpt.json. Browser WPT commands:

just wpt-dom-all
just wpt-webdriver-profile strict
just wpt-webdriver session
just wpt-webdriver browsing_context
just wpt-webdriver script
just wpt-webdriver input
just wpt-webdriver network

Optional external intrinsic providers for text/image:

CRATER_TEXT_MODULE=/abs/path/to/text-module.js \
CRATER_TEXT_FONT_PATH=/abs/path/to/font.ttf \
npx tsx scripts/wpt-runner.ts css-overflow

CRATER_IMAGE_MODULE=/abs/path/to/image-module.js \
npx tsx scripts/wpt-runner.ts css-contain

See the WPT CI Maintenance section below for shard balancing notes.

Quality contracts (pkspec)

Crater's behavioural contracts live in specs/crater.pkl (pkspec). They link to the tests that exercise them, and CI fails if an approved scenario loses its implementation.

pkf run spec-check          # contracts are linked
pkf run spec-lint            # cross-references valid
pkf run spec-test            # executable smoke tests
pkspec spec --next  specs/crater.pkl specs/tasks.Test.pkl   # next-priority drafts

Open scenario draft work is tracked in GitHub issues; see the enhancement and bug labels for the active backlog.

Performance

HTML parser benchmarks (Apple Silicon):

BenchmarkTime
Simple HTML (100 elements)~27 µs
Large document (100 sections × 20 paragraphs)~5.2 ms
Attribute-heavy (200 elements, 6 attrs each)~390 µs
Table (100×20 cells)~990 µs
moon bench -p html

Layout module usage

For projects that just want the layout engine without the rest of the browser, the crater-renderer module exposes a minimal HTML → layout API:

// Parse HTML and render layout

///|
let html = "<div style=\"display: flex; width: 300px;\"><div style=\"flex: 1\">A</div><div style=\"flex: 2\">B</div></div>"

///|
let ctx = @renderer.RenderContext::default()

///|
let layout = @renderer.render(html, ctx)

// layout contains x, y, width, height for each element

WPT CI Maintenance (Parallel Shards)

The CI workflow runs WPT compatibility checks with about 6 workers:

  • wpt-css: 4 shards (wpt-css (shard-1..4))
  • wpt-dom: 1 job (wpt-dom, runs --dom and --svg)
  • wpt-webdriver: 1 job (wpt-webdriver, runs 10 strict BiDi targets)

See the shard assignment in .github/workflows/ci.yml (wpt-css-tests, wpt-dom-tests, wpt-webdriver-tests).

Each run also publishes two summaries:

  • wpt-compat-summary: compatibility totals/pass rate (wpt-summary/wpt-compat-summary.md)
  • ci-timing-summary: queue/run bottlenecks by job and group (ci-timing/summary.md)

Use this checklist when maintaining shard balance:

  1. Open the latest GitHub Actions run and check ci-timing-summary.
  2. Compare wpt-css (shard-*) durations in "Slowest Jobs".
  3. If one shard is consistently slower (roughly >10s), move modules between shard definitions in .github/workflows/ci.yml.
  4. Keep each shard runtime close (current target is roughly 65-75s per CSS shard).
  5. Verify compatibility totals from wpt-compat-summary are not regressing.

Optional local dry-run for summaries:

mkdir -p /tmp/wpt-reports
npx tsx scripts/wpt-runner.ts css-overflow css-grid css-tables --workers 4 --json /tmp/wpt-reports/wpt-css-shard-1.json
npx tsx scripts/wpt-dom-runner.ts --dom --json /tmp/wpt-reports/wpt-dom-dom.json
npx tsx scripts/wpt-dom-runner.ts --svg --json /tmp/wpt-reports/wpt-dom-svg.json
npx tsx scripts/wpt-webdriver-runner.ts --subset --json /tmp/wpt-reports/wpt-webdriver-strict.json
npx tsx scripts/wpt-ci-summary.ts --input /tmp/wpt-reports --json /tmp/wpt-summary.json --markdown /tmp/wpt-summary.md

When a compatibility improvement/regression should become the new baseline, update tests/wpt-baseline.env (used by scripts/wpt-ci-summary.ts for CSS baseline delta).

Limitations

  • Font rendering is approximate (monospace character sizing). Text-wrap parity with Chromium is an open scenario (#154).
  • Some DOM gaps under active fix — see bug.dom.* scenarios in specs/crater.pkl.
  • No GPU-based painting — the kagura native paint backend handles common output but isn't pixel-perfect against Chromium for complex content.

Documentation

License

Apache-2.0. Layout engine derived from Taffy (MIT/Apache-2.0).

Contributors

mizchi

1,580 commits

claude

23 commits

nnabeyang

1 commits

paq

1 commits

mizchi/crater

MoonBit

74

1,605 commits

updated Sep 20, 2026

See the code

README

Crater

A headless browser environment for MoonBit, driven by Playwright over WebDriver BiDi.

Use Playwright tests on a small, predictable target instead of spinning up a full Chromium. Crater speaks the WebDriver BiDi protocol natively, so existing Playwright suites can connect to it for layout-and-DOM-level testing without the weight of a real browser.

Why

  • Playwright + BiDi is becoming the standard surface for web automation. Crater treats that surface as a first-class contract instead of an afterthought.
  • The runtime is small enough to embed in tests, CI pipelines, and design tools where Chromium is overkill.
  • A predictable layout / paint pipeline (Taffy-derived) makes regressions easier to bisect than against a moving Chromium release train.
  • Built in MoonBit, so the codebase compiles to JS / native / wasm targets out of the same source.

Quick start

pnpm install
pnpm exec playwright install chromium  # only needed for VRT reference captures
pkf run prepare

Run a Playwright test against Crater (Crater starts on demand via the project's playwright.config.ts webServer):

import { test, expect } from "@playwright/test";

test("Crater serves a static page over BiDi", async ({ page }) => {
  await page.goto("data:text/html,<h1>hello</h1>");
  await expect(page.locator("h1")).toHaveText("hello");
});

Or drive it raw from the WebDriver BiDi protocol:

import { connectCraterBidi } from "./tests/helpers/crater-bidi";

const session = await connectCraterBidi();
await session.browsingContext.navigate({
  url: "data:text/html,<form id=login>...</form>",
  wait: "complete",
});
const count = await session.script.evaluate({
  expression: "document.forms.length",
});

Supported surface

AreaStatus
WebDriver BiDi session / browsingContext / script / input / network / storage100% of the WPT subset we gate on
HTML + CSS parsing100% local parser tests, 94% WPT CSS layout pass rate
DOM (wpt/dom/nodes)100%
Shadow DOM + custom elementsCore surface; full coverage in flight (compat.web-components-* scenarios)
Cookie jar (Set-Cookie / SameSite / partitioned storage)Spec-conformant for the http-only path
CORS preflight + Access-Control-* validationEnforced on script.evaluate fetches via Phase 1 of the auth/CORS spec
Per-origin Authorization injection (Bearer / JWT)crater.setOriginAuthorization BiDi extension command
HTTP Basic 401 challenge + network.continueWithAuthPhase 2, tracked in #147
Form-based login end-to-end (form.submit / navigate wait:"complete")Working via the cookie jar + Set-Cookie ingest path
Visual regression vs Chromium referencepkf run test-visual (paint-vrt) and pkf run test-wpt-vrt

Modules

Crater is split into focused MoonBit modules under one repository. Pick the narrow module you need:

moon add mizchi/crater-layout            # Taffy-derived layout engine
moon add mizchi/css                      # CSS parser + selector matching
moon add mizchi/crater-dom               # DOM + Shadow DOM
moon add mizchi/crater-renderer          # HTML -> layout tree -> paint tree
moon add mizchi/crater-browser           # Browser shell (cookie jar, fetch, navigation)
moon add mizchi/crater-browser-runtime   # JS runtime + DOM serializer
moon add mizchi/crater-webdriver-bidi    # WebDriver BiDi protocol surface
moon add mizchi/crater-browser-http      # http / cookie / cors / samesite / auth profile
moon add mizchi/crater-wasm              # wasm component target

Each module exposes its public contract through a generated .mbti file; CI ensures the surface doesn't drift accidentally.

Architecture highlights

  • Profile-backed HTTP state. Each BiDi session carries a Profile { cookie_jar, http_cache, auth_state, preflight_cache }. Cookies, CORS preflight cache, and per-origin Authorization headers live on the same value, scoped per browsing context; user-agent emulation stays in WebDriver state.
  • Two-stage CORS. The fetch shim runs spec-faithful classify_request -> preflight (OPTIONS) -> validate_actual_response for cross-origin requests, with a JS-side preflight cache that mirrors the MoonBit PreflightCache (same Max-Age clamp, same cache key formula).
  • Caller-wins header attach. When the runtime auto-attaches cookies or Authorization, it skips if the caller (page script) already supplied that header. Same policy for both.
  • Async / sync bridge. WebDriver BiDi handlers are synchronous MoonBit code, but navigation and fetch are async JavaScript. Bridges (js_navigate_and_send_async, js_eval_and_send_async) let the JS side send BiDi responses via socket.send once the promise resolves, instead of the MoonBit handler trying (and failing) to await.

Full design documents live under docs/superpowers/specs/:

Test compatibility

Web Platform Tests (CSS)

Pass rates measured by the layout-tree-compare runner (scripts/wpt-runner.ts):

ModulePassedTotalRate
css-flexbox28228997.6%
css-grid323397.0%
css-tables303293.8%
css-display7979100.0%
css-box3030100.0%
css-sizing839488.3%
css-align374484.1%
css-position838498.8%
css-overflow21424388.1%
css-contain28330393.4%
css-variables107107100.0%
filter-effects9810692.5%
compositing22100.0%
css-logical55100.0%
css-content1250.0%
css-multicol44100.0%
css-break262796.3%
css-color3030100.0%
css-backgrounds748092.5%
css-transforms22100.0%
css-writing-modes172763.0%
css-pseudo11100.0%
css-borders4580.0%

Module-level pass / fail counts are pinned in tests/wpt-baselines/<module>.env; CI fails if the count regresses. See pkspec spec --goals specs/crater.pkl specs/tasks.Test.pkl for per-goal coverage.

WebDriver BiDi

ProfilePassedTotal
wpt/webdriver (strict subset)277277
session130130
browsing_context10081008
script10251025
input708708
network13891389
pkf run wpt           # CSS + DOM + WebDriver BiDi
just wpt-webdriver-profile strict

Layout (Taffy compatibility)

Crater's layout engine is a MoonBit port of Taffy:

ModulePassedTotalRate
Flexbox54360989.2%
Block20422690.3%
Grid26833181.0%
Total1015116687.0%

Visual regression vs Chromium

SiteDiffStatus
example.com1.16%PASS
info.cern.ch3.30%PASS
www.google.com3.80%PASS
news.ycombinator.com12.4%WARN
en.wikipedia.org7.65%WARN
just vrt-url-native https://example.com
just vrt-url https://example.com --mask-text --mask-dynamic
just test-wpt-vrt
CRATER_PAINT_BACKEND=native just test-vrt

WPT runner setup

npm run wpt:fetch-all  # Fetch all WPT tests
npm run wpt:run-all    # Run all enabled WPT tests

WPT target selection is configured in wpt.json. Browser WPT commands:

just wpt-dom-all
just wpt-webdriver-profile strict
just wpt-webdriver session
just wpt-webdriver browsing_context
just wpt-webdriver script
just wpt-webdriver input
just wpt-webdriver network

Optional external intrinsic providers for text/image:

CRATER_TEXT_MODULE=/abs/path/to/text-module.js \
CRATER_TEXT_FONT_PATH=/abs/path/to/font.ttf \
npx tsx scripts/wpt-runner.ts css-overflow

CRATER_IMAGE_MODULE=/abs/path/to/image-module.js \
npx tsx scripts/wpt-runner.ts css-contain

See the WPT CI Maintenance section below for shard balancing notes.

Quality contracts (pkspec)

Crater's behavioural contracts live in specs/crater.pkl (pkspec). They link to the tests that exercise them, and CI fails if an approved scenario loses its implementation.

pkf run spec-check          # contracts are linked
pkf run spec-lint            # cross-references valid
pkf run spec-test            # executable smoke tests
pkspec spec --next  specs/crater.pkl specs/tasks.Test.pkl   # next-priority drafts

Open scenario draft work is tracked in GitHub issues; see the enhancement and bug labels for the active backlog.

Performance

HTML parser benchmarks (Apple Silicon):

BenchmarkTime
Simple HTML (100 elements)~27 µs
Large document (100 sections × 20 paragraphs)~5.2 ms
Attribute-heavy (200 elements, 6 attrs each)~390 µs
Table (100×20 cells)~990 µs
moon bench -p html

Layout module usage

For projects that just want the layout engine without the rest of the browser, the crater-renderer module exposes a minimal HTML → layout API:

// Parse HTML and render layout

///|
let html = "<div style=\"display: flex; width: 300px;\"><div style=\"flex: 1\">A</div><div style=\"flex: 2\">B</div></div>"

///|
let ctx = @renderer.RenderContext::default()

///|
let layout = @renderer.render(html, ctx)

// layout contains x, y, width, height for each element

WPT CI Maintenance (Parallel Shards)

The CI workflow runs WPT compatibility checks with about 6 workers:

  • wpt-css: 4 shards (wpt-css (shard-1..4))
  • wpt-dom: 1 job (wpt-dom, runs --dom and --svg)
  • wpt-webdriver: 1 job (wpt-webdriver, runs 10 strict BiDi targets)

See the shard assignment in .github/workflows/ci.yml (wpt-css-tests, wpt-dom-tests, wpt-webdriver-tests).

Each run also publishes two summaries:

  • wpt-compat-summary: compatibility totals/pass rate (wpt-summary/wpt-compat-summary.md)
  • ci-timing-summary: queue/run bottlenecks by job and group (ci-timing/summary.md)

Use this checklist when maintaining shard balance:

  1. Open the latest GitHub Actions run and check ci-timing-summary.
  2. Compare wpt-css (shard-*) durations in "Slowest Jobs".
  3. If one shard is consistently slower (roughly >10s), move modules between shard definitions in .github/workflows/ci.yml.
  4. Keep each shard runtime close (current target is roughly 65-75s per CSS shard).
  5. Verify compatibility totals from wpt-compat-summary are not regressing.

Optional local dry-run for summaries:

mkdir -p /tmp/wpt-reports
npx tsx scripts/wpt-runner.ts css-overflow css-grid css-tables --workers 4 --json /tmp/wpt-reports/wpt-css-shard-1.json
npx tsx scripts/wpt-dom-runner.ts --dom --json /tmp/wpt-reports/wpt-dom-dom.json
npx tsx scripts/wpt-dom-runner.ts --svg --json /tmp/wpt-reports/wpt-dom-svg.json
npx tsx scripts/wpt-webdriver-runner.ts --subset --json /tmp/wpt-reports/wpt-webdriver-strict.json
npx tsx scripts/wpt-ci-summary.ts --input /tmp/wpt-reports --json /tmp/wpt-summary.json --markdown /tmp/wpt-summary.md

When a compatibility improvement/regression should become the new baseline, update tests/wpt-baseline.env (used by scripts/wpt-ci-summary.ts for CSS baseline delta).

Limitations

  • Font rendering is approximate (monospace character sizing). Text-wrap parity with Chromium is an open scenario (#154).
  • Some DOM gaps under active fix — see bug.dom.* scenarios in specs/crater.pkl.
  • No GPU-based painting — the kagura native paint backend handles common output but isn't pixel-perfect against Chromium for complex content.

Documentation

License

Apache-2.0. Layout engine derived from Taffy (MIT/Apache-2.0).

Contributors

mizchi

1,580 commits

claude

23 commits

nnabeyang

1 commits

paq

1 commits

Languages

MoonBit

76.0%

TypeScript

14.5%

HTML

3.7%

JavaScript

3.3%

Pkl

1.6%