39948-vm/backend/scripts/check-esm-boundaries.ts
2026-07-01 15:45:38 +02:00

91 lines
2.5 KiB
TypeScript

import { readdir, readFile } from 'node:fs/promises';
import path from 'node:path';
interface BoundaryViolation {
file: string;
reason: string;
}
const sourceRoots = ['src', 'scripts', 'tests'];
const commonJsPattern = new RegExp(
String.raw`\b(${['require\\s*\\(', 'module\\.exports', 'exports\\.'].join('|')})`,
);
async function collectFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const nestedFiles = await Promise.all(
entries.map(async (entry) => {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === 'node_modules' || entry.name === 'dist') return [];
return collectFiles(fullPath);
}
return [fullPath];
}),
);
return nestedFiles.flat();
}
function toProjectPath(filePath: string): string {
return path.relative(process.cwd(), filePath).split(path.sep).join('/');
}
function isHistoricalMigration(projectPath: string): boolean {
return projectPath.startsWith('src/db/migrations/') && projectPath.endsWith('.js');
}
function checkJsBoundary(projectPath: string): BoundaryViolation | null {
if (isHistoricalMigration(projectPath)) return null;
return {
file: projectPath,
reason: 'Unexpected JavaScript source. Use TypeScript ESM source.',
};
}
function checkTsBoundary(projectPath: string, source: string): BoundaryViolation | null {
if (!commonJsPattern.test(source)) return null;
return {
file: projectPath,
reason:
'Unexpected CommonJS syntax in TypeScript source. Use ESM import/export.',
};
}
async function checkFile(filePath: string): Promise<BoundaryViolation | null> {
const projectPath = toProjectPath(filePath);
const ext = path.extname(filePath);
if (ext !== '.js' && ext !== '.ts') return null;
const source = await readFile(filePath, 'utf8');
if (ext === '.js') {
return checkJsBoundary(projectPath);
}
return checkTsBoundary(projectPath, source);
}
async function main(): Promise<void> {
const files = (
await Promise.all(sourceRoots.map((root) => collectFiles(path.join(process.cwd(), root))))
).flat();
const checks = await Promise.all(files.map((file) => checkFile(file)));
const violations = checks.filter((violation) => violation !== null);
if (violations.length > 0) {
console.error('ESM boundary check failed:');
for (const violation of violations) {
console.error(`- ${violation.file}: ${violation.reason}`);
}
process.exitCode = 1;
return;
}
console.log('ESM boundary check passed.');
}
void main();