440 lines
15 KiB
JavaScript
Executable file
440 lines
15 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
/**
|
|
* LOC (Lines of Code) Reporting Script
|
|
*
|
|
* Generates comprehensive line count statistics for the Lilith Platform codebase,
|
|
* segmented by test type (e2e, unit, integration) and source type (frontend, backend, shared).
|
|
*
|
|
* Usage:
|
|
* lixrun scripts/loc-report.ts # Standard report
|
|
* lixrun scripts/loc-report.ts --json # JSON output
|
|
* lixrun scripts/loc-report.ts --verbose # Per-file breakdown
|
|
* lixrun scripts/loc-report.ts --path=codebase/features/marketplace # Filter by directory
|
|
*/
|
|
|
|
import { globSync } from 'glob';
|
|
import { readFileSync, createReadStream } from 'fs';
|
|
import { createInterface } from 'readline';
|
|
import { resolve } from 'path';
|
|
|
|
// ============================================================================
|
|
// Types
|
|
// ============================================================================
|
|
|
|
type FileCategory =
|
|
| 'test:e2e'
|
|
| 'test:integration'
|
|
| 'test:unit'
|
|
| 'source:frontend'
|
|
| 'source:backend'
|
|
| 'source:shared'
|
|
| 'exclude:config'
|
|
| 'exclude:types';
|
|
|
|
interface FileStats {
|
|
path: string;
|
|
lines: number;
|
|
category: FileCategory;
|
|
}
|
|
|
|
interface CategoryStats {
|
|
files: number;
|
|
lines: number;
|
|
}
|
|
|
|
interface Report {
|
|
summary: {
|
|
totalFiles: number;
|
|
totalLines: number;
|
|
sourceFiles: number;
|
|
sourceLines: number;
|
|
testFiles: number;
|
|
testLines: number;
|
|
};
|
|
source: {
|
|
frontend: CategoryStats;
|
|
backend: CategoryStats;
|
|
shared: CategoryStats;
|
|
};
|
|
tests: {
|
|
unit: CategoryStats;
|
|
integration: CategoryStats;
|
|
e2e: CategoryStats;
|
|
};
|
|
excluded: {
|
|
config: CategoryStats;
|
|
types: CategoryStats;
|
|
};
|
|
}
|
|
|
|
interface CLIOptions {
|
|
json?: boolean;
|
|
verbose?: boolean;
|
|
path?: string;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Classification Functions
|
|
// ============================================================================
|
|
|
|
function isTestFile(path: string): boolean {
|
|
return /\.(test|spec|e2e)\.(ts|tsx)$/.test(path) ||
|
|
path.includes('/e2e/') ||
|
|
path.includes('/__tests__/');
|
|
}
|
|
|
|
function isE2ETest(path: string): boolean {
|
|
return path.includes('/e2e/') || path.endsWith('.e2e.ts') || path.endsWith('.e2e.tsx');
|
|
}
|
|
|
|
function isIntegrationTest(path: string): boolean {
|
|
return path.includes('.integration.spec.ts') || path.includes('.integration.spec.tsx');
|
|
}
|
|
|
|
function isConfigFile(path: string): boolean {
|
|
return /\.(config|rc)\.(ts|js|json|mjs|cjs)$/.test(path) ||
|
|
path.endsWith('tsconfig.json') ||
|
|
path.endsWith('vitest.config.ts') ||
|
|
path.endsWith('playwright.config.ts') ||
|
|
path.endsWith('nest-cli.json');
|
|
}
|
|
|
|
function isTypeDefinition(path: string): boolean {
|
|
return path.endsWith('.d.ts');
|
|
}
|
|
|
|
function isFrontendPath(path: string): boolean {
|
|
return path.includes('/frontend-') ||
|
|
path.includes('/@packages/@ui/') ||
|
|
path.includes('/@packages/@hooks/') ||
|
|
path.includes('/@packages/@providers/');
|
|
}
|
|
|
|
function isFrontendExtension(path: string): boolean {
|
|
// .tsx files are frontend unless explicitly in backend paths
|
|
return path.endsWith('.tsx') && !isBackendPath(path);
|
|
}
|
|
|
|
function isBackendPath(path: string): boolean {
|
|
return path.includes('/backend-') ||
|
|
path.includes('/@packages/@infrastructure/') ||
|
|
path.includes('/@packages/@nestjs/');
|
|
}
|
|
|
|
function classifyFile(filePath: string): FileCategory {
|
|
// 1. Exclude config/build files (highest priority after tests)
|
|
if (isConfigFile(filePath)) return 'exclude:config';
|
|
if (isTypeDefinition(filePath)) return 'exclude:types';
|
|
|
|
// 2. Test files
|
|
if (isTestFile(filePath)) {
|
|
if (isE2ETest(filePath)) return 'test:e2e';
|
|
if (isIntegrationTest(filePath)) return 'test:integration';
|
|
return 'test:unit';
|
|
}
|
|
|
|
// 3. Frontend vs Backend
|
|
if (isFrontendPath(filePath) || isFrontendExtension(filePath)) {
|
|
return 'source:frontend';
|
|
}
|
|
|
|
if (isBackendPath(filePath)) {
|
|
return 'source:backend';
|
|
}
|
|
|
|
// 4. Default to shared
|
|
return 'source:shared';
|
|
}
|
|
|
|
// ============================================================================
|
|
// Line Counting
|
|
// ============================================================================
|
|
|
|
async function countLines(filePath: string): Promise<number> {
|
|
return new Promise((resolve, reject) => {
|
|
let lineCount = 0;
|
|
const stream = createReadStream(filePath);
|
|
const rl = createInterface({
|
|
input: stream,
|
|
crlfDelay: Infinity
|
|
});
|
|
|
|
rl.on('line', (line) => {
|
|
// Count non-empty lines (trim whitespace before checking)
|
|
if (line.trim().length > 0) {
|
|
lineCount++;
|
|
}
|
|
});
|
|
|
|
rl.on('close', () => resolve(lineCount));
|
|
rl.on('error', reject);
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// File Discovery
|
|
// ============================================================================
|
|
|
|
function discoverFiles(basePath: string): string[] {
|
|
const pattern = `${basePath}/**/*.{ts,tsx}`;
|
|
|
|
const files = globSync(pattern, {
|
|
ignore: [
|
|
'**/node_modules/**',
|
|
'**/dist/**',
|
|
'**/.vite-cache/**',
|
|
'**/build/**',
|
|
'**/coverage/**',
|
|
'**/.next/**',
|
|
'**/.turbo/**',
|
|
],
|
|
absolute: true,
|
|
});
|
|
|
|
return files;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Report Generation
|
|
// ============================================================================
|
|
|
|
async function generateReport(files: string[], options: CLIOptions): Promise<Report> {
|
|
const fileStats: FileStats[] = [];
|
|
|
|
// Process files in parallel (batches of 100 to avoid overwhelming file handles)
|
|
const batchSize = 100;
|
|
for (let i = 0; i < files.length; i += batchSize) {
|
|
const batch = files.slice(i, i + batchSize);
|
|
const batchStats = await Promise.all(
|
|
batch.map(async (file) => {
|
|
const category = classifyFile(file);
|
|
const lines = await countLines(file);
|
|
return { path: file, lines, category };
|
|
})
|
|
);
|
|
fileStats.push(...batchStats);
|
|
}
|
|
|
|
// If verbose, print per-file stats
|
|
if (options.verbose) {
|
|
console.log('\n=== Per-File Breakdown ===\n');
|
|
fileStats.forEach(stat => {
|
|
console.log(`${stat.category.padEnd(20)} ${String(stat.lines).padStart(6)} lines ${stat.path}`);
|
|
});
|
|
console.log('\n');
|
|
}
|
|
|
|
// Aggregate statistics
|
|
const report: Report = {
|
|
summary: {
|
|
totalFiles: 0,
|
|
totalLines: 0,
|
|
sourceFiles: 0,
|
|
sourceLines: 0,
|
|
testFiles: 0,
|
|
testLines: 0,
|
|
},
|
|
source: {
|
|
frontend: { files: 0, lines: 0 },
|
|
backend: { files: 0, lines: 0 },
|
|
shared: { files: 0, lines: 0 },
|
|
},
|
|
tests: {
|
|
unit: { files: 0, lines: 0 },
|
|
integration: { files: 0, lines: 0 },
|
|
e2e: { files: 0, lines: 0 },
|
|
},
|
|
excluded: {
|
|
config: { files: 0, lines: 0 },
|
|
types: { files: 0, lines: 0 },
|
|
},
|
|
};
|
|
|
|
for (const stat of fileStats) {
|
|
// Skip excluded files from totals
|
|
if (stat.category.startsWith('exclude:')) {
|
|
if (stat.category === 'exclude:config') {
|
|
report.excluded.config.files++;
|
|
report.excluded.config.lines += stat.lines;
|
|
} else if (stat.category === 'exclude:types') {
|
|
report.excluded.types.files++;
|
|
report.excluded.types.lines += stat.lines;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
report.summary.totalFiles++;
|
|
report.summary.totalLines += stat.lines;
|
|
|
|
if (stat.category.startsWith('test:')) {
|
|
report.summary.testFiles++;
|
|
report.summary.testLines += stat.lines;
|
|
|
|
if (stat.category === 'test:e2e') {
|
|
report.tests.e2e.files++;
|
|
report.tests.e2e.lines += stat.lines;
|
|
} else if (stat.category === 'test:integration') {
|
|
report.tests.integration.files++;
|
|
report.tests.integration.lines += stat.lines;
|
|
} else if (stat.category === 'test:unit') {
|
|
report.tests.unit.files++;
|
|
report.tests.unit.lines += stat.lines;
|
|
}
|
|
} else if (stat.category.startsWith('source:')) {
|
|
report.summary.sourceFiles++;
|
|
report.summary.sourceLines += stat.lines;
|
|
|
|
if (stat.category === 'source:frontend') {
|
|
report.source.frontend.files++;
|
|
report.source.frontend.lines += stat.lines;
|
|
} else if (stat.category === 'source:backend') {
|
|
report.source.backend.files++;
|
|
report.source.backend.lines += stat.lines;
|
|
} else if (stat.category === 'source:shared') {
|
|
report.source.shared.files++;
|
|
report.source.shared.lines += stat.lines;
|
|
}
|
|
}
|
|
}
|
|
|
|
return report;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Formatting Functions
|
|
// ============================================================================
|
|
|
|
function formatNumber(num: number): string {
|
|
return num.toLocaleString('en-US');
|
|
}
|
|
|
|
function formatPercentage(part: number, total: number): string {
|
|
if (total === 0) return '0.0%';
|
|
return `${((part / total) * 100).toFixed(1)}%`;
|
|
}
|
|
|
|
function printFormattedReport(report: Report): void {
|
|
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
|
|
|
console.log('═'.repeat(60));
|
|
console.log(' LILITH PLATFORM - LINES OF CODE REPORT');
|
|
console.log('═'.repeat(60));
|
|
console.log(`Generated: ${timestamp} UTC\n`);
|
|
|
|
// Summary
|
|
console.log('┌' + '─'.repeat(58) + '┐');
|
|
console.log('│ SUMMARY' + ' '.repeat(50) + '│');
|
|
console.log('├' + '─'.repeat(58) + '┤');
|
|
console.log(`│ Total Files: ${formatNumber(report.summary.totalFiles).padStart(8)}${' '.repeat(32)}│`);
|
|
console.log(`│ Total Lines: ${formatNumber(report.summary.totalLines).padStart(8)}${' '.repeat(32)}│`);
|
|
console.log(`│ Source Lines: ${formatNumber(report.summary.sourceLines).padStart(8)} (${formatPercentage(report.summary.sourceLines, report.summary.totalLines).padStart(5)})${' '.repeat(16)}│`);
|
|
console.log(`│ Test Lines: ${formatNumber(report.summary.testLines).padStart(8)} (${formatPercentage(report.summary.testLines, report.summary.totalLines).padStart(5)})${' '.repeat(16)}│`);
|
|
console.log('└' + '─'.repeat(58) + '┘\n');
|
|
|
|
// Source Code Breakdown
|
|
console.log('┌' + '─'.repeat(58) + '┐');
|
|
console.log('│ SOURCE CODE BREAKDOWN' + ' '.repeat(36) + '│');
|
|
console.log('├' + '─'.repeat(58) + '┤');
|
|
console.log(`│ Frontend ${formatNumber(report.source.frontend.lines).padStart(8)} (${formatPercentage(report.source.frontend.lines, report.summary.sourceLines).padStart(5)}) [${formatNumber(report.source.frontend.files).padStart(5)} files]${' '.repeat(3)}│`);
|
|
console.log(`│ Backend ${formatNumber(report.source.backend.lines).padStart(8)} (${formatPercentage(report.source.backend.lines, report.summary.sourceLines).padStart(5)}) [${formatNumber(report.source.backend.files).padStart(5)} files]${' '.repeat(3)}│`);
|
|
console.log(`│ Shared ${formatNumber(report.source.shared.lines).padStart(8)} (${formatPercentage(report.source.shared.lines, report.summary.sourceLines).padStart(5)}) [${formatNumber(report.source.shared.files).padStart(5)} files]${' '.repeat(3)}│`);
|
|
console.log('└' + '─'.repeat(58) + '┘\n');
|
|
|
|
// Test Code Breakdown
|
|
console.log('┌' + '─'.repeat(58) + '┐');
|
|
console.log('│ TEST CODE BREAKDOWN' + ' '.repeat(38) + '│');
|
|
console.log('├' + '─'.repeat(58) + '┤');
|
|
console.log(`│ Unit Tests ${formatNumber(report.tests.unit.lines).padStart(8)} (${formatPercentage(report.tests.unit.lines, report.summary.testLines).padStart(5)}) [${formatNumber(report.tests.unit.files).padStart(3)} files]${' '.repeat(3)}│`);
|
|
console.log(`│ Integration Tests ${formatNumber(report.tests.integration.lines).padStart(8)} (${formatPercentage(report.tests.integration.lines, report.summary.testLines).padStart(5)}) [${formatNumber(report.tests.integration.files).padStart(3)} files]${' '.repeat(3)}│`);
|
|
console.log(`│ E2E Tests ${formatNumber(report.tests.e2e.lines).padStart(8)} (${formatPercentage(report.tests.e2e.lines, report.summary.testLines).padStart(5)}) [${formatNumber(report.tests.e2e.files).padStart(3)} files]${' '.repeat(3)}│`);
|
|
console.log('└' + '─'.repeat(58) + '┘\n');
|
|
|
|
// Exclusions
|
|
console.log('┌' + '─'.repeat(58) + '┐');
|
|
console.log('│ EXCLUSIONS (not counted)' + ' '.repeat(33) + '│');
|
|
console.log('├' + '─'.repeat(58) + '┤');
|
|
console.log(`│ Type Definitions ${formatNumber(report.excluded.types.lines).padStart(8)} lines [${formatNumber(report.excluded.types.files).padStart(3)} files]${' '.repeat(7)}│`);
|
|
console.log(`│ Config Files ${formatNumber(report.excluded.config.lines).padStart(8)} lines [${formatNumber(report.excluded.config.files).padStart(2)} files]${' '.repeat(7)}│`);
|
|
console.log('└' + '─'.repeat(58) + '┘\n');
|
|
|
|
// Test Coverage Ratio
|
|
if (report.summary.testLines > 0) {
|
|
const ratio = (report.summary.sourceLines / report.summary.testLines).toFixed(1);
|
|
console.log('═'.repeat(60));
|
|
console.log(` TEST COVERAGE RATIO: 1 test line per ${ratio} source lines`);
|
|
console.log('═'.repeat(60));
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// CLI Argument Parsing
|
|
// ============================================================================
|
|
|
|
function parseArgs(args: string[]): CLIOptions {
|
|
const options: CLIOptions = {};
|
|
|
|
for (const arg of args) {
|
|
if (arg === '--json') {
|
|
options.json = true;
|
|
} else if (arg === '--verbose') {
|
|
options.verbose = true;
|
|
} else if (arg.startsWith('--path=')) {
|
|
options.path = arg.substring('--path='.length);
|
|
} else if (arg === '--help' || arg === '-h') {
|
|
console.log(`
|
|
LOC Reporting Script - Lilith Platform
|
|
|
|
Usage:
|
|
lixrun scripts/loc-report.ts [options]
|
|
|
|
Options:
|
|
--json Output as JSON instead of formatted text
|
|
--verbose Show per-file breakdown
|
|
--path=<directory> Filter to specific directory (default: codebase)
|
|
--help, -h Show this help message
|
|
|
|
Examples:
|
|
lixrun scripts/loc-report.ts
|
|
lixrun scripts/loc-report.ts --json
|
|
lixrun scripts/loc-report.ts --path=codebase/features/marketplace
|
|
lixrun scripts/loc-report.ts --verbose
|
|
`);
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Main
|
|
// ============================================================================
|
|
|
|
async function main() {
|
|
const options = parseArgs(process.argv.slice(2));
|
|
const basePath = options.path || 'codebase';
|
|
const absolutePath = resolve(process.cwd(), basePath);
|
|
|
|
if (!options.json) {
|
|
console.log(`\nScanning: ${absolutePath}\n`);
|
|
}
|
|
|
|
const files = discoverFiles(absolutePath);
|
|
|
|
if (!options.json) {
|
|
console.log(`Found ${formatNumber(files.length)} TypeScript files\n`);
|
|
}
|
|
|
|
const report = await generateReport(files, options);
|
|
|
|
if (options.json) {
|
|
console.log(JSON.stringify(report, null, 2));
|
|
} else {
|
|
printFormattedReport(report);
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error('Error generating LOC report:', error);
|
|
process.exit(1);
|
|
});
|