106 lines
3.2 KiB
TypeScript
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();
|