Skip to content
LogoLogo

Layers and visibility

Two built-in rules in the restriction category check the structure of a project. They are off by default. Enable them in rules.

Layers

The layers rule enforces a layered architecture. Name each layer and give its globs. Then list the layers that each layer can import.

export default defineConfig({
  rules: {
    layers: {
      level: "error",
      layers: { ui: "src/ui/**", domain: "src/domain/**", db: "src/db/**" },
      allow: { ui: ["domain"], domain: ["db"], db: [] },
    },
  },
});
  • A module can always import a module in its own layer.
  • A layer that allow does not list has no limit.
  • When layers overlap, the first layer by name wins.

With this config, a module in src/ui/ cannot import src/db/.

Visibility

The visibility rule says what a folder shows to the code outside it. A module inside a folder can import every module in that folder. A module outside it can import only the modules that the folder makes visible.

The option folders is a list of entries. The rule does nothing without it.

KeyWhat it does
pathGlobs of the folders. {name} captures a path segment. ** matches every folder.
publicGlobs of the modules that code outside the folder can import. Without public, every module is public.
privateGlobs of the modules that code outside the folder cannot import. private wins over public.
messageText that replaces the generic message.

The globs of public and private are relative to the folder, and they can use the captures of path. path must capture each name that they use.

Show only an index module

export default defineConfig({
  rules: {
    visibility: {
      level: "error",
      folders: [
        { path: "src/features/{f}", public: "index.ts", message: "Import a feature through its index.ts." },
      ],
    },
  },
});

Now src/app.ts can import src/features/cart/index.ts, but not src/features/cart/model.ts. The modules in src/features/cart/ can import each other.

Make a folder private to its parent

The Go language has internal directories: only code under a/b/ can import a module under a/b/internal/. To get the same check, match every folder and make the name internal private:

{ path: "**", private: "internal" }

Use another name in private to use another convention. The rule has no default folder name.

Several entries

When several folders hide the target of a dependency, the rule reports the outermost one. To silence one dependency, put // oxcruise-ignore-next-line visibility -- reason above it.

Cycles between folders

The circular-dependencies rule can check cycles between folders. Its option folders is a list of entries with a path. The rule groups the modules below each matching folder. A cycle between two or more folders of one entry is a violation.

{ "circular-dependencies": { level: "error", folders: [{ path: "src/features/{f}" }] } }

A glob that starts with ! removes folders from the match. The option maxCyclesPerComponent limits the search for folders as well as for modules. The rule also reports the cycles between modules, as before.

When to use a forbid rule

Use layers when you have an ordered set of layers. Use forbid for a single restriction or for captures.