Files
support_backend/scripts/check-architecture.ts
T
2026-08-19 16:29:17 +05:30

106 lines
3.2 KiB
TypeScript

import fs from 'fs';
import path from 'path';
const SRC_DIR = path.resolve(__dirname, '../src');
interface Violation {
file: string;
line: number;
rule: string;
detail: string;
}
const violations: Violation[] = [];
function getFilesRecursively(dir: string): string[] {
let results: string[] = [];
if (!fs.existsSync(dir)) return results;
const list = fs.readdirSync(dir);
for (const file of list) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat && stat.isDirectory()) {
results = results.concat(getFilesRecursively(filePath));
} else if (file.endsWith('.ts')) {
results.push(filePath);
}
}
return results;
}
function checkControllerDatabaseImports(filePath: string, content: string): void {
const relativePath = path.relative(SRC_DIR, filePath);
if (!relativePath.includes('controller')) return;
const lines = content.split('\n');
lines.forEach((line, idx) => {
if (
line.includes("from '@prisma/client'") ||
line.includes('from "@prisma/client"') ||
line.includes('prisma.client') ||
line.includes('prisma.plugin')
) {
violations.push({
file: relativePath,
line: idx + 1,
rule: 'NO_CONTROLLER_DIRECT_DB_ACCESS',
detail:
'Controllers must not directly import or access Prisma client. Use Services instead.',
});
}
});
}
function checkCrossModuleInternalImports(filePath: string, content: string): void {
const relativePath = path.relative(SRC_DIR, filePath);
const lines = content.split('\n');
lines.forEach((line, idx) => {
// Check for deep import from @/modules/group/module/internal
const moduleImportMatch = line.match(/from\s+['"]@\/modules\/([^'"]+)['"]/);
if (moduleImportMatch && moduleImportMatch[1]) {
const targetPath = moduleImportMatch[1];
const segments = targetPath.split('/');
// Allow @/modules/group/module or @/modules/group/module/index
if (segments.length > 2 && segments[2] !== 'index') {
violations.push({
file: relativePath,
line: idx + 1,
rule: 'NO_CROSS_MODULE_DEEP_IMPORTS',
detail: `Deep import '${targetPath}' bypasses module public API boundary (index.ts).`,
});
}
}
});
}
function runArchitectureCheck(): void {
// eslint-disable-next-line no-console
console.log('🔍 Running Architecture & Boundary Checks...');
const allFiles = getFilesRecursively(SRC_DIR);
for (const file of allFiles) {
const content = fs.readFileSync(file, 'utf-8');
checkControllerDatabaseImports(file, content);
checkCrossModuleInternalImports(file, content);
}
if (violations.length > 0) {
// eslint-disable-next-line no-console
console.error('\n❌ Architecture Violations Detected:\n');
violations.forEach((v) => {
// eslint-disable-next-line no-console
console.error(` - [${v.rule}] ${v.file}:${v.line}`);
// eslint-disable-next-line no-console
console.error(` ${v.detail}\n`);
});
process.exit(1);
} else {
// eslint-disable-next-line no-console
console.log('✅ All architecture rules and module boundaries passed successfully!');
}
}
runArchitectureCheck();