Files
atomic-design-poc/.dependency-cruiser.js
T
ehoandClaude Sonnet 5 7b6cabfc4a feat(dx): gen:context generator (WP-44)
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>
2026-07-27 14:18:43 +02:00

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/[^/]+/[^/]+' },
},
},
};