chore(core): 🔧 Update core dependency versions in loc-report.ts

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Lilith 2026-02-05 19:10:29 -08:00
parent 3c4ecca9b4
commit 4a351024a2

440
scripts/loc-report.ts Executable file
View file

@ -0,0 +1,440 @@
#!/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);
});