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
| Tool | Version | Notes |
|---|---|---|
| Node.js | ≥ 18.18 | nodejs.org |
| Rush | latest | npm install -g @microsoft/rush |
| Docker | any recent | Required 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
# 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 buildRush reads the project graph from rush.json, installs all packages into the shared
common/temp/ store, and links them using pnpm workspaces.
#Repository structure
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
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:
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:
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:
{
"operationSettings": [{ "operationName": "build", "outputFolderNames": ["dist"] }]
}Key rules:
rush build— reads and writes the cache; unchanged packages finish instantly.rush rebuild— always 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/:
| Package | Target packages | Includes |
|---|---|---|
@yoltra/eslint-config-base | @yoltra/core | ESLint recommended, typescript-eslint recommended, browser + Node globals |
@yoltra/eslint-config-react | @yoltra/react | Extends base + react-hooks + react-refresh |
Each library package has a thin eslint.config.mjs that just re-exports the shared config:
// packages/core/eslint.config.mjs
import baseConfig from "@yoltra/eslint-config-base";
export default baseConfig;// 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:
-
Follow Conventional Commits:
ts<type>(<scope>): <short description> [optional body] Signed-off-by: Your Name <you@example.com> -
Carry a DCO sign-off (
git commit -sappends 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:
Package Lines / statements Branches Functions @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.95enforces 0.95%, which is how a gate believed to require 95% passed at essentially zero for a while.
# All packages
rush test
# Single package
cd packages/core && rushx testSnapshot 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.
# Interactive prompt — select the packages you changed and the bump type
rush change
# Verify a change file exists
rush change -vChange 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
- Create the folder under
packages/ortools/. - Add a
package.jsonwith"publishConfig": { "access": "public" }. - Add a minimal
rush-project.json(declareoutputFolderNamesif the package builds). - Register the package in
rush.jsonunder"projects". - Run
rush updateto regenerate the lockfile. - 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.
"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:
"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
- Edit the relevant
package.json. - Run
rush updateto recalculate and rewrite the lockfile. - Commit both the
package.jsonchange and the updatedcommon/config/rush/pnpm-lock.yaml.
Never touch common/config/rush/pnpm-lock.yaml by hand.
#Troubleshooting
| Symptom | Fix |
|---|---|
| Missing change file (rush change -v) | rush change, commit the file in common/changes/. |
rush install peer dep errors | strictPeerDependencies: false is already set; try rush install --purge. |
| Commit rejected | Ensure Conventional Commits format + DCO sign-off (git commit -s). |
| Stale build output | rush 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 found | npm install -g @microsoft/rush |
| Wrong pnpm version in lockfile | Never run pnpm install directly; always use rush install / rush update. |