Docs

Developer Guide

Single source of truth for setting up the monorepo, understanding its structure, and doing day-to-day development work.

For the branching strategy and PR process see WORKFLOW.md. For performing releases (local + NPM) see RELEASE_GUIDE.md.


#Prerequisites

ToolVersionNotes
Node.js≥ 18.18nodejs.org
Rushlatestnpm install -g @microsoft/rush
Dockerany recentRequired only for local registry testing

Do not install pnpm globally. Rush downloads and manages pnpm internally at the exact version pinned in rush.json. Running pnpm install directly will produce incorrect results and break the lockfile.


#First-time setup

bash
# 1. Clone
git clone https://github.com/yoltra/yoltra.git
cd yoltra

# 2. Install all workspace dependencies (Rush manages pnpm)
rush install

# 3. Build the entire monorepo (graph-aware, incremental)
rush build

Rush reads the project graph from rush.json, installs all packages into the shared common/temp/ store, and links them using pnpm workspaces.


#Repository structure

ts
yoltra/
├── packages/
│   ├── core/                 @yoltra/core      — state container library
│   ├── react/                @yoltra/react     — React bindings
│   └── ds/                   @yoltra/ds        — design system for the site/docs/examples

├── devtools/
│   ├── devtools-protocol/    @yoltra/devtools-protocol      — message types + patch utils
│   ├── devtools-server/      @yoltra/devtools-server        — the hub (WebSocket relay)
│   ├── devtools-browser-agent/ @yoltra/devtools-browser-agent — store-side agent (browser)
│   ├── devtools-node-agent/  @yoltra/devtools-node-agent    — store-side agent (Node)
│   ├── devtools-ui/          @yoltra/devtools-ui            — headless hooks + loopback hub
│   ├── devtools-storeview/   @yoltra/devtools-storeview     — embeddable panel (React)
│   ├── devtools-ext/         @yoltra/devtools-ext           — MV3 browser extension shell
│   └── devtools-cli/         @yoltra/devtools-cli           — Ink terminal UI

├── tools/
│   ├── eslint-config-base/   @yoltra/eslint-config-base  — shared ESLint (Node + browser TS)
│   ├── eslint-config-react/  @yoltra/eslint-config-react — shared ESLint (React + TS)
│   ├── repo-tools/           @eraelco/repo-tools         — repo-level lint/commitlint bins
│   └── registry/             Verdaccio local registry (Docker)

├── examples/
│   └── v0/
│       ├── yoltra-mission-control/    Flagship — store + embedded DevTools panel, no install
│       ├── yoltra-react-counter/      Minimal end-to-end example
│       ├── yoltra-in-react/           Yoltra vs Redux Toolkit comparison app
│       ├── yoltra-in-nextjs/          Next.js integration example
│       └── yoltra-kinetic-logo/       SVG animation — fine-grained subscription demo

├── common/
│   ├── config/rush/          Rush config files (committed — never edit lockfile by hand)
│   ├── changes/              Rush change files (generated by `rush change`)
│   └── scripts/              Shared helpers (copy-license.cjs, etc.)

└── docs/
    ├── en/                   English documentation (this folder)
    └── es/                   Spanish translations

#Everyday commands

#Monorepo-wide

bash
rush install            # Install / sync all dependencies (after cloning or pulling)
rush update             # Regenerate lockfile (run after editing any package.json)
rush build              # Incremental build — uses cache, skips unchanged packages
rush rebuild            # Force full rebuild — bypasses cache, rebuilds everything
rush test               # Run Vitest across all packages
rush lint               # Run ESLint across all packages
rush typecheck          # Run tsc --noEmit across all packages

#Focused builds

Use --to and --from to narrow the build to a subset of the dependency graph:

bash
rush build --to @yoltra/core           # Build core and its transitive deps
rush build --to @yoltra/react          # Build react (and core first)
rush build --from @yoltra/core         # Build core and every downstream dependent
rush build --to @yoltra/react --verbose  # Same, with detailed output

#Per-package commands (rushx)

rushx runs an npm script in the current package. Change to the package directory first:

bash
cd packages/core
rushx build         # Build just this package
rushx test          # Run tests with coverage
rushx lint          # Check for lint errors
rushx lint:fix      # Auto-fix lint issues
rushx typecheck     # TypeScript type checking

cd packages/react
rushx build
rushx test
rushx docs          # Generate TypeDoc API docs

#Build cache

Rush's local build cache is enabled via common/config/rush/build-cache.json.

Each library package declares its cacheable output in rush-project.json:

json
{
  "operationSettings": [{ "operationName": "build", "outputFolderNames": ["dist"] }]
}

Key rules:

  • rush build — reads and writes the cache; unchanged packages finish instantly.
  • rush rebuildalways skips the cache; use this when you suspect a stale output.
  • Cache lives in common/temp/build-cache/ (gitignored, local only).

#ESLint architecture

Lint configuration is extracted into two shareable packages under tools/:

PackageTarget packagesIncludes
@yoltra/eslint-config-base@yoltra/coreESLint recommended, typescript-eslint recommended, browser + Node globals
@yoltra/eslint-config-react@yoltra/reactExtends base + react-hooks + react-refresh

Each library package has a thin eslint.config.mjs that just re-exports the shared config:

js
// packages/core/eslint.config.mjs
import baseConfig from "@yoltra/eslint-config-base";
export default baseConfig;
js
// packages/react/eslint.config.mjs
import reactConfig from "@yoltra/eslint-config-react";
export default reactConfig;

To add a rule globally — edit the config package in tools/. No need to touch each library's eslint.config.mjs. To override a rule for one package — extend the array in that package's eslint.config.mjs.


#Conventional commits + DCO

Every commit must:

  1. Follow Conventional Commits:

    ts
    <type>(<scope>): <short description>
    
    [optional body]
    
    Signed-off-by: Your Name <you@example.com>
  2. Carry a DCO sign-off (git commit -s appends it automatically).

Allowed <type> values: feat, fix, perf, refactor, docs, test, build, chore, revert.


#Testing & coverage

  • Runner: Vitest

  • UI helpers: @testing-library/react (for @yoltra/react)

  • Coverage thresholds are enforced per package and per metric, and they are set at the level actually met so that a regression fails the build:

    PackageLines / statementsBranchesFunctions
    @yoltra/core95%92%90%
    @yoltra/react95%91%93%

    Raising branches and functions to 95% everywhere is open work rather than a number nobody meets. Note that Vitest reads these as percentages: a threshold written 0.95 enforces 0.95%, which is how a gate believed to require 95% passed at essentially zero for a while.

bash
# All packages
rush test

# Single package
cd packages/core && rushx test

Snapshot tests are only allowed for stable, deterministic output.


#Change files (required for every publishable PR)

Any PR that modifies @yoltra/core, @yoltra/react, or another published package must include a Rush change file. CI enforces this: rush change --verify runs on every PR to main (see .github/workflows/ci.yml and the Release Guide). Verify locally before pushing with the command below.

bash
# Interactive prompt — select the packages you changed and the bump type
rush change

# Verify a change file exists
rush change -v

Change files are committed to common/changes/ alongside the code change. When a release is prepared they are consumed by rush version --bump to update package.json versions and generate CHANGELOG.md entries.

While the project is < 1.0.0: use minor for breaking changes and patch for fixes.

#Two things rush change -v will not tell you

Its failure message says "run rush change", which is no help when you already have.

A change file must be committed, not merely written or staged. The check reads change files from the diff against the target branch, so an uncommitted one is invisible to it — and the error is word-for-word identical to having written none at all. If you are staring at a file you just created while Rush insists it does not exist, commit it.

The first PR after a release bump is asked for change files it did not earn. rush version --bump rewrites dependency ranges ("@yoltra/core": "^0.3.0""^0.4.0") in every package that depends on a lockstep sibling. Rush ignores a package.json diff that only touches a project's own version field, but treats a dependency-range edit as real content — so those packages, and only those, get flagged. Expect @yoltra/react, @yoltra/devtools-node-agent and @yoltra/devtools-browser-agent. Write them a change file describing your actual work; it is not a stale cache and there is nothing to purge.


#Adding a new publishable package

  1. Create the folder under packages/ or tools/.
  2. Add a package.json with "publishConfig": { "access": "public" }.
  3. Add a minimal rush-project.json (declare outputFolderNames if the package builds).
  4. Register the package in rush.json under "projects".
  5. Run rush update to regenerate the lockfile.
  6. If it ships in sync with the product suite, set "versionPolicyName": "yoltra" (the lockstep policy — see the Release Guide); otherwise leave it unset for independent versioning.

#Build output conventions

Two rules that only bite consumers, so nothing in this repo catches them for you. Both are enforced by node common/scripts/check-publish-metadata.mjs — run it before publishing.

Name build outputs .mjs and .cjs, never .esm.js / .cjs.js. Node decides a .js file's format from the nearest package.json type field, so a bare .js means the opposite thing in a "type": "module" package than it does elsewhere. Both directions have shipped broken: a require condition pointing at a .js file inside a "type": "module" package throws ReferenceError: exports is not defined, and an import condition pointing at a .js file in a package with no type field fails on every Node before 22.7.

jsonc
"exports": {
  ".": {
    "types":   "./dist/types/index.d.ts",
    "import":  "./dist/thing.mjs",
    "require": "./dist/thing.cjs"
  }
}

Add the declaration-extension step to the package's build script:

jsonc
"build": "vite build && node ../../tools/repo-tools/bin/dts-extensions.mjs dist/types"

TypeScript emits declarations with whatever the source wrote, and this repo writes extensionless relative imports. In a "type": "module" package those do not resolve, and since nearly every consumer sets skipLibCheck: true the errors are suppressed while every re-exported symbol degrades to any — a green build with no type checking at all, which is worse than a failure because nothing about it looks like one.


#Updating dependencies

  1. Edit the relevant package.json.
  2. Run rush update to recalculate and rewrite the lockfile.
  3. Commit both the package.json change and the updated common/config/rush/pnpm-lock.yaml.

Never touch common/config/rush/pnpm-lock.yaml by hand.


#Troubleshooting

SymptomFix
Missing change file (rush change -v)rush change, commit the file in common/changes/.
rush install peer dep errorsstrictPeerDependencies: false is already set; try rush install --purge.
Commit rejectedEnsure Conventional Commits format + DCO sign-off (git commit -s).
Stale build outputrush rebuild bypasses cache and forces a full recompile.
Verdaccio: "version already exists"Bump version (rush change + rush version --bump) or wipe with docker compose down -v.
rushx not foundnpm install -g @microsoft/rush
Wrong pnpm version in lockfileNever run pnpm install directly; always use rush install / rush update.
Edit this page on GitHub