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 core/racing/domain if (normalized.includes('/core/racing/domain/')) { return 'racing-domain'; } if (normalized.includes('/core/racing-application/')) { return 'racing-application'; } if (normalized.includes('/core/racing-infrastructure/')) { return 'racing-infrastructure'; } if (normalized.includes('/core/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] || null; } // Handle: import 'x'; const sideEffectMatch = trimmed.match(/^import\s+['"](.*)['"]\s*;?$/); if (sideEffectMatch) { return sideEffectMatch[1] || null; } 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 (core/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 core/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([]); } }); });