wip
This commit is contained in:
281
tests/unit/structure/packages/PackageDependencies.test.ts
Normal file
281
tests/unit/structure/packages/PackageDependencies.test.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
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/demo-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 demo-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([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user