Skip to content

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

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

Terminal window
git clone git@gitlab.com:xmachin-es/xmachines-js.git
cd xmachines-js
npm ci

Use npm ci, not npm install. npm ci installs exact locked versions and triggers the postinstall hook that runs patch-package to apply any repository patches.

Build

Terminal window
npm run build

The 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):

Terminal window
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.

Terminal window
npm run devcontainer:up

Or open the repository in VS Code and choose Reopen in Container when prompted.

Verify Your Setup

Terminal window
npm test # Run the full test suite
npm run lint # Check for lint errors
npm run format:check # Check formatting without modifying files

All 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.md

Build Commands

All commands are run from the repository root unless otherwise noted.

CommandDescription
npm run buildTypeScript composite build — all packages in dependency order
npm run build -w @xmachines/<pkg>Build a single package (and its deps)
npm run cleanRemove coverage, Vite caches, and all dist/ directories in packages
npm testRun all unit/integration tests once
npm run test:watchRe-run tests on file changes (interactive)
npm run test:browserRun Playwright browser tests
npm run test:coverageRun tests with V8 coverage reporting
npm run coverage:reportRun tests and write an HTML coverage report
npm run coverage:summaryRun tests and write a JSON summary report
npm run test:buildType-check test files without running them (tsconfig.test.json)
npm run lintLint all packages with oxlint
npm run lint:fixAuto-fix lint issues
npm run formatFormat all files with oxfmt
npm run format:checkCheck formatting (CI mode — no writes)
npm run docsBuild 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.json coordinates all packages via a references array — it compiles nothing itself
  • Each package has composite: true in its own tsconfig.json, enabling incremental and referenced builds
  • Packages declare references to their @xmachines/* dependencies so tsc --build resolves order automatically
  • With declarationMap: true in the base config, Go to Definition in your IDE navigates to .ts source files rather than compiled .d.ts files

Build Layers

Packages are grouped into dependency layers as defined in the root tsconfig.json:

LayerPackagesDepends on
0play-signals, play, docsExternal libs only
1play-actorLayer 0
2play-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-routerLayers 0–1
3play-react-router, example demo appsLayer 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

  1. 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
  2. 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" }]
    }
  3. tsconfig.json — enable composite build:

    {
    "$schema": "https://json.schemastore.org/tsconfig",
    "extends": "./tsconfig.base.json",
    "compilerOptions": {
    "composite": true,
    "rootDir": "./src",
    "outDir": "./dist"
    },
    "include": ["src/**/*"]
    }
  4. Register in root tsconfig.json — add a reference in the correct layer:

    {
    "references": [{ "path": "./packages/your-package" }]
    }
  5. Register in root tsconfig.test.json and root vitest.config.ts (projects array).


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).

Terminal window
npm run format # Format all files
npm run format:check # Check without writing (CI mode)

Key settings (from packages/shared/config/oxfmt.config.ts):

SettingValue
Print width100 characters
IndentationTabs (useTabs: true, tabWidth: 4)
SemicolonsAlways (semi: true)
QuotesDouble (singleQuote: false)
Trailing commasAll
JSON / YAML2-space indent (override)

Linter — oxlint

oxlint is configured via oxlint.config.ts at the root (extends @xmachines/shared/oxlint).

Terminal window
npm run lint # Lint all packages
npm run lint:fix # Auto-fix lint issues

Active plugins: typescript, unicorn, import. Key rules:

RuleSeverity
typescript/no-explicit-anyerror
import/no-cycleerror
typescript/no-unused-varserror (prefix unused with _)
correctness categoryerror
suspicious categorywarn

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:

OptionValue
stricttrue
noUnusedLocalstrue
noUnusedParameterstrue
noImplicitReturnstrue
noImplicitOverridetrue
exactOptionalPropertyTypestrue
verbatimModuleSyntaxtrue
isolatedModulestrue

Use unknown with type guards instead of any. Prefix unused locals/parameters with _ to suppress errors.

Import Rules

  1. Always use .js extensions in imports, even for TypeScript source files:

    // ✅ Correct
    import { PlayError } from "./errors.js";
    // ❌ Wrong — will not resolve at runtime with NodeNext module resolution
    import { PlayError } from "./errors";
  2. Use import type for type-only imports (required by verbatimModuleSyntax):

    import type { RouteNode, RouteTree } from "../src/types.js";
  3. 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-adapter
fix/signal-watcher-cleanup
docs/play-actor-jsdoc
chore/update-vitest-4
refactor/route-map-extraction

Release branches are managed by the project maintainers:

BranchPurpose
mainStable releases
betaPre-release beta channel
pre/rcRelease 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]
TypeTriggers version bumpWhen to use
featMinorNew user-facing feature
fixPatchBug fix
docsNo bumpDocumentation only
refactorNo bumpCode restructure without behavior change
testNo bumpAdding or updating tests
choreNo bumpBuild, tooling, or dependency changes
perfNo bump (unless breaking)Performance improvements
ciNo bumpCI/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 dispose
feat(play-react): add PlayRenderer suspense boundary

Before Submitting

  1. Read the relevant RFC in packages/docs/rfc/ — ensure your change conforms to the spec

  2. Run the full check suite from the repo root:

    Terminal window
    npm run build
    npm test
    npm run lint
    npm run format:check
  3. 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)

  4. Add JSDoc — all new public exports require JSDoc with @param, @returns, and @see RFC links

  5. Never edit packages/docs/api/ — API docs are auto-generated; edit source JSDoc and regenerate with npm 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.

JobCommandArtifacts
Buildnpm run build
Test with coveragevitest run --coverageJUnit XML, Cobertura coverage report
Lintoxlint .
Auditnpm 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.