91 lines
2.5 KiB
TypeScript
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();
|