Skip to content
Fallow home
All docs pages

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.

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.

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:

SourceExample
package.json workspaces field"workspaces": ["packages/*"]
pnpm-workspace.yamlpackages: ["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.

{
  "workspaces": [
    "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:

{
  "workspace": ["./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.

// 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:

{
  "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.

{
  "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.

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.

{
  "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.

# 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'
● 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.

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.

{
  "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.

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:

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:

{
  "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.

See also