From 244527bbd6277b15d8fe5817c305d815cd42e9c8 Mon Sep 17 00:00:00 2001 From: Joris Wouter Jonkers Date: Fri, 18 Sep 2026 12:56:03 +0200 Subject: [PATCH] feat(frontend): a domain may not reach into another domain's innards either MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deep-import rule was scoped to pages and components, so the half of ADR-001 it enforces held for everything except the domains themselves. A domain reaching past another domain's index.ts was unchecked. It cannot be one pattern, because "another domain" is relative to the file doing the importing: there is one block per domain, each naming only its own files and excusing only its own name. The list is read off the directory rather than written here, so a new domain is covered the day it exists rather than the day somebody remembers this file. The blocks do not clobber the rule above them or each other — a config block naming a rule replaces an earlier one only for files that match both, and every `files` here is disjoint from pages, from components and from every other domain. Probed all four ways round: a cross-domain deep import is refused, a domain's own innards are not, and both halves of the existing rule still fire. No allowlist entry was needed. The only two cross-domain imports in the tree, BoardMemberDialog and LineupEditor, already go through `@/domains/user`. --- services/frontend/eslint.config.mjs | 35 +++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/services/frontend/eslint.config.mjs b/services/frontend/eslint.config.mjs index 9228b7137..3795b8485 100644 --- a/services/frontend/eslint.config.mjs +++ b/services/frontend/eslint.config.mjs @@ -5,6 +5,7 @@ import configTypeScript from '@typescript-eslint/eslint-plugin' import parserTypeScript from '@typescript-eslint/parser' import pluginVuetify from 'eslint-plugin-vuetify' import globals from 'globals' +import { readdirSync } from 'node:fs' /** @@ -166,4 +167,38 @@ export default [ }], }, }, + + // The same rule, pointed the other way: a domain may not reach into another domain's + // innards either. It cannot be one pattern, because "another domain" is relative to the + // file doing the importing — so there is one block per domain, each naming only its own + // files and excusing only its own name. + // + // Read off the directory rather than listed here, so a new domain is covered the day it + // exists rather than the day somebody remembers this file. + // + // These blocks do not clobber the one above, or each other: a config block naming a rule + // replaces an earlier one only for files that match both, and every `files` here is + // disjoint from `src/pages/**`, `src/components/**` and from every other domain. + ...readdirSync(new URL('./src/domains', import.meta.url), { withFileTypes: true }) + .filter(entry => entry.isDirectory()) + .map(entry => entry.name) + .map(domain => ({ + files: [`src/domains/${domain}/**/*.{ts,vue}`], + rules: { + 'no-restricted-imports': ['error', { + patterns: [ + { + // `.vue` is exempt for the reason it is exempt above: a component is + // imported where it is drawn. + regex: `^@/domains/(?!${domain}/)[a-z]+/(?!.*\\.vue$).+`, + message: + 'Another domain is entered through its index.ts (frontend ADR-001). ' + + 'Add what you need to that domain\'s index.ts and import it from ' + + `there. \`@/domains/${domain}/...\` is this domain's own business and ` + + 'stays a direct import.', + }, + ], + }], + }, + })), ]