Derived Modular Arch
Guides

Monorepo

dma check in a monorepo: multi-root discovery, workspace packages, CI.

dma check / dma doctor analyze one DMA src/ tree per root. In a monorepo you can still point at a single app, or run from the workspace root and let the CLI discover apps.

One check — one app (still valid)

npx @derived-modular/cli check apps/web
npx @derived-modular/cli check apps/admin
npx @derived-modular/cli check examples/vite-react

The path must point at the app root with src/{app|pages|routes, features, services?, shared}.

Multi-root from the monorepo root

If path has no src/, the CLI discovers DMA roots and checks each separately:

npx @derived-modular/cli check .

Default discovery: packages with src/ and a composition root (app | pages | routes).

How candidates are found: npm/bun package.json workspaces or pnpm-workspace.yaml first; otherwise a shallow directory walk (skips node_modules, dist, …).

Flags

# explicit list (skips discovery; positional path ignored)
npx @derived-modular/cli check --roots apps/web,apps/admin

# also include library packages with src/{features|services|shared} but no composition root
npx @derived-modular/cli check . --include-packages

Or put the same in dma.config.ts (see dma.config):

import { defineConfig } from "@derived-modular/cli";

export default defineConfig({
  roots: ["apps/web", "apps/admin"],
});

Each root is a separate graph. Workspace imports between packages are not merged — intentional.

Exit codes are aggregated: 1 if any root fails check; 2 if no roots found or a --roots entry has no src/.

CI for multiple apps

Single job from repo root

- run: npx @derived-modular/cli check .

Job matrix (per app)

strategy:
  matrix:
    app: [apps/web, apps/admin]
steps:
  - run: npx @derived-modular/cli check ${{ matrix.app }}

Explicit roots

npx @derived-modular/cli check --roots apps/web,apps/admin

Workspace packages (stage 4)

When a module is consumed by multiple apps, extract it into a monorepo package:

packages/
└── cart/
    ├── package.json      # exports point at public surface
    └── src/
        └── public/
            └── cart.ts

Same rules: direct paths, no barrel inside the package. Enforcement via exports in package.json and app imports to the published surface.

To run dma check on such a package (no composition root), use --include-packages or --roots packages/cart.

The app imports @my-org/cart/public/cart (or a path from exports) — as an external module lower in the layer stack, not as a feature.

More on stage 4 — Modules and public API.

Paths and tsconfig

@/ aliases resolve from the app's tsconfig.json, not the monorepo root tsconfig. Make sure paths are set in the package that owns src/.

Examples in this repository

Each example is a workspace package under examples/*:

bun run build   # build CLI and plugins
bun run --cwd examples/vite-react dma-check
# or from repo root after build:
npx @derived-modular/cli check --roots examples/vite-react

Linters in monorepo

ESLint / Biome / Oxlint configs are usually per package. @derived-modular/* plugins are wired in the config of the app where src/ lives.

Biome Grit plugins resolve paths from the project root; some monorepo / PnP layouts may have limitations — see Biome.

Migrating a monorepo

Don't enable dma check on all legacy at once. Strangler by app:

  1. One app (apps/web) — green check
  2. Others — later or with a separate plan

See Migration.

What's next

On this page