Dependency Cruiser rules
| Rule | Catches | Enable via |
|---|---|---|
| no-cycles | Circular imports | layout({ scope }) |
| packages-do-not-import-apps | Packages importing apps | layout({ scope }) |
| apps-do-not-import-other-apps | Apps importing other apps | layout({ scope }) |
| packages-imported-by-name | Local imports of packages by path instead of name | layout({ scope }) |
| packages-public-entry-only | Imports of package internals | layout({ scope }) |
| delivery-does-not-import-server | Delivery layer importing server layer | layout({ scope }) |
| server-does-not-import-delivery | Server layer importing delivery layer | layout({ scope }) |
| use-cases-do-not-import-outer-layers | Use-cases importing delivery or server | layout({ scope }) |
| no-unresolved-deep-package-imports | Unresolved deep package imports | layout({ scope }) |
| production-does-not-import-tests | Production code importing tests | layout({ scope }) |
| tests-live-in-tests-dir | Test files outside tests/ folders | layout({ scope }) |
| tests-do-not-import-internals | Tests importing package internals | layout({ scope }) |
| no-unresolved-imports | Unresolved imports | layout({ scope }) |
| app-code-in-layers | App code outside layer folders | layout({ scope }) |
| use-cases-do-not-import-use-cases | Use-case importing another use-case | layout({ scope }) |
| no-ownerless-files | utils/, helpers/, misc/ files or folders |
layout({ scope }) |
Dependency Cruiser layout preset
Section titled “Dependency Cruiser layout preset”From packages/rules/docs/dependency-cruiser.md.
Import path: @house-rules/rules/dependency-cruiser
This preset is not an ESLint preset. It is a Dependency Cruiser configuration for a workspace monorepo with apps under apps/ and packages under packages/. The layout(options) factory returns a whole config object, forbidden rules and options together, that a .dependency-cruiser.cjs file can export as-is. tests/fixtures/dependency-cruiser/layout-hosti.snapshot.json is a snapshot of layout({ scope: "@hosti/" }), and a test compares them. The first 13 rules and the options block are drunk-cat-stack’s hand-written .dependency-cruiser.cjs from before 0.4.0. Version 0.4.0 adds three rules, so drunk-cat-stack no longer equals layout() until it switches to layout() in the next phase.
module.exports = require("@house-rules/rules/dependency-cruiser").layout({ scope: "@acme/" });Run it with the Dependency Cruiser CLI:
npx depcruise --config .dependency-cruiser.cjs apps packagesThe file is CommonJS and loads no other module. require() and import both work, and neither loads ESLint, @eslint/css, or @eslint/markdown. Dependency Cruiser is an optional peer dependency (^18.4.0), so install it yourself:
Expected layout
Section titled “Expected layout”apps/<app>/src/delivery/ delivery layerapps/<app>/src/server/ server layerapps/<app>/src/use-cases/ use-cases layer, one use-case per top-level entryapps/<app>/src/main.ts entry files, the only code directly in src/packages/<pkg>/src/index.ts the one public entry of a packagepackages/<pkg>/src/internal/ private package code<workspace>/src/**/tests/*.test.ts tests, directly inside a tests folder under src/Every folder name above is an option. The src segment is fixed.
Options
Section titled “Options”| Option | Default | Meaning |
|---|---|---|
scope |
required | npm scope of the workspace packages, such as "@acme/". A missing trailing / is added. Used by no-unresolved-deep-package-imports. |
appsDir |
"apps" |
Folder that holds the apps. |
packagesDir |
"packages" |
Folder that holds the packages. |
layers.delivery |
"delivery" |
Delivery folder under <app>/src/. |
layers.server |
"server" |
Server folder under <app>/src/. |
layers.useCases |
"use-cases" |
Use-cases folder under <app>/src/. |
publicEntry |
"src/index.ts" |
The only file of a package that another workspace may import. |
internalDir |
"src/internal" |
Private package code that tests may not import. |
appEntryFiles |
["main.ts", "index.ts"] |
File names that may sit directly in <app>/src/. Names, not paths. [] allows none. |
ownerlessNames |
["utils", "helpers", "misc"] |
File and folder names that no-ownerless-files rejects. A name matches utils.ts, utils.test.ts, utils/, but not string-utils.ts or utilities.ts. Must not be empty. |
testsDir |
"tests" |
Name of the folder every test file must sit in directly, somewhere under <workspace>/src/. It also counts as a test path for production-does-not-import-tests, next to test, tests, and __tests__. |
Option values are folder names and paths, not regular expressions. The factory escapes them. Each layer root ends in (?:/|$), so delivery-legacy is not delivery. An unknown option or layer name throws, and so does a path that is empty or starts or ends with /.
All 16 rules run at error level.
| Rule | Reports |
|---|---|
no-cycles |
Any import cycle. |
packages-do-not-import-apps |
A package importing app code. |
apps-do-not-import-other-apps |
An app importing another app. |
packages-imported-by-name |
A relative-path import into another package. Import it by its package name. |
packages-public-entry-only |
Another workspace reaching a package file other than its publicEntry. |
delivery-does-not-import-server |
Delivery code importing server code. |
server-does-not-import-delivery |
Server code importing delivery code. |
use-cases-do-not-import-outer-layers |
Use-cases code importing delivery or server code. |
no-unresolved-deep-package-imports |
An unresolved deep import such as @acme/bookings/internal/x. |
production-does-not-import-tests |
Source code under src/ importing a test file or anything in a test folder. |
no-unresolved-imports |
Any import that does not resolve, including a workspace package the importer does not declare. |
tests-live-in-tests-dir |
A *.test.* or *.spec.* file in a workspace that does not sit directly in a testsDir folder under src/. A package-root tests/, a subfolder of tests/, and unit-tests/ all fail. |
tests-do-not-import-internals |
A test file, or a file in a test folder, importing packages/<pkg>/src/internal/. Test through the public entry. |
app-code-in-layers |
A file under <app>/src/ outside the three layer folders, unless it is an appEntryFiles file directly in src/. Test files and files in test folders are left to the test rules. |
use-cases-do-not-import-use-cases |
A use-case importing another use-case. A use-case is one top-level entry under use-cases/: a file, or a folder with everything in it. Imports inside one entry are fine. Test files are never the importer, and a test path is never the target. |
no-ownerless-files |
A file or folder named after one of ownerlessNames, anywhere under appsDir or packagesDir. Name the file after what it owns instead. |
A deep import that does not resolve fires both no-unresolved-deep-package-imports and no-unresolved-imports. That is intended: the first names the cause.
tests-live-in-tests-dir, app-code-in-layers and no-ownerless-files are module rules, so they report the file itself. A dependency rule would miss a test that imports nothing, or only a node_modules package such as @effect/vitest, because the excluded node_modules leaves such a test with no dependencies to match. Dependency Cruiser has no plain “every module” condition, so the rule uses numberOfDependentsLessThan: 100, which every file meets. Module rules see only files Dependency Cruiser parses: JS and TS files. A utils/ folder that holds only CSS or JSON goes unreported.
Dependency Cruiser options
Section titled “Dependency Cruiser options”The returned options block:
- parses with SWC (
parser: "swc"). Install@swc/corefor that. Without it, Dependency Cruiser falls back to its other parsers. - skips
node_modules,dist,coverage,generated,.turbo, and.agent_sources, and does not follownode_modules. - sets
skipAnalysisNotInRules: trueandtsPreCompilationDeps: "specify". - resolves packages through their
exportsfield, with theimport,require,node, anddefaultconditions.
Adding project rules
Section titled “Adding project rules”layout() returns a fresh object on every call. Append your own rules to forbidden, or change options:
const { layout } = require("@house-rules/rules/dependency-cruiser");
const config = layout({ scope: "@acme/", layers: { useCases: "application" } });config.forbidden.push({ name: "no-lodash", severity: "error", from: {}, to: { path: "^node_modules/lodash/" },});module.exports = config;