# Workspaces

> Analyze a whole monorepo with no workspace setup. Fallow detects npm, yarn, pnpm, and Deno workspaces, resolves cross-package imports, and supports per-package configuration.

> Page index of all docs: https://fallow.tools/llms.txt
> Docs MCP server (search and read the docs): https://fallow.tools/docs/mcp
> HTML version of this page: https://fallow.tools/docs/configuration/workspaces/

Fallow analyzes a monorepo as one project graph. When only a sibling package uses an export, fallow does not report it as unused. You do not configure workspaces, because fallow reads the npm, yarn, pnpm, and Deno workspace formats.

Fallow resolves cross-package imports through `node_modules` symlinks or Deno import maps. You can scope the output to one package or to a set of packages.

> **Info:** Workspace detection is automatic. Fallow finds and analyzes all packages if your repo has one of these:
>
> - A `workspaces` field in `package.json`.
> - A `pnpm-workspace.yaml` file.
> - A `workspace` field in a root `deno.json` or `deno.jsonc`.
> - A `references` field in the root `tsconfig.json` (TypeScript project references).

## Workspace auto-detection

Fallow detects workspaces from these sources:

| Source | Example |
|:-------|:--------|
| `package.json` `workspaces` field | `"workspaces": ["packages/*"]` |
| `pnpm-workspace.yaml` | `packages: ["packages/*", "apps/*"]` |
| `deno.json` / `deno.jsonc` `workspace` field | `"workspace": ["./packages/*"]` |
| `tsconfig.json` `references` field | `"references": [{ "path": "./packages/core" }]` |

Fallow adds the packages from all sources together. When the repo declares no workspace patterns, fallow also scans the directories up to two levels below the root. It adds each directory with a `package.json` as a workspace package.

**package.json (npm / yarn)**

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

**pnpm-workspace.yaml**

```yaml
packages:
  - "packages/*"
  - "apps/*"
```

## Deno workspaces

Fallow reads a root `deno.json` or `deno.jsonc` the same way it reads `package.json` for npm. The `workspace` field lists member globs. It accepts the bare array form and the object form:

**deno.json (array form)**

```jsonc
{
  "workspace": ["./apps/*", "./packages/*"]
}
```

**deno.json (object form)**

```jsonc
{
  "workspace": {
    "members": ["./apps/*", "./packages/*"]
  }
}
```

Fallow reads the `name` and `exports` of each member like the same fields in `package.json`. For example, `@scope/lib` with `"exports": { ".": "./mod.ts" }` resolves like an npm package export map. When a member directory has both a `package.json` and a `deno.json`, fallow takes the package identity from `package.json`.

Fallow resolves import maps (`imports` in the root or member configs) per package. An exact specifier match wins. If there is no exact match, the longest matching slash-prefix entry applies.

Deno workspace member names count as listed dependencies. When you import a sibling package by name, fallow does not report it as an unlisted dependency.

With a pure Deno root (no root `package.json`), fallow does not warn about a missing `node_modules` directory. If the root `deno.json` or `deno.jsonc` is malformed, fallow stops with an analysis error (exit 2). It does not continue with an empty workspace list.

## Cross-workspace imports

When `apps/web` imports from `@myorg/ui`, fallow follows the `node_modules` symlink back to the workspace source files. Fallow canonicalizes symlinked paths, so it records the usage on the real source file.

**apps/web/src/App.tsx**

```ts
// Resolves to packages/ui/src/Button.tsx via node_modules symlink
import { Button } from '@myorg/ui'
```

### Pnpm content-addressable store

pnpm uses a content-addressable store with a `.pnpm` virtual store inside `node_modules`. Fallow detects these paths and maps them back to the workspace source files. Fallow also tracks cross-package usage correctly in the strict isolation mode of pnpm.

## Package exports resolution

Fallow resolves the `exports` field in the `package.json` of each package, including subpath patterns:

**packages/ui/package.json**

```json
{
  "name": "@myorg/ui",
  "exports": {
    ".": "./src/index.ts",
    "./icons": "./src/icons/index.ts",
    "./theme/*": "./src/theme/*.ts"
  }
}
```

An import of `@myorg/ui/icons` resolves to `packages/ui/src/icons/index.ts`.

## Output directory mapping

Packages often point `exports` or `main` at build output. Fallow maps these paths back to the source file, so usage counts against the code you edit. This applies to the `dist`, `build`, `out`, `esm`, and `cjs` directories. Fallow tries the same relative path under `src/` with the source extensions `.ts`, `.tsx`, `.mts`, `.cts`, `.js`, `.jsx`, `.mjs`, and `.cjs`.

Fallow also maps `lib/` targets back to `src/`, but only when the `lib/` file is not in the analyzed files. This is the case before the build, or when `.gitignore` or `ignorePatterns` excludes `lib/`. A package with hand-written source in `lib/` keeps its result. A relative import of a missing `lib/` file stays an unresolved import.

**packages/utils/package.json**

```json
{
  "main": "dist/index.js"
}
```

Fallow maps `dist/index.js` back to `src/index.ts` (or `.js`, `.tsx`, and so on) if the source file exists.

The same rule applies to a nested path and to a type target. For example, `dist/define/index.mjs` maps to `src/define/index.ts`, and a `types` entry `dist/index.d.ts` maps to `src/index.ts`. When a `tsconfig.json` maps the output path to a file that does not exist, fallow also tries the file under `src/`.

## Per-package tsconfig.json

Fallow finds the `tsconfig.json` file in each workspace package automatically. It uses the path aliases of a package to resolve imports in that package.

**packages/ui/tsconfig.json**

```json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}
```

## Scoping output to workspaces

To see the issues of one or more workspaces only, pass `-w` or `--workspace`. Fallow still analyzes the full monorepo graph and filters only the reported issues.

A pattern has one of these forms:

- **Exact package name** or **workspace path**: `web`, `@myorg/ui`, `apps/web`
- **Glob**, matched against both the package name and the workspace path relative to the repo root: `apps/*`, `@scope/*`
- **Negation** with a `!` prefix: `!apps/legacy`

Separate values with commas, or repeat the flag. Quote patterns that contain `!` or glob characters, so that your shell does not expand them.

```bash
# Single package (back-compat with earlier fallow versions)
fallow dead-code -w @myorg/ui

# Multiple packages (comma-separated or repeated)
fallow dead-code -w web,admin
fallow dead-code -w web -w admin

# Glob: all apps, all scoped packages
fallow dead-code -w 'apps/*'
fallow dead-code -w '@myorg/*'

# Everything under apps/ except legacy
fallow dead-code -w 'apps/*,!apps/legacy'

# Everything except a workspace (negation only)
fallow dead-code -w '!apps/legacy'
```

**$ fallow dead-code -w 'apps/*,!apps/legacy'**

```bash
● Unused exports (2)
  apps/web/src/Button.tsx
    :15 ButtonVariant
  apps/admin/src/Modal.tsx
    :8  ModalContext
  Exported symbols with zero references

✗ 2 issues (31ms, 1,247 files)
```

To scope the output to the workspaces that changed since a git ref, pass `--changed-workspaces <REF>` instead. You cannot combine it with `--workspace`.

> **Tip:** For CI matrix jobs, combine `--workspace` with `--changed-since`. Fallow applies both filters, so you get only the changed files inside the matched workspaces.

## Per-package change baselines

In a monorepo, packages often follow different release branches. `workspaces.changedSince` maps a workspace root to its own Git ref. For a mapped package, `check`, `dead-code`, `dupes` and the editor report only the findings in files that changed since that ref. Unlisted packages and files outside every workspace stay in full scope.

**.fallowrc.json**

```jsonc
{
  "workspaces": {
    "changedSince": {
      "packages/web": "main",
      "packages/legacy": "release/2024.10"
    }
  }
}
```

**fallow.toml**

```toml
[workspaces.changedSince]
"packages/web" = "main"
"packages/legacy" = "release/2024.10"
```

Write each key as `fallow list --workspaces` prints the workspace root: relative to the project root, with `/` separators. A file belongs to its nearest workspace, so a nested workspace uses its own entry. A clone group or a cycle stays in the report when one of its files is in scope.

The map follows these rules:

- A global `--changed-since` replaces the map for one run.
- `--no-package-baselines` or `FALLOW_PACKAGE_BASELINES=false` turns the map off, so every package is in full scope. In VS Code, set `fallow.packageBaselines` to `false`.
- `audit`, `health` and `security` do not read the map. In a bare `fallow` run, the health section is not scoped by it.
- A malformed key or ref is a config error and exits with code 2.
- A key that names no workspace of the project, or a ref that Git cannot resolve (for example in a shallow CI clone), turns the whole map off for that run. Fallow prints a warning and reports every package in full scope. The report is wider than you asked for, never narrower. A run from a package subdirectory loads the parent config, so its keys name no workspace there, and the map turns off in the same way.

JSON reports list the applied refs in `package_baselines`, for example `[{ "workspace_root": "packages/web", "reference": "main" }]`. `request_outcomes["package-baselines"]` is `applied` when the map scoped the run, and `not-applied` with a `reason` and a `message` when it turned off. A narrowed run names `package-baselines` in `baseline_staleness.scope_reasons`.

> **Warning:** The map applies to every run, so a baseline saved under it holds only the findings in changed files of the mapped packages. Fallow warns when it saves such a baseline and records the scope in the file. Save and gate a whole-project baseline with `--no-package-baselines`:
>
> ```bash
> fallow dead-code --no-package-baselines --save-baseline fallow-baselines/dead-code.json
> fallow dead-code --no-package-baselines --baseline fallow-baselines/dead-code.json --fail-on-stale-baseline
> ```
>
> The GitHub Action and the GitLab template add `--no-package-baselines` to their whole-project baseline check for you.

## Additional workspace patterns

If fallow does not detect all your workspace packages, add patterns in your config:

**.fallowrc.json**

```jsonc
{
  "workspaces": {
    "patterns": ["tools/*", "shared/*"]
  }
}
```

**fallow.toml**

```toml
[workspaces]
patterns = ["tools/*", "shared/*"]
```

These patterns add to the packages that fallow detects from `package.json`, `pnpm-workspace.yaml`, a root Deno config, or `tsconfig.json` references.

Each package that these patterns find is a full workspace package. It gets its own plugins, entry points, and dependency checks. Use this for packages that the package manager does not declare, for example app packages in an Nx monorepo that only have a `project.json` and a `package.json`.

These commands and options use the packages from `workspaces.patterns`:

- The analysis of every command that loads a config.
- `fallow list --workspaces`.
- `--workspace` and `--changed-workspaces`.
- `--group-by package`.

The base checkout of `fallow audit` and `fallow security` also uses them, and the `scope-workspaces` next step lists the packages that they declare. `fallow init` and `fallow coverage setup` read only the patterns in `package.json`, `pnpm-workspace.yaml`, and the other manifests.

To make sure that fallow finds a package, run this command:

```bash
fallow list --workspaces
```

## See also

- [Configuration](https://fallow.tools/docs/configuration/overview/): Reference for every field in the config file.
- [fallow dead-code](https://fallow.tools/docs/cli/dead-code/): CLI reference for running analysis across workspaces.
