Files
atomic-design-poc/eslint.config.mjs
Edwin van den Houdt f9b76e7f6a feat(boundaries): WP-03 — contracts purity + ApiClient confinement
Lint-enforce two architecture rules that were only documented (ADR-0001),
landing the rules with the fixes so the build stays green:

- contracts/ imports nothing: dashboard-view.dto.ts is now pure wire shapes
  (inline string-union enums, no domain imports). The DashboardView FE-view
  type moves to the adapter, which maps wire → domain (compiler-enforced seam).
- ApiClient lives only in infrastructure: change-request-form (UI) no longer
  injects ApiClient — a new ChangeRequestAdapter owns the client and the submit
  becomes a createSubmitChangeRequest() command factory (createDraftSync shape).
  draft-sync's wire-DTO import becomes type-only (allowed via allowTypeImports).
- Role type moves to shared/domain/role.ts; the ?role= reader stays in
  shared/infrastructure/role.ts.
- eslint: contracts import-ban + @typescript-eslint/no-restricted-imports on
  api-client (value-only; type imports permitted; infra + shared/upload exempt).

Also fixes a PRE-EXISTING bug found while verifying the flow: change-request-form
never imported FormsModule, so (ngSubmit) didn't bind and the submit button did a
native form submit (page reload) instead of submitting. Verified end-to-end in the
running app: submit → command → adapter → backend → reference, success alert shown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:19:58 +02:00

147 lines
5.1 KiB
JavaScript

import tseslint from 'typescript-eslint';
/**
* Enforces the architecture's working agreements that were previously only
* documented (CLAUDE.md): no `any`, domain/ stays framework-free, and the
* dependency direction between contexts (herregistratie → registratie → shared,
* auth → shared; shared depends on nothing). Boundary rules use path patterns on
* the import aliases, so they read as the direction statement they enforce.
*/
export default [
{
ignores: [
'dist/**',
'node_modules/**',
'storybook-static/**',
'.angular/**',
'coverage/**',
'backend/**',
// Generated client — owns its own /* eslint-disable */ header.
'src/app/shared/infrastructure/api-client.ts',
],
},
{
files: ['src/**/*.ts'],
languageOptions: {
parser: tseslint.parser,
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
},
plugins: { '@typescript-eslint': tseslint.plugin },
rules: { '@typescript-eslint/no-explicit-any': 'error' },
},
// Tests legitimately use `any` to feed invalid messages/states into reducers.
{
files: ['src/**/*.spec.ts'],
rules: { '@typescript-eslint/no-explicit-any': 'off' },
},
// domain/ = pure business rules + types. No Angular, ever.
{
files: ['src/app/**/domain/**/*.ts'],
rules: {
'no-restricted-imports': [
'error',
{ patterns: [{ group: ['@angular/*', '@angular/**'], message: 'domain/ must stay framework-free (pure TS) — no Angular imports.' }] },
],
},
},
// shared/ is the base layer: it may not depend on any feature context.
// The dev-only debug panel is the sanctioned exception (it observes every store).
{
files: ['src/app/shared/**/*.ts'],
ignores: ['src/app/shared/ui/debug-state/**'],
rules: {
'no-restricted-imports': [
'error',
{ patterns: [{ group: ['@auth/*', '@registratie/*', '@herregistratie/*', '@brief/*'], message: 'shared/ must not depend on a feature context.' }] },
],
},
},
// auth/ may depend only on shared.
{
files: ['src/app/auth/**/*.ts'],
rules: {
'no-restricted-imports': [
'error',
{ patterns: [{ group: ['@registratie/*', '@herregistratie/*', '@brief/*'], message: 'auth/ may depend only on shared.' }] },
],
},
},
// registratie/ may depend on shared, not on herregistratie (direction points the other way).
{
files: ['src/app/registratie/**/*.ts'],
rules: {
'no-restricted-imports': [
'error',
{ patterns: [{ group: ['@herregistratie/*', '@brief/*'], message: 'Dependencies point herregistratie → registratie → shared, never back.' }] },
],
},
},
// brief/ (letter composition) is an independent leaf context: it may depend only on shared.
{
files: ['src/app/brief/**/*.ts'],
rules: {
'no-restricted-imports': [
'error',
{ patterns: [{ group: ['@auth/*', '@registratie/*', '@herregistratie/*'], message: 'brief/ may depend only on shared.' }] },
],
},
},
// contracts/ is the FE⇄BE wire seam: pure DTO shapes that must import NOTHING
// (CLAUDE.md §1, ADR-0001) — not Angular, not a context alias, not relative app
// code. Enums are inlined string-literal unions; the adapter's parse* maps them.
// (This comes after the per-context rules so it wins for contracts files.)
{
files: ['src/app/**/contracts/**/*.ts'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['@angular/**', '@shared/**', '@auth/**', '@registratie/**', '@herregistratie/**', '@brief/**', './*', '../*', './**', '../**'],
message: 'contracts/ is the wire seam — it must import NOTHING (pure DTO shapes). Map wire → domain in the infrastructure adapter, not here.',
},
],
},
],
},
},
// BFF-lite anti-corruption boundary (ADR-0001): the ApiClient (the network
// client) may be imported as a VALUE only from infrastructure-role files.
// Type-only imports of generated wire DTOs are allowed anywhere — they grant no
// network access. UI/application reach the network through an adapter or command.
{
files: ['src/app/**/*.ts'],
plugins: { '@typescript-eslint': tseslint.plugin },
rules: {
'@typescript-eslint/no-restricted-imports': [
'error',
{
patterns: [
{
group: ['@shared/infrastructure/api-client'],
allowTypeImports: true,
message: 'The ApiClient lives only in infrastructure/ adapters (ADR-0001). UI/application call an adapter or a command, not the network client. (Type-only DTO imports are fine: use `import type`.)',
},
],
},
],
},
},
// …the infrastructure adapters ARE that boundary and own the client. shared/upload
// is a feature-scoped adapter that lives outside a /infrastructure/ folder.
{
files: ['src/app/**/infrastructure/**/*.ts', 'src/app/shared/upload/**/*.ts'],
plugins: { '@typescript-eslint': tseslint.plugin },
rules: { '@typescript-eslint/no-restricted-imports': 'off' },
},
];