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