Monorepo setup
Workspaces, --filter grammar, --affected, lpm deploy.
LPM treats workspaces as a first-class concept — declare them in package.json, then every command that operates on the dep graph (install, uninstall, run, lint, fmt, check) accepts --filter, --all, and --affected to scope work. This guide covers a real monorepo from scratch.
1. Declare workspaces
{
"name": "my-repo",
"private": true,
"workspaces": ["packages/*", "apps/*"]
}Or the object form (yarn-style):
{ "workspaces": { "packages": ["packages/*", "apps/*"] } }LPM accepts both. Add member packages under those globs — each member has its own package.json. Run:
lpm installLPM walks the workspace, builds a topological graph, and links every member's deps (including cross-member workspace:* references) into the right node_modules/.
2. Add a dep to one member
lpm install react --filter webAdds react to packages/web/package.json and runs the install pipeline scoped to packages/web/. The other members are untouched.
For a tooling dep that lives at the workspace root:
lpm install -D typescript -w-w / --workspace-root writes to the root package.json > devDependencies instead of any member.
3. The filter grammar
--filter is the most-used flag in monorepo workflows. --filter-prod accepts the same grammar but skips devDependencies during closure expansion. Both are available across install, uninstall, run, lint, fmt, check, deploy, and the standalone lpm filter preview command.
| Filter | Matches |
|---|---|
web | The member literally named web |
@scope/* | Every member with a name under a scope |
@scope/*{./apps/web} | Members matching both the name atom and exact path |
./apps/* | Every member under a path glob |
{./apps/web} | Exact path match (path with literal characters) |
[origin/main] | Members changed since a git ref |
web... | web and every member web depends on (transitive deps — downstream closure) |
web^... | Same, but excluding web itself (deps-only) |
...web | web and every member that depends on web (transitive dependents — upstream closure) |
...^web | Same, but excluding web itself (dependents-only) |
!web | Exclusion — remove web from the resulting set |
Filters compose: --filter web --filter api unions the two sets. --filter web --filter '!web-tests' selects web minus web-tests.
Use --filter-prod ...shared for production-only dependent closures: members that reach shared only through devDependencies are not selected.
Substring matching is not supported.
--filter coredoes NOT match@babel/core. Write--filter '*/core'for that.
Preview what a filter expression would select without running anything:
lpm filter web...
lpm filter --filter-prod ...shared
lpm filter '[main]' --changed-files-ignore-pattern '**/README.md'
lpm filter '...[main]' --test-pattern '**/*.test.js'
lpm filter web... --explain # full per-package trace (which filter matched, how)4. --affected for CI
lpm run test --affectedSelects only members affected by changes vs the base branch (default main). Pair with --base develop to change the branch:
lpm run test --affected --base develop
lpm run test --affected --changed-files-ignore-pattern '**/README.md'
lpm run test --affected --test-pattern '**/*.test.js'Saves CI minutes — a one-line README change in apps/marketing doesn't trigger a full-monorepo test run.
Persist noisy git-diff paths and test-only globs in lpm.toml when every CI run should use them:
[workspace]
changed-files-ignore-pattern = ["**/README.md", "docs/**"]
test-pattern = ["**/*.test.js", "**/*.spec.ts"]--test-pattern keeps the directly changed package selected, but it stops test-only changes from expanding to every dependent package.
--fail-if-no-match makes a typo'd filter exit non-zero. Recommended in CI:
lpm run test --filter web --fail-if-no-match5. Cross-member deps
{
"dependencies": {
"@my-co/shared": "workspace:*"
}
}workspace:* tells the resolver "use the local workspace member, not a published version." LPM links the two via symlink (in isolated layout) and respects topological order in tasks (dependsOn: ["^build"] waits for upstream member's build task to finish first).
6. Deploy builds with lpm deploy
For Docker / serverless deploys, you don't want to ship the entire monorepo. lpm deploy materializes one member's deploy closure into a self-contained directory:
lpm deploy /prod/api --filter api
lpm deploy /prod/api --filter api --no-optional
lpm deploy /prod/api --filter api --devThe output dir has:
- The targeted member's publishable source files (honors
files,.npmignore, then.gitignore, while still excluding.env*,node_modules,.git, etc.) - Local workspace dependencies copied under
.lpm/deploy-workspace/and referenced with relativefile:specs - A populated
node_modules/ - A deploy-local
.lpm/store/andlpm.lockfor the deploy tree
In a Dockerfile:
FROM workspace as pruned
RUN lpm deploy /prod/api --filter api
FROM node:22-alpine
COPY --from=pruned /prod/api /app
WORKDIR /app
CMD ["node", "server.js"]Constraints:
--filteror--filter-prodis required and must match exactly one member.- The output directory must be outside the workspace tree.
--prodis the default dependency mode. Use--devfor a dev-dependency deploy tree, and--no-optionalto omit optional dependencies.
Common pitfalls
--filter coredoesn't match@babel/core. Substring matching is not supported. Use--filter '*/core'.- Multi-member install prompts you for confirmation by default. If a
--filterexpression would mutate more than one member'spackage.json,lpm installasks before writing. Use-yin scripts. lpm deploytargets one member. If your filter matches more than one workspace package, narrow it before deploying.
See also
- Workspaces — full filter grammar reference
lpm filter— preview what a filter selectslpm install --filter— install scoped to memberslpm run --filter --affected— task running with workspace selectionlpm deploy— production-closure materialization