Files
atomic-design-poc/.dependency-cruiser.base.js
T
ehoandClaude Opus 5 9520d6c24e refactor(shared): move upload/ into infrastructure/domain/application (RB-24)
libs/shared/src/upload/ held a network adapter, an Elm machine, and two
application-layer coordinators outside the folder-per-layer convention every
other context follows. The dependency-cruiser rule carved an exception around
the misplaced adapter instead of the violation being fixed.

Move all five files to the layer each belongs to (git mv), update every
import across 24 consumer files, then delete the carve-out clause from
.dependency-cruiser.base.js. No export renamed, no file split, no spec
content changed.

Deleting the carve-out exposed a second, pre-existing rule violation:
ui-not-infrastructure had never fired against upload.adapter.ts because its
old path did not match /infrastructure/. Three UI components injected
UploadAdapter directly for its one-line contentUrl() wrapper. Route each
through the existing pure uploadContentUrl() function via the application
layer (upload-controller's new previewUrlFor, OrgTemplateStore's new
previewUrlFor) instead — the same idiom brief.store.ts already used.

npm run ci passes; dep:check is clean for both apps with the carve-out gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 20:40:41 +02:00

147 lines
5.9 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/; elsewhere type-only.',
severity: 'error',
from: { pathNot: '/infrastructure/' },
to: {
path: '^libs/shared/src/infrastructure/api-client\\.ts$',
dependencyTypesNot: ['type-only'],
},
},
{
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)/[^/]+' },
},
},
};
};