nodejs-quickstart-structure 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +4 -3
  3. package/lib/generator.js +17 -3
  4. package/lib/modules/app-setup.js +111 -19
  5. package/lib/modules/caching-setup.js +13 -0
  6. package/lib/modules/config-files.js +25 -62
  7. package/lib/modules/database-setup.js +35 -30
  8. package/lib/modules/kafka-setup.js +78 -10
  9. package/package.json +1 -2
  10. package/templates/clean-architecture/js/src/errors/BadRequestError.js +1 -1
  11. package/templates/clean-architecture/js/src/errors/BadRequestError.spec.js.ejs +21 -0
  12. package/templates/clean-architecture/js/src/errors/NotFoundError.js +1 -1
  13. package/templates/clean-architecture/js/src/errors/NotFoundError.spec.js.ejs +21 -0
  14. package/templates/clean-architecture/js/src/infrastructure/log/logger.spec.js.ejs +63 -0
  15. package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +2 -3
  16. package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +81 -0
  17. package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +8 -4
  18. package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +102 -0
  19. package/templates/clean-architecture/js/src/interfaces/graphql/context.spec.js.ejs +31 -0
  20. package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.spec.js.ejs +49 -0
  21. package/templates/clean-architecture/js/src/interfaces/routes/api.spec.js.ejs +38 -0
  22. package/templates/clean-architecture/js/src/usecases/CreateUser.spec.js.ejs +51 -0
  23. package/templates/clean-architecture/js/src/usecases/GetAllUsers.spec.js.ejs +61 -0
  24. package/templates/clean-architecture/ts/src/errors/BadRequestError.spec.ts.ejs +21 -0
  25. package/templates/clean-architecture/ts/src/errors/BadRequestError.ts +1 -1
  26. package/templates/clean-architecture/ts/src/errors/NotFoundError.spec.ts.ejs +21 -0
  27. package/templates/clean-architecture/ts/src/errors/NotFoundError.ts +1 -1
  28. package/templates/clean-architecture/ts/src/infrastructure/log/logger.spec.ts.ejs +64 -0
  29. package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +85 -0
  30. package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.ts.ejs +2 -3
  31. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +166 -0
  32. package/templates/clean-architecture/ts/src/interfaces/graphql/context.spec.ts.ejs +32 -0
  33. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.spec.ts.ejs +51 -0
  34. package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.spec.ts.ejs +40 -0
  35. package/templates/clean-architecture/ts/src/usecases/createUser.spec.ts.ejs +51 -0
  36. package/templates/clean-architecture/ts/src/usecases/getAllUsers.spec.ts.ejs +63 -0
  37. package/templates/clean-architecture/ts/src/utils/errorMiddleware.ts.ejs +1 -2
  38. package/templates/common/caching/js/memoryCache.spec.js.ejs +101 -0
  39. package/templates/common/caching/js/redisClient.spec.js.ejs +149 -0
  40. package/templates/common/caching/ts/memoryCache.spec.ts.ejs +102 -0
  41. package/templates/common/caching/ts/redisClient.spec.ts.ejs +157 -0
  42. package/templates/common/database/js/database.spec.js.ejs +56 -0
  43. package/templates/common/database/js/models/User.js.ejs +22 -0
  44. package/templates/common/database/js/models/User.spec.js.ejs +84 -0
  45. package/templates/common/database/js/mongoose.spec.js.ejs +43 -0
  46. package/templates/common/database/ts/database.spec.ts.ejs +56 -0
  47. package/templates/common/database/ts/models/User.spec.ts.ejs +84 -0
  48. package/templates/common/database/ts/models/User.ts.ejs +26 -0
  49. package/templates/common/database/ts/mongoose.spec.ts.ejs +42 -0
  50. package/templates/common/eslint.config.mjs.ejs +11 -2
  51. package/templates/common/health/js/healthRoute.spec.js.ejs +70 -0
  52. package/templates/common/health/ts/healthRoute.spec.ts.ejs +76 -0
  53. package/templates/common/jest.config.js.ejs +19 -5
  54. package/templates/common/kafka/js/config/kafka.spec.js.ejs +21 -0
  55. package/templates/common/kafka/js/services/kafkaService.js.ejs +9 -5
  56. package/templates/common/kafka/js/services/kafkaService.spec.js.ejs +60 -0
  57. package/templates/common/kafka/ts/config/kafka.spec.ts.ejs +21 -0
  58. package/templates/common/kafka/ts/services/kafkaService.spec.ts.ejs +61 -0
  59. package/templates/common/kafka/ts/services/kafkaService.ts.ejs +1 -1
  60. package/templates/common/package.json.ejs +0 -3
  61. package/templates/common/shutdown/js/gracefulShutdown.spec.js.ejs +160 -0
  62. package/templates/common/shutdown/ts/gracefulShutdown.spec.ts.ejs +158 -0
  63. package/templates/common/src/utils/errorMiddleware.spec.js.ejs +79 -0
  64. package/templates/common/src/utils/errorMiddleware.spec.ts.ejs +94 -0
  65. package/templates/common/tsconfig.json +1 -1
  66. package/templates/mvc/js/src/controllers/userController.js.ejs +4 -31
  67. package/templates/mvc/js/src/controllers/userController.spec.js.ejs +170 -0
  68. package/templates/mvc/js/src/errors/BadRequestError.js +1 -1
  69. package/templates/mvc/js/src/errors/BadRequestError.spec.js.ejs +21 -0
  70. package/templates/mvc/js/src/errors/NotFoundError.js +1 -1
  71. package/templates/mvc/js/src/errors/NotFoundError.spec.js.ejs +21 -0
  72. package/templates/mvc/js/src/graphql/context.spec.js.ejs +29 -0
  73. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.spec.js.ejs +47 -0
  74. package/templates/mvc/js/src/index.js.ejs +1 -1
  75. package/templates/mvc/js/src/routes/api.spec.js.ejs +36 -0
  76. package/templates/mvc/js/src/utils/logger.spec.js.ejs +63 -0
  77. package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +185 -0
  78. package/templates/mvc/ts/src/controllers/userController.ts.ejs +4 -31
  79. package/templates/mvc/ts/src/errors/BadRequestError.spec.ts.ejs +21 -0
  80. package/templates/mvc/ts/src/errors/BadRequestError.ts +1 -1
  81. package/templates/mvc/ts/src/errors/NotFoundError.spec.ts.ejs +21 -0
  82. package/templates/mvc/ts/src/errors/NotFoundError.ts +1 -1
  83. package/templates/mvc/ts/src/graphql/context.spec.ts.ejs +30 -0
  84. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.spec.ts.ejs +51 -0
  85. package/templates/mvc/ts/src/routes/api.spec.ts.ejs +40 -0
  86. package/templates/mvc/ts/src/utils/errorMiddleware.ts.ejs +1 -2
  87. package/templates/mvc/ts/src/utils/logger.spec.ts.ejs +64 -0
  88. package/docs/demo.gif +0 -0
  89. package/docs/generateCase.md +0 -265
  90. package/docs/generatorFlow.md +0 -233
  91. package/docs/releaseNoteRule.md +0 -42
  92. package/docs/ruleDevelop.md +0 -30
  93. package/templates/common/tests/health.test.ts.ejs +0 -24
package/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.14.0] - 2026-03-09
9
+ ### Added
10
+ - **Unit test:**
11
+ - Unit Testing Framework: Integrated Jest and ts-jest as the core testing suite.
12
+
13
+ - Scaffolding Logic: Automatically generates .spec.ts files in the tests/ directory mirroring the src/ structure during project initialization.
14
+
15
+ - Quality Gates: Implemented a mandatory Coverage Threshold (>=70%) for lines and functions to ensure long-term maintainability.
16
+
17
+ - Mocking Standards: Included pre-configured mocks for Mongoose (database pings) and Express request/response objects.
18
+
19
+ - NPM Scripts: Added npm test, npm run test:watch, and npm run test:coverage for seamless developer workflow.
20
+
8
21
  ## [1.13.0] - 2026-03-05
9
22
  ### Added
10
23
  - **Enhanced Health Check System:**
package/README.md CHANGED
@@ -31,14 +31,14 @@ We don't just generate boilerplate; we generate **production-ready** foundations
31
31
  - **🔍 Code Quality**: Pre-configured `Eslint` and `Prettier` for consistent coding standards.
32
32
  - **🛡️ Security**: Built-in `Helmet`, `HPP`, `CORS`, and Rate-Limiting middleware.
33
33
  - **🚨 Error Handling**: Centralized global error middleware with custom error classes and structured JSON responses. GraphQL uses Apollo's `formatError` hook; REST uses Express error middleware.
34
- - **🧪 Testing Strategy**: Integrated `Jest` and `Supertest` setup for Unit and Integration testing.
34
+ - **🧪 Testing Excellence**: Integrated `Jest` and `Supertest`. Every generated project maintains **>70% Unit Test coverage** for controllers, services, and resolvers out of the box.
35
35
  - **🔄 CI/CD Integration**: Pre-configured workflows for **GitHub Actions**, **Jenkins**, and **GitLab CI**.
36
36
  - **⚓ Git Hooks**: `Husky` and `Lint-Staged` to ensure no bad code is ever committed.
37
37
  - **🤝 Reliability**: Advanced Health Checks (`/health`) with deep database pings and Graceful Shutdown workflows for zero-downtime rollouts.
38
38
  - **🐳 DevOps**: Highly optimized **Multi-Stage Dockerfile** for small, secure production images.
39
39
  - **🚀 Deployment**: Ship confidently with an integrated **PM2 Ecosystem Configuration** for zero-downtime reloads and robust process management.
40
40
 
41
- ## 🧩 240+ Project Combinations
41
+ ## 🧩 480+ Project Combinations
42
42
 
43
43
  The CLI supports a massive number of configurations to fit your exact needs:
44
44
 
@@ -46,7 +46,8 @@ The CLI supports a massive number of configurations to fit your exact needs:
46
46
  - **MVC Architecture**: 180 variants (Languages × View Engines × Databases × Communication Patterns × Caching)
47
47
  - **Clean Architecture**: 60 variants (Languages × Databases × Communication Patterns × Caching)
48
48
  - **480 Total Scenarios**:
49
- - Every combination can be generated with or without **GitHub Actions CI/CD**, doubling the possibilities.
49
+ - Every combination can be generated with or without (**GitHub Actions CI/CD** / **Jenkins** or **GitLab CI**), tripling the possibilities.
50
+ - Every single one of these 480 scenarios is verified to be compatible with our 70% Coverage Threshold policy.
50
51
 
51
52
  For a detailed list of all supported cases, check out [docs/generateCase.md](docs/generateCase.md).
52
53
 
package/lib/generator.js CHANGED
@@ -2,7 +2,7 @@ import path from 'path';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { setupProjectDirectory, copyBaseStructure, copyCommonFiles } from './modules/project-setup.js';
4
4
  import { renderPackageJson, renderDockerCompose, renderReadme, renderDockerfile, renderProfessionalConfig, setupCiCd, renderTestSample, renderEnvExample, renderPm2Config } from './modules/config-files.js';
5
- import { renderIndexFile, renderEnvConfig, renderErrorMiddleware, renderDynamicComponents, renderSwaggerConfig, setupViews as setupSrcViews } from './modules/app-setup.js';
5
+ import { renderIndexFile, renderEnvConfig, renderErrorMiddleware, renderDynamicComponents, renderSwaggerConfig, setupViews as setupSrcViews, processAllTests } from './modules/app-setup.js';
6
6
  import { setupDatabase } from './modules/database-setup.js';
7
7
  import { setupKafka, setupViews } from './modules/kafka-setup.js';
8
8
  import { setupCaching } from './modules/caching-setup.js';
@@ -11,6 +11,17 @@ const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = path.dirname(__filename);
12
12
 
13
13
  export const generateProject = async (config) => {
14
+ // 0. Normalize configuration with defaults
15
+ config = {
16
+ viewEngine: 'None',
17
+ caching: 'None',
18
+ dbName: 'demo',
19
+ ciProvider: 'None',
20
+ communication: 'REST APIs',
21
+ database: 'None',
22
+ ...config
23
+ };
24
+
14
25
  const { projectName, architecture, language } = config;
15
26
  const targetDir = path.resolve(process.cwd(), projectName);
16
27
  const templatesDir = path.join(__dirname, '../templates');
@@ -67,8 +78,8 @@ export const generateProject = async (config) => {
67
78
  await renderSwaggerConfig(templatesDir, targetDir, config);
68
79
 
69
80
  // 13. Professional Config & Tests
70
- await renderProfessionalConfig(templatesDir, targetDir, language);
71
- await renderTestSample(templatesDir, targetDir, language);
81
+ await renderProfessionalConfig(templatesDir, targetDir, config);
82
+ await renderTestSample(templatesDir, targetDir, config);
72
83
 
73
84
  // 14. CI/CD
74
85
  await setupCiCd(templatesDir, targetDir, config);
@@ -79,6 +90,9 @@ export const generateProject = async (config) => {
79
90
  // 16. PM2 Configuration
80
91
  await renderPm2Config(templatesDir, targetDir, config);
81
92
 
93
+ // 17. Process All Tests
94
+ await processAllTests(targetDir, config);
95
+
82
96
  console.log(`
83
97
  ====================================================
84
98
  Node.js Project Created Successfully!
@@ -10,14 +10,7 @@ export const renderIndexFile = async (templatePath, targetDir, config) => {
10
10
 
11
11
  if (await fs.pathExists(indexTemplateSource)) {
12
12
  const indexTemplate = await fs.readFile(indexTemplateSource, 'utf-8');
13
- const indexContent = ejs.render(indexTemplate, {
14
- communication,
15
- viewEngine,
16
- database,
17
- architecture,
18
- projectName,
19
- caching
20
- });
13
+ const indexContent = ejs.render(indexTemplate, { ...config });
21
14
  await fs.writeFile(indexPath, indexContent);
22
15
  await fs.remove(path.join(targetDir, 'src', `${indexFileName}.ejs`));
23
16
  }
@@ -37,11 +30,7 @@ export const renderEnvConfig = async (templatePath, targetDir, config) => {
37
30
 
38
31
  if (await fs.pathExists(envTemplatePath)) {
39
32
  const envTemplate = await fs.readFile(envTemplatePath, 'utf-8');
40
- const envContent = ejs.render(envTemplate, {
41
- database,
42
- caching,
43
- communication
44
- });
33
+ const envContent = ejs.render(envTemplate, { ...config });
45
34
  await fs.writeFile(envDestPath, envContent);
46
35
  await fs.remove(envTemplatePath);
47
36
  }
@@ -79,6 +68,16 @@ export const renderErrorMiddleware = async (templatePath, targetDir, config) =>
79
68
  await fs.remove(mwEjsCopy);
80
69
  }
81
70
  }
71
+
72
+ // Render errorMiddleware spec template
73
+ const specExt = language === 'TypeScript' ? 'ts' : 'js';
74
+ const specTemplatePath = path.join(templatePath, '../../common/src/utils', `errorMiddleware.spec.${specExt}.ejs`);
75
+ if (await fs.pathExists(specTemplatePath)) {
76
+ const testUtilsDir = path.join(targetDir, 'tests', 'utils');
77
+ await fs.ensureDir(testUtilsDir);
78
+ const specContent = ejs.render(await fs.readFile(specTemplatePath, 'utf-8'), config);
79
+ await fs.writeFile(path.join(testUtilsDir, `errorMiddleware.spec.${specExt}`), specContent);
80
+ }
82
81
  };
83
82
 
84
83
  export const renderDynamicComponents = async (templatePath, targetDir, config) => {
@@ -87,36 +86,71 @@ export const renderDynamicComponents = async (templatePath, targetDir, config) =
87
86
  // MVC Controller
88
87
  if (architecture === 'MVC') {
89
88
  const userControllerName = language === 'TypeScript' ? 'userController.ts' : 'userController.js';
89
+ const userControllerSpecName = language === 'TypeScript' ? 'userController.spec.ts' : 'userController.spec.js';
90
+
90
91
  const userControllerPath = path.join(targetDir, 'src/controllers', userControllerName);
92
+ const userControllerSpecPath = path.join(targetDir, 'tests/controllers', userControllerSpecName);
93
+
91
94
  const userControllerTemplate = path.join(templatePath, 'src/controllers', `${userControllerName}.ejs`);
95
+ const userControllerSpecTemplate = path.join(templatePath, 'src/controllers', `${userControllerSpecName}.ejs`);
92
96
 
93
97
  if (await fs.pathExists(userControllerTemplate)) {
94
- const content = ejs.render(await fs.readFile(userControllerTemplate, 'utf-8'), { database, caching, communication: config.communication });
98
+ const content = ejs.render(await fs.readFile(userControllerTemplate, 'utf-8'), { ...config });
95
99
  await fs.writeFile(userControllerPath, content);
96
100
  await fs.remove(path.join(targetDir, 'src/controllers', `${userControllerName}.ejs`));
97
101
  }
102
+
103
+ if (await fs.pathExists(userControllerSpecTemplate)) {
104
+ await fs.ensureDir(path.join(targetDir, 'tests/controllers'));
105
+ const content = ejs.render(await fs.readFile(userControllerSpecTemplate, 'utf-8'), { ...config });
106
+ await fs.writeFile(userControllerSpecPath, content);
107
+ await fs.remove(path.join(targetDir, 'src/controllers', `${userControllerSpecName}.ejs`));
108
+ }
98
109
  }
99
110
  // Clean Architecture Repo
100
111
  else if (architecture === 'Clean Architecture') {
101
112
  const repoName = language === 'TypeScript' ? 'UserRepository.ts' : 'UserRepository.js';
113
+ const repoSpecName = language === 'TypeScript' ? 'UserRepository.spec.ts' : 'UserRepository.spec.js';
114
+
102
115
  const repoPath = path.join(targetDir, 'src/infrastructure/repositories', repoName);
116
+ const repoSpecPath = path.join(targetDir, 'tests/infrastructure/repositories', repoSpecName);
117
+
103
118
  const repoTemplate = path.join(templatePath, 'src/infrastructure/repositories', `${repoName}.ejs`);
119
+ const repoSpecTemplate = path.join(templatePath, 'src/infrastructure/repositories', `${repoSpecName}.ejs`);
104
120
 
105
121
  if (await fs.pathExists(repoTemplate)) {
106
- const content = ejs.render(await fs.readFile(repoTemplate, 'utf-8'), { database });
122
+ const content = ejs.render(await fs.readFile(repoTemplate, 'utf-8'), { ...config });
107
123
  await fs.writeFile(repoPath, content);
108
124
  await fs.remove(path.join(targetDir, 'src/infrastructure/repositories', `${repoName}.ejs`));
109
125
  }
126
+ if (await fs.pathExists(repoSpecTemplate)) {
127
+ await fs.ensureDir(path.join(targetDir, 'tests/infrastructure/repositories'));
128
+ const content = ejs.render(await fs.readFile(repoSpecTemplate, 'utf-8'), { ...config });
129
+ await fs.writeFile(repoSpecPath, content);
130
+ await fs.remove(path.join(targetDir, 'src/infrastructure/repositories', `${repoSpecName}.ejs`));
131
+ }
110
132
 
111
133
  const controllerName = language === 'TypeScript' ? 'userController.ts' : 'userController.js';
134
+ const controllerSpecName = language === 'TypeScript' ? 'userController.spec.ts' : 'userController.spec.js';
135
+
112
136
  const controllerPath = path.join(targetDir, 'src/interfaces/controllers', controllerName);
137
+ const controllerSpecPath = path.join(targetDir, 'tests/interfaces/controllers', controllerSpecName);
138
+
113
139
  const controllerTemplate = path.join(templatePath, 'src/interfaces/controllers', `${controllerName}.ejs`);
140
+ const controllerSpecTemplate = path.join(templatePath, 'src/interfaces/controllers', `${controllerSpecName}.ejs`);
114
141
 
115
142
  if (await fs.pathExists(controllerTemplate)) {
116
- const content = ejs.render(await fs.readFile(controllerTemplate, 'utf-8'), { communication: config.communication });
143
+ const content = ejs.render(await fs.readFile(controllerTemplate, 'utf-8'), { ...config });
117
144
  await fs.writeFile(controllerPath, content);
118
145
  await fs.remove(path.join(targetDir, 'src/interfaces/controllers', `${controllerName}.ejs`));
119
146
  }
147
+
148
+ if (await fs.pathExists(controllerSpecTemplate)) {
149
+ await fs.ensureDir(path.join(targetDir, 'tests/interfaces/controllers'));
150
+ const content = ejs.render(await fs.readFile(controllerSpecTemplate, 'utf-8'), { ...config });
151
+ await fs.writeFile(controllerSpecPath, content);
152
+ await fs.remove(path.join(targetDir, 'src/interfaces/controllers', `${controllerSpecName}.ejs`));
153
+ }
120
154
  }
121
155
  // Render Server (Clean Arch JS only)
122
156
  if (architecture === 'Clean Architecture' && language === 'JavaScript') {
@@ -125,7 +159,7 @@ export const renderDynamicComponents = async (templatePath, targetDir, config) =
125
159
  const serverTemplate = path.join(templatePath, 'src/infrastructure/webserver', `${serverName}.ejs`);
126
160
 
127
161
  if (await fs.pathExists(serverTemplate)) {
128
- const content = ejs.render(await fs.readFile(serverTemplate, 'utf-8'), { communication: config.communication, database, caching });
162
+ const content = ejs.render(await fs.readFile(serverTemplate, 'utf-8'), { ...config });
129
163
  await fs.writeFile(serverPath, content);
130
164
  await fs.remove(path.join(targetDir, 'src/infrastructure/webserver', `${serverName}.ejs`));
131
165
  }
@@ -151,8 +185,20 @@ export const renderDynamicComponents = async (templatePath, targetDir, config) =
151
185
  }
152
186
  await fs.ensureDir(routeDestDir);
153
187
 
154
- const healthRouteContent = ejs.render(await fs.readFile(healthTemplatePath, 'utf-8'), { database, architecture });
188
+ const healthRouteContent = ejs.render(await fs.readFile(healthTemplatePath, 'utf-8'), { ...config });
155
189
  await fs.writeFile(path.join(routeDestDir, `healthRoute.${healthExt}`), healthRouteContent);
190
+
191
+ // Render health route spec template
192
+ const healthSpecTemplatePath = path.join(templatePath, '../../common/health', healthExt, `healthRoute.spec.${healthExt}.ejs`);
193
+ if (await fs.pathExists(healthSpecTemplatePath)) {
194
+ let testRouteDestDir = path.join(targetDir, 'tests', 'routes');
195
+ if (architecture === 'Clean Architecture') {
196
+ testRouteDestDir = path.join(targetDir, 'tests', 'interfaces', 'routes');
197
+ }
198
+ await fs.ensureDir(testRouteDestDir);
199
+ const specContent = ejs.render(await fs.readFile(healthSpecTemplatePath, 'utf-8'), config);
200
+ await fs.writeFile(path.join(testRouteDestDir, `healthRoute.spec.${healthExt}`), specContent);
201
+ }
156
202
  }
157
203
 
158
204
  // Advanced Graceful Shutdown Modularization
@@ -163,8 +209,17 @@ export const renderDynamicComponents = async (templatePath, targetDir, config) =
163
209
  let utilsDestDir = path.join(targetDir, 'src', 'utils');
164
210
  await fs.ensureDir(utilsDestDir);
165
211
 
166
- const shutdownContent = ejs.render(await fs.readFile(shutdownTemplatePath, 'utf-8'), { database, architecture, caching, communication: config.communication });
212
+ const shutdownContent = ejs.render(await fs.readFile(shutdownTemplatePath, 'utf-8'), { ...config });
167
213
  await fs.writeFile(path.join(utilsDestDir, `gracefulShutdown.${shutdownExt}`), shutdownContent);
214
+
215
+ // Render graceful shutdown spec template
216
+ const shutdownSpecTemplatePath = path.join(templatePath, '../../common/shutdown', shutdownExt, `gracefulShutdown.spec.${shutdownExt}.ejs`);
217
+ if (await fs.pathExists(shutdownSpecTemplatePath)) {
218
+ const testUtilsDestDir = path.join(targetDir, 'tests', 'utils');
219
+ await fs.ensureDir(testUtilsDestDir);
220
+ const specContent = ejs.render(await fs.readFile(shutdownSpecTemplatePath, 'utf-8'), config);
221
+ await fs.writeFile(path.join(testUtilsDestDir, `gracefulShutdown.spec.${shutdownExt}`), specContent);
222
+ }
168
223
  }
169
224
 
170
225
  // GraphQL Setup
@@ -271,3 +326,40 @@ export const setupViews = async (templatesDir, targetDir, config) => {
271
326
  }
272
327
  }
273
328
  };
329
+
330
+ export const processAllTests = async (targetDir, config) => {
331
+ const srcDir = path.join(targetDir, 'src');
332
+ const testsDir = path.join(targetDir, 'tests');
333
+
334
+ const processDir = async (currentDir) => {
335
+ if (!(await fs.pathExists(currentDir))) return;
336
+ const items = await fs.readdir(currentDir);
337
+ for (const item of items) {
338
+
339
+ const itemPath = path.join(currentDir, item);
340
+ const stat = await fs.stat(itemPath);
341
+ if (stat.isDirectory()) {
342
+ await processDir(itemPath);
343
+ } else if (itemPath.endsWith('.spec.ts') ||
344
+ itemPath.endsWith('.spec.js') ||
345
+ itemPath.endsWith('.spec.ts.ejs') ||
346
+ itemPath.endsWith('.spec.js.ejs')) {
347
+ const relativePath = path.relative(srcDir, itemPath);
348
+
349
+ const cleanRelativePath = relativePath.replace(/\.ejs$/, '');
350
+
351
+ const targetTestPath = path.join(testsDir, cleanRelativePath);
352
+
353
+ await fs.ensureDir(path.dirname(targetTestPath));
354
+
355
+ const template = await fs.readFile(itemPath, 'utf-8');
356
+ const content = ejs.render(template, config);
357
+
358
+ await fs.writeFile(targetTestPath, content);
359
+ await fs.remove(itemPath);
360
+ }
361
+ }
362
+ };
363
+
364
+ await processDir(srcDir);
365
+ };
@@ -55,6 +55,19 @@ export const setupCaching = async (templatesDir, targetDir, config) => {
55
55
  const cacheTemplate = await fs.readFile(cacheSource, 'utf-8');
56
56
  const content = ejs.render(cacheTemplate, { loggerPath });
57
57
  await fs.writeFile(cacheTarget, content);
58
+
59
+ // Render Spec if exists
60
+ const specTemplateName = clientObj.replace(`.${langExt}`, `.spec.${langExt}.ejs`);
61
+ const specTemplateSource = path.join(templatesDir, 'common', 'caching', langExt, specTemplateName);
62
+ if (await fs.pathExists(specTemplateSource)) {
63
+ const specTemplate = await fs.readFile(specTemplateSource, 'utf-8');
64
+ const specLoggerPath = architecture === 'Clean Architecture' ? '@/infrastructure/log/logger' : '@/utils/logger';
65
+ const specRedisPath = architecture === 'Clean Architecture' ? '@/infrastructure/caching/redisClient' : '@/config/redisClient';
66
+ const specContent = ejs.render(specTemplate, { ...config, loggerPath: specLoggerPath, redisClientPath: specRedisPath });
67
+ const specTarget = cacheTarget.replace(`${path.sep}src${path.sep}`, `${path.sep}tests${path.sep}`).replace(`.${langExt}`, `.spec.${langExt}`);
68
+ await fs.ensureDir(path.dirname(specTarget));
69
+ await fs.writeFile(specTarget, specContent);
70
+ }
58
71
  }
59
72
  }
60
73
  };
@@ -3,127 +3,90 @@ import path from 'path';
3
3
  import ejs from 'ejs';
4
4
 
5
5
  export const renderPackageJson = async (templatesDir, targetDir, config) => {
6
- const { projectName, database, communication, language, viewEngine, caching } = config;
7
6
  const packageJsonPath = path.join(targetDir, 'package.json');
8
7
  const packageTemplate = await fs.readFile(path.join(templatesDir, 'common', 'package.json.ejs'), 'utf-8');
9
- const packageContent = ejs.render(packageTemplate, {
10
- projectName,
11
- database,
12
- communication,
13
- language,
14
- communication,
15
- language,
16
- viewEngine,
17
- caching
18
- });
8
+ const packageContent = ejs.render(packageTemplate, { ...config });
19
9
  await fs.writeFile(packageJsonPath, packageContent);
20
10
  };
21
11
 
22
12
  export const renderDockerCompose = async (templatesDir, targetDir, config) => {
23
- const { projectName, database, dbName, communication, caching } = config;
24
13
  const dockerComposePath = path.join(targetDir, 'docker-compose.yml');
25
14
  const dockerTemplate = await fs.readFile(path.join(templatesDir, 'common', 'docker-compose.yml.ejs'), 'utf-8');
26
- const dockerContent = ejs.render(dockerTemplate, {
27
- projectName,
28
- database,
29
- dbName,
30
- communication,
31
- caching
32
- });
15
+ const dockerContent = ejs.render(dockerTemplate, { ...config });
33
16
  await fs.writeFile(dockerComposePath, dockerContent);
34
17
  };
35
18
 
36
19
  export const renderReadme = async (templatesDir, targetDir, config) => {
37
- const { projectName, architecture, database, communication, language, ciProvider, caching } = config;
38
20
  const readmePath = path.join(targetDir, 'README.md');
39
21
  const readmeTemplate = await fs.readFile(path.join(templatesDir, 'common', 'README.md.ejs'), 'utf-8');
40
- const readmeContent = ejs.render(readmeTemplate, {
41
- projectName,
42
- architecture,
43
- database,
44
- communication,
45
- caching,
46
- language,
47
- ciProvider
48
- });
22
+ const readmeContent = ejs.render(readmeTemplate, { ...config });
49
23
  await fs.writeFile(readmePath, readmeContent);
50
24
  };
51
25
 
52
26
  export const renderDockerfile = async (templatesDir, targetDir, config) => {
53
- const { language, viewEngine } = config;
54
27
  const dockerfileTemplate = await fs.readFile(path.join(templatesDir, 'common', 'Dockerfile'), 'utf-8');
55
- const dockerfileContent = ejs.render(dockerfileTemplate, {
56
- language,
57
- viewEngine
58
- });
28
+ const dockerfileContent = ejs.render(dockerfileTemplate, { ...config });
59
29
  await fs.writeFile(path.join(targetDir, 'Dockerfile'), dockerfileContent);
60
30
  };
61
31
 
62
32
  export const renderPm2Config = async (templatesDir, targetDir, config) => {
63
- const { projectName, database, dbName, communication, language, caching } = config;
64
33
  const pm2ConfigPath = path.join(targetDir, 'ecosystem.config.js');
65
34
  const pm2Template = await fs.readFile(path.join(templatesDir, 'common', 'ecosystem.config.js.ejs'), 'utf-8');
66
- const pm2Content = ejs.render(pm2Template, {
67
- projectName,
68
- database,
69
- dbName,
70
- communication,
71
- language,
72
- caching
73
- });
35
+ const pm2Content = ejs.render(pm2Template, { ...config });
74
36
  await fs.writeFile(pm2ConfigPath, pm2Content);
75
37
  };
76
38
 
77
- export const renderProfessionalConfig = async (templatesDir, targetDir, language) => {
39
+ export const renderProfessionalConfig = async (templatesDir, targetDir, config) => {
78
40
  const eslintTemplate = await fs.readFile(path.join(templatesDir, 'common', 'eslint.config.mjs.ejs'), 'utf-8');
79
- const eslintContent = ejs.render(eslintTemplate, { language });
41
+ const eslintContent = ejs.render(eslintTemplate, { ...config });
80
42
  await fs.writeFile(path.join(targetDir, 'eslint.config.mjs'), eslintContent);
81
43
 
82
44
  await fs.copy(path.join(templatesDir, 'common', '.prettierrc'), path.join(targetDir, '.prettierrc'));
83
45
  await fs.copy(path.join(templatesDir, 'common', '.lintstagedrc'), path.join(targetDir, '.lintstagedrc'));
84
46
 
85
47
  const jestTemplate = await fs.readFile(path.join(templatesDir, 'common', 'jest.config.js.ejs'), 'utf-8');
86
- const jestContent = ejs.render(jestTemplate, { language });
48
+ const jestContent = ejs.render(jestTemplate, { ...config });
87
49
  await fs.writeFile(path.join(targetDir, 'jest.config.js'), jestContent);
88
50
  };
89
51
 
90
52
  export const setupCiCd = async (templatesDir, targetDir, config) => {
91
- const { ciProvider, projectName } = config;
53
+ const { ciProvider } = config;
92
54
  if (ciProvider === 'GitHub Actions') {
93
55
  await fs.ensureDir(path.join(targetDir, '.github/workflows'));
94
56
  await fs.copy(path.join(templatesDir, 'common', '_github/workflows/ci.yml'), path.join(targetDir, '.github/workflows/ci.yml'));
95
57
  } else if (ciProvider === 'Jenkins') {
96
58
  const jenkinsTemplate = await fs.readFile(path.join(templatesDir, 'common', 'Jenkinsfile.ejs'), 'utf-8');
97
- const jenkinsContent = ejs.render(jenkinsTemplate, { projectName });
59
+ const jenkinsContent = ejs.render(jenkinsTemplate, { ...config });
98
60
  await fs.writeFile(path.join(targetDir, 'Jenkinsfile'), jenkinsContent);
99
61
  } else if (ciProvider === 'GitLab CI') {
100
62
  const gitlabTemplate = await fs.readFile(path.join(templatesDir, 'common', '.gitlab-ci.yml.ejs'), 'utf-8');
101
- const gitlabContent = ejs.render(gitlabTemplate, { projectName });
63
+ const gitlabContent = ejs.render(gitlabTemplate, { ...config });
102
64
  await fs.writeFile(path.join(targetDir, '.gitlab-ci.yml'), gitlabContent);
103
65
  }
104
66
  };
105
67
 
106
- export const renderTestSample = async (templatesDir, targetDir, language) => {
68
+ export const renderTestSample = async (templatesDir, targetDir, config) => {
107
69
  await fs.ensureDir(path.join(targetDir, 'tests'));
108
- const healthTestTemplate = await fs.readFile(path.join(templatesDir, 'common', 'tests', 'health.test.ts.ejs'), 'utf-8');
109
- const healthTestContent = ejs.render(healthTestTemplate, { language });
110
- const testFileName = language === 'TypeScript' ? 'health.test.ts' : 'health.test.js';
111
- await fs.writeFile(path.join(targetDir, 'tests', testFileName), healthTestContent);
70
+ if (config.language === 'TypeScript') {
71
+ const testsTsConfig = {
72
+ "extends": "../tsconfig.json",
73
+ "compilerOptions": {
74
+ "types": ["jest", "node"],
75
+ "rootDir": "../tests",
76
+ "noEmit": true
77
+ },
78
+ "include": ["**/*.ts"]
79
+ };
80
+ await fs.writeFile(path.join(targetDir, 'tests', 'tsconfig.json'), JSON.stringify(testsTsConfig, null, 4));
81
+ }
112
82
  };
113
83
 
114
84
  export const renderEnvExample = async (templatesDir, targetDir, config) => {
115
- const { database, dbName, communication, projectName, caching } = config;
116
85
  const envExamplePath = path.join(targetDir, '.env.example');
117
86
  const envPath = path.join(targetDir, '.env');
118
87
  const envTemplate = await fs.readFile(path.join(templatesDir, 'common', '.env.example.ejs'), 'utf-8');
119
88
 
120
- const envContent = ejs.render(envTemplate, {
121
- database,
122
- dbName,
123
- communication,
124
- projectName,
125
- caching
126
- });
89
+ const envContent = ejs.render(envTemplate, { ...config });
127
90
 
128
91
  await fs.writeFile(envExamplePath, envContent);
129
92
  await fs.writeFile(envPath, envContent);
@@ -10,7 +10,7 @@ export const setupDatabase = async (templatesDir, targetDir, config) => {
10
10
  if (database === 'MongoDB') {
11
11
  // Copy migrate-mongo config
12
12
  const migrateConfigTemplate = await fs.readFile(path.join(templatesDir, 'common', 'migrate-mongo-config.js.ejs'), 'utf-8');
13
- const migrateConfigContent = ejs.render(migrateConfigTemplate, { dbName });
13
+ const migrateConfigContent = ejs.render(migrateConfigTemplate, { ...config });
14
14
  await fs.writeFile(path.join(targetDir, 'migrate-mongo-config.js'), migrateConfigContent);
15
15
 
16
16
  // Setup migrations directory
@@ -58,8 +58,19 @@ export const setupDatabase = async (templatesDir, targetDir, config) => {
58
58
 
59
59
  if (await fs.pathExists(dbConfigTemplateSource)) {
60
60
  const dbTemplate = await fs.readFile(dbConfigTemplateSource, 'utf-8');
61
- const dbContent = ejs.render(dbTemplate, { database, dbName, architecture });
61
+ const dbContent = ejs.render(dbTemplate, { ...config });
62
62
  await fs.writeFile(dbConfigTarget, dbContent);
63
+
64
+ // Render Spec
65
+ const specTemplateName = dbConfigFileName.replace(`.${langExt}`, `.spec.${langExt}.ejs`);
66
+ const specTemplateSource = path.join(templatesDir, 'common', 'database', langExt, specTemplateName);
67
+ if (await fs.pathExists(specTemplateSource)) {
68
+ const specTemplate = await fs.readFile(specTemplateSource, 'utf-8');
69
+ const specContent = ejs.render(specTemplate, { ...config });
70
+ const specTarget = dbConfigTarget.replace(`${path.sep}src${path.sep}`, `${path.sep}tests${path.sep}`).replace(`.${langExt}`, `.spec.${langExt}`);
71
+ await fs.ensureDir(path.dirname(specTarget));
72
+ await fs.writeFile(specTarget, specContent);
73
+ }
63
74
  }
64
75
  } else if (architecture === 'MVC') {
65
76
  // Even if DB is None, MVC needs src/config for other things (like swagger or general config)
@@ -75,37 +86,31 @@ export const generateModels = async (templatesDir, targetDir, config) => {
75
86
  const langExt = language === 'TypeScript' ? 'ts' : 'js';
76
87
  const modelFileName = language === 'TypeScript' ? 'User.ts' : 'User.js';
77
88
 
78
- if (database !== 'None') {
79
- const sourceModelName = database === 'MongoDB' ? `${modelFileName}.mongoose.ejs` : `${modelFileName}.ejs`;
80
- const modelTemplateSource = path.join(templatesDir, 'common', 'database', langExt, 'models', sourceModelName);
81
- let modelTarget;
82
-
83
- if (architecture === 'MVC') {
84
- await fs.ensureDir(path.join(targetDir, 'src/models'));
85
- modelTarget = path.join(targetDir, 'src/models', modelFileName);
86
- } else {
87
- await fs.ensureDir(path.join(targetDir, 'src/infrastructure/database/models'));
88
- modelTarget = path.join(targetDir, 'src/infrastructure/database/models', modelFileName);
89
- }
89
+ const sourceModelName = database === 'MongoDB' ? `${modelFileName}.mongoose.ejs` : `${modelFileName}.ejs`;
90
+ const modelTemplateSource = path.join(templatesDir, 'common', 'database', langExt, 'models', sourceModelName);
91
+ let modelTarget;
90
92
 
91
- if (await fs.pathExists(modelTemplateSource)) {
92
- const modelTemplate = await fs.readFile(modelTemplateSource, 'utf-8');
93
- const modelContent = ejs.render(modelTemplate, { architecture });
94
- await fs.writeFile(modelTarget, modelContent);
95
- }
93
+ if (architecture === 'MVC') {
94
+ await fs.ensureDir(path.join(targetDir, 'src/models'));
95
+ modelTarget = path.join(targetDir, 'src/models', modelFileName);
96
96
  } else {
97
- // Mock Models for None DB
98
- const mockModelContent = language === 'TypeScript'
99
- ? `export interface User { id: string; name: string; email: string; }\n\nexport default class UserModel {\n static mockData: User[] = [];\n}`
100
- : `class UserModel {\n static mockData = [];\n}\n\nmodule.exports = UserModel;`;
97
+ await fs.ensureDir(path.join(targetDir, 'src/infrastructure/database/models'));
98
+ modelTarget = path.join(targetDir, 'src/infrastructure/database/models', modelFileName);
99
+ }
101
100
 
102
- if (architecture === 'MVC') {
103
- await fs.ensureDir(path.join(targetDir, 'src/models'));
104
- await fs.writeFile(path.join(targetDir, 'src/models', modelFileName), mockModelContent);
105
- } else {
106
- // Clean Arch
107
- await fs.ensureDir(path.join(targetDir, 'src/infrastructure/database/models'));
108
- await fs.writeFile(path.join(targetDir, 'src/infrastructure/database/models', modelFileName), mockModelContent);
101
+ if (await fs.pathExists(modelTemplateSource)) {
102
+ const modelTemplate = await fs.readFile(modelTemplateSource, 'utf-8');
103
+ const modelContent = ejs.render(modelTemplate, { ...config });
104
+ await fs.writeFile(modelTarget, modelContent);
105
+
106
+ // Render Spec
107
+ const modelSpecTemplateSource = path.join(templatesDir, 'common', 'database', langExt, 'models', `User.spec.${langExt}.ejs`);
108
+ if (await fs.pathExists(modelSpecTemplateSource)) {
109
+ const modelSpecTemplate = await fs.readFile(modelSpecTemplateSource, 'utf-8');
110
+ const modelSpecContent = ejs.render(modelSpecTemplate, { ...config });
111
+ const modelSpecTarget = modelTarget.replace(`${path.sep}src${path.sep}`, `${path.sep}tests${path.sep}`).replace(`.${langExt}`, `.spec.${langExt}`);
112
+ await fs.ensureDir(path.dirname(modelSpecTarget));
113
+ await fs.writeFile(modelSpecTarget, modelSpecContent);
109
114
  }
110
115
  }
111
116
  };