Splitting a Package Out of a Monorepo
Sometimes a package outgrows the monorepo it was born in: it becomes an open-source project that needs its own issue tracker and contributors, a different team takes ownership with a different release cadence, or licensing and access requirements demand a separate repository. Extracting it well means keeping its history, keeping its npm name and version line, and replacing every internal workspace: dependency with a normal published dependency — without breaking the applications that still use it. This guide walks through the extraction with git filter-repo, the dependency changes on both sides, and the release handover.
Signals that a package should leave
- Its audience is outside the monorepo. An open-source library needs public issues, external contributors and a repository people can star and clone on its own.
- Its release cadence differs. A package released weekly by one team while the monorepo releases daily (or the reverse) creates friction in shared release tooling.
- Access or compliance differs. Code that must be private to a subset of engineers, or public while the rest is private, is easier to govern in its own repository.
- It no longer changes with anything else. If a year of history shows no pull requests that touched this package and another package together, the monorepo is not buying anything for it.
If none of these apply, keep it in the monorepo — splitting reintroduces version bumps and cross-repository coordination for every change. The broader trade-offs are covered in Monorepo Migration and Adoption.
Extracting the history
The goal is a new repository whose root is the package folder, with every commit that touched that folder and nothing else.
# 1. Fresh clone of the monorepo (filter-repo rewrites it)
git clone https://git.example.com/acme/acme.git /tmp/acme-sdk
cd /tmp/acme-sdk
# 2. Keep only the package folder and move it to the repository root
git filter-repo --subdirectory-filter packages/sdk
# 3. Keep only this package's release tags, renamed back to plain versions
git tag -l | grep -v '^@acme/sdk@' | xargs -r git tag -d
git filter-repo --tag-rename '@acme/sdk@':'v' --force
# 4. Push to the new repository
git remote add origin https://git.example.com/acme/acme-sdk.git
git push origin main --tags
--subdirectory-filter is the inverse of the --to-subdirectory-filter used when merging repositories in — see Merging Repositories While Preserving Git History. Commits that touched only other packages disappear; commits that touched this package and others keep only this package's part of the diff.
Making the extracted package self-contained
Inside the monorepo, the package leaned on shared infrastructure. In the new repository it needs its own:
- Dependencies on other internal packages. Any
workspace:range in itspackage.jsonmust become a published version range ("@acme/utils": "^3.2.0"). If a dependency was never published, either publish it, or copy the needed code in and remove the dependency. - Shared configuration.
extends: "@acme/tsconfig/library.json"and the shared ESLint config either become published config packages or are inlined into the new repository. - Tooling and scripts — build, test and release scripts that relied on the monorepo's task runner and release tool.
- CI — a new pipeline for build, test and publish, including provenance if the package had it.
- Lockfile — generate a fresh one with a clean install; the monorepo's lockfile does not apply.
Updating the monorepo
Consumers left in the monorepo now depend on a package that is no longer local. Change their dependency from workspace:^ to a published range, and remove the package folder in the same pull request:
# In the monorepo
git rm -r packages/sdk
pnpm --filter "@acme/*" update @acme/sdk@^4.1.0 # consumers now use the registry version
pnpm install
Do this only after the extracted repository has published a version that the consumers can use — ideally the same code as the last monorepo release, published as the next patch version from the new repository. That way, the switch from local to registry changes nothing functionally.
Also remove the package from release configuration (Changesets fixed or linked groups, CODEOWNERS entries, task-runner overrides) so nothing references a folder that no longer exists.
Release handover
Version continuity matters to consumers. The new repository should publish the next version after the last one released from the monorepo, under the same npm name, with the same dist-tags. Check npm view @acme/sdk versions --json and continue from the highest. If the monorepo used a release tool that tracks versions through changeset files or tags, make sure the new repository's release tool starts from the correct current version — for Changesets, the version field in package.json is the source of truth; for semantic-release, the imported v* tags are.
Transfer publishing rights too: the new repository's CI needs publish access (a token or trusted publishing configured for the new repository), and the monorepo's release pipeline should lose it for that package. Trusted publishing is described in Publishing from CI with npm Trusted Publishing.
Alternatives to a full split
Before extracting, consider whether a lighter change solves the underlying problem.
Mirroring for open source. If the goal is public visibility while development stays in the monorepo, a read-only mirror repository updated by CI (git subtree split --prefix=packages/sdk pushed to the mirror on every release) gives the public a repository to browse and clone without changing where work happens. External contributions then need a process to bring pull requests back into the monorepo, which works for occasional contributors but not for an active community.
Separate release configuration. If the friction is release cadence, most release tools support per-package configuration inside a monorepo: independent versioning, separate changeset groups, or a dedicated release workflow triggered only by that package's changes.
Access control within the monorepo. If the friction is ownership, CODEOWNERS and required reviews may be enough, as covered in Setting Up CODEOWNERS for Monorepo Packages. Genuine confidentiality — code some engineers must not read — does require a separate repository.
Keeping the two sides compatible after the split
Once the package lives elsewhere, monorepo applications consume it through the registry like any third-party dependency. That brings back version management: a fix in the SDK reaches the applications only after a release and an update. Configure your dependency bot to watch the package with an auto-merge rule for patch releases, so fixes flow quickly, and add a contract test in the monorepo — a small test that exercises the SDK's public API the way applications use it — so a breaking change in a minor release is caught at the update pull request rather than in production. If changes routinely need to land in both repositories at once, that is strong evidence the split was premature.
Worked example: open-sourcing an SDK
A company decides to open-source its JavaScript SDK, which lives in its private monorepo alongside internal applications. They extract packages/sdk with --subdirectory-filter, scan the extracted history for secrets and internal hostnames (finding and rewriting two internal URLs in old test fixtures), replace its dependency on the private @acme/http package by inlining the forty lines it used, and publish 4.1.1 from the new public repository with provenance. The internal applications switch from workspace:^ to ^4.1.1. Contributors outside the company can now open issues and pull requests, and internal teams consume the SDK like any other dependency.
Prevention and guardrails
- Extract from a fresh clone, never by rewriting the monorepo.
- Scan the extracted history for secrets and internal references before pushing it anywhere public.
- Publish from the new repository before switching consumers.
- Continue the version line and npm name, and move publish rights explicitly.
Frequently Asked Questions
Can I keep developing the package in both places for a while? Avoid it. Two sources of truth guarantee divergence. Pick a cut-over date, freeze changes in the monorepo folder, extract, and make the new repository the only home.
What if the package depends on unpublished internal packages? Publish them, inline the code, or keep the package in the monorepo. An extracted package must install from public (or private registry) sources alone.
Does extraction affect the monorepo's history? No. The monorepo keeps every commit, including the package's history up to the removal. Only the fresh clone is rewritten.
How do I point people from the old folder to the new repository?
Leave a short README.md in the monorepo at the old path for one release cycle, stating where the package now lives, then remove it. Update links in documentation and the package's repository field in package.json so the npm page points to the new home.
Related
- Monorepo Migration and Adoption covers when to consolidate and when to separate.
- Merging Repositories While Preserving Git History is the reverse operation.
- Renaming a Package Without Stranding Users helps if the split also changes the package name.
- Transferring Package Ownership to an Organization covers moving publish rights.