Workspaces
Monorepo support, selectors, and workspace configuration
snpm has first-class workspace support: a single lockfile at the workspace root, fan-out commands, dependency-graph aware selectors, and shared catalogs.
Discovery
snpm discovers a workspace by walking up from the current directory and looking for, in order:
snpm-workspace.yamlpnpm-workspace.yamlpackage.jsonwith aworkspacesfield (string array or{ packages: [...] }form)
Whichever is found first wins, and its parent directory becomes the workspace root.
snpm-workspace.yaml
packages:
- "packages/*"
- "apps/*"
# Optional shared catalogs
catalog:
react: ^18.2.0
typescript: ^5.4.0
catalogs:
build:
vite: ^5.0.0
testing:
vitest: ^1.0.0
# Optional: hoisting override for the workspace
hoisting: single-version
# Optional: dependency script allow/deny lists
onlyBuiltDependencies:
- esbuild
- sharp
ignoredBuiltDependencies:
- fsevents
# Optional: packages that must stay in the project-local virtual store
disableGlobalVirtualStoreForPackages:
- next
- vitepnpm-workspace.yaml is parsed with the same shape, plus catalog / catalogs aliases.
Example layout
my-monorepo/
├── snpm-workspace.yaml
├── package.json
├── snpm-lock.yaml ← one lockfile at the root
├── packages/
│ ├── ui/package.json
│ └── utils/package.json
└── apps/
└── web/package.jsonworkspace: protocol
Local packages can reference each other with workspace::
{
"dependencies": {
"@acme/ui": "workspace:*",
"@acme/utils": "workspace:^"
}
}| Version | Meaning |
|---|---|
workspace:* | Any version of the local package |
workspace:^ | Caret range of the local package's current version |
workspace:~ | Tilde range of the local package's current version |
workspace:1.2.3 | Specific local version |
On publish, these specifiers are rewritten to concrete versions.
Common commands
# install everything from the workspace root
snpm install
# install for a single workspace project
snpm install -w @acme/api
# run a script in every workspace project
snpm run build -r
# run a script in a subset (see selector syntax below)
snpm run test --filter "@acme/*"
# add/remove across workspaces
snpm add -r -D vitest
snpm remove -r lodash
# inspect across workspaces
snpm outdated -r
snpm why typescript -r
snpm list -rEvery workspace-aware command supports the same selectors: add, remove, run, exec, upgrade, outdated, list, why, and publish.
Selector syntax
--filter and --filter-prod accept a dependency-graph-aware selector grammar. Multiple --filter values combine — positive selectors union, negative selectors subtract.
| Pattern | Meaning |
|---|---|
pkg-name | Exact project name |
@scope/*, foo-? | Glob over project names |
./packages/api, ../lib, /abs/path | Match by project path (relative to the workspace root, or absolute) |
pkg... | The package and everything it transitively depends on |
...pkg | The package and everything that transitively depends on it |
^pkg... / pkg^... | Same as above, but exclude the seed itself |
[<git-ref>] | Projects with files changed since <git-ref> (runs git diff --name-only <ref> from the workspace root) |
!pkg | Exclude (must be combined with at least one positive selector) |
--filter-prod uses the same grammar but walks only dependencies / optionalDependencies for graph operators, so it ignores devDependency edges.
Examples
# every project under packages/api
snpm run test --filter ./packages/api
# api plus its dependencies
snpm run build --filter api...
# everything that depends on the shared utils package
snpm run test --filter ...@acme/utils
# everything affected since the last main commit
snpm run test --filter "[origin/main]"
# everything in the @acme scope except docs
snpm run lint --filter "@acme/*" --filter "!@acme/docs"Catalogs
Define dependency versions once at the workspace root and reference them with the catalog: protocol:
catalog:
react: ^18.2.0
typescript: ^5.4.0
catalogs:
build:
vite: ^5.0.0
testing:
vitest: ^1.0.0{
"dependencies": {
"react": "catalog:"
},
"devDependencies": {
"vite": "catalog:build",
"vitest": "catalog:testing"
}
}When snpm-workspace.yaml and snpm-catalog.yaml both define a key, the workspace file wins. See Catalog for the full reference.
Overrides
Pin transitive dependency versions for the whole workspace via either snpm-overrides.yaml or package.json:
overrides:
lodash: ^4.17.21
"@babel/core": ^7.23.0{
"snpm": {
"overrides": { "lodash": "^4.17.21" }
},
"pnpm": {
"overrides": { "axios": "^1.6.0" }
}
}Both snpm.overrides and pnpm.overrides are honored.
Per-project install
You can also cd into a workspace project and run snpm install from there. snpm will still discover the workspace root and write a single lockfile at the top.
Best practices
-
Scope your packages (
@acme/ui,@acme/utils) so selectors and link names stay tidy. -
Use catalogs for shared deps to eliminate version drift across packages.
-
Reach for
[git-ref]selectors in CI to only build what changed:snpm run test --filter "[origin/main]". -
Pin the snpm version in the workspace root
package.json:{ "packageManager": "snpm@2026.5.16" }Combined with
snpm-switch, every contributor and CI job will run the samesnpmbuild.