Skills › Skills For Real Engineers › setup-ts-deep-modules › dependency-cruiser.config.cjs
dependency-cruiser.config.cjs
95 lines · skills/in-progress/setup-ts-deep-modules/dependency-cruiser.config.cjs at 4ae2317
1// @ts-check
2// Deep-module enforcement for dependency-cruiser.
3//
4// Each package under the packages root is a DEEP MODULE: a lot of behaviour
5// behind a small interface. A package's PUBLIC SURFACE is its ENTRY POINTS:
6// the files at the package root. Implementation lives in SUBFOLDERS and is
7// private (by convention `lib/` for implementation and `tests/` for tests,
8// though any subfolder is private). A package may expose several small entry
9// points (index.ts, client.ts, server.ts, …); prefer that over one giant
10// barrel index.
11//
12// The only thing you should ever need to edit here is PACKAGES_ROOT.
14/** Where packages live. One immediate child dir per package (flat, no nesting). */
15const PACKAGES_ROOT = "src/packages";
17// --- derived patterns (no need to edit) -------------------------------------
18const R = PACKAGES_ROOT;
19/**
20 * A package's private internals: anything nested inside a package subfolder.
21 * The package's root files are its entry points and are NOT matched here:
22 * they stay importable from outside.
23 */
24const PACKAGE_INTERNALS = `^${R}/[^/]+/[^/]+/`;
26/** @type {import('dependency-cruiser').IConfiguration} */
27module.exports = {
28 forbidden: [
29 {
30 name: "entrypoint-boundary-from-app",
31 comment:
32 "App/root code may import a package's entry points (its root files), but nothing inside its subfolders.",
33 severity: "error",
34 from: { pathNot: `^${R}/` }, // importer is NOT inside any package
35 to: { path: PACKAGE_INTERNALS },
36 },
37 {
38 name: "entrypoint-boundary-across-packages",
39 comment:
40 "A package's own files import each other freely, but may reach OTHER packages only through their entry points, never their internals.",
41 severity: "error",
42 // importer is inside a package ($1), but is not a test file
43 from: { path: `^${R}/([^/]+)/`, pathNot: `^${R}/[^/]+/tests/` },
44 to: {
45 path: PACKAGE_INTERNALS,
46 pathNot: `^${R}/$1/`, // same package → intra-package freedom
47 },
48 },
49 {
50 name: "tests-through-entrypoints",
51 comment:
52 "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.",
53 severity: "error",
54 from: { path: `^${R}/([^/]+)/tests/` }, // a test file, in package $1
55 to: {
56 path: PACKAGE_INTERNALS,
57 pathNot: `^${R}/$1/tests/`, // own tests/ fixtures → allowed
58 },
59 },
60 {
61 name: "tests-folder-is-private",
62 comment:
63 "A package's tests/ folder is reachable only from tests: nothing else may import fixtures.",
64 severity: "error",
65 from: { pathNot: `^${R}/[^/]+/tests/` }, // importer is not itself a test
66 to: { path: `^${R}/[^/]+/tests/` },
67 },
68 {
69 name: "no-circular",
70 comment: "No dependency cycles. Scope to `^${R}/` if you want to allow cycles outside packages.",
71 severity: "error",
72 from: {},
73 to: { circular: true },
74 },
76 // --- Layering (optional, off by default) ----------------------------------
77 // Interface-hiding controls HOW you import (through the entry points).
78 // Layering controls WHICH packages may depend on which. Add your own rules
79 // here, e.g.:
80 //
81 // {
82 // name: "ui-may-not-depend-on-billing",
83 // severity: "error",
84 // from: { path: `^${R}/ui/` },
85 // to: { path: `^${R}/billing/` },
86 // },
87 ],
88 options: {
89 doNotFollow: { path: "node_modules" },
90 tsConfig: { fileName: "tsconfig.json" },
91 enhancedResolveOptions: {
92 extensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
93 },
94 },
95};