LPM-cli

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

package.json
{
  "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 install

LPM 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 web

Adds 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.

FilterMatches
webThe 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)
...webweb and every member that depends on web (transitive dependents — upstream closure)
...^webSame, but excluding web itself (dependents-only)
!webExclusion — 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 core does 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 --affected

Selects 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-match

5. Cross-member deps

packages/web/package.json
{
  "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 --dev

The 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 relative file: specs
  • A populated node_modules/
  • A deploy-local .lpm/store/ and lpm.lock for 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:

  • --filter or --filter-prod is required and must match exactly one member.
  • The output directory must be outside the workspace tree.
  • --prod is the default dependency mode. Use --dev for a dev-dependency deploy tree, and --no-optional to omit optional dependencies.

Common pitfalls

  • --filter core doesn't match @babel/core. Substring matching is not supported. Use --filter '*/core'.
  • Multi-member install prompts you for confirmation by default. If a --filter expression would mutate more than one member's package.json, lpm install asks before writing. Use -y in scripts.
  • lpm deploy targets one member. If your filter matches more than one workspace package, narrow it before deploying.

See also