This commit is contained in:
326
src/analyzers/projectTypeDetector.ts
Normal file
326
src/analyzers/projectTypeDetector.ts
Normal file
@@ -0,0 +1,326 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { ProjectType } from '../types';
|
||||
|
||||
interface FrameworkPattern {
|
||||
name: string;
|
||||
indicators: string[];
|
||||
language: string;
|
||||
}
|
||||
|
||||
interface LanguagePattern {
|
||||
extensions: string[];
|
||||
configFiles: string[];
|
||||
language: string;
|
||||
}
|
||||
|
||||
export class ProjectTypeDetector {
|
||||
private languagePatterns: LanguagePattern[] = [
|
||||
{
|
||||
extensions: ['.ts', '.tsx'],
|
||||
language: 'TypeScript',
|
||||
configFiles: ['tsconfig.json'],
|
||||
},
|
||||
{
|
||||
extensions: ['.js', '.jsx'],
|
||||
language: 'JavaScript',
|
||||
configFiles: ['package.json', 'jsconfig.json'],
|
||||
},
|
||||
{
|
||||
extensions: ['.py'],
|
||||
language: 'Python',
|
||||
configFiles: ['pyproject.toml', 'setup.py', 'requirements.txt'],
|
||||
},
|
||||
{
|
||||
extensions: ['.go'],
|
||||
language: 'Go',
|
||||
configFiles: ['go.mod', 'go.sum'],
|
||||
},
|
||||
{
|
||||
extensions: ['.rs'],
|
||||
language: 'Rust',
|
||||
configFiles: ['Cargo.toml', 'Cargo.lock'],
|
||||
},
|
||||
{
|
||||
extensions: ['.java'],
|
||||
language: 'Java',
|
||||
configFiles: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
|
||||
},
|
||||
{
|
||||
extensions: ['.c', '.h'],
|
||||
language: 'C',
|
||||
configFiles: ['CMakeLists.txt', 'Makefile'],
|
||||
},
|
||||
{
|
||||
extensions: ['.cpp', '.cc', '.cxx', '.hpp'],
|
||||
language: 'C++',
|
||||
configFiles: ['CMakeLists.txt', 'Makefile'],
|
||||
},
|
||||
{
|
||||
extensions: ['.cs'],
|
||||
language: 'C#',
|
||||
configFiles: ['*.csproj', '*.sln'],
|
||||
},
|
||||
{
|
||||
extensions: ['.rb'],
|
||||
language: 'Ruby',
|
||||
configFiles: ['Gemfile', 'Rakefile'],
|
||||
},
|
||||
{
|
||||
extensions: ['.php'],
|
||||
language: 'PHP',
|
||||
configFiles: ['composer.json', 'phpunit.xml'],
|
||||
},
|
||||
{
|
||||
extensions: ['.swift'],
|
||||
language: 'Swift',
|
||||
configFiles: ['Package.swift', '*.xcodeproj'],
|
||||
},
|
||||
];
|
||||
|
||||
private frameworkPatterns: FrameworkPattern[] = [
|
||||
{
|
||||
name: 'React',
|
||||
indicators: ['react', 'react-dom', 'react-scripts'],
|
||||
language: 'TypeScript',
|
||||
},
|
||||
{
|
||||
name: 'Vue',
|
||||
indicators: ['vue', 'vue-loader', '@vue/'],
|
||||
language: 'TypeScript',
|
||||
},
|
||||
{
|
||||
name: 'Express',
|
||||
indicators: ['express'],
|
||||
language: 'JavaScript',
|
||||
},
|
||||
{
|
||||
name: 'Next.js',
|
||||
indicators: ['next'],
|
||||
language: 'TypeScript',
|
||||
},
|
||||
{
|
||||
name: 'NestJS',
|
||||
indicators: ['@nestjs/'],
|
||||
language: 'TypeScript',
|
||||
},
|
||||
{
|
||||
name: 'Django',
|
||||
indicators: ['django'],
|
||||
language: 'Python',
|
||||
},
|
||||
{
|
||||
name: 'FastAPI',
|
||||
indicators: ['fastapi', 'uvicorn'],
|
||||
language: 'Python',
|
||||
},
|
||||
{
|
||||
name: 'Flask',
|
||||
indicators: ['flask'],
|
||||
language: 'Python',
|
||||
},
|
||||
{
|
||||
name: 'Gin',
|
||||
indicators: ['gin-gonic/gin'],
|
||||
language: 'Go',
|
||||
},
|
||||
{
|
||||
name: 'Echo',
|
||||
indicators: ['labstack/echo'],
|
||||
language: 'Go',
|
||||
},
|
||||
{
|
||||
name: 'Actix-web',
|
||||
indicators: ['actix-web'],
|
||||
language: 'Rust',
|
||||
},
|
||||
{
|
||||
name: 'Rocket',
|
||||
indicators: ['rocket'],
|
||||
language: 'Rust',
|
||||
},
|
||||
{
|
||||
name: 'Spring Boot',
|
||||
indicators: ['spring-boot'],
|
||||
language: 'Java',
|
||||
},
|
||||
];
|
||||
|
||||
async detect(dir: string): Promise<ProjectType> {
|
||||
const languages = await this.detectLanguages(dir);
|
||||
const primaryLanguage = await this.determinePrimaryLanguage(languages);
|
||||
const frameworks = await this.detectFrameworks(dir, primaryLanguage);
|
||||
const buildTools = await this.detectBuildTools(dir, languages);
|
||||
|
||||
return {
|
||||
primaryLanguage,
|
||||
languages,
|
||||
frameworks,
|
||||
buildTools,
|
||||
};
|
||||
}
|
||||
|
||||
private async determinePrimaryLanguage(
|
||||
languages: string[]
|
||||
): Promise<string> {
|
||||
if (languages.length === 0) return 'Unknown';
|
||||
return languages[0];
|
||||
}
|
||||
|
||||
private async detectLanguages(dir: string): Promise<string[]> {
|
||||
const detectedLanguages: string[] = [];
|
||||
const contents = await fs.promises.readdir(dir, { withFileTypes: true });
|
||||
|
||||
const fileExtensions = new Set<string>();
|
||||
for (const item of contents) {
|
||||
if (item.isFile()) {
|
||||
const ext = path.extname(item.name).toLowerCase();
|
||||
fileExtensions.add(ext);
|
||||
} else if (item.isDirectory()) {
|
||||
const subDirContents = await fs.promises.readdir(
|
||||
path.join(dir, item.name),
|
||||
{ withFileTypes: true }
|
||||
);
|
||||
for (const subItem of subDirContents) {
|
||||
if (subItem.isFile()) {
|
||||
const ext = path.extname(subItem.name).toLowerCase();
|
||||
fileExtensions.add(ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const pattern of this.languagePatterns) {
|
||||
for (const ext of pattern.extensions) {
|
||||
if (fileExtensions.has(ext)) {
|
||||
if (!detectedLanguages.includes(pattern.language)) {
|
||||
detectedLanguages.push(pattern.language);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const pattern of this.languagePatterns) {
|
||||
for (const configFile of pattern.configFiles) {
|
||||
const fullPath = path.join(dir, configFile);
|
||||
if (await this.fileExists(fullPath)) {
|
||||
if (!detectedLanguages.includes(pattern.language)) {
|
||||
detectedLanguages.push(pattern.language);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return detectedLanguages;
|
||||
}
|
||||
|
||||
private async detectFrameworks(
|
||||
dir: string,
|
||||
primaryLanguage: string
|
||||
): Promise<string[]> {
|
||||
const detectedFrameworks: string[] = [];
|
||||
|
||||
for (const item of this.frameworkPatterns) {
|
||||
if (item.language !== primaryLanguage) continue;
|
||||
|
||||
const packageJsonPath = path.join(dir, 'package.json');
|
||||
if (await this.fileExists(packageJsonPath)) {
|
||||
const content = await fs.promises.readFile(packageJsonPath, 'utf-8');
|
||||
const packageJson = JSON.parse(content);
|
||||
const allDeps = {
|
||||
...packageJson.dependencies,
|
||||
...packageJson.devDependencies,
|
||||
};
|
||||
|
||||
for (const indicator of item.indicators) {
|
||||
if (Object.keys(allDeps).some(dep => dep.startsWith(indicator))) {
|
||||
if (!detectedFrameworks.includes(item.name)) {
|
||||
detectedFrameworks.push(item.name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item.language === 'Python') {
|
||||
const requirementsPath = path.join(dir, 'requirements.txt');
|
||||
const pyprojectPath = path.join(dir, 'pyproject.toml');
|
||||
|
||||
const filesToCheck = [requirementsPath, pyprojectPath];
|
||||
for (const filePath of filesToCheck) {
|
||||
if (await this.fileExists(filePath)) {
|
||||
const content = await fs.promises.readFile(filePath, 'utf-8');
|
||||
for (const indicator of item.indicators) {
|
||||
if (content.toLowerCase().includes(indicator.toLowerCase())) {
|
||||
if (!detectedFrameworks.includes(item.name)) {
|
||||
detectedFrameworks.push(item.name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item.language === 'Go') {
|
||||
const goModPath = path.join(dir, 'go.mod');
|
||||
if (await this.fileExists(goModPath)) {
|
||||
const content = await fs.promises.readFile(goModPath, 'utf-8');
|
||||
for (const indicator of item.indicators) {
|
||||
if (content.includes(indicator)) {
|
||||
if (!detectedFrameworks.includes(item.name)) {
|
||||
detectedFrameworks.push(item.name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return detectedFrameworks;
|
||||
}
|
||||
|
||||
private async detectBuildTools(
|
||||
dir: string,
|
||||
languages: string[]
|
||||
): Promise<string[]> {
|
||||
const buildTools: string[] = [];
|
||||
|
||||
const buildToolFiles: Record<string, string[]> = {
|
||||
npm: ['package.json'],
|
||||
yarn: ['yarn.lock', '.yarnrc'],
|
||||
pnpm: ['pnpm-lock.yaml'],
|
||||
make: ['Makefile'],
|
||||
cmake: ['CMakeLists.txt'],
|
||||
gradle: ['build.gradle', 'build.gradle.kts'],
|
||||
maven: ['pom.xml'],
|
||||
cargo: ['Cargo.toml', 'Cargo.lock'],
|
||||
};
|
||||
|
||||
for (const [tool, files] of Object.entries(buildToolFiles)) {
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file);
|
||||
if (await this.fileExists(filePath)) {
|
||||
if (!buildTools.includes(tool)) {
|
||||
buildTools.push(tool);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buildTools;
|
||||
}
|
||||
|
||||
private async fileExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.promises.access(filePath, fs.constants.F_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user