Files
gridpilot.gg/tests/unit/structure/packages/PackageDependencies.test.ts
2025-12-04 15:15:24 +01:00

281 lines
7.7 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
const repoRoot = path.resolve(__dirname, '../../../..');
const packagesRoot = path.join(repoRoot, 'packages');
type PackageKind =
| 'racing-domain'
| 'racing-application'
| 'racing-infrastructure'
| 'racing-demo-infrastructure'
| 'other';
interface TsFile {
filePath: string;
kind: PackageKind;
}
function classifyFile(filePath: string): PackageKind {
const normalized = filePath.replace(/\\/g, '/');
// Bounded-context domain lives under packages/racing/domain
if (normalized.includes('/packages/racing/domain/')) {
return 'racing-domain';
}
if (normalized.includes('/packages/racing-application/')) {
return 'racing-application';
}
if (normalized.includes('/packages/racing-infrastructure/')) {
return 'racing-infrastructure';
}
if (normalized.includes('/packages/racing-demo-infrastructure/')) {
return 'racing-demo-infrastructure';
}
return 'other';
}
function collectTsFiles(dir: string): TsFile[] {
const entries = fs.readdirSync(dir, { withFileTypes: true });
const files: TsFile[] = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...collectTsFiles(fullPath));
} else if (entry.isFile()) {
if (
entry.name.endsWith('.ts') ||
entry.name.endsWith('.tsx')
) {
const kind = classifyFile(fullPath);
if (kind !== 'other') {
files.push({ filePath: fullPath, kind });
}
}
}
}
return files;
}
interface ImportViolation {
file: string;
line: number;
moduleSpecifier: string;
reason: string;
}
function extractImportModule(line: string): string | null {
const trimmed = line.trim();
if (!trimmed.startsWith('import')) return null;
// Handle: import ... from 'x';
const fromMatch = trimmed.match(/from\s+['"](.*)['"]/);
if (fromMatch) {
return fromMatch[1];
}
// Handle: import 'x';
const sideEffectMatch = trimmed.match(/^import\s+['"](.*)['"]\s*;?$/);
if (sideEffectMatch) {
return sideEffectMatch[1];
}
return null;
}
describe('Package dependency structure for racing slice', () => {
const tsFiles = collectTsFiles(packagesRoot);
it('enforces import boundaries for racing-domain', () => {
const violations: ImportViolation[] = [];
const forbiddenPrefixes = [
'@gridpilot/racing-application',
'@gridpilot/racing-infrastructure',
'@gridpilot/racing-demo-infrastructure',
'apps/',
'@/',
'react',
'next',
'electron',
];
for (const { filePath, kind } of tsFiles) {
if (kind !== 'racing-domain') continue;
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split(/\r?\n/);
lines.forEach((line, index) => {
const moduleSpecifier = extractImportModule(line);
if (!moduleSpecifier) return;
for (const prefix of forbiddenPrefixes) {
if (moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix)) {
violations.push({
file: filePath,
line: index + 1,
moduleSpecifier,
reason: 'racing-domain must not depend on application, infrastructure, apps, or UI frameworks',
});
}
}
});
}
if (violations.length > 0) {
const message =
'Found forbidden imports in racing domain layer (packages/racing/domain):\n' +
violations
.map(
(v) =>
`- ${v.file}:${v.line} :: import '${v.moduleSpecifier}' // ${v.reason}`,
)
.join('\n');
expect(message).toBe('');
} else {
expect(violations).toEqual([]);
}
});
it('enforces import boundaries for racing-application', () => {
const violations: ImportViolation[] = [];
const forbiddenPrefixes = [
'@gridpilot/racing-infrastructure',
'@gridpilot/racing-demo-infrastructure',
'apps/',
'@/',
];
const allowedPrefixes = [
'@gridpilot/racing',
'@gridpilot/shared-result',
'@gridpilot/identity',
];
for (const { filePath, kind } of tsFiles) {
if (kind !== 'racing-application') continue;
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split(/\r?\n/);
lines.forEach((line, index) => {
const moduleSpecifier = extractImportModule(line);
if (!moduleSpecifier) return;
for (const prefix of forbiddenPrefixes) {
if (moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix)) {
violations.push({
file: filePath,
line: index + 1,
moduleSpecifier,
reason: 'racing-application must not depend on infrastructure or apps',
});
}
}
if (moduleSpecifier.startsWith('@gridpilot/')) {
const isAllowed = allowedPrefixes.some((prefix) =>
moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix),
);
if (!isAllowed) {
violations.push({
file: filePath,
line: index + 1,
moduleSpecifier,
reason: 'racing-application should only depend on domain, shared-result, or other domain packages',
});
}
}
});
}
if (violations.length > 0) {
const message =
'Found forbidden imports in packages/racing-application:\n' +
violations
.map(
(v) =>
`- ${v.file}:${v.line} :: import '${v.moduleSpecifier}' // ${v.reason}`,
)
.join('\n');
expect(message).toBe('');
} else {
expect(violations).toEqual([]);
}
});
it('enforces import boundaries for racing infrastructure packages', () => {
const violations: ImportViolation[] = [];
const forbiddenPrefixes = ['apps/', '@/'];
const allowedPrefixes = [
'@gridpilot/racing',
'@gridpilot/shared-result',
'@gridpilot/testing-support',
'@gridpilot/social',
];
for (const { filePath, kind } of tsFiles) {
if (
kind !== 'racing-infrastructure' &&
kind !== 'racing-demo-infrastructure'
) {
continue;
}
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split(/\r?\n/);
lines.forEach((line, index) => {
const moduleSpecifier = extractImportModule(line);
if (!moduleSpecifier) return;
for (const prefix of forbiddenPrefixes) {
if (moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix)) {
violations.push({
file: filePath,
line: index + 1,
moduleSpecifier,
reason: 'racing infrastructure must not depend on apps or @/ aliases',
});
}
}
if (moduleSpecifier.startsWith('@gridpilot/')) {
const isAllowed = allowedPrefixes.some((prefix) =>
moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix),
);
if (!isAllowed) {
violations.push({
file: filePath,
line: index + 1,
moduleSpecifier,
reason: 'racing infrastructure should depend only on domain, shared-result, or testing-support',
});
}
}
});
}
if (violations.length > 0) {
const message =
'Found forbidden imports in racing infrastructure packages:\n' +
violations
.map(
(v) =>
`- ${v.file}:${v.line} :: import '${v.moduleSpecifier}' // ${v.reason}`,
)
.join('\n');
expect(message).toBe('');
} else {
expect(violations).toEqual([]);
}
});
});