UNPKG

clean-gen

Version:

A cross-platform CLI tool to generate NestJS clean architecture modules

431 lines (383 loc) 12.8 kB
#!/usr/bin/env node const fs = require("fs").promises; const path = require("path"); // Colors for output const COLORS = { GREEN: "\x1b[32m", YELLOW: "\x1b[33m", RED: "\x1b[31m", NC: "\x1b[0m", }; class ModuleGenerator { constructor(moduleNameLower, moduleNamePascal, moduleNameCapitalized) { this.moduleNameLower = moduleNameLower; this.moduleNamePascal = moduleNamePascal; this.moduleNameCapitalized = moduleNameCapitalized; this.rootDir = "src"; this.domainDir = path.join(this.rootDir, "domain"); this.infraDir = path.join(this.rootDir, "infrastructure"); this.useCasesDir = path.join(this.rootDir, "usecases"); } async createDir(dirPath) { try { await fs.mkdir(dirPath, { recursive: true }); console.log(`${COLORS.GREEN}✔ Created directory:${COLORS.NC} ${dirPath}`); } catch (error) { if (error.code === "EEXIST") { console.log( `${COLORS.YELLOW}⚠ Directory already exists:${COLORS.NC} ${dirPath}` ); } else { throw new Error( `Failed to create directory: ${dirPath} - ${error.message}` ); } } } async createFile(filePath, content) { try { if (!(await fs.stat(filePath).catch(() => false))) { await fs.writeFile(filePath, content.trim(), "utf8"); console.log(`${COLORS.GREEN}✔ Created file:${COLORS.NC} ${filePath}`); } else { console.log( `${COLORS.YELLOW}⚠ File already exists:${COLORS.NC} ${filePath}` ); } } catch (error) { throw new Error(`Failed to create file: ${filePath} - ${error.message}`); } } async setupDirectories() { const dirs = [ path.join(this.domainDir, "dtos"), path.join(this.domainDir, "repositories"), path.join(this.domainDir, "models"), path.join(this.infraDir, "controllers"), path.join(this.infraDir, "entities"), path.join(this.infraDir, "repositories", this.moduleNameLower), this.useCasesDir, ]; await Promise.all(dirs.map((dir) => this.createDir(dir))); } async generateFiles() { const files = [ { path: path.join( this.domainDir, "dtos", `${this.moduleNameLower}.dto.ts` ), content: this.getDtoContent(), }, { path: path.join( this.domainDir, "repositories", `${this.moduleNameLower}.interface.ts` ), content: this.getRepositoryInterfaceContent(), }, { path: path.join( this.domainDir, "models", `${this.moduleNameLower}.model.ts` ), content: this.getModelContent(), }, { path: path.join( this.infraDir, "repositories", this.moduleNameLower, `${this.moduleNameLower}.controller.ts` ), content: this.getControllerContent(), }, { path: path.join( this.infraDir, "entities", `${this.moduleNameLower}.entity.ts` ), content: this.getEntityContent(), }, { path: path.join( this.infraDir, "repositories", this.moduleNameLower, `${this.moduleNameLower}.repository.ts` ), content: this.getRepositoryImplContent(), }, { path: path.join(this.useCasesDir, `${this.moduleNameLower}.usecase.ts`), content: "", }, { path: path.join( this.infraDir, "repositories", this.moduleNameLower, `${this.moduleNameLower}.module.ts` ), content: this.getModuleContent(), }, { path: path.join( this.infraDir, "repositories", this.moduleNameLower, `${this.moduleNameLower}.repositories.ts` ), content: this.getModuleContent(), }, { path: path.join( this.infraDir, "repositories", `repositories.module.ts` ), content: this.getRepoModuleContent(), }, ]; await Promise.all( files.map((file) => this.createFile(file.path, file.content)) ); } async updateAppModule() { const appModulePath = path.join(this.rootDir, "app.module.ts"); const importLine = `import { ${this.moduleNamePascal}Module } from './infrastructure/${this.moduleNameLower}/${this.moduleNameLower}.module';`; const moduleEntry = `${this.moduleNamePascal}Module`; try { let content = await fs.readFile(appModulePath, "utf8").catch(() => ""); if (!content) { content = this.getDefaultAppModuleContent(importLine, moduleEntry); await this.createFile(appModulePath, content); return; } if (!content.includes(moduleEntry)) { const lines = content.split("\n"); const importIndex = lines.findIndex((line) => line.startsWith("import { Module }") ); lines.splice(importIndex + 1, 0, importLine); const importsIndex = lines.findIndex((line) => line.includes("imports: [") ); if (importsIndex !== -1) { const importLine = lines[importsIndex]; lines[importsIndex] = importLine.includes("[]") ? ` imports: [${moduleEntry}]` : importLine.replace("imports: [", `imports: [${moduleEntry}, `); } await fs.writeFile(appModulePath, lines.join("\n"), "utf8"); console.log(`${COLORS.GREEN}✔ Updated ${appModulePath}${COLORS.NC}`); } else { console.log( `${COLORS.YELLOW}⚠ Module already in ${appModulePath}${COLORS.NC}` ); } } catch (error) { throw new Error(`Failed to update app.module.ts: ${error.message}`); } } async execute() { try { console.log( `${COLORS.YELLOW}Generating module: ${this.moduleNameLower}${COLORS.NC}` ); await this.setupDirectories(); await this.generateFiles(); await this.updateAppModule(); console.log( `${COLORS.GREEN}✔ Module generation completed successfully${COLORS.NC}` ); } catch (error) { console.error(`${COLORS.RED}❌ Error:${COLORS.NC} ${error.message}`); process.exit(1); } } getRepoModuleContent() { return ` import { Module } from '@nestjs/common'; @Module({ imports: [], controllers: [], providers: [], exports: [], }) export class RepositoriesModule {} `; } // Content generation methods getDtoContent() { return ` import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString } from 'class-validator'; export class ${this.moduleNamePascal}Dto { @ApiProperty({ required: true }) @IsNotEmpty() @IsString() name: string; } `; } getRepositoryInterfaceContent() { return ` import { ${this.moduleNamePascal} } from '../models/${this.moduleNameLower}.model'; export interface ${this.moduleNamePascal}Repository { findAll(): Promise<${this.moduleNamePascal}[]>; findById(id: number): Promise<${this.moduleNamePascal} | null>; create(${this.moduleNameLower}: ${this.moduleNamePascal}): Promise<${this.moduleNamePascal}>; } `; } getModelContent() { return ` export class ${this.moduleNamePascal} { id: number; name: string; constructor(id: number, name: string) { this.id = id; this.name = name; } } `; } getControllerContent() { return ` import { Controller, Get, Post, Body } from '@nestjs/common'; import { ${this.moduleNamePascal}Usecase } from '../../usecases/${this.moduleNameLower}.usecase'; import { ${this.moduleNamePascal}Dto } from '../../domain/dtos/${this.moduleNameLower}.dto'; @Controller('${this.moduleNameLower}') export class ${this.moduleNamePascal}Controller { constructor(private readonly ${this.moduleNameLower}Usecase: ${this.moduleNamePascal}Usecase) {} @Get() findAll() { return this.${this.moduleNameLower}Usecase.findAll(); } @Post() create(@Body() ${this.moduleNameLower}Dto: ${this.moduleNamePascal}Dto) { return this.${this.moduleNameLower}Usecase.create(${this.moduleNameLower}Dto); } } `; } getEntityContent() { return ` import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; @Entity() export class ${this.moduleNamePascal}Entity { @PrimaryGeneratedColumn() id: number; @Column() name: string; } `; } getRepositoryImplContent() { return ` import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ${this.moduleNamePascal}Entity } from '../../entities/${this.moduleNameLower}.entity'; import { ${this.moduleNamePascal}Repository } from '../../domain/repositories/${this.moduleNameLower}.interface'; import { ${this.moduleNamePascal} } from '../../domain/models/${this.moduleNameLower}.model'; @Injectable() export class ${this.moduleNamePascal}RepositoryImpl implements ${this.moduleNamePascal}Repository { constructor( @InjectRepository(${this.moduleNamePascal}Entity) private readonly repository: Repository<${this.moduleNamePascal}Entity>, ) {} async findAll(): Promise<${this.moduleNamePascal}[]> { const entities = await this.repository.find(); return entities.map(entity => new ${this.moduleNamePascal}(entity.id, entity.name)); } async findById(id: number): Promise<${this.moduleNamePascal} | null> { const entity = await this.repository.findOne({ where: { id } }); return entity ? new ${this.moduleNamePascal}(entity.id, entity.name) : null; } async create(${this.moduleNameLower}: ${this.moduleNamePascal}): Promise<${this.moduleNamePascal}> { const entity = this.repository.create({ name: ${this.moduleNameLower}.name }); const savedEntity = await this.repository.save(entity); return new ${this.moduleNamePascal}(savedEntity.id, savedEntity.name); } } `; } getUsecaseContent() { return ` import { Injectable } from '@nestjs/common'; import { ${this.moduleNamePascal}Repository } from '../domain/repositories/${this.moduleNameLower}.interface'; import { ${this.moduleNamePascal} } from '../domain/models/${this.moduleNameLower}.model'; import { ${this.moduleNamePascal}Dto } from '../domain/dtos/${this.moduleNameLower}.dto'; @Injectable() export class ${this.moduleNamePascal}Usecase { constructor(private readonly ${this.moduleNameLower}Repository: ${this.moduleNamePascal}Repository) {} async findAll(): Promise<${this.moduleNamePascal}[]> { return this.${this.moduleNameLower}Repository.findAll(); } async create(${this.moduleNameLower}Dto: ${this.moduleNamePascal}Dto): Promise<${this.moduleNamePascal}> { const ${this.moduleNameLower} = new ${this.moduleNamePascal}(0, ${this.moduleNameLower}Dto.name); return this.${this.moduleNameLower}Repository.create(${this.moduleNameLower}); } } `; } getModuleContent() { return ` import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ${this.moduleNamePascal}Controller } from './${this.moduleNameLower}.controller'; import { ${this.moduleNamePascal}RepositoryImpl } from './${this.moduleNameLower}.repository'; import { ${this.moduleNamePascal}Usecase } from '../../usecases/${this.moduleNameLower}.usecase'; import { ${this.moduleNamePascal}Entity } from '../../entities/${this.moduleNameLower}.entity'; @Module({ imports: [TypeOrmModule.forFeature([${this.moduleNamePascal}Entity])], controllers: [${this.moduleNamePascal}Controller], providers: [${this.moduleNamePascal}RepositoryImpl, ${this.moduleNamePascal}Usecase], exports: [${this.moduleNamePascal}RepositoryImpl], }) export class ${this.moduleNamePascal}Module {} `; } getDefaultAppModuleContent(importLine, moduleEntry) { return ` import { Module } from '@nestjs/common'; ${importLine} @Module({ imports: [${moduleEntry}] }) export class AppModule {} `; } } module.exports = async function createModule( moduleNameLower, moduleNamePascal, moduleNameCapitalized ) { const generator = new ModuleGenerator( moduleNameLower, moduleNamePascal, moduleNameCapitalized ); await generator.execute(); }; // CLI execution if (require.main === module) { const args = process.argv.slice(2); if (args.length < 1) { console.error(`${COLORS.RED}❌ Please provide a module name${COLORS.NC}`); console.log(`Usage: node ${path.basename(__filename)} <module-name>`); process.exit(1); } const moduleNameLower = args[0].toLowerCase(); const moduleNamePascal = moduleNameLower.charAt(0).toUpperCase() + moduleNameLower.slice(1); const moduleNameCapitalized = moduleNameLower.toUpperCase(); createModule(moduleNameLower, moduleNamePascal, moduleNameCapitalized); }