clean-gen
Version:
A cross-platform CLI tool to generate NestJS clean architecture modules
431 lines (383 loc) • 12.8 kB
JavaScript
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';
export class RepositoriesModule {}
`;
}
// Content generation methods
getDtoContent() {
return `
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class ${this.moduleNamePascal}Dto {
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';
export class ${this.moduleNamePascal}Controller {
constructor(private readonly ${this.moduleNameLower}Usecase: ${this.moduleNamePascal}Usecase) {}
findAll() {
return this.${this.moduleNameLower}Usecase.findAll();
}
create( ${this.moduleNameLower}Dto: ${this.moduleNamePascal}Dto) {
return this.${this.moduleNameLower}Usecase.create(${this.moduleNameLower}Dto);
}
}
`;
}
getEntityContent() {
return `
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
export class ${this.moduleNamePascal}Entity {
id: number;
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';
export class ${this.moduleNamePascal}RepositoryImpl implements ${this.moduleNamePascal}Repository {
constructor(
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';
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';
export class ${this.moduleNamePascal}Module {}
`;
}
getDefaultAppModuleContent(importLine, moduleEntry) {
return `
import { Module } from '@nestjs/common';
${importLine}
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);
}