UNPKG

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
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); } }