AD-G2 ("OrganizerLayoutV2 wraps AppShellV2") was in tension with AD-G5:
src/layouts/OrganizerLayoutV2.vue classified as the v1 `layouts` zone,
which is deliberately barred from components-v2. New `layouts-v2` zone
(src/layouts/*V2*.vue, mode:file) gets pages-v2-equivalent v2 capability;
the v1 `layouts` zone is unchanged so v2 isolation is preserved. RFC
AD-G5 amended; locked by 3 boundaries-v2.spec.ts regression tests (7/7).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
382 lines
17 KiB
JavaScript
382 lines
17 KiB
JavaScript
// Sessie 3c (WS-6) — closes the apps/app ESLint config gap.
|
|
// Adapted from the Vuexy reference (resources/vuexy-admin-v10.11.1/.../full-version/.eslintrc.cjs)
|
|
// minus the Vuexy-internal lint rules (valid-appcardcode-*, internal regex
|
|
// rules) that don't apply outside the demo project. Plugin set matches
|
|
// what's installed in apps/app's package.json.
|
|
module.exports = {
|
|
root: true,
|
|
env: {
|
|
browser: true,
|
|
node: true,
|
|
es2022: true,
|
|
},
|
|
extends: [
|
|
'@antfu/eslint-config-vue',
|
|
'plugin:vue/vue3-recommended',
|
|
'plugin:import/recommended',
|
|
'plugin:import/typescript',
|
|
'plugin:promise/recommended',
|
|
'plugin:sonarjs/recommended',
|
|
'plugin:@typescript-eslint/recommended',
|
|
'plugin:case-police/recommended',
|
|
'plugin:regexp/recommended',
|
|
],
|
|
parser: 'vue-eslint-parser',
|
|
parserOptions: {
|
|
ecmaVersion: 13,
|
|
parser: '@typescript-eslint/parser',
|
|
sourceType: 'module',
|
|
},
|
|
plugins: [
|
|
'vue',
|
|
'@typescript-eslint',
|
|
'regex',
|
|
'regexp',
|
|
'boundaries',
|
|
'local-rules',
|
|
],
|
|
ignorePatterns: [
|
|
'src/plugins/iconify/*.js',
|
|
'node_modules',
|
|
'dist',
|
|
'*.d.ts',
|
|
'vendor',
|
|
'*.json',
|
|
'src/@core/**',
|
|
'src/@layouts/**',
|
|
],
|
|
rules: {
|
|
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
|
|
|
'comma-spacing': ['error', { before: false, after: true }],
|
|
'key-spacing': ['error', { afterColon: true }],
|
|
'n/prefer-global/process': ['off'],
|
|
'sonarjs/cognitive-complexity': ['off'],
|
|
|
|
'vue/first-attribute-linebreak': ['error', {
|
|
singleline: 'beside',
|
|
multiline: 'below',
|
|
}],
|
|
|
|
'antfu/top-level-function': 'off',
|
|
|
|
// Project rule (CLAUDE.md frontend rules): no `any`. Override the
|
|
// Vuexy reference (which sets this off) — Crewli's stricter posture.
|
|
'@typescript-eslint/no-explicit-any': 'error',
|
|
|
|
'indent': ['error', 2, { SwitchCase: 1 }],
|
|
'comma-dangle': ['error', 'always-multiline'],
|
|
'object-curly-spacing': ['error', 'always'],
|
|
'camelcase': 'error',
|
|
'max-len': 'off',
|
|
'semi': ['error', 'never'],
|
|
'arrow-parens': ['error', 'as-needed'],
|
|
'newline-before-return': 'error',
|
|
|
|
'lines-around-comment': [
|
|
'error',
|
|
{
|
|
beforeBlockComment: true,
|
|
beforeLineComment: true,
|
|
allowBlockStart: true,
|
|
allowClassStart: true,
|
|
allowObjectStart: true,
|
|
allowArrayStart: true,
|
|
ignorePattern: '!SECTION',
|
|
},
|
|
],
|
|
|
|
'@typescript-eslint/no-unused-vars': ['error', {
|
|
varsIgnorePattern: '^_+$',
|
|
argsIgnorePattern: '^_+$',
|
|
}],
|
|
|
|
'array-element-newline': ['error', 'consistent'],
|
|
'array-bracket-newline': ['error', 'consistent'],
|
|
|
|
'vue/multi-word-component-names': 'off',
|
|
|
|
'padding-line-between-statements': [
|
|
'error',
|
|
{ blankLine: 'always', prev: 'expression', next: 'const' },
|
|
{ blankLine: 'always', prev: 'const', next: 'expression' },
|
|
{ blankLine: 'always', prev: 'multiline-const', next: '*' },
|
|
{ blankLine: 'always', prev: '*', next: 'multiline-const' },
|
|
],
|
|
|
|
'import/prefer-default-export': 'off',
|
|
'import/newline-after-import': ['error', { count: 1 }],
|
|
'no-restricted-imports': ['error', 'vuetify/components', {
|
|
name: 'vue3-apexcharts',
|
|
message: 'apexcharts are auto imported',
|
|
}],
|
|
|
|
'import/extensions': [
|
|
'error',
|
|
'ignorePackages',
|
|
{
|
|
js: 'never',
|
|
jsx: 'never',
|
|
ts: 'never',
|
|
tsx: 'never',
|
|
},
|
|
],
|
|
|
|
'import/no-unresolved': [2, {
|
|
ignore: [
|
|
'~pages$',
|
|
'virtual:meta-layouts',
|
|
'#auth$',
|
|
'#components$',
|
|
'.*\\?raw',
|
|
],
|
|
}],
|
|
|
|
'no-shadow': 'off',
|
|
'@typescript-eslint/no-shadow': ['error'],
|
|
'@typescript-eslint/consistent-type-imports': 'error',
|
|
|
|
// CLAUDE.md frontend convention — backend enums are mirrored as
|
|
// `as const` objects WITH a same-named `type` alias. The two live
|
|
// in different namespaces (value vs. type) and are intentional;
|
|
// both base `no-redeclare` and the typed variant flag them anyway.
|
|
'no-redeclare': 'off',
|
|
'@typescript-eslint/no-redeclare': 'off',
|
|
|
|
'promise/always-return': 'off',
|
|
'promise/catch-or-return': 'off',
|
|
|
|
'vue/block-tag-newline': 'error',
|
|
'vue/component-api-style': 'error',
|
|
'vue/component-name-in-template-casing': ['error', 'PascalCase', {
|
|
registeredComponentsOnly: false,
|
|
ignores: ['/^swiper-/'],
|
|
}],
|
|
'vue/custom-event-name-casing': ['error', 'camelCase', {
|
|
ignores: [
|
|
'/^(click):[a-z]+((\\d)|([A-Z0-9][a-z0-9]+))*([A-Z])?/',
|
|
],
|
|
}],
|
|
'vue/define-macros-order': 'error',
|
|
'vue/html-comment-content-newline': 'error',
|
|
'vue/html-comment-content-spacing': 'error',
|
|
'vue/html-comment-indent': 'error',
|
|
'vue/match-component-file-name': 'error',
|
|
'vue/no-child-content': 'error',
|
|
'vue/require-default-prop': 'off',
|
|
|
|
'vue/no-duplicate-attr-inheritance': 'error',
|
|
'vue/no-empty-component-block': 'error',
|
|
'vue/no-multiple-objects-in-class': 'error',
|
|
'vue/no-reserved-component-names': 'error',
|
|
'vue/no-template-target-blank': 'error',
|
|
'vue/no-useless-mustaches': 'error',
|
|
'vue/no-useless-v-bind': 'error',
|
|
'vue/padding-line-between-blocks': 'error',
|
|
'vue/prefer-separate-static-class': 'error',
|
|
'vue/prefer-true-attribute-shorthand': 'error',
|
|
'vue/v-on-function-call': 'error',
|
|
'vue/no-restricted-class': ['error', '/^(p|m)(l|r)-/'],
|
|
'vue/valid-v-slot': ['error', { allowModifiers: true }],
|
|
|
|
'vue/no-irregular-whitespace': 'error',
|
|
'vue/template-curly-spacing': 'error',
|
|
|
|
'sonarjs/no-duplicate-string': 'off',
|
|
'sonarjs/no-nested-template-literals': 'off',
|
|
|
|
'regex/invalid': [
|
|
'error',
|
|
[
|
|
{
|
|
regex: '@/assets/images',
|
|
replacement: '@images',
|
|
message: 'Use \'@images\' path alias for image imports',
|
|
},
|
|
{
|
|
regex: '@/assets/styles',
|
|
replacement: '@styles',
|
|
message: 'Use \'@styles\' path alias for importing styles from \'src/assets/styles\'',
|
|
},
|
|
],
|
|
'\\.eslintrc\\.cjs',
|
|
],
|
|
|
|
// Architectural import boundaries (WS-3 1c, audit:
|
|
// dev-docs/WS-3-SESSION-1C-AUDIT.md). The matrix is layered:
|
|
// types → utils → lib → composables → stores → components → layouts → pages.
|
|
// The `lib → stores` edge is intentionally disallowed; lib/axios.ts
|
|
// uses dynamic `await import('@/stores/...')` for its 4 store reads
|
|
// so the static-import surface stays clean.
|
|
//
|
|
// WS-3 PR-B1 activated TECH-WS3-BOUNDARIES-SUBZONES: components and
|
|
// pages now have organizer/portal/shared sub-zones (§4.2 charter).
|
|
// The cross-context edges (organizer ↛ portal, shared ↛ portal/organizer)
|
|
// are forbidden so a future portal-only refactor cannot leak into
|
|
// the organizer surface and vice versa.
|
|
//
|
|
// FORWARD-FLAG: when src/plugins/1.router/ migrates to src/router/
|
|
// in a later WS-3 PR (TECH-WS3-BOUNDARIES-ROUTER-ZONE), add
|
|
// `{ type: 'router', pattern: 'src/router/**' }` to boundaries/elements
|
|
// and `{ from: 'router', allow: ['types', 'utils', 'lib', 'plugins',
|
|
// 'stores'] }` to the rules.
|
|
'boundaries/element-types': ['error', {
|
|
default: 'disallow',
|
|
rules: [
|
|
{ from: 'types', allow: ['types'] },
|
|
{ from: 'utils', allow: ['types', 'utils'] },
|
|
{ from: 'lib', allow: ['types', 'utils', 'lib'] },
|
|
{ from: 'plugins', allow: ['types', 'utils', 'lib', 'plugins', 'stores', 'stores-portal'] },
|
|
{ from: 'composables-forms', allow: ['types', 'utils', 'lib', 'composables-forms'] },
|
|
{ from: 'composables', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'stores-portal'] },
|
|
{ from: 'stores-portal', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'stores-portal'] },
|
|
|
|
// useAuthStore.clearAll() / .logout() invokes usePortalStore.reset()
|
|
// via dynamic import to clear portal sessionStorage on session-end.
|
|
// The merged auth store is the canonical session-cleanup hub — this
|
|
// edge replaces the deleted stores-portal/usePortalAuthStore which
|
|
// previously owned the cross-zone call (WS-3 PR-B2a).
|
|
{ from: 'stores', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'stores-portal'] },
|
|
{ from: 'navigation', allow: ['types', 'utils', 'navigation'] },
|
|
|
|
// components-shared may read app-wide stores (useAuthStore,
|
|
// useNotificationStore — not stores-portal) so canonical shared
|
|
// chrome (ContextSwitcher, future global indicators) can stay in
|
|
// components/shared/ without re-homing to components/layout/.
|
|
// Portal-specific state stays out of shared by design (WS-3 PR-B2a).
|
|
{ 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).
|
|
// layouts-v2 (src/layouts/*V2*.vue, e.g. OrganizerLayoutV2) is
|
|
// the v2 shell-composition zone: SAME v2 capability as pages-v2
|
|
// (may import components-v2 + navigation) so AD-G2's
|
|
// "OrganizerLayoutV2 wraps AppShellV2" holds, WITHOUT widening
|
|
// the v1 `layouts` zone (still cannot reach components-v2 →
|
|
// AD-G5 isolation intact). Locked by tests/unit/boundaries-v2.spec.ts.
|
|
{ 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: 'layouts-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'] },
|
|
|
|
{ from: 'pages-register', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'plugins', 'components-shared', 'layouts'] },
|
|
{ from: 'pages-portal', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'stores-portal', 'navigation', 'components-shared', 'components-portal', 'layouts', 'plugins'] },
|
|
{ from: 'pages-organizer', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'navigation', 'components', 'components-shared', 'components-organizer', 'layouts', 'plugins'] },
|
|
{ from: 'pages-platform', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'navigation', 'components', 'components-shared', 'components-organizer', 'layouts', 'plugins'] },
|
|
{ from: 'pages', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'stores-portal', 'navigation', 'components', 'components-shared', 'components-portal', 'components-organizer', 'layouts'] },
|
|
],
|
|
}],
|
|
'boundaries/no-unknown': 'off', // External packages are fine.
|
|
'boundaries/no-unknown-files': 'off', // The ignore list handles vendor/generated.
|
|
},
|
|
settings: {
|
|
'import/resolver': {
|
|
node: true,
|
|
typescript: {},
|
|
},
|
|
|
|
// Element-type assignment: first-match-wins. Order matters — narrower
|
|
// sub-zones declared before their broader parents.
|
|
'boundaries/elements': [
|
|
{ type: 'types', pattern: 'src/types/**' },
|
|
{ type: 'utils', pattern: 'src/utils/**' },
|
|
{ type: 'lib', pattern: 'src/lib/**' },
|
|
{ type: 'plugins', pattern: 'src/plugins/**' },
|
|
{ type: 'composables-forms', pattern: 'src/composables/forms/**' },
|
|
{ type: 'composables', pattern: 'src/composables/**' },
|
|
{ type: 'stores-portal', pattern: 'src/stores/portal/**' },
|
|
{ type: 'stores', pattern: 'src/stores/**' },
|
|
{ type: 'navigation', pattern: 'src/navigation/**' },
|
|
|
|
// components/shared/** is the canonical sub-zone. components/auth/**
|
|
// and components/settings/** are folded in here as legacy cross-context
|
|
// siblings (MFA dialogs + password-requirements widgets used by both
|
|
// organizer reset-password and portal wachtwoord-instellen / profiel
|
|
// flows). PR-B2 may rehome these under components/shared/{auth,settings}/.
|
|
{ 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/**' },
|
|
// mode:'file' is REQUIRED for a single-file pattern. Without it
|
|
// eslint-plugin-boundaries matches in the default 'folder' mode,
|
|
// so 'src/components/Icon.vue' never matches and Icon.vue falls
|
|
// through to the generic `components` catch-all below — breaking
|
|
// the sanctioned components-v2 → Icon bridge (RFC AD-G5). The
|
|
// forms/** entry above is a folder glob so it is unaffected.
|
|
{ type: 'components-foundation', pattern: 'src/components/Icon.vue', mode: 'file' },
|
|
{ type: 'components-v2', pattern: 'src/components-v2/**' },
|
|
{ type: 'components', pattern: 'src/components/**' },
|
|
// layouts-v2 MUST precede the generic `layouts` element: first
|
|
// match wins. The single `*` does not cross `/`, so this matches
|
|
// only top-level v2 layout files (src/layouts/OrganizerLayoutV2.vue)
|
|
// and NOT src/layouts/components/AppShellV2.vue (subdir → stays
|
|
// `layouts`, which is correct: AppShellV2 imports only stores).
|
|
// mode:'file' is REQUIRED for a file-glob element (same reason as
|
|
// the Icon.vue bridge above) — RFC AD-G5 / boundaries-v2.spec.ts.
|
|
{ type: 'layouts-v2', pattern: 'src/layouts/*V2*.vue', mode: 'file' },
|
|
{ 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': [
|
|
'src/@core/**', // vendored Vuexy
|
|
'src/@layouts/**', // vendored Vuexy
|
|
'src/App.vue', // orchestration root
|
|
'src/main.ts', // orchestration root
|
|
'src/assets/**', // static media
|
|
'src/styles/**', // SCSS
|
|
'**/*.d.ts', // generated declarations
|
|
],
|
|
'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 <script setup>/comment boundary
|
|
// (the <script> tag isn't a JS block-start so allowBlockStart
|
|
// doesn't kick in, while vue/block-tag-newline forbids the blank
|
|
// line that lines-around-comment wants). Disable both
|
|
// beforeBlockComment and beforeLineComment for *.vue so a leading
|
|
// comment in <script setup> is allowed without a preceding blank.
|
|
{
|
|
files: ['*.vue'],
|
|
rules: {
|
|
'lines-around-comment': ['error', {
|
|
beforeBlockComment: false,
|
|
beforeLineComment: false,
|
|
allowBlockStart: true,
|
|
allowClassStart: true,
|
|
allowObjectStart: true,
|
|
allowArrayStart: true,
|
|
ignorePattern: '!SECTION',
|
|
}],
|
|
},
|
|
},
|
|
],
|
|
}
|