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
  • pnpm via corepack (corepack enable; version pinned by the packageManager field — the project uses pnpm workspaces)
  • Git

No global tool installs are required beyond Node.js. All build, lint, and format tooling is installed locally via pnpm install --frozen-lockfile.

Clone and Install

Terminal window
git clone git@gitlab.com:xmachin-es/xmachines-js.git
cd xmachines-js
pnpm install --frozen-lockfile

Use pnpm install --frozen-lockfile (not pnpm install) — it installs the exact versions from the lockfile.

Build

Terminal window
pnpm 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
pnpm --filter @xmachines/<package-name> run build

Dev Container (Optional)

A fully configured dev container is provided at .devcontainer/. It includes Docker-outside-of-Docker, Claude Code, and OpenCode.

Terminal window
pnpm run devcontainer:up

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

Verify Your Setup

Terminal window
pnpm test # Run the full test suite
pnpm run lint # Check for lint errors
pnpm 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 a pnpm 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
pnpm run buildTypeScript composite build — all packages in dependency order
pnpm --filter @xmachines/<pkg> run buildBuild a single package (and its deps)
pnpm run cleanRemove coverage, Vite caches, and all dist/ directories in packages
pnpm testRun all unit/integration tests once
pnpm run test:watchRe-run tests on file changes (interactive)
pnpm run test:browserRun Playwright browser tests
pnpm run test:coverageRun tests with V8 coverage reporting
pnpm run coverage:reportRun tests and write an HTML coverage report
pnpm run coverage:summaryRun tests and write a JSON summary report
pnpm run test:buildType-check test files without running them (tsconfig.test.json)
pnpm run lintLint all packages with oxlint
pnpm run lint:fixAuto-fix lint issues
pnpm run formatFormat all files with oxfmt
pnpm run format:checkCheck formatting (CI mode — no writes)
pnpm 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
pnpm run format # Format all files
pnpm 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
pnpm run lint # Lint all packages
pnpm 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
    pnpm run build
    pnpm test
    pnpm run lint
    pnpm 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 pnpm run docs

Merge Request Checklist

  • Branch is up to date with main
  • All tests pass (pnpm test)
  • Lint passes (pnpm run lint)
  • Formatting is correct (pnpm run format:check)
  • Build succeeds (pnpm 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
Buildpnpm run build
Test with coveragevitest run --coverageJUnit XML, Cobertura coverage report
Lintoxlint .
Auditpnpm audit --audit-level=high
Semantic release(automated on main/beta/pre/rc)CHANGELOG, npm publish, GitLab release

See Architecture for the package layering and data flow details, and Configuration for tooling configuration reference.