// Dependency-cruiser (WP-38, generalized for the WP-67 monorepo split): the single // declarative source for the bounded-context + atomic-layer boundaries. Each app // (apps/ssp, apps/behandelportal) is cruised SEPARATELY against its own tsconfig.json // (see .dependency-cruiser..js) — a single merged tsconfig can't resolve both // apps' `@auth/*` alias at once, since each points at a different physical directory. // This file is the shared rule *factory*; it never runs standalone. // // libs/shared and libs/beheer are cross-app libraries, not per-app feature contexts: // any app context may import either; neither may import an app's feature context; // libs/shared may not import libs/beheer (shared stays the base, beheer a peer leaf). /** * @param {Record} contextAllowed context name -> the OTHER * app-local contexts it may additionally import (besides itself + libs/shared|beheer). * `null` = unrestricted (showcase, the sanctioned teaching page). * @param {string} appName the apps/ directory this config cruises. * @param {string} tsConfigFileName this app's own tsconfig.json (resolves its aliases). */ module.exports = function buildConfig(contextAllowed, appName, tsConfigFileName) { const FEATURES = Object.keys(contextAllowed).join('|'); const appRoot = `apps/${appName}/src/app`; const contextRule = (from) => { const allowed = contextAllowed[from]; if (allowed === null) return null; const forbidden = Object.keys(contextAllowed) .filter((name) => name !== from && !allowed.includes(name)) .join('|'); return { name: `${appName}-${from}-scope`, comment: `${from} may depend only on its allowed contexts (+ libs/shared|beheer). See CLAUDE.md §1.`, severity: 'error', from: { path: `^${appRoot}/${from}/` }, to: { path: `^${appRoot}/(${forbidden})/` }, }; }; // Atomic-layer rules apply uniformly across this app's tree AND both libraries. const anyRoot = `(${appRoot}|libs/shared/src|libs/beheer/src)`; return { forbidden: [ // --- Bounded-context direction (the "dependencies point inward" spine) --- { name: 'shared-no-features', comment: 'libs/shared is the base — it must not import any app feature context.', severity: 'error', from: { path: '^libs/shared/src/' }, to: { path: `^${appRoot}/(${FEATURES})/` }, }, { name: 'beheer-no-features', comment: 'libs/beheer is a cross-app library — it must not import any app feature context.', severity: 'error', from: { path: '^libs/beheer/src/' }, to: { path: `^${appRoot}/(${FEATURES})/` }, }, { name: 'shared-no-beheer', comment: 'libs/shared stays the base — it must not depend on the beheer library.', severity: 'error', from: { path: '^libs/shared/src/' }, to: { path: '^libs/beheer/src/' }, }, { name: `${appName}-no-other-app`, comment: "An app may not import another app's source directly.", severity: 'error', from: { path: `^${appRoot}/` }, to: { path: '^apps/(?!' + appName + '/)' }, }, ...Object.keys(contextAllowed).map(contextRule).filter(Boolean), // --- 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: `^${anyRoot}/.*/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: `^${anyRoot}/.*/contracts/` }, to: { pathNot: '/contracts/', path: `^(${anyRoot}/|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: `^${anyRoot}/.*(/ui/|/layout/)`, pathNot: '\\.stories\\.ts$|\\.spec\\.ts$', }, to: { path: '/infrastructure/', dependencyTypesNot: ['type-only'] }, }, { name: 'apiclient-infrastructure-only', comment: 'The generated ApiClient is a value only inside infrastructure/; elsewhere type-only.', severity: 'error', from: { pathNot: '/infrastructure/' }, to: { path: '^libs/shared/src/infrastructure/api-client\\.ts$', dependencyTypesNot: ['type-only'], }, }, // --- Atomic ladder within libs/shared/src/ui (folder = layer, CLAUDE.md decision 2) --- { name: 'atoms-compose-nothing-above', comment: 'An atom composes nothing above it — no molecule or organism. See CLAUDE.md §2.', severity: 'error', from: { path: '^libs/shared/src/ui/atoms/' }, to: { path: '^libs/shared/src/ui/(molecules|organisms)/' }, }, { name: 'molecules-below-organisms', comment: 'A molecule composes nothing above it — no organism. See CLAUDE.md §2.', severity: 'error', from: { path: '^libs/shared/src/ui/molecules/' }, to: { path: '^libs/shared/src/ui/organisms/' }, }, { name: 'design-system-not-layout', comment: 'The design system (ui/) does not depend on layout/ templates. See CLAUDE.md §2.', severity: 'error', from: { path: '^libs/shared/src/ui/' }, to: { path: '^libs/shared/src/layout/' }, }, { name: 'no-testing-in-production', comment: 'Test-only fixture helpers (libs/shared/src/testing/** and any *.testing.ts) are reached from specs/stories only — production code gets its data through the real domain/application doors (ADR-0006), never the test escape hatch.', severity: 'error', from: { pathNot: '\\.(spec|stories)\\.ts$|\\.testing\\.ts$|^libs/shared/src/testing/', }, to: { path: '^libs/shared/src/testing/|\\.testing\\.ts$' }, }, // --- 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: tsConfigFileName }, tsPreCompilationDeps: true, // needed so `type-only` imports are distinguished enhancedResolveOptions: { exportsFields: ['exports'], conditionNames: ['import', 'require', 'node', 'default'], }, reporterOptions: { archi: { collapsePattern: '^(apps/[^/]+/src/app|libs/[^/]+/src)/[^/]+' }, }, }, }; };