#!/usr/bin/env ts-node // arch-guard.ts — Pre-commit hook: enforces {{ARCHITECTURE}} import boundaries // Generated for {{PROJECT_NAME}} ({{ARCHITECTURE}} architecture) import * as fs from 'fs'; import * as path from 'path'; import { execSync } from 'child_process'; const RED = '\x1b[31m'; const YELLOW = '\x1b[33m'; const GREEN = '\x1b[32m'; const RESET = '\x1b[0m'; interface ArchRule { sourcePattern: RegExp; forbiddenImports: RegExp[]; message: string; } // Architecture-specific rules for {{ARCHITECTURE}} const ARCH_RULES: ArchRule[] = [ ...getArchRules() ]; function getArchRules(): ArchRule[] { const arch = '{{ARCH_RAW}}'; if (arch === 'clean') { return [ { sourcePattern: /features\/\w+\/domain\//, forbiddenImports: [/features\/\w+\/data\//, /features\/\w+\/presentation\//], message: 'domain/ must not import from data/ or presentation/', }, { sourcePattern: /features\/\w+\/presentation\//, forbiddenImports: [/features\/\w+\/data\//], message: 'presentation/ must not import from data/ directly (use domain interfaces)', }, ]; } if (arch === 'feature_first') { return [ { // A feature file must not import from another feature sourcePattern: /features\/(\w+)\//, forbiddenImports: [], // checked dynamically below message: 'Features must not import from other feature folders', }, ]; } if (arch === 'mvvm') { return [ { sourcePattern: /viewmodels\//, forbiddenImports: [/package:flutter\//, /widgets\//], message: 'ViewModels must not import Flutter widgets — they must be plain Dart testable', }, ]; } return []; } function getChangedDartFiles(): string[] { try { const result = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8' }); return result.split('\n').filter(f => f.endsWith('.dart') && fs.existsSync(f)); } catch { return []; } } function checkFile(filePath: string): string[] { const violations: string[] = []; const content = fs.readFileSync(filePath, 'utf8'); const imports = content.match(/^import\s+'[^']+'/gm) ?? []; for (const rule of ARCH_RULES) { if (!rule.sourcePattern.test(filePath)) continue; // Feature-first cross-feature check if ('{{ARCH_RAW}}' === 'feature_first') { const srcFeatureMatch = filePath.match(/features\/(\w+)\//); if (srcFeatureMatch) { const srcFeature = srcFeatureMatch[1]; for (const imp of imports) { const impFeatureMatch = imp.match(/features\/(\w+)\//); if (impFeatureMatch && impFeatureMatch[1] !== srcFeature) { violations.push( `${RED}ARCH VIOLATION${RESET} in ${filePath}:\n` + ` ${YELLOW}${rule.message}${RESET}\n` + ` Import: ${imp}\n` + ` Fix: Move shared code to core/ or shared/` ); } } } continue; } // Standard forbidden import check for (const imp of imports) { for (const forbidden of rule.forbiddenImports) { if (forbidden.test(imp)) { violations.push( `${RED}ARCH VIOLATION${RESET} in ${filePath}:\n` + ` ${YELLOW}${rule.message}${RESET}\n` + ` Import: ${imp}` ); } } } } return violations; } const changedFiles = getChangedDartFiles(); if (changedFiles.length === 0) { console.log(`${GREEN}✔ arch-guard: no Dart files changed${RESET}`); process.exit(0); } console.log(`🏛 arch-guard checking ${changedFiles.length} file(s) for {{ARCHITECTURE}} violations...`); const allViolations: string[] = []; for (const file of changedFiles) { allViolations.push(...checkFile(file)); } if (allViolations.length > 0) { console.error(`\n${RED}✘ Architecture boundary violations detected:${RESET}\n`); for (const v of allViolations) console.error(v + '\n'); console.error(`Total: ${allViolations.length} violation(s). Fix before committing.`); process.exit(1); } console.log(`${GREEN}✔ arch-guard: no violations found${RESET}`);