layers

Higher layers may import lower layers, never the reverse.

Example

[contracts.package-tiers]
type = "layers"
layers = [
  "pkg:web | pkg:sdk | apps/* | services/*",
  "pkg:@acme/ui | pkg:@acme/forms",
  "pkg:@acme/client",
  "pkg:@acme/core",
  "pkg:@acme/types : pkg:@acme/config",
]
exclude_kinds = ["type"]
web sdk apps/* services/* @acme/ui @acme/forms @acme/client @acme/core @acme/types @acme/config higher lower upward
Imports may only point down. @acme/core importing @acme/forms points up, and web importing sdk crosses an independent layer.

The workspace packages are in five tiers, highest first, and each may import any tier below it but nothing above. In the top tier, web, sdk, the apps and the services may not import one another. At the bottom, @acme/types and @acme/config may import each other. Type-only imports don't count.

Writing layers

Each entry of layers is a string:

  • "a" is a layer. Entries are selectors, so "apps/*" is one layer of every app, which may import each other.
  • "a | b" is one layer of independent siblings, which may not import each other.
  • "a : b" is one layer of siblings that may import each other. The : must have spaces around it, since pkg:, npm: and node: contain one too.
  • "(a)" is an optional layer, which a container may not have.

An entry may also be a table, { layer = "a | b", closed = true }. Layers above a closed layer may not import the layers below it: they must go through it.

layers = ["app", { layer = "service", closed = true }, "db"]
app service db closed must go through service
app may import service, which may import db, but app may not import db past the closed layer.

Layers may not overlap. The illegal dependencies are those from a lower layer to a higher one, between independent siblings, and across a closed layer. Each is searched for without passing through any other layer, so a chain low → mid → high is reported as low → mid and mid → high.

Containers

[contracts.domain-internals]
type = "layers"
containers = ["packages/api/src/domains/*"]
layers = ["hooks", "api", "types"]
packages/api/src/domains/billing packages/api/src/domains/users hooks api types hooks api types not checked
Each domain is checked on its own. An import from one domain into another is outside the contract.

Inside each domain, hooks may import api and types, api may import types, and types may import neither. Each domain is checked on its own, so imports between domains are outside this contract.

Options

OptionDefault
layersrequiredOrdered from highest to lowest; see Writing layers.
containersnoneSelectors. If set, the layers are relative to each container, and each container is checked on its own.
exhaustivefalseEvery child of every container must be declared as a layer. Needs containers.
exhaustive_ignores[]Names of children that exhaustive doesn't require, with or without their extension.
allow_indirect_importsfalseOnly direct imports count.

Common options

Like every contract type, layers also takes:

OptionDefault
typerequiredThe contract type, such as "layers".
namethe idThe title shown in the output.
severity"error""warn" reports a broken contract without failing the check, for rolling a contract out gradually.
exclude_kinds[analysis] exclude_kindsThe import kinds to leave out, e.g. ["type"].
reexports[analysis] reexports"transparent" sees an import of names from a [pass-through module](../concepts.md#pass-through modules) as imports of the modules that define them, as bundlers load them; "opaque" as an import of the pass-through module. Not for encapsulation.
ignore_imports[]"<selector> -> <selector>" entries: the direct imports between them are left out of the contract.
unmatched_ignore_imports"error"Whether an ignore_imports entry that matches no import breaks the contract ("error"), is a warning ("warn") or is fine ("none").
guidancenoneText shown under each violation, such as how to fix it.