From 93e4fe398b6619232fd4f08426018cf15c9c641e Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Sat, 16 May 2026 11:30:03 +0200 Subject: [PATCH] feat(lint): enforce definePage layout meta on pages-v2 Adds a custom ESLint rule (local-rules/require-v2-layout-meta) that fails any src/pages-v2/**.vue page missing definePage({ meta: { layout: 'OrganizerLayoutV2' } }) (or PortalLayoutV2 under pages-v2/portal), preventing a silent wrong-shell fallback to the default layout (RFC-WS-GUI-REDESIGN AD-G2). Wires eslint-plugin-local-rules + a pages-v2 override. The RuleTester spec is called at top level (ESLint RuleTester self-manages describe/it under Vitest) and vitest.config.ts gains the eslint-rules test glob so the spec is discovered. Co-Authored-By: Claude Opus 4.7 --- apps/app/.eslintrc.cjs | 7 +++ apps/app/eslint-local-rules.cjs | 5 ++ .../__tests__/require-v2-layout-meta.spec.ts | 43 ++++++++++++++ .../eslint-rules/require-v2-layout-meta.cjs | 59 +++++++++++++++++++ apps/app/package.json | 1 + apps/app/pnpm-lock.yaml | 8 +++ apps/app/vitest.config.ts | 1 + 7 files changed, 124 insertions(+) create mode 100644 apps/app/eslint-local-rules.cjs create mode 100644 apps/app/eslint-rules/__tests__/require-v2-layout-meta.spec.ts create mode 100644 apps/app/eslint-rules/require-v2-layout-meta.cjs diff --git a/apps/app/.eslintrc.cjs b/apps/app/.eslintrc.cjs index e9474110..5d74d343 100644 --- a/apps/app/.eslintrc.cjs +++ b/apps/app/.eslintrc.cjs @@ -33,6 +33,7 @@ module.exports = { 'regex', 'regexp', 'boundaries', + 'local-rules', ], ignorePatterns: [ 'src/plugins/iconify/*.js', @@ -328,6 +329,12 @@ module.exports = { 'boundaries/include': ['src/**/*.{ts,vue,tsx}'], }, overrides: [ + { + files: ['src/pages-v2/**/*.vue'], + rules: { + 'local-rules/require-v2-layout-meta': 'error', + }, + }, // Vue SFCs: the base lines-around-comment rule conflicts with // vue/block-tag-newline at the ', + }, + { + filename: 'src/pages/dashboard.vue', + code: '', + }, + ], + invalid: [ + { + filename: 'src/pages-v2/dashboard.vue', + code: '', + errors: [{ messageId: 'missing' }], + }, + { + filename: 'src/pages-v2/events/index.vue', + code: '', + errors: [{ messageId: 'wrongLayout' }], + }, + ], +}) diff --git a/apps/app/eslint-rules/require-v2-layout-meta.cjs b/apps/app/eslint-rules/require-v2-layout-meta.cjs new file mode 100644 index 00000000..9b2d9ace --- /dev/null +++ b/apps/app/eslint-rules/require-v2-layout-meta.cjs @@ -0,0 +1,59 @@ +/** + * Enforces that every src/pages-v2/**.vue page declares + * definePage({ meta: { layout: 'OrganizerLayoutV2' } }) + * (or 'PortalLayoutV2' for src/pages-v2/portal/**). Without this a v2 + * page silently falls back to the `default` layout — a no-error + * wrong-shell bug. RFC-WS-GUI-REDESIGN AD-G2. + */ +'use strict' + +module.exports = { + meta: { + type: 'problem', + docs: { description: 'require definePage layout meta on pages-v2' }, + messages: { + missing: 'pages-v2 page must call definePage({ meta: { layout: ... } }).', + wrongLayout: 'pages-v2 layout must be {{expected}} (got {{actual}}).', + }, + schema: [], + }, + create(context) { + const filename = (context.filename || context.getFilename() || '').replace(/\\/g, '/') + if (!filename.includes('src/pages-v2/')) + return {} + + const expected = filename.includes('src/pages-v2/portal/') + ? 'PortalLayoutV2' + : 'OrganizerLayoutV2' + + let sawDefinePage = false + + return { + CallExpression(node) { + if (node.callee.type !== 'Identifier' || node.callee.name !== 'definePage') + return + sawDefinePage = true + + const arg = node.arguments[0] + const metaProp = arg && arg.type === 'ObjectExpression' + ? arg.properties.find(p => p.key && p.key.name === 'meta') + : null + const layoutProp = metaProp && metaProp.value.type === 'ObjectExpression' + ? metaProp.value.properties.find(p => p.key && p.key.name === 'layout') + : null + + if (!layoutProp || layoutProp.value.value !== expected) { + context.report({ + node, + messageId: 'wrongLayout', + data: { expected, actual: layoutProp ? String(layoutProp.value.value) : 'none' }, + }) + } + }, + 'Program:exit': function (node) { + if (!sawDefinePage) + context.report({ node, messageId: 'missing' }) + }, + } + }, +} diff --git a/apps/app/package.json b/apps/app/package.json index d0ae0e0b..c381d3a9 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -138,6 +138,7 @@ "eslint-plugin-jest": "27.9.0", "eslint-plugin-jsdoc": "46.10.1", "eslint-plugin-jsonc": "2.21.0", + "eslint-plugin-local-rules": "3.0.2", "eslint-plugin-markdown": "3.0.1", "eslint-plugin-n": "16.6.2", "eslint-plugin-no-only-tests": "3.3.0", diff --git a/apps/app/pnpm-lock.yaml b/apps/app/pnpm-lock.yaml index 51373bcb..08f10fd3 100644 --- a/apps/app/pnpm-lock.yaml +++ b/apps/app/pnpm-lock.yaml @@ -352,6 +352,9 @@ importers: eslint-plugin-jsonc: specifier: 2.21.0 version: 2.21.0(eslint@8.57.1) + eslint-plugin-local-rules: + specifier: 3.0.2 + version: 3.0.2 eslint-plugin-markdown: specifier: 3.0.1 version: 3.0.1(eslint@8.57.1) @@ -3623,6 +3626,9 @@ packages: peerDependencies: eslint: '>=6.0.0' + eslint-plugin-local-rules@3.0.2: + resolution: {integrity: sha512-IWME7GIYHXogTkFsToLdBCQVJ0U4kbSuVyDT+nKoR4UgtnVrrVeNWuAZkdEu1nxkvi9nsPccGehEEF6dgA28IQ==} + eslint-plugin-markdown@3.0.1: resolution: {integrity: sha512-8rqoc148DWdGdmYF6WSQFT3uQ6PO7zXYgeBpHAOAakX/zpq+NvFYbDA/H7PYzHajwtmaOzAwfxyl++x0g1/N9A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10061,6 +10067,8 @@ snapshots: transitivePeerDependencies: - '@eslint/json' + eslint-plugin-local-rules@3.0.2: {} + eslint-plugin-markdown@3.0.1(eslint@8.57.1): dependencies: eslint: 8.57.1 diff --git a/apps/app/vitest.config.ts b/apps/app/vitest.config.ts index 99614750..46f744ef 100644 --- a/apps/app/vitest.config.ts +++ b/apps/app/vitest.config.ts @@ -43,6 +43,7 @@ export default defineConfig({ 'tests/unit/**/*.{test,spec}.ts', 'tests/*.{test,spec}.ts', 'src/**/__tests__/**/*.{test,spec}.ts', + 'eslint-rules/**/__tests__/**/*.{test,spec}.ts', ], setupFiles: ['./tests/setup.ts'], },