# Monorepo setup (/docs/guides/monorepo-setup)



LPM CLI 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 [#1-declare-workspaces]

```json title="package.json"
{
  "name": "my-repo",
  "private": true,
  "workspaces": ["packages/*", "apps/*"]
}
```

Or the object form (yarn-style):

```json
{ "workspaces": { "packages": ["packages/*", "apps/*"] } }
```

LPM CLI accepts both. Add member packages under those globs — each member has its own `package.json`. Run:

```bash
lpm install
```

LPM CLI 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 [#2-add-a-dep-to-one-member]

```bash
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:

```bash
lpm install -D typescript -w
```

`-w` / `--workspace-root` writes to the root `package.json > devDependencies` instead of any member.

## 3. The filter grammar [#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`](/docs/packages/workspaces) 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 &#x2A;*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 core` does NOT match `@babel/core`. Write `--filter '*/core'` for that.

Preview what a filter expression would select without running anything:

```bash
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 [#4---affected-for-ci]

```bash
lpm run test --affected
```

Selects only members affected by changes vs the base branch (default `main`). Pair with `--base develop` to change the branch:

```bash
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:

```toml
[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:

```bash
lpm run test --filter web --fail-if-no-match
```

## 5. Cross-member deps [#5-cross-member-deps]

```json title="packages/web/package.json"
{
  "dependencies": {
    "@my-co/shared": "workspace:*"
  }
}
```

`workspace:*` tells the resolver "use the local workspace member, not a published version." LPM CLI 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` [#6-deploy-builds-with-lpm-deploy]

For Docker / serverless deploys, you don't want to ship the entire monorepo. [`lpm deploy`](/docs/packages/workspaces) materializes one member's deploy closure into a self-contained directory:

```bash
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:

```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 [#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 [#see-also]

* [Workspaces](/docs/packages/workspaces) — full filter grammar reference
* [`lpm filter`](/docs/packages/workspaces) — preview what a filter selects
* [`lpm install --filter`](/docs/packages/install#workspaces) — install scoped to members
* [`lpm run --filter --affected`](/docs/dev/run#workspaces) — task running with workspace selection
* [`lpm deploy`](/docs/packages/workspaces) — production-closure materialization
