Zenith

zenith is a language-agnostic architecture linter for monorepos.

You write down how the parts of your codebase may depend on each other, as contracts in a zenith.toml, and zenith check reports every import, or chain of imports, that breaks one. A check of a monorepo with thousands of files takes well under a second.

If you have used import-linter in Python, the contracts will feel familiar: zenith brings the same ideas to monorepos, whatever their languages.

page.tsx tabs.ts upload.tsx sidebar.tsx images.ts npm:sharp Your project files and workspace packages web/src/ app/page.tsx app/tabs.ts components/sidebar.tsx components/upload.tsx lib/images.ts zenith.toml [contracts.web-layers] type = "layers" [contracts.client-safety] type = "forbidden" The import graph every import, resolved The report each contract, kept or broken web-layers broken: 1 violation client-safety broken: 1 violation exit code 1 so the CI job fails build check report
zenith checks every contract against your project's imports, and reports the violations.

An example

Two contracts: the first puts the app's code in layers, so that lower layers never import higher ones, and the second keeps server-only code out of client bundles:

[contracts.web-layers]
type = "layers"
layers = ["web/src/app", "web/src/components", "web/src/lib"]
guidance = "Lower layers must not import higher ones."

[contracts.client-safety]
type = "forbidden"
from = [{ path = "web/src/**", directive = "use client" }]
to = ["npm:sharp", "node:*", "npm:server-only"]
guidance = "Keep server-only modules out of client bundles."

zenith check finds a component that imports from the app layer above it, and a client component that reaches sharp through another module. It shows each violation with the import, or the chain of imports, that makes it, and how to fix it, then a line per contract:

error[web-layers]: `web/src/components` must not import `web/src/app`
 --> web/src/components/sidebar.tsx:3:32
  |
3 | import { DASHBOARD_TABS } from "@/app/tabs";
  |                                ^^^^^^^^^^^^ imports `web/src/app/tabs.ts`
  |
  = help: move the imported code into a module that may be imported here, or remove this import
  = help: Lower layers must not import higher ones.

error[client-safety]: `web/src/components/upload.tsx` must not import `npm:sharp`
 --> web/src/components/upload.tsx:3:24
  |
3 | import { resize } from "@/lib/images";
  |                        ^^^^^^^^^^^^^^ starts a chain of 2 imports
  |
  = note: import chain:
            web/src/components/upload.tsx:3
            -> web/src/lib/images.ts:1
            -> npm:sharp
  = help: remove this import, or one further along the chain
  = help: Keep server-only modules out of client bundles.

Contracts
  ✗ web-layers     layers     broken (1 violation)
  ✗ client-safety  forbidden  broken (1 violation)

0 kept, 2 broken · 412 files, 968 imports

What it does

  • Seven contract types: forbidden, independence, layers, protected, acyclic, encapsulation and limits.
  • Several languages: JavaScript and TypeScript, Go, Python, Rust and Terraform, in one import graph, with the same contracts for each.
  • Monorepo aware: workspace packages, Go modules, Python packages and Cargo crates are found automatically, and can be named in contracts with pkg: selectors.
  • Built for adoption: a baseline records the violations a codebase already has, so only new ones fail, and suppressions exempt single imports.
  • Made for CI: a stable exit code, and compact, json and github output formats.