Monorepo Tooling
This document defines the complete tooling stack for the StateLoom monorepo. Every tool choice is justified by the project's constraints: solo contributor, ~15–20 packages, library-first (not application-first), and strict TypeScript throughout.
Tool Stack Summary
| Concern | Tool | Why |
|---|---|---|
| Package manager | pnpm | Strict isolation, workspace:^ protocol, fastest |
| Build orchestrator | Turborepo | Minimal config, task caching, zero opinions |
| Library bundler | tsup | esbuild-powered, ESM+CJS+dts in one config |
| App bundler | Vite | Native framework plugins, fast HMR |
| Test runner | Vitest | Vite-native, workspace-aware, Jest-compatible API |
| Linter | ESLint v9 (flat config) | Single root config, typescript-eslint, extensible |
| Formatter | Prettier | Universal standard, no config debates |
| TypeScript | Strict mode + project references | Maximum type safety |
| Versioning | Changesets | Coordinated multi-package releases |
| Git hooks | simple-git-hooks + lint-staged | Lightweight, no Husky overhead |
| Docs site | VitePress + Mermaid + Shiki | Markdown-first, fast, batteries-included |
| CI | GitHub Actions | Lint, typecheck, build, test, size check |
| Node version | 20 LTS | Stable, corepack support |
Package Manager: pnpm
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'examples/*'Why pnpm over npm/yarn:
- Strict dependency resolution: Isolated
node_modulesprevents phantom dependencies across 15+ packages - Content-addressable store: Hard links mean near-zero disk duplication
workspace:^protocol: Inter-package references resolve correctly during both development and publishing- Speed: Fastest install times of any package manager
- Ecosystem adoption: Used by Vue, Vite, TanStack, Turborepo
# .npmrc
strict-peer-dependencies=true
auto-install-peers=false
shamefully-hoist=falseBuild Orchestrator: Turborepo
Turborepo provides task caching and dependency-aware parallel execution with a single config file.
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json", "tsup.config.ts"],
"outputs": ["dist/**"],
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**", "__tests__/**", "vitest.config.ts"],
},
"test:watch": {
"cache": false,
"persistent": true,
},
"lint": {
"inputs": ["src/**", "eslint.config.ts"],
},
"typecheck": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json"],
},
"dev": {
"cache": false,
"persistent": true,
"dependsOn": ["^build"],
},
"clean": {
"cache": false,
},
},
}Key behaviors:
"dependsOn": ["^build"]— build dependencies before building the package- Task caching — unchanged packages skip rebuilds (~200ms restore vs 30s build)
--filter— run tasks for specific packages:turbo run test --filter=@stateloom/core- Affected detection —
turbo run test --filter=...[origin/main]runs only changed packages + dependents
Library Bundler: tsup
tsup produces ESM + CJS dual outputs with .d.ts declarations from minimal config:
// packages/core/tsup.config.ts
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
sourcemap: true,
clean: true,
treeshake: true,
minify: false, // consumers handle minification
outDir: 'dist',
});Why tsup over Vite library mode: tsup generates .d.ts natively and handles CJS/ESM dual output with proper package.json exports field. Vite library mode requires vite-plugin-dts and more configuration.
Package.json Exports
Every package uses conditional exports:
{
"type": "module",
"sideEffects": false,
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs",
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs",
},
},
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"files": ["dist", "README.md"],
}Test Runner: Vitest
Vitest runs in workspace mode with per-package configs:
// vitest.workspace.ts (root)
export default ['packages/*/vitest.config.ts', 'examples/*/vitest.config.ts'];// packages/core/vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
coverage: {
provider: 'v8',
thresholds: {
statements: 95,
branches: 95,
functions: 95,
lines: 95,
},
},
},
});Coverage target: near 100%. Every package must maintain 95%+ coverage. Coverage reports are generated per-package and aggregated in CI.
Linting: ESLint v9 (Flat Config)
A single eslint.config.ts at the root covers all packages:
// eslint.config.ts
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
// Enforce strict TypeScript patterns
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-call': 'error',
'@typescript-eslint/no-unsafe-member-access': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
],
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
// No enums — use const objects + as const
'no-restricted-syntax': [
'error',
{
selector: 'TSEnumDeclaration',
message:
'Use `as const` objects instead of enums. See docs/contributing/coding-guidelines.',
},
],
},
},
{
ignores: ['**/dist/**', '**/node_modules/**', '**/.turbo/**'],
},
);Formatting: Prettier
// .prettierrc
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"arrowParens": "always",
"endOfLine": "lf",
}TypeScript: Strict Mode
// tsconfig.base.json
{
"compilerOptions": {
"strict": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"isolatedModules": true,
"skipLibCheck": true,
"lib": ["ES2022"],
},
}Each package extends this:
// packages/core/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
},
"include": ["src"],
"references": [],
}Versioning: Changesets
// .changeset/config.json
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [["@stateloom/*"]],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["*-example"],
}Release Workflow
pnpm changeset # describe changes
pnpm changeset version # bump versions + update CHANGELOGs
pnpm sync-example-versions # sync example deps to new version
git add . && git commit -m "version packages"
git push # CI runs release workflowSyncing Example Versions
Examples use real npm version ranges (e.g., ^1.0.0-alpha.0) instead of workspace:^ so that StackBlitz can install packages from the npm registry when loading examples directly from the GitHub repo. pnpm still resolves these to local workspace packages during development because the version range matches.
After any version bump, run:
pnpm sync-example-versions # reads version from packages/core
pnpm sync-example-versions 1.2.0-beta.0 # explicit version overrideThe script updates all @stateloom/* dependencies in examples/*/package.json to ^<version>.
Git Hooks: simple-git-hooks + lint-staged
// In root package.json
{
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged",
},
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,yml,yaml}": ["prettier --write"],
},
}CI/CD: GitHub Actions
PR Checks (ci.yml)
name: CI
on: [pull_request]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm build
- run: pnpm testRelease (release.yml)
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'pnpm', registry-url: 'https://registry.npmjs.org' }
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm test
- uses: changesets/action@v1
with:
publish: pnpm release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}Bundle Size Check (size-check.yml)
name: Size Check
on: [pull_request]
jobs:
size:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm sizePre-Publish Validation
Before each publish, these tools validate package correctness:
| Tool | Purpose |
|---|---|
| publint | Validate package.json exports, files, types |
| arethetypeswrong | Check .d.ts resolution for ESM/CJS |
| size-limit | Track and enforce bundle size budgets |
| knip | Find unused exports, dependencies, files |
Root Scripts
{
"scripts": {
"build": "turbo run build",
"test": "turbo run test",
"test:watch": "turbo run test:watch",
"lint": "turbo run lint",
"lint:fix": "eslint --fix .",
"typecheck": "turbo run typecheck",
"format": "prettier --write .",
"clean": "turbo run clean && rm -rf node_modules",
"changeset": "changeset",
"version-packages": "changeset version",
"release": "turbo run build && changeset publish",
"new-package": "tsx scripts/new-package.ts",
"sync-example-versions": "tsx scripts/sync-example-versions.ts",
"size": "tsx scripts/check-bundle-size.ts",
},
}Documentation Site: VitePress
The docs site uses VitePress with vitepress-plugin-mermaid for browser-side diagram rendering and Shiki (built-in) for syntax highlighting. See Documentation Guidelines for details.