pnpm vs Lerna: filtering in a multi-package repository
Everyone heard about Lerna, which is “A tool for managing JavaScript projects with multiple packages.” A lot fewer devs heard about pnpm, which is a fast, disk space efficient package manager for JavaScript. Both Lerna and pnpm try to improve tooling for multi-package repositories (MPR). For Lerna it was the reason for creation. For pnpm, MPR support is a nice bonus feature that is implemented via a set of commands called recursive. Of course, there are many differences between how Lerna manages a multi-package repo vs pnpm. In this article, I want to compare one seemingly simple aspect: filtering.
Filtering in an MPR is important because, during development, changes are mainly made inside one or two packages. It wouldn’t make sense to run commands on the whole repository if only a few packages were modified.
Filtering in Lerna
Filtering in Lerna (as of v3.2.1
) is achieved by the following flags:
scope
- Include only packages with names matching the given glob.include-filtered-dependents
- Include all transitive dependents when running a command regardless of--scope
,--ignore
, or--since
.include-filtered-dependencies
- Include all transitive dependencies when running a command regardless of--scope
,--ignore
, or--since
.ignore
- Exclude packages with names matching the given glob.private
- Include private packages. Pass --no-private to exclude private packages.since
- Only include packages that have been updated since the specified [ref]. If no ref is passed, it defaults to the most recent tag.
These flags make filtering in Lerna quite powerful. However, they are pretty hard to type. Let’s say you downloaded a repository and want to work only on login-page
component. You'd want to run installation for login-page
and any of its dependencies:
lerna bootstrap --scope login-page --include-filtered-dependencies
Or maybe you changed a component called site-header
and would like to run tests on all the dependent packages:
lerna run test --scope site-header --include-filtered-dependents
These flags are not only hard to type but also hard to remember and easy to mix up.
Filtering in pnpm
Unlike Lerna, pnpm uses a special package selector syntax to restrict its commands. So instead of memorizing a set of long flag names, you should only remember a very easy-to-remember selector syntax.
As of version 2.15.0
, these are the selectors that pnpm supports:
<pattern>
- Restricts the scope to package names matching the given pattern. E.g.:foo
,@bar/*
<pattern>...
- Includes all direct and indirect dependencies of the matched packages. E.g.:foo...
...<pattern>
- Includes all direct and indirect dependents of the matched packages. E.g.:...foo
,...@bar/*
./<directory>
- Includes all packages that are inside a given subdirectory. E.g.:./components
.
- Includes all packages that are under the current working directory.
These filters may be specified either via the --filter
flag or after a --
at the end of the command.
Note: as of
v2.15.0
, filters after--
are not supported byrun
,exec
,test
So if you want to bootstrap login-page
and all of its dependencies, this is the way to do it with pnpm:
pnpm recursive install -- login-page...
And if you want to run tests of site-header
and all of its dependents, use the ...<pattern>
selector:
pnpm recursive test --filter ...site-header
Of course, you can combine as many selectors as you’d like:
pnpm recursive update -- ...site-header login-page... ./utils @style/*
The command above updates dependencies in:
site-header
- dependents of
site-header
login-page
- dependencies of
login-page
- all packages that are located in the
utils
directory - all packages from the
@style
scope
pnpm might not have yet all the features that Lerna provides but for many users it might be enough already.
If you haven’t heard about pnpm yet, I recommend reading also Flat node_modules is not the only way which explains the unique node_modules structure created by pnpm.