Vue 3 with Composition API, Pinia, Vue Router, Tailwind v4: - Vite build with @/ path aliases and backend proxy - TypeScript strict mode with proper type imports - Pinia stores (auth, game) with setup syntax and persistence - Vue Router with auth guards and lazy-loaded routes - Tailwind v4 with custom theme (Pokemon types, dark UI) - Vitest configured for component testing - ESLint v9 flat config with Vue/TypeScript support Pages: Home, Login, Register, Campaign, Collection, DeckBuilder, Match Components: AppHeader with auth-aware navigation Types: Card, GameState, Player, Deck, Campaign types Also adds frontend-code-audit skill with patterns for: - Error handling (unhandled promises, empty catches) - Security (XSS, token storage, input validation) - Architecture (Composition API, Pinia patterns, Phaser rules) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
205 lines
7.3 KiB
YAML
205 lines
7.3 KiB
YAML
# Frontend Architecture Anti-Patterns
|
|
# Patterns that violate Mantimon TCG frontend architecture principles
|
|
|
|
name: Architecture
|
|
description: |
|
|
Detects violations of the project's frontend architecture principles:
|
|
- Vue Composition API patterns (no Options API)
|
|
- Pinia store patterns (setup syntax, proper actions)
|
|
- Phaser integration (rendering only, no game logic)
|
|
- Component organization (composables for reuse)
|
|
- Backend authority (client never authoritative for game state)
|
|
|
|
patterns:
|
|
# Vue Composition API
|
|
- id: options-api-usage
|
|
description: Options API instead of Composition API
|
|
grep_pattern: 'export default \{[\s\S]*?(data\(\)|methods:|computed:|watch:)'
|
|
multiline: true
|
|
file_glob: '*.vue'
|
|
context_lines: 5
|
|
verdict_hints:
|
|
ISSUE_IF: "Using Options API - project uses Composition API only"
|
|
OK_IF: "Never OK - convert to <script setup> with Composition API"
|
|
|
|
- id: missing-script-setup
|
|
description: Script without setup attribute
|
|
grep_pattern: '<script lang="ts">\s*$'
|
|
file_glob: '*.vue'
|
|
context_lines: 2
|
|
verdict_hints:
|
|
ISSUE_IF: "Missing setup - should use <script setup lang=\"ts\">"
|
|
OK_IF: "Rare cases needing defineComponent; document why"
|
|
|
|
- id: options-api-definecomponent
|
|
description: defineComponent with options object
|
|
grep_pattern: 'defineComponent\(\{[\s\S]*?(data|methods|computed):'
|
|
multiline: true
|
|
file_glob: '*.vue'
|
|
context_lines: 5
|
|
verdict_hints:
|
|
ISSUE_IF: "Options API via defineComponent"
|
|
OK_IF: "Never OK - use <script setup>"
|
|
|
|
# Pinia Store Patterns
|
|
- id: store-options-syntax
|
|
description: Pinia store using options syntax instead of setup
|
|
grep_pattern: 'defineStore\([^,]+,\s*\{[\s\S]*?state:'
|
|
multiline: true
|
|
file_glob: 'stores/*.ts'
|
|
context_lines: 5
|
|
verdict_hints:
|
|
ISSUE_IF: "Options syntax - project uses setup store syntax"
|
|
OK_IF: "Never OK - convert to setup syntax with ref/computed"
|
|
|
|
- id: store-mutation-outside-action
|
|
description: Store state mutated directly from component
|
|
grep_pattern: 'Store\(\)\.\w+\s*='
|
|
file_glob: '*.vue'
|
|
context_lines: 3
|
|
verdict_hints:
|
|
ISSUE_IF: "Direct state mutation - use store action instead"
|
|
OK_IF: "Assigning to local ref that shadows store property"
|
|
|
|
- id: store-no-error-state
|
|
description: Store without error state for async operations
|
|
grep_pattern: 'defineStore.*\{[\s\S]*?async.*fetch[\s\S]*?\}'
|
|
multiline: true
|
|
file_glob: 'stores/*.ts'
|
|
context_lines: 20
|
|
verdict_hints:
|
|
ISSUE_IF: "Async store without error ref for failure handling"
|
|
OK_IF: "Error state exists; or errors handled by caller"
|
|
|
|
# Phaser Integration
|
|
- id: phaser-game-logic
|
|
description: Game logic in Phaser scene (should be backend)
|
|
grep_pattern: '(calculateDamage|resolveTurn|checkWin|applyEffect)'
|
|
file_glob: 'game/**/*.ts'
|
|
context_lines: 5
|
|
verdict_hints:
|
|
ISSUE_IF: "Game logic in Phaser - backend is authoritative"
|
|
OK_IF: "Animation/display logic with same name; not actual game rules"
|
|
|
|
- id: phaser-direct-api
|
|
description: Phaser scene making API calls directly
|
|
grep_pattern: '(fetch|axios|api\.)'
|
|
file_glob: 'game/**/*.ts'
|
|
context_lines: 4
|
|
verdict_hints:
|
|
ISSUE_IF: "Phaser calling API - should go through Vue layer"
|
|
OK_IF: "Asset loading (images, audio); not game data"
|
|
|
|
- id: phaser-store-import
|
|
description: Phaser importing store directly (use bridge)
|
|
grep_pattern: 'import.*from.*stores/'
|
|
file_glob: 'game/**/*.ts'
|
|
context_lines: 3
|
|
verdict_hints:
|
|
ISSUE_IF: "Direct store import - use event bridge pattern"
|
|
OK_IF: "Type-only import (import type); bridge.ts file"
|
|
|
|
- id: phaser-modifies-state
|
|
description: Phaser scene modifying game state
|
|
grep_pattern: 'gameState\.\w+\s*='
|
|
file_glob: 'game/**/*.ts'
|
|
context_lines: 4
|
|
verdict_hints:
|
|
ISSUE_IF: "Phaser modifying state - should only render"
|
|
OK_IF: "Local animation state; not shared game state"
|
|
|
|
# Component Organization
|
|
- id: api-call-in-component
|
|
description: Direct fetch/API call in component (use composable)
|
|
grep_pattern: 'fetch\(|axios\.|api\.'
|
|
file_glob: 'components/**/*.vue'
|
|
context_lines: 5
|
|
verdict_hints:
|
|
ISSUE_IF: "API call in component - extract to composable"
|
|
OK_IF: "Component is in pages/; or uses composable internally"
|
|
|
|
- id: large-component
|
|
description: Component with many lines (consider splitting)
|
|
grep_pattern: '</script>'
|
|
file_glob: '*.vue'
|
|
context_lines: 0
|
|
verdict_hints:
|
|
ISSUE_IF: "Script section >150 lines - split into composables"
|
|
OK_IF: "Page component; or logic is cohesive and documented"
|
|
|
|
- id: prop-drilling
|
|
description: Props passed through multiple levels
|
|
grep_pattern: 'defineProps.*\{[\s\S]*?(Props|\.\.\.)'
|
|
multiline: true
|
|
file_glob: '*.vue'
|
|
context_lines: 10
|
|
verdict_hints:
|
|
ISSUE_IF: "Props drilled >2 levels - use provide/inject or store"
|
|
OK_IF: "Props are component-specific; not passed through"
|
|
|
|
# Type Safety
|
|
- id: any-type-usage
|
|
description: Using 'any' type
|
|
grep_pattern: ':\s*any\b|as\s+any\b|<any>'
|
|
file_glob: '*.{vue,ts}'
|
|
context_lines: 2
|
|
verdict_hints:
|
|
ISSUE_IF: "Explicit any - defeats TypeScript benefits"
|
|
OK_IF: "Temporary during migration; or external lib without types"
|
|
|
|
- id: type-assertion-abuse
|
|
description: Type assertion (as) that might hide errors
|
|
grep_pattern: '\s+as\s+[A-Z]\w+'
|
|
file_glob: '*.{vue,ts}'
|
|
context_lines: 3
|
|
verdict_hints:
|
|
ISSUE_IF: "Assertion used to bypass type error"
|
|
OK_IF: "Narrowing from unknown; or after runtime check"
|
|
|
|
- id: missing-prop-types
|
|
description: Props without TypeScript types
|
|
grep_pattern: 'defineProps\(\[\s*["\']'
|
|
file_glob: '*.vue'
|
|
context_lines: 3
|
|
verdict_hints:
|
|
ISSUE_IF: "Array prop syntax - no type safety"
|
|
OK_IF: "Never OK - use defineProps<{...}>()"
|
|
|
|
# Import Organization
|
|
- id: relative-import-deep
|
|
description: Deep relative imports (use @ alias)
|
|
grep_pattern: 'from ["\']\.\.\/\.\.\/\.\.\/'
|
|
file_glob: '*.{vue,ts}'
|
|
context_lines: 2
|
|
verdict_hints:
|
|
ISSUE_IF: "Deep relative path - use @/ alias"
|
|
OK_IF: "Never OK for 3+ levels - always use @/"
|
|
|
|
- id: missing-type-import
|
|
description: Type import without 'import type'
|
|
grep_pattern: 'import \{[^}]*(Type|Interface|Props)[^}]*\} from'
|
|
file_glob: '*.{vue,ts}'
|
|
context_lines: 2
|
|
verdict_hints:
|
|
ISSUE_IF: "Type imported as value - use 'import type'"
|
|
OK_IF: "Also used as value (class, enum); not pure type"
|
|
|
|
# Backend Authority
|
|
- id: client-state-authority
|
|
description: Client treating its state as authoritative
|
|
grep_pattern: '(emit|send)\([^)]*\{[^}]*(hp|damage|winner|gameState)'
|
|
file_glob: '*.{vue,ts}'
|
|
context_lines: 5
|
|
verdict_hints:
|
|
ISSUE_IF: "Sending computed game state to server - server is authority"
|
|
OK_IF: "Sending action intent (playCard, attack); server computes result"
|
|
|
|
- id: local-game-calculation
|
|
description: Calculating game outcomes client-side
|
|
grep_pattern: '(damage|hp|energy)\s*[-+*]=|Math\.(random|floor).*damage'
|
|
file_glob: '*.{vue,ts}'
|
|
context_lines: 5
|
|
verdict_hints:
|
|
ISSUE_IF: "Game calculation on client - server must compute"
|
|
OK_IF: "Animation preview; actual result from server"
|