Contributing
Repo layout
The repository is a Bun workspace with two workspace globs, packages/* and examples/*, plus a docs app under apps/*. Notable packages:
| Package | Purpose |
|---|---|
packages/parser |
A @babel/parser fork: MX in expression position lowers to JSX, for the SolidMX host. |
packages/core |
The Marko-node consumer every MX host is built on: structural lowerings, the IR, HostDeclarations, Emitter<Out>, three stateful-tag hooks, two front doors. |
packages/targets/html |
The html target: .mx/.marko compile to a pure (input) => string function. |
packages/hosts/astro |
The Astro host: components, pages, and .astro.mx templates, all rendered to static markup. |
packages/tooling/language-server |
A diagnostics-only LSP server for MX hosts. |
packages/editors/zed |
The Zed editor extension (three languages plus the language server registration). |
packages/oracle |
The parity-checking harness described below. |
Each package publishes its own README with install instructions, API surface, and implementation notes specific to that package — the pages under Hosts and Editors summarize the parts a consumer needs; the READMEs go deeper for a contributor.
Verify and its rules
bun run verify (or moon run :verify) is the single command that must pass before anything merges. It chains: a pre-verify step that clears stale test evidence, a build, a typecheck pass, lint, the vitest suite, the Bun-runtime test suite, the tree-sitter grammar test, a consumer-facing smoke check, and a coverage-verification script — in that order, because later steps depend on artifacts earlier steps produce.
Three rules this chain is built around:
- A check that can never fail is not a check. Every gate in the chain is expected to be able to fail, and is proven to fail at least once against a deliberately broken input before it’s trusted. A script that only prints success is a bug, not a safety net.
- Evidence, not vibes. The coverage-verification step doesn’t ask “does this package have test files” — it reads the actual output of the test run that just happened (a JSON report, or a marker file written only on success) and checks its timestamp is newer than the start of this verification run. A package can’t be marked “tested” by leftover evidence from a previous run.
- Clean-clone proofs. Anything that depends on generated or vendored files (a build step, a vendored grammar) is expected to work from a fresh clone with no manual setup step, and CI is what actually proves this — a green run on a contributor’s machine with stale local state doesn’t count.
The docs site as a verify step
This site (apps/docs) is wired into verify and CI the same way: bun run --cwd apps/docs build runs as part of the chain, and a broken page — a bad internal link, invalid config, a markdown file that fails to parse — fails the build and fails verify. The generated site/ output directory is not committed.
The oracles
Each host is checked against real, working reference code rather than only against expectations recorded in a test file. There are four runners, and they answer two different questions.
| Command | What it compares | Current state |
|---|---|---|
bun run oracle |
Each SolidMX fixture against a hand-written Solid twin, across both Solid 2 backends and both generate variants | 20 rows, all pass |
bun run oracle:marko |
Each stock fixture rendered through the real Marko toolchain and through the html target | 43 fixtures: 41 pass, 2 reasoned skips, 0 bugs |
bun run oracle:preact |
The same 43 fixtures rendered with preact-render-to-string |
30 pass, 13 reasoned skips, 0 bugs |
bun run oracle:react |
The same 43 fixtures rendered with react-dom/server |
30 pass, 13 reasoned skips, 0 bugs |
oracle asks does MX produce what a hand-written component would. The other three ask does the structural core still mean what Marko means — the regression guard for the subset rule itself. A skip is a recorded, reasoned classification in the fixture’s meta.json, not unfinished work; a run also fails if it processed too few fixtures, so a gate that silently does nothing cannot pass.
The three host oracles are CI jobs rather than part of verify: the Marko toolchain is a real install and memory cost.
Adding a fixture
A Marko-parity fixture is a directory under the html target’s fixtures-marko/ holding input.marko, input.json (the props) and expected.html. Generate expected.html from real Marko; never hand-write it — a hand-written expectation records what you believed, not what Marko does. If a construct is deliberately out of scope for a host, add a meta.json naming the reason instead of deleting the fixture.
A SolidMX fixture is a directory holding input.solid.mx and a hand-written twin.tsx. The twin must not introduce whitespace MX drops: MX follows Marko’s rule that a whitespace-only run containing a newline disappears, so text<p>… goes on one line in the twin wherever the MX source separates them only by indentation.
How to add a host
A host is a HostDeclarations object plus an Emitter<Out> over @mxlang/core, and whatever integration glue its target ecosystem needs (a bundler plugin, a loader, a framework-specific renderer). Concretely:
- Decide what your target’s “public input language” is — the ordinary source format its own toolchain already knows how to compile (JSX text, a plain string-returning function, etc.).
- Write the
HostDeclarations: which names are elements versus components, which tags are inert or errors here, how a modifier or an attribute method is rejected in your target’s own words, and — if your host wants stateful tags to mean something — the three hooks (claim a tag, hoist a statement, rewrite identifier references). - Write the
Emitter<Out>: one method per IR kind. Throw, naming the construct, for anything your target cannot express — never silently skip a kind. - Add fixtures and wire your host into an oracle so its output is checked against real reference code, not only assertions in a test file.
See Core and hosts and Policy and hooks for the interface itself.
How to add a fixture
A fixture for the Marko-parity oracle lives in its own directory alongside a plain .marko input file, a small JSON file of props to render it with, and an expected.html file — generated from a real run of Marko’s own toolchain, never hand-written, so the comparison is always against ground truth rather than a guess at what Marko would do. A fixture for the framework-parity oracle instead pairs an MX source file with a hand-written twin in the target framework’s native syntax; both are compiled and their outputs compared across every backend and variant the target supports.
Consumer Check
The verification chain includes a consumer smoke check. This step simulates how an end-user would consume the package, building and packing a tarball (via npm pack) and running a minimal test project against it. This proves the published artifact is structurally sound.
Adding a Host to the Shared JSX Emitter
Many targets (React, Preact) share the same JSX structure. To add a new host on the shared JSX emitter:
- Provide a dialect object containing only the differences (e.g.,
classNamevsclass,htmlForvsfor). - Implement your specific runtime boundaries if needed (like
<try>'sMxErrorBoundary). - Point the shared emitter at your dialect object.
The Edit Check Hook
An edit hook (.claude/hyper.json) runs up to two checks after an edit, each with its own file extensions:
biome check .— formatting and lint over the whole tree, after an edit to a.ts,.tsxor.jsonfile.- A per-package TypeScript check, after an edit to a
.tsor.tsxfile only. Each package with atsconfig.jsonis checked with its owntypecheckscript when it has one (which is what lets the examples runmx-tsc), and with plaintsc --noEmit -potherwise.
So editing a .json file runs the first check alone.
Both commands use ./node_modules/.bin paths directly, so they work without shell shims (proto, bun, nvm wrappers). One deliberate exception: a package whose typecheck depends on the built mx-tsc binary falls back to plain tsc when that binary is not built yet, rather than failing — so a fresh worktree still gets useful checks before its first bun run build.