From fd7c5861a1812cb679ae5ea22a80e5c374cdb13a Mon Sep 17 00:00:00 2001 From: michee Date: Mon, 21 Apr 2025 13:04:21 +0000 Subject: [PATCH] refactor: overhaul of app structure and generation logic --- src/utils/app-structure-generator.js | 470 +++++++++++++++++++-------- src/utils/setup.js | 101 +++--- 2 files changed, 388 insertions(+), 183 deletions(-) diff --git a/src/utils/app-structure-generator.js b/src/utils/app-structure-generator.js index 6f509eb..e5611b7 100644 --- a/src/utils/app-structure-generator.js +++ b/src/utils/app-structure-generator.js @@ -1,189 +1,373 @@ -import { promises as fs } from 'fs'; -import path from 'path'; -import chalk from 'chalk'; +import chalk from "chalk"; +import { promises as fs } from "fs"; +import path from "path"; const templates = { - controller: (appName) => `/* eslint-disable @typescript-eslint/no-unused-vars */ -import { Request, Response, NextFunction } from 'express'; - -export class ${convertToPascalCase(appName)}Controller {} + readme: (appName) => `# ${capitalize(appName)} Application +Generated with NTW CLI app generation tool`, + commonIndex: () => `// Common utilities and shared logic can be added here +export * from './utils'; `, - - model: (appName) => `import { BaseModel, createBaseSchema } from '@nodesandbox/repo-framework'; -import { I${convertToPascalCase(appName)}Model } from '../types'; - -const ${convertToConstantCase(appName)}_MODEL_NAME = '${convertToPascalCase(appName)}'; - -const ${convertToCamelCase(appName)}Schema = createBaseSchema( - { - name: { - type: String, - required: true, - }, - }, - { - modelName: ${convertToConstantCase(appName)}_MODEL_NAME, - }, -); - -const ${convertToPascalCase(appName)}Model = new BaseModel( - ${convertToConstantCase(appName)}_MODEL_NAME, - ${convertToCamelCase(appName)}Schema, -).getModel(); - -export { ${convertToPascalCase(appName)}Model }; + coreApiIndex: () => `export * from './controllers'; +export * from './dtos'; +export * from './middlewares'; +export * from './routes'; `, + coreApiControllersIndex: (appName) => `export * from './${convertToKebabCase( + appName + )}.controller'; +`, + controller: ( + appName + ) => `import { Request, Response, NextFunction } from 'express'; - repository: (appName) => `import { Model } from 'mongoose'; -import { I${convertToPascalCase(appName)}Model } from '../types'; -import { BaseRepository } from '@nodesandbox/repo-framework'; - -export class ${convertToPascalCase(appName)}Repository extends BaseRepository { - constructor(model: Model) { - super(model); +export class ${convertToPascalCase(appName)}Controller { + public static create(req: Request, res: Response, next: NextFunction) { + // Logic for creating an entity } } `, - - service: (appName) => `import { I${convertToPascalCase(appName)}Model } from '../types'; -import { ${convertToPascalCase(appName)}Repository } from '../repositories'; -import { ${convertToPascalCase(appName)}Model } from '../models'; -import { BaseService } from '@nodesandbox/repo-framework'; - -class ${convertToPascalCase(appName)}Service extends BaseService< - I${convertToPascalCase(appName)}Model, - ${convertToPascalCase(appName)}Repository -> { - constructor() { - const ${convertToCamelCase(appName)}Repo = new ${convertToPascalCase(appName)}Repository( - ${convertToPascalCase(appName)}Model - ); - super(${convertToCamelCase(appName)}Repo, true, [ - /*'attribute_to_populate'*/ - ]); // This will populate the entity field - this.allowedFilterFields = []; // To filter on these fields, we need to set this - this.searchFields = ['name']; // This will use the search keyword - - /** - * The allowedFilterFields and searchFields are used to filter and search on the entity fields. - * These declarations are there to ensure what fields are allowed to be used for filtering and searching. - * If you want to filter on a field that is not declared here, you can add it to the allowedFilterFields array. - * If you want to search on a field that is not declared here, you can add it to the searchFields array. - */ - } + coreApiDtosIndex: () => `export * from './request'; +export * from './response'; +`, + coreApiDtosRequestIndex: () => `export * from './create-${convertToKebabCase( + appName + )}.request'; +`, + createRequest: (appName) => `export interface Create${convertToPascalCase( + appName + )}Request { + name: string; + description?: string; } - -export default new ${convertToPascalCase(appName)}Service(); `, - - types: (appName) => `import { IBaseModel } from '@nodesandbox/repo-framework'; -import { Document } from 'mongoose'; - -export interface I${convertToPascalCase(appName)} { + coreApiDtosResponseIndex: () => `export * from './create-${convertToKebabCase( + appName + )}.response'; +`, + createResponse: (appName) => `export interface Create${convertToPascalCase( + appName + )}Response { + id: string; name: string; + description?: string; + createdAt: Date; } - -export interface I${convertToPascalCase(appName)}Model - extends I${convertToPascalCase(appName)}, - IBaseModel, - Document {} `, - + coreApiMiddlewaresIndex: () => `// Middleware logic can be added here +`, + coreApiRoutesIndex: (appName) => `export { default as ${convertToPascalCase( + appName + )}Routes } from './${convertToKebabCase(appName)}.routes'; +`, routes: (appName) => `import { Router } from 'express'; import { ${convertToPascalCase(appName)}Controller } from '../controllers'; const router = Router(); -// e.g. -// router.post('/', ${convertToPascalCase(appName)}Controller.yourFunction); +router.post('/${convertToKebabCase(appName)}', ${convertToPascalCase( + appName + )}Controller.create); export default router; `, - - folderIndex: (appName, folder) => { - const exports = { - controllers: `export * from './${convertToKebabCase(appName)}.controller';\n`, - models: `export * from './${convertToKebabCase(appName)}.model';\n`, - repositories: `export * from './${convertToKebabCase(appName)}.repo';\n`, - routes: `export { default as ${convertToPascalCase(appName)}Routes } from './${convertToKebabCase(appName)}.routes';\n`, - services: `export { default as ${convertToPascalCase(appName)}Service } from './${convertToKebabCase(appName)}.service';\n`, - types: `export * from './${convertToKebabCase(appName)}';\n`, - }; - return exports[folder] || ''; + coreBusinessIndex: () => `export * from './repositories'; +export * from './services'; +`, + coreBusinessRepositoriesIndex: ( + appName + ) => `export * from './${convertToKebabCase(appName)}.repo'; +`, + repo: (appName) => `// Repository logic for ${convertToPascalCase( + appName + )} entity +export class ${convertToPascalCase(appName)}Repository {} +`, + coreBusinessServicesIndex: ( + appName + ) => `export { default as ${convertToPascalCase( + appName + )}Service } from './${convertToKebabCase(appName)}.service'; +`, + service: (appName) => `// Service logic for ${convertToPascalCase( + appName + )} entity +export class ${convertToPascalCase(appName)}Service {} +`, + coreDomainIndex: () => `export * from './models'; +export * from './types'; +`, + coreDomainModelsIndex: (appName) => `export * from './${convertToKebabCase( + appName + )}.model'; +`, + model: (appName) => `// Mongoose model for ${convertToPascalCase( + appName + )} entity +export class ${convertToPascalCase(appName)}Model {} +`, + coreDomainTypesIndex: (appName) => `export * from './${convertToKebabCase( + appName + )}'; +`, + type: (appName) => `export interface ${convertToPascalCase(appName)} { + id: string; + name: string; + description?: string; + createdAt: Date; +} +`, + coreIndex: () => `export * from './api'; +export * from './business'; +export * from './domain'; +`, + docsSwagger: () => `{ + "openapi": "3.0.0", + "info": { + "title": "NTW API Documentation", + "version": "1.0.0" }, - - rootIndex: () => `/** - * The only thing that needs to be exported here is the router - */ - -export * from './routes'; + "paths": {} +} +`, + jestConfig: () => `export default { + preset: 'ts-jest', + testEnvironment: 'node', +}; +`, + testsPlaceholder: () => `describe('Placeholder Test Suite', () => { + it('should pass', () => { + expect(true).toBe(true); + }); +}); `, }; export async function generateAppStructure(options) { - const { appName, baseDir = 'apps', includeTests = true } = options; - const extension = 'ts'; + const { appName, baseDir = "apps", includeTests = true } = options; + const extension = "ts"; const appPath = path.join(process.cwd(), baseDir, appName); - const folders = [ - 'controllers', - 'middlewares', - 'models', - 'repositories', - 'routes', - 'services', - 'types', - 'validators', + const structure = [ + { + path: "common", + files: [{ name: `index.${extension}`, content: templates.commonIndex() }], + }, + { + path: "core/api", + files: [ + { name: `index.${extension}`, content: templates.coreApiIndex() }, + ], + children: [ + { + path: "controllers", + files: [ + { + name: `index.${extension}`, + content: templates.coreApiControllersIndex(appName), + }, + { + name: `${convertToKebabCase(appName)}.controller.${extension}`, + content: templates.controller(appName), + }, + ], + }, + { + path: "dtos", + files: [ + { + name: `index.${extension}`, + content: templates.coreApiDtosIndex(), + }, + ], + children: [ + { + path: "request", + files: [ + { + name: `index.${extension}`, + content: templates.coreApiDtosRequestIndex(), + }, + { + name: `create-todo.request.${extension}`, + content: templates.createRequest(), + }, + ], + }, + { + path: "response", + files: [ + { + name: `index.${extension}`, + content: templates.coreApiDtosResponseIndex(), + }, + { + name: `create-todo.response.${extension}`, + content: templates.createResponse(), + }, + ], + }, + ], + }, + { + path: "middlewares", + files: [ + { + name: `index.${extension}`, + content: templates.coreApiMiddlewaresIndex(), + }, + ], + }, + { + path: "routes", + files: [ + { + name: `index.${extension}`, + content: templates.coreApiRoutesIndex(appName), + }, + { + name: `${convertToKebabCase(appName)}.routes.${extension}`, + content: templates.routes(appName), + }, + ], + }, + ], + }, + { + path: "core/business", + files: [ + { name: `index.${extension}`, content: templates.coreBusinessIndex() }, + ], + children: [ + { + path: "repositories", + files: [ + { + name: `index.${extension}`, + content: templates.coreBusinessRepositoriesIndex(appName), + }, + { + name: `${convertToKebabCase(appName)}.repo.${extension}`, + content: templates.repo(), + }, + ], + }, + { + path: "services", + files: [ + { + name: `index.${extension}`, + content: templates.coreBusinessServicesIndex(appName), + }, + { + name: `${convertToKebabCase(appName)}.service.${extension}`, + content: templates.service(), + }, + ], + }, + ], + }, + { + path: "core/domain", + files: [ + { name: `index.${extension}`, content: templates.coreDomainIndex() }, + ], + children: [ + { + path: "models", + files: [ + { + name: `index.${extension}`, + content: templates.coreDomainModelsIndex(appName), + }, + { + name: `${convertToKebabCase(appName)}.model.${extension}`, + content: templates.model(), + }, + ], + }, + { + path: "types", + files: [ + { + name: `index.${extension}`, + content: templates.coreDomainTypesIndex(appName), + }, + { + name: `${convertToKebabCase(appName)}.${extension}`, + content: templates.type(), + }, + ], + }, + ], + }, + { + path: "docs", + files: [{ name: "swagger.json", content: templates.docsSwagger() }], + }, + { + path: "tests", + files: [ + { + name: `placeholder.spec.${extension}`, + content: templates.testsPlaceholder(), + }, + ], + children: [ + { path: "e2e", files: [{ name: "empty.txt", content: "" }] }, + { path: "integration", files: [{ name: "empty.txt", content: "" }] }, + { path: "unit", files: [{ name: "empty.txt", content: "" }] }, + ], + }, ]; try { await fs.mkdir(appPath, { recursive: true }); console.log(chalk.green(`āœ“ Created app directory: ${appPath}`)); - for (const folder of folders) { - const folderPath = path.join(appPath, folder); - await fs.mkdir(folderPath, { recursive: true }); - console.log(chalk.green(`āœ“ Created directory: ${folder}`)); - - switch (folder) { - case 'controllers': - await generateFile(folderPath, `${appName}.controller.${extension}`, templates.controller(appName)); - break; - case 'models': - await generateFile(folderPath, `${appName}.model.${extension}`, templates.model(appName)); - break; - case 'repositories': - await generateFile(folderPath, `${appName}.repo.${extension}`, templates.repository(appName)); - break; - case 'services': - await generateFile(folderPath, `${appName}.service.${extension}`, templates.service(appName)); - break; - case 'routes': - await generateFile(folderPath, `${appName}.routes.${extension}`, templates.routes(appName)); - break; - case 'types': - await generateFile(folderPath, `${appName}.${extension}`, templates.types(appName)); - break; - } - // Create index file for the folder - await generateFile(folderPath, `index.${extension}`, templates.folderIndex(appName, folder)); + for (const item of structure) { + await createFolderAndFiles(appPath, item); } // Create root index file for the application await generateFile(appPath, `index.${extension}`, templates.rootIndex()); - const readmePath = path.join(appPath, 'README.md'); - const readmeContent = `# ${capitalize(appName)} Application\n\nGenerated with NTW CLI app generation tool`; - await fs.writeFile(readmePath, readmeContent); - console.log(chalk.green('āœ“ Created README.md')); + // Jest Config + await generateFile( + appPath, + `jest.config.${extension}`, + templates.jestConfig() + ); + + await generateFile(appPath, "README.md", templates.readme(appName)); - console.log(chalk.blue('\nšŸŽ‰ Application structure generated successfully!')); + console.log( + chalk.blue("\nšŸŽ‰ Application structure generated successfully!") + ); } catch (error) { - console.error(chalk.red('Error generating application structure:'), error); + console.error(chalk.red("Error generating application structure:"), error); throw error; } } +async function createFolderAndFiles(basePath, item) { + const fullPath = path.join(basePath, item.path); + await fs.mkdir(fullPath, { recursive: true }); + console.log(chalk.green(`āœ“ Created directory: ${item.path}`)); + + if (item.files) { + for (const file of item.files) { + await generateFile(fullPath, file.name, file.content); + } + } + + if (item.children) { + for (const child of item.children) { + await createFolderAndFiles(fullPath, child); + } + } +} + async function generateFile(folderPath, filename, content) { const filePath = path.join(folderPath, filename); await fs.writeFile(filePath, content); @@ -207,9 +391,9 @@ function convertToCamelCase(str) { } function convertToConstantCase(str) { - return str.replace(/[-_]/g, '_').toUpperCase(); + return str.replace(/[-_]/g, "_").toUpperCase(); } function convertToKebabCase(str) { - return str.replace(/[_\s]/g, '-').toLowerCase(); + return str.replace(/[_\s]/g, "-").toLowerCase(); } diff --git a/src/utils/setup.js b/src/utils/setup.js index d03f1b1..467d72c 100644 --- a/src/utils/setup.js +++ b/src/utils/setup.js @@ -1,10 +1,10 @@ -import path from 'path'; -import simpleGit from 'simple-git'; -import { exec } from 'child_process'; -import fs, { writeFile } from 'fs'; -import chalk from 'chalk'; -import ora from 'ora'; -import { createConfigFile } from './config.js'; +import chalk from "chalk"; +import { exec } from "child_process"; +import fs, { writeFile } from "fs"; +import ora from "ora"; +import path from "path"; +import simpleGit from "simple-git"; +import { createConfigFile } from "./config.js"; const noDemoRouterIndexFileContent = `import { Router } from 'express'; import { DevRoutes } from 'modules/features/dev'; @@ -24,7 +24,7 @@ export class RouterModule { RouterModule.router.use('', DevRoutes); } } -` +`; const noDemoAppsIndexContent = `/** * Export all the apps routes here @@ -36,72 +36,93 @@ export function initializeProject(projectName, includeDemo, showTips) { const git = simpleGit(); const spinner = ora(`Setting up your project: ${projectName}...`).start(); - git.clone('https://github.com/fless-lab/ntw-init.git', projectPath) + git + .clone( + "https://github.com/fless-lab/Node-TypeScript-Wizard.git", + projectPath + ) .then(() => { - spinner.text = 'Configuring project...'; + spinner.text = "Configuring project..."; - fs.rmSync(path.join(projectPath, '.git'), { recursive: true, force: true }); + fs.rmSync(path.join(projectPath, ".git"), { + recursive: true, + force: true, + }); return simpleGit(projectPath).init(); }) .then(() => { - spinner.succeed('Project configured successfully.'); + spinner.succeed("Project configured successfully."); - const installSpinner = ora('Step 1/2: Installing project dependencies. This may take a few moments...').start(); + const installSpinner = ora( + "Step 1/2: Installing project dependencies. This may take a few moments..." + ).start(); - exec('npm install', { cwd: projectPath }, (err, stdout, stderr) => { + exec("npm install", { cwd: projectPath }, (err, stdout, stderr) => { if (err) { - installSpinner.fail('Failed to install dependencies.'); + installSpinner.fail("Failed to install dependencies."); console.error(chalk.red(`Error: ${stderr}`)); return; } - installSpinner.succeed('Step 1/2: All dependencies have been installed successfully.'); + installSpinner.succeed( + "Step 1/2: All dependencies have been installed successfully." + ); - installSpinner.start('Step 2/2: Finalizing project setup...'); + installSpinner.start("Step 2/2: Finalizing project setup..."); createConfigFile(projectPath, projectName); setTimeout(() => { - installSpinner.succeed('Step 2/2: Project setup completed.'); + installSpinner.succeed("Step 2/2: Project setup completed."); const newGit = simpleGit(projectPath); - newGit.add('.', () => { + newGit.add(".", () => { showTips(projectName); }); }, 1000); }); }) - .then (() => { - if(!includeDemo){ - const appsIndexPath = projectPath + '/src/apps/index.ts'; - fs.writeFile(appsIndexPath, noDemoAppsIndexContent, 'utf-8', (error) => { - if (error) { - console.error('Error writing file:', error); - return; + .then(() => { + if (!includeDemo) { + const appsIndexPath = projectPath + "/src/apps/index.ts"; + fs.writeFile( + appsIndexPath, + noDemoAppsIndexContent, + "utf-8", + (error) => { + if (error) { + console.error("Error writing file:", error); + return; + } } - }); - - const pathToDemo = projectPath + '/src/apps/demo'; + ); + + const pathToDemo = projectPath + "/src/apps/demo"; fs.rm(pathToDemo, { recursive: true, force: true }, (error) => { if (error) { - console.error('Error removing file:', error); + console.error("Error removing file:", error); return; } }); - - const routerIndexPath = projectPath + '/src/modules/router/index.ts'; - fs.writeFile(routerIndexPath, noDemoRouterIndexFileContent, 'utf-8', (error) => { - if (error) { - console.error('Error writing file:', error); - return; + + const routerIndexPath = projectPath + "/src/modules/router/index.ts"; + fs.writeFile( + routerIndexPath, + noDemoRouterIndexFileContent, + "utf-8", + (error) => { + if (error) { + console.error("Error writing file:", error); + return; + } } - }); + ); } }) .catch((err) => { - spinner.fail('Error during project setup.'); - console.error(chalk.red('Error:', err)); + spinner.fail("Error during project setup."); + console.error(chalk.red("Error:", err)); }); -} \ No newline at end of file +}