// Dependency-cruiser (WP-38): the single declarative source for the app's bounded-context // + atomic-layer boundaries — and the graph you can SEE (`npm run dep:graph`). Replaces the // hand-duplicated `no-restricted-imports` blocks that had to be copied per context (and that // left `herregistratie` without one). ESLint keeps only the rules dep-cruiser can't express // (no-explicit-any, template a11y). // // Contexts: shared (base) · auth · registratie · herregistratie · brief · beheer · showcase. // Allowed cross-context edges: everyone → shared; herregistratie → registratie; showcase → * // (the sanctioned teaching page). Nobody imports showcase. const FEATURES = 'auth|registratie|herregistratie|brief|beheer|showcase'; /** A context may import shared + itself; this lists the OTHER contexts it may NOT import. */ const contextRule = (name, from, forbiddenContexts) => ({ name, comment: `${from} may depend only on its allowed contexts (+ shared). See CLAUDE.md §1.`, severity: 'error', from: { path: `^src/app/${from}/` }, to: { path: `^src/app/(${forbiddenContexts})/` }, }); module.exports = { forbidden: [ // --- Bounded-context direction (the "dependencies point inward" spine) --- { name: 'shared-no-features', comment: 'shared/ is the base — it must not import any feature context.', severity: 'error', from: { path: '^src/app/shared/', pathNot: '^src/app/shared/ui/debug-state/' }, to: { path: `^src/app/(${FEATURES})/` }, }, contextRule('auth-only-shared', 'auth', 'registratie|herregistratie|brief|beheer|showcase'), contextRule( 'registratie-only-shared', 'registratie', 'auth|herregistratie|brief|beheer|showcase', ), // herregistratie MAY import registratie (+ shared) — the one sanctioned cross-feature edge. contextRule('herregistratie-scope', 'herregistratie', 'auth|brief|beheer|showcase'), contextRule('brief-only-shared', 'brief', 'auth|registratie|herregistratie|beheer|showcase'), contextRule('beheer-only-shared', 'beheer', 'auth|registratie|herregistratie|brief|showcase'), // showcase/ is exempt (reads every context by design); nothing imports it — covered by the // rules above each forbidding `→ showcase`. // --- Atomic-layer rules (dependencies point inward: ui → application → domain) --- { name: 'domain-is-pure', comment: 'domain/ is framework-free business logic — no Angular.', severity: 'error', from: { path: '/domain/' }, to: { path: 'node_modules/@angular/' }, }, { name: 'contracts-import-nothing', comment: 'contracts/ are pure wire DTO shapes — they import nothing (ADR-0001).', severity: 'error', from: { path: '/contracts/' }, to: { pathNot: '/contracts/', path: '^(src/app/|node_modules/@angular/)' }, }, { name: 'ui-not-infrastructure', comment: 'ui/ + layout/ reach data through an application store/command, never infrastructure directly (type-only DTO imports allowed).', severity: 'error', from: { path: '(/ui/|/layout/)', pathNot: '\\.stories\\.ts$|\\.spec\\.ts$|^src/app/shared/ui/debug-state/', }, to: { path: '/infrastructure/', dependencyTypesNot: ['type-only'] }, }, { name: 'apiclient-infrastructure-only', comment: 'The generated ApiClient is a value only inside infrastructure/ (+ shared/upload); elsewhere type-only.', severity: 'error', from: { pathNot: '/infrastructure/|^src/app/shared/upload/' }, to: { path: '^src/app/shared/infrastructure/api-client\\.ts$', dependencyTypesNot: ['type-only'], }, }, // --- Hygiene (cheap wins a graph makes obvious) --- { name: 'no-circular', comment: 'No cyclic dependencies.', severity: 'error', from: {}, to: { circular: true }, }, ], options: { doNotFollow: { path: 'node_modules' }, tsConfig: { fileName: 'tsconfig.json' }, // resolves @shared/@registratie/… path aliases tsPreCompilationDeps: true, // needed so `type-only` imports are distinguished enhancedResolveOptions: { exportsFields: ['exports'], conditionNames: ['import', 'require', 'node', 'default'], }, reporterOptions: { // Context-level architecture graph for `npm run dep:graph` (mermaid — no graphviz needed). archi: { collapsePattern: '^src/app/[^/]+/[^/]+' }, }, }, };