@morodomi/ait3
Version:
AIT³ Development Platform - AI + Ticket + Test + Tool driven development methodology
189 lines (188 loc) • 6.76 kB
JavaScript
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;
}
}