woaru
Version:
Universal Project Setup Autopilot - Analyze and automatically configure development tools for ANY programming language
426 lines • 16.6 kB
JavaScript
import fs from 'fs-extra';
import * as path from 'path';
import { glob } from 'glob';
import { LanguageDetector } from './LanguageDetector.js';
/**
* ProjectAnalyzer - Comprehensive project analysis for code structure, dependencies, and frameworks
*
* The ProjectAnalyzer class provides deep insights into project structure, language detection,
* dependency analysis, and framework identification across multiple programming languages.
* It serves as the foundation for WOARU's intelligent project understanding and tool recommendations.
*
* @example
* ```typescript
* const analyzer = new ProjectAnalyzer();
* const analysis = await analyzer.analyzeProject('./my-project');
* console.log(`Primary language: ${analysis.language}`);
* console.log(`Frameworks: ${analysis.framework.join(', ')}`);
* console.log(`Dependencies: ${analysis.dependencies.length}`);
* ```
*
* @since 1.0.0
*/
export class ProjectAnalyzer {
/** Language detection engine for multi-language project support */
languageDetector;
/**
* Creates a new ProjectAnalyzer instance with language detection capabilities
*
* @example
* ```typescript
* const analyzer = new ProjectAnalyzer();
* ```
*/
constructor() {
this.languageDetector = new LanguageDetector();
}
/**
* Performs comprehensive analysis of a project directory
*
* Analyzes the project structure, detects programming languages and frameworks,
* extracts dependencies and development dependencies, identifies package managers,
* and catalogs configuration files for intelligent tool recommendations.
*
* @param projectPath - Absolute path to the project directory to analyze
* @returns Promise resolving to comprehensive project analysis results
*
* @throws {Error} When project language cannot be detected or path is invalid
*
* @example
* ```typescript
* const analyzer = new ProjectAnalyzer();
*
* // Analyze a TypeScript project
* const analysis = await analyzer.analyzeProject('/path/to/typescript-project');
* console.log(`Language: ${analysis.language}`); // "TypeScript"
* console.log(`Frameworks: ${analysis.framework}`); // ["react", "nextjs"]
* console.log(`Package Manager: ${analysis.packageManager}`); // "npm"
*
* // Analyze a Python project
* const pyAnalysis = await analyzer.analyzeProject('/path/to/python-project');
* console.log(`Dependencies: ${pyAnalysis.dependencies.length}`);
* ```
*/
async analyzeProject(projectPath) {
// Detect primary language first
const primaryLanguage = await this.languageDetector.detectPrimaryLanguage(projectPath);
const allLanguages = await this.languageDetector.detectLanguages(projectPath);
if (primaryLanguage === 'unknown') {
throw new Error('Could not detect project language. Make sure you are in a valid project directory.');
}
// Get language-specific dependencies
const dependencies = await this.getDependencies(projectPath, primaryLanguage);
const devDependencies = await this.getDevDependencies(projectPath, primaryLanguage);
const scripts = await this.getScripts(projectPath, primaryLanguage);
// Detect frameworks based on language
const frameworks = await this.languageDetector.detectFrameworks(projectPath, primaryLanguage);
const analysis = {
language: primaryLanguage === 'javascript'
? await this.detectLanguage(projectPath)
: this.languageDetector.getLanguageInfo(primaryLanguage)?.name ||
primaryLanguage,
framework: frameworks,
packageManager: await this.detectPackageManager(projectPath),
dependencies,
devDependencies,
scripts,
configFiles: await this.findConfigFiles(projectPath),
structure: await this.analyzeStructure(projectPath),
detectedLanguages: allLanguages,
};
return analysis;
}
async readPackageJson(packageJsonPath) {
try {
return await fs.readJson(packageJsonPath);
}
catch {
return null;
}
}
async detectLanguage(projectPath) {
const tsconfigPath = path.join(projectPath, 'tsconfig.json');
const hasTypeScript = await fs.pathExists(tsconfigPath);
if (hasTypeScript) {
return 'TypeScript';
}
// Check for TypeScript files in the project
const tsFiles = await glob('**/*.{ts,tsx}', {
cwd: projectPath,
ignore: ['node_modules/**', 'dist/**', 'build/**'],
});
return tsFiles.length > 0 ? 'TypeScript' : 'JavaScript';
}
async detectFrameworks(projectPath, packageJson) {
const frameworks = [];
const allDeps = {
...(packageJson.dependencies || {}),
...(packageJson.devDependencies || {}),
};
// Next.js
if (allDeps.next ||
(await fs.pathExists(path.join(projectPath, 'next.config.js'))) ||
(await fs.pathExists(path.join(projectPath, 'next.config.ts')))) {
frameworks.push('nextjs');
}
// React
if (allDeps.react) {
frameworks.push('react');
}
// Vue
if (allDeps.vue) {
frameworks.push('vue');
}
// Express
if (allDeps.express) {
frameworks.push('express');
}
// Nuxt
if (allDeps.nuxt) {
frameworks.push('nuxt');
}
// Vite
if (allDeps.vite ||
(await fs.pathExists(path.join(projectPath, 'vite.config.js'))) ||
(await fs.pathExists(path.join(projectPath, 'vite.config.ts')))) {
frameworks.push('vite');
}
return frameworks;
}
async detectPackageManager(projectPath) {
// Node.js package managers
if (await fs.pathExists(path.join(projectPath, 'pnpm-lock.yaml'))) {
return 'pnpm';
}
if (await fs.pathExists(path.join(projectPath, 'yarn.lock'))) {
return 'yarn';
}
if (await fs.pathExists(path.join(projectPath, 'package-lock.json'))) {
return 'npm';
}
// Python package managers
if (await fs.pathExists(path.join(projectPath, 'poetry.lock'))) {
return 'poetry';
}
if (await fs.pathExists(path.join(projectPath, 'Pipfile.lock'))) {
return 'pip';
}
if (await fs.pathExists(path.join(projectPath, 'requirements.txt'))) {
return 'pip';
}
// Rust
if ((await fs.pathExists(path.join(projectPath, 'Cargo.lock'))) ||
(await fs.pathExists(path.join(projectPath, 'Cargo.toml')))) {
return 'cargo';
}
// Java
if (await fs.pathExists(path.join(projectPath, 'pom.xml'))) {
return 'maven';
}
if (await fs.pathExists(path.join(projectPath, 'build.gradle'))) {
return 'gradle';
}
// C#/.NET
const csprojFiles = await glob('*.csproj', { cwd: projectPath });
if (csprojFiles.length > 0) {
return 'dotnet';
}
// Go
if (await fs.pathExists(path.join(projectPath, 'go.mod'))) {
return 'go';
}
// PHP
if (await fs.pathExists(path.join(projectPath, 'composer.lock'))) {
return 'composer';
}
// Ruby
if (await fs.pathExists(path.join(projectPath, 'Gemfile.lock'))) {
return 'gem';
}
return 'npm'; // default
}
async findConfigFiles(projectPath) {
const configPatterns = [
'*.config.{js,ts,json}',
'.*rc',
'.*rc.{js,ts,json}',
'tsconfig.json',
'babel.config.{js,json}',
'.babelrc',
'webpack.config.{js,ts}',
'rollup.config.{js,ts}',
'jest.config.{js,ts,json}',
'tailwind.config.{js,ts}',
'postcss.config.{js,ts}',
'.env*',
];
const configFiles = [];
for (const pattern of configPatterns) {
try {
const files = await glob(pattern, {
cwd: projectPath,
ignore: ['node_modules/**', 'dist/**', 'build/**'],
});
configFiles.push(...files);
}
catch {
// Continue if pattern fails
}
}
return [...new Set(configFiles)].sort();
}
async getDependencies(projectPath, language) {
switch (language) {
case 'javascript': {
const packageJson = await this.readPackageJson(path.join(projectPath, 'package.json'));
return packageJson ? Object.keys(packageJson.dependencies || {}) : [];
}
case 'python': {
return await this.getPythonDependencies(projectPath);
}
case 'csharp': {
return await this.getCSharpDependencies(projectPath);
}
default:
return [];
}
}
async getDevDependencies(projectPath, language) {
switch (language) {
case 'javascript': {
const packageJson = await this.readPackageJson(path.join(projectPath, 'package.json'));
return packageJson
? Object.keys(packageJson.devDependencies || {})
: [];
}
case 'python': {
// Python doesn't really have dev dependencies in the same way
// But we can check for test/dev packages
return await this.getPythonDevDependencies(projectPath);
}
default:
return [];
}
}
async getScripts(projectPath, language) {
switch (language) {
case 'javascript': {
const packageJson = await this.readPackageJson(path.join(projectPath, 'package.json'));
return packageJson
? packageJson.scripts || {}
: {};
}
case 'python': {
return await this.getPythonScripts(projectPath);
}
default:
return {};
}
}
async getPythonDependencies(projectPath) {
const dependencies = [];
// Check requirements.txt
const requirementsPath = path.join(projectPath, 'requirements.txt');
if (await fs.pathExists(requirementsPath)) {
const content = await fs.readFile(requirementsPath, 'utf-8');
const lines = content
.split('\n')
.filter(line => line.trim() && !line.startsWith('#'));
dependencies.push(...lines.map(line => line.split('==')[0].split('>=')[0].split('~=')[0].trim()));
}
// Check pyproject.toml
const pyprojectPath = path.join(projectPath, 'pyproject.toml');
if (await fs.pathExists(pyprojectPath)) {
const content = await fs.readFile(pyprojectPath, 'utf-8');
// Simple regex to extract dependencies - would need proper TOML parser for production
const depMatches = content.match(/dependencies\s*=\s*\[([\s\S]*?)\]/);
if (depMatches) {
const depLines = depMatches[1].split(',');
dependencies.push(...depLines
.map(line => line.replace(/['"]/g, '').split('==')[0].split('>=')[0].trim())
.filter(dep => dep));
}
}
return [...new Set(dependencies)];
}
async getPythonDevDependencies(projectPath) {
const devDeps = [];
// Check for common dev/test packages
const allDeps = await this.getPythonDependencies(projectPath);
const devPackages = [
'pytest',
'black',
'flake8',
'mypy',
'ruff',
'pre-commit',
'isort',
];
devDeps.push(...allDeps.filter(dep => devPackages.some(devPkg => dep.toLowerCase().includes(devPkg))));
return devDeps;
}
async getPythonScripts(projectPath) {
const scripts = {};
// Check pyproject.toml for scripts
const pyprojectPath = path.join(projectPath, 'pyproject.toml');
if (await fs.pathExists(pyprojectPath)) {
const content = await fs.readFile(pyprojectPath, 'utf-8');
// Look for common script patterns
if (content.includes('pytest'))
scripts.test = 'pytest';
if (content.includes('black'))
scripts.format = 'black .';
if (content.includes('ruff'))
scripts.lint = 'ruff check .';
}
// Check for common Python patterns
if (await fs.pathExists(path.join(projectPath, 'manage.py'))) {
scripts.runserver = 'python manage.py runserver';
scripts.migrate = 'python manage.py migrate';
}
return scripts;
}
async getCSharpDependencies(projectPath) {
const dependencies = [];
// Check .csproj files
const csprojFiles = await glob('**/*.csproj', { cwd: projectPath });
for (const file of csprojFiles) {
try {
const content = await fs.readFile(path.join(projectPath, file), 'utf-8');
// Simple regex to extract PackageReference - would need proper XML parser
const packageMatches = content.match(/<PackageReference\s+Include="([^"]+)"/g);
if (packageMatches) {
dependencies.push(...packageMatches
.map(match => match.match(/Include="([^"]+)"/)?.[1] || '')
.filter(pkg => pkg));
}
}
catch {
// Skip files that can't be read
}
}
return [...new Set(dependencies)];
}
async analyzeStructure(projectPath) {
try {
const files = await glob('**/*.{js,jsx,ts,tsx,vue}', {
cwd: projectPath,
ignore: [
'node_modules/**',
'dist/**',
'build/**',
'.next/**',
'coverage/**',
],
});
// Limit to reasonable number of files for analysis
return files.slice(0, 100).sort();
}
catch {
return [];
}
}
/**
* Extracts project metadata from package.json or infers from directory structure
*
* Reads and parses package.json to extract project name, version, description,
* and author information. Falls back to directory-based inference if package.json
* is not available or cannot be parsed.
*
* @param projectPath - Absolute path to the project directory
* @returns Promise resolving to project metadata object
*
* @example
* ```typescript
* const analyzer = new ProjectAnalyzer();
* const metadata = await analyzer.getProjectMetadata('./my-project');
*
* console.log(metadata.name); // "my-awesome-app"
* console.log(metadata.version); // "1.2.3"
* console.log(metadata.description); // "An awesome application"
* console.log(metadata.author); // "John Doe <john@example.com>"
*
* // For projects without package.json
* const metadata2 = await analyzer.getProjectMetadata('./python-project');
* console.log(metadata2.name); // "python-project" (directory name)
* console.log(metadata2.version); // "0.0.0" (default)
* ```
*/
async getProjectMetadata(projectPath) {
const packageJsonPath = path.join(projectPath, 'package.json');
const packageJson = await this.readPackageJson(packageJsonPath);
if (!packageJson) {
return {
name: path.basename(projectPath),
version: '0.0.0',
};
}
return {
name: packageJson.name || path.basename(projectPath),
version: packageJson.version || '0.0.0',
description: packageJson.description,
author: packageJson.author,
};
}
}
//# sourceMappingURL=ProjectAnalyzer.js.map