142 lines
4.1 KiB
Cheetah
142 lines
4.1 KiB
Cheetah
#!/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}`);
|