.dependency-cruiser.cjs
// @ts-check
// Deep-module enforcement for dependency-cruiser.
//
// Each package under the packages root is a deep module: a lot of behaviour behind a small
// interface. A package's public surface is its entry points, the files at the package root.
// Implementation lives in subfolders and is private: by convention `lib/` for implementation and
// `tests/` for tests, though any subfolder is private. A package may expose several small entry
// points (index.ts, client.ts, server.ts, ...); prefer that over one giant barrel index.
//
// The only thing you should ever need to edit here is PACKAGES_ROOT.
/** Where packages live. One immediate child dir per package (flat, no nesting). */
const PACKAGES_ROOT = 'src/packages'
// --- derived patterns (no need to edit) -------------------------------------
const R = PACKAGES_ROOT
/**
* A package's private internals: anything nested inside a package subfolder. The package's root
* files are its entry points and are NOT matched here; they stay importable from outside.
*/
const PACKAGE_INTERNALS = `^${R}/[^/]+/[^/]+/`
/* global module */
/** @type {import('dependency-cruiser').IConfiguration} */
module.exports = {
forbidden: [
{
name: 'entrypoint-boundary-from-app',
comment:
"App/root code may import a package's entry points (its root files), but nothing inside " +
'its subfolders.',
severity: 'error',
from: { pathNot: `^${R}/` }, // importer is NOT inside any package
to: { path: PACKAGE_INTERNALS }
},
{
name: 'entrypoint-boundary-across-packages',
comment:
"A package's own files import each other freely, but may reach OTHER packages only " +
'through their entry points, never their internals.',
severity: 'error',
// importer is inside a package ($1), but is not a test file
from: { path: `^${R}/([^/]+)/`, pathNot: `^${R}/[^/]+/tests/` },
to: {
path: PACKAGE_INTERNALS,
pathNot: `^${R}/$1/` // same package: intra-package freedom
}
},
{
name: 'tests-through-entrypoints',
comment:
"A package's tests exercise it through its entry points like everyone else: they may " +
"import any package's entry points and their own tests/ fixtures, but never any " +
"package's internals, not even their own.",
severity: 'error',
from: { path: `^${R}/([^/]+)/tests/` }, // a test file, in package $1
to: {
path: PACKAGE_INTERNALS,
pathNot: `^${R}/$1/tests/` // own tests/ fixtures: allowed
}
},
{
name: 'tests-folder-is-private',
comment:
"A package's tests/ folder is reachable only from tests; nothing else may import fixtures.",
severity: 'error',
from: { pathNot: `^${R}/[^/]+/tests/` }, // importer is not itself a test
to: { path: `^${R}/[^/]+/tests/` }
},
{
name: 'no-circular',
comment:
'No runtime import cycles. Scope `from` to `^${R}/` to allow cycles outside packages.',
severity: 'error',
from: {},
// A cycle closed only by `import type` edges is erased at compile time and cannot bite at
// runtime, so it is allowed (dependency-cruiser's own template makes the same exception).
// tsPreCompilationDeps is what makes those edges visible to the cruise at all.
to: { circular: true, viaOnly: { dependencyTypesNot: ['type-only'] } }
},
{
name: 'mcp-server-has-no-electron',
comment:
'The MCP server runs under ELECTRON_RUN_AS_NODE, where `electron` resolves to a path ' +
'string and its APIs are undefined (ADR-0038). Its own files never import it.',
severity: 'error',
from: { path: '^src/mcp/' },
to: { path: '(^|/)node_modules/electron/' }
}
// --- Layering (optional, off by default) ----------------------------------
// Interface-hiding controls HOW you import (through the entry points). Layering controls
// WHICH packages may depend on which. Add your own rules here, e.g.:
//
// {
// name: 'ui-may-not-depend-on-billing',
// severity: 'error',
// from: { path: `^${R}/ui/` },
// to: { path: `^${R}/billing/` }
// }
],
options: {
doNotFollow: { path: 'node_modules' },
// Read imports from the TypeScript source, not the transpiled output: with the default
// (false) an import whose bindings are unused or type-only is elided before the cruise
// and a deep import can slip through.
tsPreCompilationDeps: true,
// dependency-cruiser reads one tsconfig, and no per-process project carries every alias, so
// the cruise gets its own: tsconfig.boundaries.json maps @main, @renderer, @shared and
// @extension. An alias missing there leaves its edge unresolved, which no rule can see.
tsConfig: { fileName: 'tsconfig.boundaries.json' }
// No `enhancedResolveOptions.extensions` override: dependency-cruiser derives the list from
// the transpilers it finds (.ts/.tsx/.mts/.cts with typescript installed) and maps an import
// by emitted name (`./impl.js`, `./impl.mjs`) back to its TypeScript source. An explicit list
// that omits .mts/.cts leaves such an edge unresolved, and no rule can see an unresolved edge.
}
}