UNPKG

@morodomi/ait3

Version:

AIT³ Development Platform - AI + Ticket + Test + Tool driven development methodology

189 lines (188 loc) 6.76 kB
import linguist from 'linguist-js'; import { readdir, stat } from 'fs/promises'; import { join, extname } from 'path'; export class LinguistLanguageDetector { rootPath; constructor(rootPath) { this.rootPath = rootPath; } async detectLanguages(path) { const targetPath = path || this.rootPath; try { // Use linguist-js for accurate language detection const result = await linguist(targetPath); // Handle different response formats from linguist-js const languages = result.languages || result; if (!languages || typeof languages !== 'object' || Object.keys(languages).length === 0) { return []; } // Find the highest percentage for primary language detection const maxPercentage = Math.max(...Object.values(languages).map((lang) => { if (lang && typeof lang === 'object' && 'percentage' in lang) { return lang.percentage || 0; } return 0; })); // Convert linguist results to our format const languageResults = Object.entries(languages) .filter(([name]) => { // Filter out metadata fields return name !== 'count' && name !== 'bytes' && name !== 'lines' && name !== 'results' && name !== 'total' && name !== 'unknown'; }) .map(([name, data]) => { const percentage = data && typeof data === 'object' && 'percentage' in data ? data.percentage || 0 : 0; let files = 0; if (data && typeof data === 'object' && 'files' in data) { const filesData = data.files; if (Array.isArray(filesData)) { files = filesData.length; } else if (typeof filesData === 'number') { files = filesData; } } return { name, percentage, files, primaryLanguage: percentage === maxPercentage }; }) .filter(lang => lang.files > 0 || lang.percentage > 0) .sort((a, b) => b.percentage - a.percentage); // If no languages detected, use fallback if (languageResults.length === 0) { return this.fallbackDetection(targetPath); } return languageResults; } catch { // Fallback to simple file extension-based detection return this.fallbackDetection(targetPath); } } async getPrimaryLanguage(path) { const languages = await this.detectLanguages(path); if (languages.length === 0) { return null; } // Return the first language (highest percentage) return languages[0]; } async fallbackDetection(targetPath) { try { const fileExtensions = await this.collectFileExtensions(targetPath); if (fileExtensions.size === 0) { return []; } // Map extensions to languages const languageCounts = new Map(); for (const [ext, count] of fileExtensions) { const language = this.getLanguageFromExtension(ext); if (language) { languageCounts.set(language, (languageCounts.get(language) || 0) + count); } } // Calculate total files const totalFiles = Array.from(languageCounts.values()).reduce((a, b) => a + b, 0); if (totalFiles === 0) { return []; } // Find primary language const maxFiles = Math.max(...Array.from(languageCounts.values())); // Convert to LanguageResult format const results = Array.from(languageCounts.entries()) .map(([name, files]) => ({ name, percentage: (files / totalFiles) * 100, files, primaryLanguage: files === maxFiles })) .sort((a, b) => b.files - a.files); return results; } catch { return []; } } async collectFileExtensions(dir, extensions = new Map()) { try { const entries = await readdir(dir); for (const entry of entries) { const fullPath = join(dir, entry); const stats = await stat(fullPath); if (stats.isDirectory() && !this.shouldIgnoreDirectory(entry)) { await this.collectFileExtensions(fullPath, extensions); } else if (stats.isFile()) { const ext = extname(entry).toLowerCase(); if (ext) { extensions.set(ext, (extensions.get(ext) || 0) + 1); } } } } catch { // Ignore errors in subdirectories } return extensions; } shouldIgnoreDirectory(name) { const ignoreDirs = [ 'node_modules', '.git', 'dist', 'build', 'coverage', '.next', '__pycache__', '.pytest_cache', 'vendor' ]; return ignoreDirs.includes(name); } getLanguageFromExtension(ext) { const extensionMap = { '.ts': 'TypeScript', '.tsx': 'TypeScript', '.js': 'JavaScript', '.jsx': 'JavaScript', '.py': 'Python', '.php': 'PHP', '.java': 'Java', '.cs': 'C#', '.rb': 'Ruby', '.go': 'Go', '.rs': 'Rust', '.swift': 'Swift', '.kt': 'Kotlin', '.cpp': 'C++', '.c': 'C', '.h': 'C', '.hpp': 'C++', '.json': 'JSON', '.yaml': 'YAML', '.yml': 'YAML', '.xml': 'XML', '.html': 'HTML', '.css': 'CSS', '.scss': 'SCSS', '.sass': 'Sass', '.less': 'Less', '.sql': 'SQL', '.sh': 'Shell', '.bash': 'Shell', '.ps1': 'PowerShell', '.r': 'R', '.scala': 'Scala', '.dart': 'Dart', '.lua': 'Lua', '.perl': 'Perl', '.pl': 'Perl' }; return extensionMap[ext] || null; } }