From b1d3b9f53b147a233efe2acb8890a0d4aac9f2b6 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Sat, 16 May 2026 09:46:31 +0200 Subject: [PATCH] feat(lint): add components-v2/pages-v2 boundary zones (no back-port) Adds three new eslint-plugin-boundaries element zones and their matrix rows so the GUI-redesign v2 surface is structurally isolated: v1 code cannot import from v2 (back-porting forbidden), v2 can reach the narrow FormField/Icon bridge via the components-foundation zone, and pages-v2 can import from components-v2. Backed by a Vitest spec running via the ESLint Node API (node environment; happy-dom's document object breaks the case-police resolver). Adds a placeholder src/components-v2/shared/X.vue so the resolver can classify the import target during the test (unresolvable imports are not boundary- checked by the plugin). Co-Authored-By: Claude Opus 4.7 --- apps/app/.eslintrc.cjs | 21 ++++++++++ apps/app/src/components-v2/shared/X.vue | 10 +++++ apps/app/tests/unit/boundaries-v2.spec.ts | 50 +++++++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 apps/app/src/components-v2/shared/X.vue create mode 100644 apps/app/tests/unit/boundaries-v2.spec.ts diff --git a/apps/app/.eslintrc.cjs b/apps/app/.eslintrc.cjs index 7ff5661b..e9474110 100644 --- a/apps/app/.eslintrc.cjs +++ b/apps/app/.eslintrc.cjs @@ -247,6 +247,13 @@ module.exports = { { from: 'components-shared', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'components-shared'] }, { from: 'components-portal', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'stores-portal', 'components-shared', 'components-portal'] }, { from: 'components-organizer', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'components-shared', 'components-organizer'] }, + // v2 zones. components-v2 may use the FormField/Icon bridge + // (components-foundation) but NOT any other v1 component zone. + // No v1 `from` rule lists components-v2/pages-v2 → back-porting + // is structurally impossible (RFC-WS-GUI-REDESIGN AD-G5). + { from: 'components-foundation', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'components-foundation'] }, + { from: 'components-v2', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'components-v2', 'components-foundation'] }, + { from: 'pages-v2', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'navigation', 'components-v2', 'components-foundation', 'layouts', 'plugins'] }, { from: 'components', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'components', 'components-shared', 'components-organizer'] }, { from: 'layouts', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'stores-portal', 'navigation', 'components', 'components-shared', 'components-portal', 'components-organizer', 'layouts'] }, @@ -287,12 +294,26 @@ module.exports = { { type: 'components-shared', pattern: 'src/components/{shared,auth,settings}/**' }, { type: 'components-portal', pattern: 'src/components/portal/**' }, { type: 'components-organizer', pattern: 'src/components/organizer/**' }, + // GUI-redesign v2 zones (RFC-WS-GUI-REDESIGN AD-G5). Declared + // before the generic `components` catch-all. `components-foundation` + // is the ONLY sanctioned v1→v2 bridge (FormField + Icon — audited + // to live in the generic `components` zone, not components-shared). + // Two-entry form used: eslint-plugin-boundaries 6.0.2 does not honour + // micromatch brace expansion in `pattern` (the brace glob matched in + // isolation but the plugin's internal resolver did not produce the + // expected classification). RFC §14 fallback: two entries with the + // same `type` so both src/components/forms/** and src/components/Icon.vue + // are captured before the generic `components` catch-all. + { type: 'components-foundation', pattern: 'src/components/forms/**' }, + { type: 'components-foundation', pattern: 'src/components/Icon.vue' }, + { type: 'components-v2', pattern: 'src/components-v2/**' }, { type: 'components', pattern: 'src/components/**' }, { type: 'layouts', pattern: 'src/layouts/**' }, { type: 'pages-register', pattern: 'src/pages/register/**' }, { type: 'pages-portal', pattern: 'src/pages/portal/**' }, { type: 'pages-platform', pattern: 'src/pages/platform/**' }, { type: 'pages-organizer', pattern: 'src/pages/{events,members,organisation,account-settings,dashboard,invitations}/**' }, + { type: 'pages-v2', pattern: 'src/pages-v2/**' }, { type: 'pages', pattern: 'src/pages/**' }, ], 'boundaries/ignore': [ diff --git a/apps/app/src/components-v2/shared/X.vue b/apps/app/src/components-v2/shared/X.vue new file mode 100644 index 00000000..6640e9ed --- /dev/null +++ b/apps/app/src/components-v2/shared/X.vue @@ -0,0 +1,10 @@ + + + diff --git a/apps/app/tests/unit/boundaries-v2.spec.ts b/apps/app/tests/unit/boundaries-v2.spec.ts new file mode 100644 index 00000000..b271a6d3 --- /dev/null +++ b/apps/app/tests/unit/boundaries-v2.spec.ts @@ -0,0 +1,50 @@ +// @vitest-environment node +// ESLint Node API tests must run in the Node environment — the default +// happy-dom environment's `document` object causes case-police's dirs.cjs +// (which uses `document.currentScript.src` for __dirname resolution) +// to fail with "The URL must be of scheme file". +import { fileURLToPath } from 'node:url' +import path from 'node:path' +import { describe, expect, it } from 'vitest' +import { ESLint } from 'eslint' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const rootDir = path.resolve(__dirname, '../../') + +const eslint = new ESLint({ cwd: rootDir }) + +async function boundaryErrors(filePath: string, code: string) { + const [result] = await eslint.lintText(code, { filePath }) + + return result.messages.filter(m => m.ruleId === 'boundaries/element-types') +} + +describe('boundaries — v2 zones', () => { + it('allows pages-v2 → components-v2', async () => { + const errs = await boundaryErrors( + 'src/pages-v2/dashboard.vue', + '', + ) + + expect(errs).toHaveLength(0) + }) + + it('allows components-v2 → components-foundation (FormField bridge)', async () => { + const errs = await boundaryErrors( + 'src/components-v2/forms/Demo.vue', + '', + ) + + expect(errs).toHaveLength(0) + }) + + it('forbids v1 components → components-v2 (no back-porting)', async () => { + const errs = await boundaryErrors( + 'src/components/organizer/Legacy.vue', + '', + ) + + expect(errs.length).toBeGreaterThan(0) + }) +})