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>
147 lines
5.9 KiB
JavaScript
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)/[^/]+' },
|
|
},
|
|
},
|
|
};
|
|
};
|