npm run gen:context scaffolds a bounded context: folders + starter page, the @<ctx>/* tsconfig alias, a dependency-cruiser boundary entry, and a lazy authGuard route. Refactors .dependency-cruiser.js's per-context contextRule calls into a single CONTEXT_ALLOWED map that every rule derives from, so adding a context is really one config entry (verified behavior-preserving: same dep:check counts, same graph output). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
119 lines
4.7 KiB
JavaScript
119 lines
4.7 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.
|
|
|
|
// Single source of truth for bounded-context boundaries: each entry maps a context name to the
|
|
// OTHER contexts it may additionally import (besides itself + shared). `showcase` maps to `null`
|
|
// — sanctioned to import every context (the teaching page); nothing else may import it. Add a
|
|
// context here — nowhere else — when scaffolding one (see `gen:context`, WP-44); FEATURES and
|
|
// every contextRule below are derived from this object.
|
|
const CONTEXT_ALLOWED = {
|
|
auth: [],
|
|
registratie: [],
|
|
herregistratie: ['registratie'], // the one sanctioned cross-feature edge
|
|
brief: [],
|
|
beheer: [],
|
|
showcase: null,
|
|
};
|
|
|
|
const FEATURES = Object.keys(CONTEXT_ALLOWED).join('|');
|
|
|
|
/** A context may import shared + itself + its allowed list; forbidden = every other context. */
|
|
const contextRule = (from) => {
|
|
const allowed = CONTEXT_ALLOWED[from];
|
|
if (allowed === null) return null; // unrestricted (showcase) — no rule to generate
|
|
const forbidden = Object.keys(CONTEXT_ALLOWED)
|
|
.filter((name) => name !== from && !allowed.includes(name))
|
|
.join('|');
|
|
return {
|
|
name: `${from}-scope`,
|
|
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/(${forbidden})/` },
|
|
};
|
|
};
|
|
|
|
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})/` },
|
|
},
|
|
...Object.keys(CONTEXT_ALLOWED).map(contextRule).filter(Boolean),
|
|
// 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/[^/]+/[^/]+' },
|
|
},
|
|
},
|
|
};
|