Restructures into apps/ssp + apps/behandelportal (two Angular projects) plus libs/shared + libs/beheer (cross-app libraries), replacing WP-61's separate sibling repo. That split had already produced real drift: a hand-vendored copy of the backend's OpenAPI doc, a shared/ui+layout tree forked and silently diverging (7 files), and beheer + the styles.scss token bridge duplicated byte-for-byte across both repos. - git mv the SSP's src/app/* into apps/ssp/; fold shared/, beheer/, environments/, the Storybook docs/*.mdx, and styles.scss into libs/shared + libs/beheer (all confirmed identical between the two repos before merging). auth stays deliberately duplicated per ADR-0002 (actor-specific, expected to diverge) - amended there. - One generated API client (libs/shared), no more vendored swagger.json. - .dependency-cruiser split into a base factory + one config per app, and Storybook into .storybook-ssp/.storybook-behandelportal - both forced by the @auth/* alias resolving to different directories per app. - SiteHeaderComponent/ShellComponent gained HEADER_NAV_ITEMS/ HEADER_ADMIN_LINKS/DEBUG_PANEL injection tokens so each app supplies its own nav/admin-links/dev-panel instead of one being hardcoded. - CLAUDE.md, ARCHITECTURE.md, dependencies.md, and ADR-0002 updated; WP-67 backlog entry documents the full decision trail. npm run ci green (lint, dep:check x2, 360 tests across ssp/ behandelportal/shared/beheer, both localized builds, backend tests, snippet + api-client drift); both dev servers, both Storybook instances, and docker compose verified working. The old sibling repo (/home/eho/repos/behandelportal) is left untouched, not deleted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
136 lines
5.4 KiB
JavaScript
136 lines
5.4 KiB
JavaScript
// 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.<app>.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<string, string[] | null>} 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/<appName> 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/ (+ shared/upload); elsewhere type-only.',
|
|
severity: 'error',
|
|
from: { pathNot: '/infrastructure/|^libs/shared/src/upload/' },
|
|
to: {
|
|
path: '^libs/shared/src/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: 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)/[^/]+' },
|
|
},
|
|
},
|
|
};
|
|
};
|