Development
This guide covers everything you need to set up a local development environment, understand the build system, and contribute code to the XMachines JS monorepo.
Table of Contents
- Local Setup
- Monorepo Structure
- Build Commands
- TypeScript Composite Build System
- Code Style
- Branch Conventions
- PR Process
Local Setup
Prerequisites
- Node.js
>= 22.0.0 - npm (bundled with Node.js — the project uses npm workspaces)
- Git
No global tool installs are required beyond Node.js. All build, lint, and format tooling is installed locally via npm ci.
Clone and Install
git clone git@gitlab.com:xmachin-es/xmachines-js.gitcd xmachines-jsnpm ciUse
npm ci, notnpm install.npm ciinstalls exact locked versions and triggers thepostinstallhook that runspatch-packageto apply any repository patches.
Build
npm run buildThe root-level tsc --build command uses TypeScript project references to build all packages in the correct dependency order automatically. You never need to sequence builds manually.
To build a single package (and its upstream dependencies):
npm run build -w @xmachines/<package-name>Dev Container (Optional)
A fully configured dev container is provided at .devcontainer/. It includes Docker-outside-of-Docker, Claude Code, and OpenCode.
npm run devcontainer:upOr open the repository in VS Code and choose Reopen in Container when prompted.
Verify Your Setup
npm test # Run the full test suitenpm run lint # Check for lint errorsnpm run format:check # Check formatting without modifying filesAll three should pass without errors on a freshly cloned repository.
Monorepo Structure
The repository is organized as an npm workspaces monorepo with all packages under packages/:
packages/├── shared/ # Shared configs (tsconfig, oxlint, oxfmt, vitest)├── play/ # Core protocol (PlayEvent, PlayError)├── play-signals/ # TC39 Signals polyfill wrapper├── play-actor/ # Abstract actor base (AbstractActor, Routable, Viewable)├── play-xstate/ # XState v5 adapter (definePlayer, PlayerActor)├── play-router/ # Route extraction and RouterBridgeBase├── play-dom/ # Vanilla DOM renderer├── play-dom-router/ # DOM router adapter├── play-react/ # React renderer (PlayRenderer)├── play-react-router/ # React Router v7 adapter├── play-vue/ # Vue 3 renderer├── play-vue-router/ # Vue Router adapter├── play-solid/ # SolidJS renderer├── play-solid-router/ # SolidJS Router adapter├── play-svelte/ # Svelte renderer├── play-svelte-spa-router/ # Svelte SPA Router adapter├── play-sveltekit-router/ # SvelteKit Router adapter├── play-tanstack-react-router/ # TanStack Router (React)├── play-tanstack-solid-router/ # TanStack Router (SolidJS)└── docs/ # API docs, guides, RFCs (@xmachines/docs)Many packages also contain an examples/ subdirectory with runnable demo apps.
Standard Package Layout
packages/<name>/├── src/ # TypeScript source files├── dist/ # Build output (gitignored)├── test/ # Test files (*.spec.ts or *.test.ts)├── examples/ # Runnable demo apps (optional)├── package.json # Must have "type": "module"├── tsconfig.json # Composite build config (extends tsconfig.base.json)├── tsconfig.base.json # Local base (extends @xmachines/shared/tsconfig)├── vitest.config.ts # Package test config└── README.mdBuild Commands
All commands are run from the repository root unless otherwise noted.
| Command | Description |
|---|---|
npm run build | TypeScript composite build — all packages in dependency order |
npm run build -w @xmachines/<pkg> | Build a single package (and its deps) |
npm run clean | Remove coverage, Vite caches, and all dist/ directories in packages |
npm test | Run all unit/integration tests once |
npm run test:watch | Re-run tests on file changes (interactive) |
npm run test:browser | Run Playwright browser tests |
npm run test:coverage | Run tests with V8 coverage reporting |
npm run coverage:report | Run tests and write an HTML coverage report |
npm run coverage:summary | Run tests and write a JSON summary report |
npm run test:build | Type-check test files without running them (tsconfig.test.json) |
npm run lint | Lint all packages with oxlint |
npm run lint:fix | Auto-fix lint issues |
npm run format | Format all files with oxfmt |
npm run format:check | Check formatting (CI mode — no writes) |
npm run docs | Build packages, generate TypeDoc API docs, then format |
TypeScript Composite Build System
The monorepo uses TypeScript project references for correct build-order management. No manual sequencing or separate build scripts are needed.
How It Works
- The root
tsconfig.jsoncoordinates all packages via areferencesarray — it compiles nothing itself - Each package has
composite: truein its owntsconfig.json, enabling incremental and referenced builds - Packages declare
referencesto their@xmachines/*dependencies sotsc --buildresolves order automatically - With
declarationMap: truein the base config, Go to Definition in your IDE navigates to.tssource files rather than compiled.d.tsfiles
Build Layers
Packages are grouped into dependency layers as defined in the root tsconfig.json:
| Layer | Packages | Depends on |
|---|---|---|
| 0 | play-signals, play, docs | External libs only |
| 1 | play-actor | Layer 0 |
| 2 | play-router, play-dom-router, play-sveltekit-router, play-xstate, play-react, play-vue, play-solid, play-svelte, play-dom, play-tanstack-react-router, play-vue-router, play-solid-router, play-svelte-spa-router, play-tanstack-solid-router | Layers 0–1 |
| 3 | play-react-router, example demo apps | Layer 2 |
Mermaid Diagram
flowchart LR
subgraph L0["Layer 0 — no internal deps"]
play-signals
play
docs
end
subgraph L1["Layer 1"]
play-actor
end
subgraph L2["Layer 2 — view renderers & router adapters"]
play-router
play-dom-router
play-sveltekit-router
play-xstate
play-react
play-vue
play-solid
play-svelte
play-dom
play-tanstack-react-router
play-vue-router
play-solid-router
play-svelte-spa-router
play-tanstack-solid-router
end
subgraph L3["Layer 3 — application layer"]
play-react-router
examples["example demo apps"]
end
L0 --> L1
L1 --> L2
L2 --> L3
Adding a New Package
-
Create the package directory following the standard structure:
packages/<your-package>/├── src/│ └── index.ts├── test/├── package.json # must have "type": "module"├── tsconfig.json├── tsconfig.base.json├── vitest.config.ts└── README.md -
tsconfig.base.json— extend the shared config:{"$schema": "https://json.schemastore.org/tsconfig","extends": "@xmachines/shared/tsconfig","compilerOptions": {"skipLibCheck": true}}If the package depends on other monorepo packages, add
references:{"extends": "@xmachines/shared/tsconfig","compilerOptions": { "skipLibCheck": true },"references": [{ "path": "../play" }, { "path": "../play-actor" }]} -
tsconfig.json— enable composite build:{"$schema": "https://json.schemastore.org/tsconfig","extends": "./tsconfig.base.json","compilerOptions": {"composite": true,"rootDir": "./src","outDir": "./dist"},"include": ["src/**/*"]} -
Register in root
tsconfig.json— add a reference in the correct layer:{"references": [{ "path": "./packages/your-package" }]} -
Register in root
tsconfig.test.jsonand rootvitest.config.ts(projectsarray).
Code Style
All style rules are mandatory — CI enforces them on every merge request.
Formatter — oxfmt
oxfmt (Biome-based) is configured via oxfmt.config.ts at the root (extends @xmachines/shared/oxfmt).
npm run format # Format all filesnpm run format:check # Check without writing (CI mode)Key settings (from packages/shared/config/oxfmt.config.ts):
| Setting | Value |
|---|---|
| Print width | 100 characters |
| Indentation | Tabs (useTabs: true, tabWidth: 4) |
| Semicolons | Always (semi: true) |
| Quotes | Double (singleQuote: false) |
| Trailing commas | All |
| JSON / YAML | 2-space indent (override) |
Linter — oxlint
oxlint is configured via oxlint.config.ts at the root (extends @xmachines/shared/oxlint).
npm run lint # Lint all packagesnpm run lint:fix # Auto-fix lint issuesActive plugins: typescript, unicorn, import. Key rules:
| Rule | Severity |
|---|---|
typescript/no-explicit-any | error |
import/no-cycle | error |
typescript/no-unused-vars | error (prefix unused with _) |
correctness category | error |
suspicious category | warn |
Editor Config
.editorconfig is present at the root and enforces:
- Tabs for indentation in all source files
- 2-space indent for JSON/YAML
- LF line endings, UTF-8, insert final newline
TypeScript Strict Mode
All packages extend @xmachines/shared/tsconfig which enables full strict mode. Mandatory constraints:
| Option | Value |
|---|---|
strict | true |
noUnusedLocals | true |
noUnusedParameters | true |
noImplicitReturns | true |
noImplicitOverride | true |
exactOptionalPropertyTypes | true |
verbatimModuleSyntax | true |
isolatedModules | true |
Use unknown with type guards instead of any. Prefix unused locals/parameters with _ to suppress errors.
Import Rules
-
Always use
.jsextensions in imports, even for TypeScript source files:// ✅ Correctimport { PlayError } from "./errors.js";// ❌ Wrong — will not resolve at runtime with NodeNext module resolutionimport { PlayError } from "./errors"; -
Use
import typefor type-only imports (required byverbatimModuleSyntax):import type { RouteNode, RouteTree } from "../src/types.js"; -
Import order: Node.js built-ins (
node:prefix) → external packages → internal@xmachines/*packages → relative imports
Branch Conventions
Use a <type>/ prefix that matches the conventional commit type for the work being done:
feat/vue-router-adapterfix/signal-watcher-cleanupdocs/play-actor-jsdocchore/update-vitest-4refactor/route-map-extractionRelease branches are managed by the project maintainers:
| Branch | Purpose |
|---|---|
main | Stable releases |
beta | Pre-release beta channel |
pre/rc | Release candidate channel |
Do not manually version packages or create release branches — releases are automated via semantic-release triggered by CI.
PR Process
Commit Messages
This project uses Conventional Commits — changelogs and version bumps are generated automatically from commit history.
<type>[(<scope>)][!]: <short description>
[optional body]
[optional footer]| Type | Triggers version bump | When to use |
|---|---|---|
feat | Minor | New user-facing feature |
fix | Patch | Bug fix |
docs | No bump | Documentation only |
refactor | No bump | Code restructure without behavior change |
test | No bump | Adding or updating tests |
chore | No bump | Build, tooling, or dependency changes |
perf | No bump (unless breaking) | Performance improvements |
ci | No bump | CI/CD pipeline changes |
Breaking changes: append ! after the type (feat!:) or add BREAKING CHANGE: in the footer.
Use the package short-name as scope when the change is isolated to one package:
fix(play-actor): correct signal cleanup on disposefeat(play-react): add PlayRenderer suspense boundaryBefore Submitting
-
Read the relevant RFC in
packages/docs/rfc/— ensure your change conforms to the spec -
Run the full check suite from the repo root:
Terminal window npm run buildnpm testnpm run lintnpm run format:check -
Write tests — new code must meet coverage thresholds (80% lines/functions/statements and 75% branches at the monorepo level; core packages enforce higher per-package thresholds)
-
Add JSDoc — all new public exports require JSDoc with
@param,@returns, and@seeRFC links -
Never edit
packages/docs/api/— API docs are auto-generated; edit source JSDoc and regenerate withnpm run docs
Merge Request Checklist
- Branch is up to date with
main - All tests pass (
npm test) - Lint passes (
npm run lint) - Formatting is correct (
npm run format:check) - Build succeeds (
npm run build) - New code has tests at or above coverage thresholds
- All new public exports have JSDoc
- Conventional commit format used on all commits
- RFC read and implementation conforms to spec
CI Pipeline
The GitLab CI pipeline runs automatically on merge requests, pushes to main, and git tags.
| Job | Command | Artifacts |
|---|---|---|
| Build | npm run build | — |
| Test with coverage | vitest run --coverage | JUnit XML, Cobertura coverage report |
| Lint | oxlint . | — |
| Audit | npm audit --audit-level=high | — |
| Semantic release | (automated on main/beta/pre/rc) | CHANGELOG, npm publish, GitLab release |
See ARCHITECTURE.md for the package layering and data flow details, and CONFIGURATION.md for tooling configuration reference.