Syncpack: Herding Your Monorepo's Dependency Versions Into Line
Picture this: you open a bug report where useContext mysteriously returns null in production. After an hour of head-scratching, you discover the culprit. Your web package depends on react@^18.2.0, your admin package pins react@18.0.0, and your shared ui package quietly asks for react@^18.3.1. Your bundler dutifully installs three copies of React, and now your hooks are talking past each other. This is the kind of slow-burn pain that Syncpack exists to extinguish.
syncpack is a command-line tool that audits and fixes how dependency versions are declared across all the package.json files in a JavaScript monorepo. Think of it as a linter and autofixer for version strings. It does not install anything, it does not touch your lockfile or node_modules, and it never reaches out to rewrite your code. It reads the version specifiers you have written and rewrites them to match a policy you choose, then hands control back to your package manager. It is tiny (around 68 KB), MIT licensed, and quietly load-bearing at the likes of AWS, Microsoft, Vercel, Cloudflare, and DataDog, even though most developers have never heard of it.
Why Your Workspace Drifts
In a single-package project, you have exactly one package.json, so version drift is impossible. The moment you grow into a workspace with a dozen packages, the same dependency starts sprouting subtly different versions in each one. Nobody does this on purpose; it accumulates one hurried npm install at a time.
The consequences pile up. You get duplicate installs and a bloated node_modules, where multiple copies of the same library waste disk and confuse singletons. You get subtle runtime bugs like the React example above. You get a chaotic mix of range styles, where some packages pin exact versions and others use carets with no governing policy. And worst of all, upgrading a single dependency means manually spelunking through dozens of files.
Syncpack detects every one of these mismatches and can fix them in one command, so every package agrees on, say, react@18.3.1, or whatever policy you decide is correct.
What It Brings to the Table
Syncpack packs a surprising amount into one small binary:
- Find and fix version mismatches across all workspace packages with a single command.
- Single version policy with customizable partitions, so you can force one React version everywhere while still allowing a legacy app to follow different rules.
- Semver range enforcement, letting you require that production dependencies be exact while development dependencies use a tilde, for example.
- Pin or ban dependencies, locking a package to a fixed version or forbidding a deprecated library outright.
- Update outdated versions from npm, like
npm-check-updatesbut monorepo-aware and with an interactive picker. - Format and sort package.json into a conventional field order with alphabetized nested objects.
- Catalog support for both pnpm and Bun, including auto-migration and version bumping.
- A supply-chain guard that can exclude releases newer than a chosen number of days to avoid freshly compromised packages.
Getting It Into Your Project
Install Syncpack as a development dependency, or run it ad hoc with your package runner.
npm install --save-dev syncpack
# or run it without installing
npx syncpack list
yarn add --dev syncpack
It requires Node 14.17 or newer, which any modern setup comfortably clears.
The Core Loop: Look, Lint, Fix
The everyday rhythm of Syncpack is three commands. Start by surveying the landscape with list, which prints every dependency and tells you whether it matches its group.
syncpack list
When you are ready to enforce a policy, reach for lint. It exits with a non-zero status code the moment any specifier violates your version or semver rules, which makes it perfect to drop into a CI pipeline.
# Fail the build if any version or range is out of policy
syncpack lint
# Narrow the scope to specific dependency types or names
syncpack lint --dependency-types prod,dev
syncpack lint --dependencies '**react**'
Once lint has told you what is wrong, fix makes it right. It rewrites the offending specifiers across every package.json so they conform.
# Autofix everything lint complains about
syncpack fix
# Only touch one dependency
syncpack fix --dependencies react
# Force exact specifiers
syncpack fix --specifier-types exact
One important habit to build: after fix (or update) rewrites your files, Syncpack has only changed the declared versions. You still need to run your package manager's install step to actually reconcile node_modules and the lockfile. It is easy to forget this in CI and wonder why nothing changed on disk.
A Note for Anyone Following Old Tutorials
If you have searched for Syncpack tutorials, you have almost certainly seen list-mismatches and fix-mismatches. Those were the classic commands, and they live on in muscle memory and countless blog posts, but they were merged and renamed in version 14. On current releases, lint absorbs the old list-mismatches plus lint-semver-ranges, and fix absorbs fix-mismatches plus set-semver-ranges. A few command-line flags moved at the same time: --types became --dependency-types, --specs became --specifier-types, and the regex-based --filter became the glob-based --dependencies. If a snippet from 2023 is not working, this rename is almost always why.
Tidying Files Without Touching Versions
Version logic and cosmetic formatting are now strictly separate jobs. The format command sorts package.json fields into a conventional order and alphabetizes nested objects, and it does nothing to your version numbers.
syncpack format
This separation is deliberate and recent. In older versions, fix-mismatches also reformatted files, which produced noisy diffs that buried the meaningful version changes. As of version 14, fix leaves formatting entirely alone, so if you want both tidy structure and consistent versions, run format and fix as two distinct steps.
Writing a Configuration File
For anything beyond the defaults, Syncpack reads a config file. It accepts a generous range of formats, including .syncpackrc, .syncpackrc.json, .syncpackrc.yaml, syncpack.config.ts, and even a syncpack property inside package.json. A published JSON schema gives you editor autocomplete.
{
"$schema": "./node_modules/syncpack/schema.json",
"indent": " ",
"sortFirst": ["name", "version", "description", "scripts"],
"sortAz": ["dependencies", "devDependencies", "peerDependencies"]
}
If you prefer TypeScript, the package exports an RcFile type so your config is type-checked.
export default {
indent: " ",
} satisfies import("syncpack").RcFile;
Version Groups: Different Rules for Different Corners
The real power of Syncpack lives in version groups. Each group partitions a set of dependencies and applies its own policy, and the first matching rule wins, so order your rules from most specific to most general.
To forbid a deprecated package from creeping back in, mark a group as banned. Anyone who tries to add it will fail lint.
{
"versionGroups": [
{
"label": "Prohibited Dependencies",
"dependencies": ["old-library", "@deprecated/**"],
"isBanned": true,
"severity": { "IsBanned": "error" }
}
]
}
To lock a dependency to one exact version everywhere, pin it.
{
"versionGroups": [
{ "dependencies": ["react", "react-dom"], "pinVersion": "18.3.1" }
]
}
Beyond banned and pinned, groups support several other behaviors. A snappedTo group aligns versions to a reference package, sameRange ensures all declared ranges remain mutually compatible, and ignored excludes a dependency from every check. Note that dependencies arrays use glob patterns, not regular expressions, so @deprecated/** matches a scope rather than being read as a regex.
Semver Groups: Governing the Range Style
While version groups care about the number, semver groups care about the prefix, the ^, ~, or exact pin in front of it. A common and sensible policy is to require exact versions for production dependencies while allowing a tilde for development ones.
{
"semverGroups": [
{
"dependencies": ["@company/internal-*"],
"packages": ["@company/app"],
"range": "^"
},
{ "dependencyTypes": ["prod"], "range": "" },
{ "dependencyTypes": ["dev"], "range": "~" }
]
}
Here an empty string means an exact version, ^ means caret, and ~ means tilde. As with version groups, the first match wins, so a more specific rule near the top will shadow the general ones below it.
Catalogs: The Modern Reason to Look Again
Newer package managers offer catalogs, a way to centralize a single version of each dependency in pnpm-workspace.yaml (for pnpm) or the Bun equivalent, which every workspace package then references by alias. Syncpack is one of the very few tools that understands catalogs natively. It can auto-migrate your scattered dependencies into a catalog, lint and fix catalog entries, and bump catalog versions during an update.
# Lint or update only the pnpm catalog entries
syncpack lint --dependency-types pnpmCatalog
syncpack update --dependency-types pnpmCatalog
If you have been eyeing catalogs but dreading the migration, this support alone is worth the install.
Bumping Everything From npm
Syncpack also covers the job most people reach for npm-check-updates to do, except it works across the whole workspace and respects your groups. The update command pulls fresh versions from npm.
# Bump everything to the latest published version
syncpack update --target latest
# Pick patch updates interactively
syncpack update --interactive --target patch
# Only update the AWS SDK packages
syncpack update --dependencies '@aws-sdk/**'
The --target flag accepts latest, minor, or patch. In interactive mode the arrow keys navigate, space toggles a selection, and a toggles everything at once. For teams worried about supply-chain attacks, the minimumReleaseAge config option tells update to skip any release younger than a set number of days, giving the community time to flag a compromised package before it lands in your tree.
Wiring It Into CI
Putting the pieces together, a typical guardrail looks like a single lint step in your pipeline that fails the build on any drift, paired with a local fix-and-format workflow developers run before committing.
# In CI: fail on any inconsistency
syncpack lint
# Locally, before committing
syncpack fix
syncpack format
npm install
Syncpack pairs naturally with other tools rather than replacing them. Teams often run a structural linter like @manypkg/cli for workspace shape, Syncpack for version consistency, and Renovate or Dependabot for automated upstream update PRs. Each operates at a different layer, and together they keep a large monorepo honest.
The Tidy Conclusion
Version drift is one of those problems that stays invisible until it bites, and by then you are debugging duplicate Reacts at midnight. Syncpack turns that invisible chaos into something you can lint, fix, and enforce in CI, all without touching your lockfile or your source. Start small with npx syncpack list to see what your workspace looks like today, graduate to a lint step in CI, and reach for version groups, semver groups, and catalogs as your policies sharpen. It is a small tool with an outsized calming effect, and once your packages all agree on the same versions, you will wonder how you ever lived without it.