modurize
Version:
Intelligent CLI tool to scaffold dynamic, context-aware modules for Node.js apps with smart CRUD generation and database integration
238 lines (197 loc) • 7.61 kB
JavaScript
import fs from 'fs';
import path from 'path';
export class ProjectAnalyzer {
constructor(projectRoot = process.cwd()) {
this.projectRoot = projectRoot;
this.analysis = {};
}
async analyze() {
this.analysis = {
framework: await this.detectFramework(),
database: await this.detectDatabase(),
codeStyle: await this.detectCodeStyle(),
existingModules: await this.findExistingModules(),
patterns: await this.analyzePatterns(),
dependencies: await this.getDependencies()
};
return this.analysis;
}
async detectFramework() {
const packageJsonPath = path.join(this.projectRoot, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return 'express'; // Default
}
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
if (dependencies.fastify) return 'fastify';
if (dependencies.koa) return 'koa';
if (dependencies.express) return 'express';
return 'express'; // Default
} catch (error) {
return 'express';
}
}
async detectDatabase() {
const packageJsonPath = path.join(this.projectRoot, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return 'none';
}
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
if (dependencies.mongoose || dependencies['mongodb']) return 'mongodb';
if (dependencies.sequelize || dependencies.pg) return 'postgresql';
if (dependencies.mysql2 || dependencies.mysql) return 'mysql';
if (dependencies.sqlite3) return 'sqlite';
if (dependencies.prisma) return 'prisma';
return 'none';
} catch (error) {
return 'none';
}
}
async detectCodeStyle() {
const modulesDir = path.join(this.projectRoot, 'src/modules');
if (!fs.existsSync(modulesDir)) {
return { style: 'func', typescript: false };
}
try {
const files = fs.readdirSync(modulesDir);
let classCount = 0;
let funcCount = 0;
let tsCount = 0;
let jsCount = 0;
for (const file of files) {
const filePath = path.join(modulesDir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
const moduleFiles = fs.readdirSync(filePath);
for (const moduleFile of moduleFiles) {
if (moduleFile.endsWith('.ts')) tsCount++;
if (moduleFile.endsWith('.js')) jsCount++;
if (moduleFile.includes('controller') || moduleFile.includes('service')) {
const content = fs.readFileSync(path.join(filePath, moduleFile), 'utf8');
if (content.includes('export class')) classCount++;
if (content.includes('export function') || content.includes('export const')) funcCount++;
}
}
}
}
return {
style: classCount > funcCount ? 'class' : 'func',
typescript: tsCount > jsCount
};
} catch (error) {
return { style: 'func', typescript: false };
}
}
async findExistingModules() {
const modulesDir = path.join(this.projectRoot, 'src/modules');
if (!fs.existsSync(modulesDir)) {
return [];
}
try {
const modules = fs.readdirSync(modulesDir)
.filter(item => {
const itemPath = path.join(modulesDir, item);
return fs.statSync(itemPath).isDirectory();
})
.map(moduleName => {
const modulePath = path.join(modulesDir, moduleName);
const files = fs.readdirSync(modulePath);
return {
name: moduleName,
files: files,
hasController: files.some(f => f.includes('controller')),
hasService: files.some(f => f.includes('service')),
hasModel: files.some(f => f.includes('model')),
hasRoutes: files.some(f => f.includes('routes')),
hasMiddleware: files.some(f => f.includes('middleware')),
hasValidator: files.some(f => f.includes('validator')),
hasTests: files.some(f => f.includes('test'))
};
});
return modules;
} catch (error) {
return [];
}
}
async analyzePatterns() {
const existingModules = await this.findExistingModules();
const patterns = {
naming: {},
structure: {},
imports: {},
exports: {}
};
if (existingModules.length === 0) {
return patterns;
}
// Analyze naming patterns
const moduleNames = existingModules.map(m => m.name);
patterns.naming = {
case: this.detectNamingCase(moduleNames),
pluralization: this.detectPluralization(moduleNames)
};
// Analyze file structure patterns
const fileStructures = existingModules.map(m => m.files);
patterns.structure = {
fileExtensions: this.detectFileExtensions(fileStructures),
fileNaming: this.detectFileNaming(fileStructures)
};
return patterns;
}
detectNamingCase(names) {
if (names.every(name => /^[a-z][a-z0-9-]*$/.test(name))) return 'kebab';
if (names.every(name => /^[a-z][a-zA-Z0-9]*$/.test(name))) return 'camel';
if (names.every(name => /^[A-Z][a-zA-Z0-9]*$/.test(name))) return 'pascal';
return 'kebab'; // Default
}
detectPluralization(names) {
const pluralEndings = ['s', 'es', 'ies'];
const hasPlural = names.some(name => pluralEndings.some(ending => name.endsWith(ending)));
return hasPlural ? 'plural' : 'singular';
}
detectFileExtensions(fileStructures) {
const allFiles = fileStructures.flat();
const tsFiles = allFiles.filter(f => f.endsWith('.ts')).length;
const jsFiles = allFiles.filter(f => f.endsWith('.js')).length;
return tsFiles > jsFiles ? 'ts' : 'js';
}
detectFileNaming(fileStructures) {
const allFiles = fileStructures.flat();
const hasHyphens = allFiles.some(f => f.includes('-'));
const hasUnderscores = allFiles.some(f => f.includes('_'));
if (hasHyphens) return 'kebab';
if (hasUnderscores) return 'snake';
return 'camel';
}
async getDependencies() {
const packageJsonPath = path.join(this.projectRoot, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return {};
}
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
return { ...packageJson.dependencies, ...packageJson.devDependencies };
} catch (error) {
return {};
}
}
getRecommendations() {
const { framework, database, codeStyle, patterns } = this.analysis;
return {
useTypeScript: codeStyle.typescript,
useClass: codeStyle.style === 'class',
includeTests: this.shouldIncludeTests(),
namingConvention: patterns.naming?.case || 'kebab',
fileExtension: patterns.structure?.fileExtensions || 'js',
databaseIntegration: database !== 'none',
frameworkSpecific: framework !== 'express'
};
}
shouldIncludeTests() {
const dependencies = this.analysis.dependencies;
return !!(dependencies.jest || dependencies.mocha || dependencies.vitest);
}
}