Open Source
Monorepo for OSS Projects
Babel maintains 75+ npm packages from a single repository. React, Vue, TypeScript - all monorepos. How do you manage dozens of packages without losing your mind?
- Babel: 75+ packages in one repository - a single PR can atomically change the parser and the transformer together
- React: react, react-dom, react-reconciler, scheduler - all in one repo, guaranteed compatible with every release
- Vercel uses Turborepo in their own monorepo, open-sourced it, and remote cache saves thousands of CI minutes per day
- Remix, Svelte, Astro - all use Changesets for transparent versioning with human-readable changelog entries
Why Monorepo in OSS
A **monorepo** is a single Git repository that houses multiple packages. The opposite is a **polyrepo**: each package in its own repository. This isn't about "one repo for everything"; it's about keeping related packages together where it makes sense.
Real-world OSS monorepos: **Babel** (75+ packages: `@babel/core`, `@babel/parser`, `@babel/traverse`...), **React** (`react`, `react-dom`, `react-reconciler`, `scheduler`), **Vue 3** (`@vue/reactivity`, `@vue/runtime-core`, `@vue/compiler-core`), **Vitest**, **Changesets**, **TypeScript**.
**When monorepo makes sense:** packages share code and change together; atomic changes - one commit modifies `core` and `parser` simultaneously; shared tooling (eslint, tsconfig, jest config); package compatibility must be guaranteed before publishing.
**When polyrepo is better:** packages are independent and rarely change together; different teams with different release cadences; different tech stacks (Go backend, TS frontend).
**npm/yarn/pnpm workspaces** are the foundational mechanism for monorepos. `pnpm-workspace.yaml` or the `workspaces` field in `package.json` tells the package manager where to find local packages. Turborepo and Nx are tooling layers built on top of this.
In the React repository, a single commit changes behavior in `react-reconciler` and fixes `react-dom` to keep them compatible. In a polyrepo, this kind of atomic PR would be:
Turborepo and Nx
**Turborepo** (by Vercel) is a high-performance build system for monorepos with intelligent caching. The core idea: if input files haven't changed, don't re-run the task - use the cached output instead.
**Remote cache** allows sharing the cache between developers and CI. Verified cache artifacts are downloaded from Vercel's server or a self-hosted instance. When a developer runs `git pull`, they immediately get the CI cache - no need to rebuild what CI already built.
**Nx** (by Nrwl) is the alternative with more features: code generators, dependency graph visualization, affected commands (run tasks only for changed packages). Significantly more complex than Turborepo and requires more configuration.
**Choosing between them:** Turborepo is minimal - perfect when workspaces are already configured and caching is the main need. Nx is for when you need generators, strict architectural constraints, or are working on a very large project. Babel, Jest, Vercel use Turborepo. Angular, NgRx use Nx.
In turbo.json, the `build` pipeline has `"dependsOn": ["^build"]`. The `^` prefix means:
Changesets: Versioning in a Monorepo
In a monorepo, a tricky question arises: `react` and `react-dom` changed together - how should they be versioned? Bumping everything simultaneously loses semantic versioning meaning. **Changesets** solves this through intent files.
The Changesets workflow has three stages: **1)** Developer creates a `.changeset/*.md` file alongside their code in the PR. **2)** CI runs changeset-bot, which checks for a changeset in the PR and leaves a comment. **3)** On merge to main, a special "Version Packages" PR accumulates all changeset files and applies them.
**How well-known projects use it:** Remix requires a changeset with every PR that changes behavior; Svelte and SvelteKit use changeset-bot on every PR; Astro treats changesets as a required part of the contributor workflow; pnpm also uses Changesets.
**Alternative: Lerna + Conventional Commits.** Lerna - the oldest monorepo tool, can automatically derive version bumps from conventional commits. But Changesets gives contributors explicit control and produces human-readable changelog entries.
A monorepo means everything must be deployed together - independent service releases require separate repositories
A monorepo is a shared source repository, not a shared deployment unit. Google's monorepo contains thousands of independently deployable services. Build systems like Bazel compute a precise dependency graph and rebuild only what changed
The confusion stems from conflating code organization with deployment topology. Polyrepos create physical boundaries; monorepos remove them while build tooling maintains logical independence
A contributor opens a PR to a monorepo project using Changesets. The changeset-bot comments: "No changeset found". This means:
Key Takeaways
- Monorepo is justified when packages are tightly related: atomic changes, shared tooling, guaranteed compatibility
- pnpm/yarn/npm workspaces are the foundation; Turborepo and Nx add caching and task orchestration on top
- turbo.json pipeline with dependsOn: ['^build'] ensures correct build order for upstream dependencies
- Turborepo remote cache lets developers and CI share artifacts - rebuilds only happen when inputs actually change
- Changesets solves versioning in a monorepo: explicit intent files in PRs → automated version bumps and changelogs
Related Topics
Monorepo is inseparable from CI/CD - without automation the benefits disappear.
- CI/CD for OSS — GitHub Actions + Turborepo remote cache: CI shares cache with developers and vice versa
- RFC Process and Breaking Changes — In a monorepo, a breaking change in one package requires coordinated versioning - that's exactly what the RFC process is for
Вопросы для размышления
- You have a monorepo with packages A, B, and C, where B depends on A and C depends on B. How would you configure turbo.json to ensure builds run in the correct order A→B→C?
- A contributor adds a new feature to @my/utils and fixes a bug in @my/ui in the same PR. How should the changeset be created: one file for both packages or two separate files? Why?