Adopt dependency-cruiser as the single declarative source for bounded-context + atomic-layer boundaries, replacing the per-context no-restricted-imports blocks that had to be hand-copied (and had left herregistratie uncovered). `.dependency-cruiser.js` encodes context direction (everyone→shared, herregistratie→registratie, showcase→*), domain-purity, contracts-import-nothing, ui↛infrastructure, ApiClient confinement, and no-circular. `npm run dep:check` enforces (wired into ci-local.sh + the frontend CI job); `npm run dep:graph` emits a committed mermaid context×layer graph. ESLint slimmed to no-explicit-any + template a11y. Docs + new-context skill updated to the single source. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
107 lines
4.4 KiB
JavaScript
107 lines
4.4 KiB
JavaScript
// 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/[^/]+/[^/]+' },
|
|
},
|
|
},
|
|
};
|