nodejs-quickstart-structure 1.14.0 → 1.16.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.
- package/CHANGELOG.md +32 -0
- package/bin/index.js +84 -80
- package/lib/generator.js +11 -1
- package/lib/modules/app-setup.js +3 -3
- package/lib/modules/config-files.js +27 -2
- package/lib/modules/kafka-setup.js +70 -24
- package/package.json +8 -3
- package/templates/clean-architecture/js/src/index.js.ejs +1 -3
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +11 -10
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +16 -1
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +36 -0
- package/templates/clean-architecture/ts/src/config/swagger.ts.ejs +1 -1
- package/templates/clean-architecture/ts/src/index.ts.ejs +12 -14
- package/templates/clean-architecture/ts/src/infrastructure/log/logger.spec.ts.ejs +0 -1
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +19 -0
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts.ejs +16 -0
- package/templates/common/.cursorrules.ejs +60 -0
- package/templates/common/.dockerignore +2 -0
- package/templates/common/.gitlab-ci.yml.ejs +5 -5
- package/templates/common/Dockerfile +2 -0
- package/templates/common/Jenkinsfile.ejs +1 -1
- package/templates/common/README.md.ejs +34 -1
- package/templates/common/_github/workflows/ci.yml +7 -4
- package/templates/common/database/js/models/User.js.ejs +2 -1
- package/templates/common/database/ts/models/User.ts.ejs +4 -3
- package/templates/common/eslint.config.mjs.ejs +30 -3
- package/templates/common/jest.config.js.ejs +4 -1
- package/templates/common/kafka/js/messaging/baseConsumer.js.ejs +30 -0
- package/templates/common/kafka/js/messaging/baseConsumer.spec.js.ejs +58 -0
- package/templates/common/kafka/js/messaging/userEventSchema.js.ejs +11 -0
- package/templates/common/kafka/js/messaging/userEventSchema.spec.js.ejs +27 -0
- package/templates/common/kafka/js/messaging/welcomeEmailConsumer.js.ejs +31 -0
- package/templates/common/kafka/js/messaging/welcomeEmailConsumer.spec.js.ejs +49 -0
- package/templates/common/kafka/js/services/kafkaService.js.ejs +75 -23
- package/templates/common/kafka/js/services/kafkaService.spec.js.ejs +53 -7
- package/templates/common/kafka/ts/messaging/baseConsumer.spec.ts.ejs +50 -0
- package/templates/common/kafka/ts/messaging/baseConsumer.ts.ejs +27 -0
- package/templates/common/kafka/ts/messaging/userEventSchema.spec.ts.ejs +51 -0
- package/templates/common/kafka/ts/messaging/userEventSchema.ts.ejs +11 -0
- package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.spec.ts.ejs +49 -0
- package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.ts.ejs +25 -0
- package/templates/common/kafka/ts/services/kafkaService.spec.ts.ejs +22 -2
- package/templates/common/kafka/ts/services/kafkaService.ts.ejs +72 -12
- package/templates/common/package.json.ejs +6 -4
- package/templates/common/prompts/add-feature.md.ejs +26 -0
- package/templates/common/prompts/project-context.md.ejs +43 -0
- package/templates/common/prompts/troubleshoot.md.ejs +28 -0
- package/templates/mvc/js/src/controllers/userController.js.ejs +14 -0
- package/templates/mvc/js/src/controllers/userController.spec.js.ejs +39 -0
- package/templates/mvc/js/src/index.js.ejs +12 -11
- package/templates/mvc/ts/src/config/swagger.ts.ejs +1 -1
- package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +18 -0
- package/templates/mvc/ts/src/controllers/userController.ts.ejs +16 -0
- package/templates/mvc/ts/src/index.ts.ejs +13 -16
- package/templates/mvc/ts/src/utils/logger.spec.ts.ejs +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ 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.16.0] - 2026-03-14
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Robust Kafka Singleton Implementation**: Refactored `KafkaService` to a strict singleton pattern across all architectures with `connectionPromise` and automated retry logic for resilient messaging.
|
|
12
|
+
- **BaseConsumer Standards**: Implemented constructor guards in `BaseConsumer` to prevent direct instantiation of abstract messaging classes.
|
|
13
|
+
- **Professional Docker Log Hygiene**: Added `NPM_CONFIG_UPDATE_NOTIFIER=false` to both builder and production stages to suppress non-essential npm upgrade notifications.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Docker Build & Type Safety**: Resolved a critical build failure in MVC TypeScript projects using EJS/Pug by addressing missing Express `Request` and `Response` type imports.
|
|
17
|
+
- **Network Resilience**: Removed redundant `npm install -g npm@latest` from the `Dockerfile` template to fix `ECONNRESET` failures during project verification on unstable networks.
|
|
18
|
+
- **Controller Testing Modernization**: Refactored `userController` spec templates (TS and JS) to correctly mock and verify the shared Kafka singleton, fixing persistent unit test failures.
|
|
19
|
+
- **Database Mocking Refinement**: Resolved a data flow bug in the "None" database mock where generated IDs were being overwritten, and enhanced TypeScript types to eliminate `any` in repository patterns.
|
|
20
|
+
|
|
21
|
+
## [1.15.1] - 2026-03-12
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **Magic AI Scaffolding**: Automated AI context generation ( `.cursorrules`, `prompts/`) by intelligently inferring project goals from the project name, removing the need for manual business domain prompts.
|
|
25
|
+
- **Enhanced README**: Added specialized guidance for AI-native development with Cursor and LLMs.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **GitHub Actions Compliance**: Fully resolved Node.js 20 deprecation warnings by moving `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24` to global workflow scope.
|
|
29
|
+
- **CI Modernization**: Upgraded daily audit and GitLab CI runners to Node.js 22.
|
|
30
|
+
- **CI Enforcement**: Updated all CI/CD templates (GitHub, GitLab, Jenkins) to strictly enforce the >70% test coverage gate.
|
|
31
|
+
|
|
32
|
+
## [1.15.0] - 2026-03-12
|
|
33
|
+
### Added
|
|
34
|
+
- **AI-Native Scaffolding & Agent Skill Templates:**
|
|
35
|
+
- Added a new CLI prompt for `businessDomain` to inject custom domain knowledge into generated templates.
|
|
36
|
+
- Generates a `.cursorrules` file at the root to enforce >70% Test coverage and Architecture patterns automatically for AI Code Editors.
|
|
37
|
+
- Scaffolds a `prompts/` directory with specialized Agent Skill templates (`project-context.md`, `add-feature.md`, `troubleshoot.md`) designed to provide deep structural understanding to LLMs like ChatGPT or Claude.
|
|
38
|
+
- Added an "AI-Native Development" section to the generated `README.md` and prominent print messages in the CLI upon successful completion.
|
|
39
|
+
|
|
8
40
|
## [1.14.0] - 2026-03-09
|
|
9
41
|
### Added
|
|
10
42
|
- **Unit test:**
|
package/bin/index.js
CHANGED
|
@@ -1,81 +1,85 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from 'commander';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { getProjectDetails } from '../lib/prompts.js';
|
|
6
|
-
import { generateProject } from '../lib/generator.js';
|
|
7
|
-
import { readFileSync } from 'fs';
|
|
8
|
-
import { join, dirname } from 'path';
|
|
9
|
-
import { fileURLToPath } from 'url';
|
|
10
|
-
|
|
11
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
|
13
|
-
|
|
14
|
-
const program = new Command();
|
|
15
|
-
|
|
16
|
-
program
|
|
17
|
-
.name('nodejs-quickstart')
|
|
18
|
-
.description('🚀 CLI to scaffold production-ready Node.js microservices.\n\nGenerates projects with:\n- MVC or Clean Architecture\n- REST or Kafka\n- MySQL, PostgreSQL, or MongoDB\n- Docker, Flyway & Mongoose support')
|
|
19
|
-
.version(pkg.version, '-v, --version', 'Output the current version')
|
|
20
|
-
.addHelpText('after', `
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.
|
|
27
|
-
.
|
|
28
|
-
.option('
|
|
29
|
-
.option('-
|
|
30
|
-
.option('-
|
|
31
|
-
.option('
|
|
32
|
-
.option('-
|
|
33
|
-
.option('--
|
|
34
|
-
.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
console.log(chalk.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
console.log(chalk.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
console.log(chalk.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { getProjectDetails } from '../lib/prompts.js';
|
|
6
|
+
import { generateProject } from '../lib/generator.js';
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { join, dirname } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
|
13
|
+
|
|
14
|
+
const program = new Command();
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.name('nodejs-quickstart')
|
|
18
|
+
.description('🚀 CLI to scaffold production-ready Node.js microservices.\n\nGenerates projects with:\n- MVC or Clean Architecture\n- REST or Kafka\n- MySQL, PostgreSQL, or MongoDB\n- Docker, Flyway & Mongoose support')
|
|
19
|
+
.version(pkg.version, '-v, --version', 'Output the current version')
|
|
20
|
+
.addHelpText('after', `\n${chalk.yellow('Example:')}\n $ nodejs-quickstart init ${chalk.gray('# Start the interactive setup')}\n`);
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.command('init')
|
|
24
|
+
.description('Initialize a new Node.js project')
|
|
25
|
+
.option('-n, --project-name <name>', 'Project name')
|
|
26
|
+
.option('-l, --language <language>', 'Language (JavaScript, TypeScript)')
|
|
27
|
+
.option('-a, --architecture <architecture>', 'Architecture (MVC, Clean Architecture)')
|
|
28
|
+
.option('--view-engine <view>', 'View Engine (None, EJS, Pug) - MVC only')
|
|
29
|
+
.option('-d, --database <database>', 'Database (MySQL, PostgreSQL)')
|
|
30
|
+
.option('--db-name <name>', 'Database name')
|
|
31
|
+
.option('-c, --communication <communication>', 'Communication (REST APIs, GraphQL, Kafka)')
|
|
32
|
+
.option('--ci-provider <provider>', 'CI/CD Provider (None, GitHub Actions, Jenkins)')
|
|
33
|
+
.option('--caching <type>', 'Caching Layer (None/Redis)')
|
|
34
|
+
.action(async (options) => {
|
|
35
|
+
// Fix for Commander camelCase conversion
|
|
36
|
+
if (options.ciProvider) {
|
|
37
|
+
options.ciProvider = options.ciProvider;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(chalk.blue('Welcome to the Node.js Quickstart Generator!'));
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const answers = await getProjectDetails(options);
|
|
44
|
+
console.log(chalk.green('\nConfiguration received:'));
|
|
45
|
+
console.log(JSON.stringify(answers, null, 2));
|
|
46
|
+
|
|
47
|
+
console.log(chalk.yellow('\nGenerating project...'));
|
|
48
|
+
await generateProject(answers);
|
|
49
|
+
|
|
50
|
+
console.log(chalk.green('\n✔ Project generated successfully!'));
|
|
51
|
+
|
|
52
|
+
console.log(chalk.magenta('\n🚀 Project is AI-Ready!'));
|
|
53
|
+
console.log(chalk.magenta('-----------------------------------------'));
|
|
54
|
+
console.log(chalk.magenta('🤖 We detected you are using AI tools.'));
|
|
55
|
+
console.log(chalk.magenta(`📍 Use Cursor? We've configured '.cursorrules' for you.`));
|
|
56
|
+
console.log(chalk.magenta(`📍 Use ChatGPT/Gemini? Check the 'prompts/' folder for Agent Skills.`));
|
|
57
|
+
console.log(chalk.magenta('-----------------------------------------'));
|
|
58
|
+
|
|
59
|
+
let manualStartInstructions = `\n${chalk.yellow('Development:')}\n cd ${answers.projectName}\n npm install`;
|
|
60
|
+
|
|
61
|
+
const needsInfrastructure = answers.database !== 'None' || answers.caching === 'Redis' || answers.communication === 'Kafka';
|
|
62
|
+
|
|
63
|
+
if (needsInfrastructure) {
|
|
64
|
+
let servicesToStart = '';
|
|
65
|
+
if (answers.database !== 'None') servicesToStart += ' db';
|
|
66
|
+
if (answers.caching === 'Redis') servicesToStart += ' redis';
|
|
67
|
+
if (answers.communication === 'Kafka') servicesToStart += ' zookeeper kafka';
|
|
68
|
+
|
|
69
|
+
manualStartInstructions += `\n docker-compose up -d${servicesToStart} # Start infrastructure first\n npm run dev`;
|
|
70
|
+
} else {
|
|
71
|
+
manualStartInstructions += `\n npm run dev`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(chalk.cyan(`\nNext steps:\n cd ${answers.projectName}\n npm install\n docker-compose up\n-----------------------${manualStartInstructions}\n\n${chalk.yellow('Production (PM2):')}\n npm run build\n npm run deploy\n npx pm2 logs`));
|
|
75
|
+
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error(chalk.red('Error generating project:'), error);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
program.parse(process.argv);
|
|
82
|
+
|
|
83
|
+
if (!process.argv.slice(2).length) {
|
|
84
|
+
program.outputHelp();
|
|
85
|
+
}
|
package/lib/generator.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import { setupProjectDirectory, copyBaseStructure, copyCommonFiles } from './modules/project-setup.js';
|
|
4
|
-
import { renderPackageJson, renderDockerCompose, renderReadme, renderDockerfile, renderProfessionalConfig, setupCiCd, renderTestSample, renderEnvExample, renderPm2Config } from './modules/config-files.js';
|
|
4
|
+
import { renderPackageJson, renderDockerCompose, renderReadme, renderDockerfile, renderProfessionalConfig, setupCiCd, renderTestSample, renderEnvExample, renderPm2Config, renderAiNativeFiles } from './modules/config-files.js';
|
|
5
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';
|
|
@@ -81,6 +81,9 @@ export const generateProject = async (config) => {
|
|
|
81
81
|
await renderProfessionalConfig(templatesDir, targetDir, config);
|
|
82
82
|
await renderTestSample(templatesDir, targetDir, config);
|
|
83
83
|
|
|
84
|
+
// 13.5 AI-Native Scaffolding
|
|
85
|
+
await renderAiNativeFiles(templatesDir, targetDir, config);
|
|
86
|
+
|
|
84
87
|
// 14. CI/CD
|
|
85
88
|
await setupCiCd(templatesDir, targetDir, config);
|
|
86
89
|
|
|
@@ -114,6 +117,13 @@ export const generateProject = async (config) => {
|
|
|
114
117
|
✅ Docker: Production-ready multi-stage build
|
|
115
118
|
${config.ciProvider !== 'None' ? `✅ CI/CD: ${config.ciProvider} Workflow ready` : '❌ CI/CD: Skipped (User preferred)'}
|
|
116
119
|
|
|
120
|
+
----------------------------------------------------
|
|
121
|
+
🚀 Project is AI-Ready!
|
|
122
|
+
----------------------------------------------------
|
|
123
|
+
🤖 We detected you are using AI tools.
|
|
124
|
+
📍 Use Cursor? We've configured '.cursorrules' for you.
|
|
125
|
+
📍 Use ChatGPT/Gemini? Check the 'prompts/' folder for Agent Skills.
|
|
126
|
+
|
|
117
127
|
----------------------------------------------------
|
|
118
128
|
👉 Next Steps:
|
|
119
129
|
----------------------------------------------------
|
package/lib/modules/app-setup.js
CHANGED
|
@@ -165,8 +165,8 @@ export const renderDynamicComponents = async (templatePath, targetDir, config) =
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
// Cleanup REST routes if
|
|
169
|
-
if (config.communication !== 'REST APIs') {
|
|
168
|
+
// Cleanup REST routes if neither REST APIs nor Kafka is selected
|
|
169
|
+
if (config.communication !== 'REST APIs' && config.communication !== 'Kafka') {
|
|
170
170
|
if (architecture === 'MVC') {
|
|
171
171
|
await fs.remove(path.join(targetDir, 'src/routes'));
|
|
172
172
|
} else if (architecture === 'Clean Architecture') {
|
|
@@ -280,7 +280,7 @@ export const renderSwaggerConfig = async (templatesDir, targetDir, config) => {
|
|
|
280
280
|
}
|
|
281
281
|
await fs.ensureDir(configDir);
|
|
282
282
|
|
|
283
|
-
if (communication === 'REST APIs') {
|
|
283
|
+
if (communication === 'REST APIs' || communication === 'Kafka') {
|
|
284
284
|
const swaggerYmlTemplateSource = path.join(templatesDir, 'common', 'swagger.yml.ejs');
|
|
285
285
|
if (await fs.pathExists(swaggerYmlTemplateSource)) {
|
|
286
286
|
const ymlContent = ejs.render(await fs.readFile(swaggerYmlTemplateSource, 'utf-8'), { projectName });
|
|
@@ -49,6 +49,31 @@ export const renderProfessionalConfig = async (templatesDir, targetDir, config)
|
|
|
49
49
|
await fs.writeFile(path.join(targetDir, 'jest.config.js'), jestContent);
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
export const renderAiNativeFiles = async (templatesDir, targetDir, config) => {
|
|
53
|
+
// 1. .cursorrules
|
|
54
|
+
const cursorRulesPath = path.join(targetDir, '.cursorrules');
|
|
55
|
+
const cursorRulesTemplate = await fs.readFile(path.join(templatesDir, 'common', '.cursorrules.ejs'), 'utf-8');
|
|
56
|
+
const cursorRulesContent = ejs.render(cursorRulesTemplate, { ...config });
|
|
57
|
+
await fs.writeFile(cursorRulesPath, cursorRulesContent);
|
|
58
|
+
|
|
59
|
+
// 2. prompts/
|
|
60
|
+
const promptsDirTarget = path.join(targetDir, 'prompts');
|
|
61
|
+
await fs.ensureDir(promptsDirTarget);
|
|
62
|
+
|
|
63
|
+
const promptsSourceDir = path.join(templatesDir, 'common', 'prompts');
|
|
64
|
+
const promptFiles = await fs.readdir(promptsSourceDir);
|
|
65
|
+
|
|
66
|
+
for (const file of promptFiles) {
|
|
67
|
+
if (file.endsWith('.ejs')) {
|
|
68
|
+
const templatePath = path.join(promptsSourceDir, file);
|
|
69
|
+
const targetFilePath = path.join(promptsDirTarget, file.replace('.ejs', ''));
|
|
70
|
+
const templateContent = await fs.readFile(templatePath, 'utf-8');
|
|
71
|
+
const renderedContent = ejs.render(templateContent, { ...config });
|
|
72
|
+
await fs.writeFile(targetFilePath, renderedContent);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
52
77
|
export const setupCiCd = async (templatesDir, targetDir, config) => {
|
|
53
78
|
const { ciProvider } = config;
|
|
54
79
|
if (ciProvider === 'GitHub Actions') {
|
|
@@ -72,10 +97,10 @@ export const renderTestSample = async (templatesDir, targetDir, config) => {
|
|
|
72
97
|
"extends": "../tsconfig.json",
|
|
73
98
|
"compilerOptions": {
|
|
74
99
|
"types": ["jest", "node"],
|
|
75
|
-
"rootDir": "
|
|
100
|
+
"rootDir": "..",
|
|
76
101
|
"noEmit": true
|
|
77
102
|
},
|
|
78
|
-
"include": ["**/*.ts"]
|
|
103
|
+
"include": ["**/*.ts", "../src/**/*.ts"]
|
|
79
104
|
};
|
|
80
105
|
await fs.writeFile(path.join(targetDir, 'tests', 'tsconfig.json'), JSON.stringify(testsTsConfig, null, 4));
|
|
81
106
|
}
|
|
@@ -8,7 +8,14 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
|
|
|
8
8
|
|
|
9
9
|
const langExt = language === 'TypeScript' ? 'ts' : 'js';
|
|
10
10
|
const kafkaSource = path.join(templatesDir, 'common', 'kafka', langExt);
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
// 1. Copy necessary directories individually (to avoid orphaned templates in src)
|
|
13
|
+
if (await fs.pathExists(path.join(kafkaSource, 'services'))) {
|
|
14
|
+
await fs.copy(path.join(kafkaSource, 'services'), path.join(targetDir, 'src/services'));
|
|
15
|
+
}
|
|
16
|
+
if (await fs.pathExists(path.join(kafkaSource, 'config'))) {
|
|
17
|
+
await fs.copy(path.join(kafkaSource, 'config'), path.join(targetDir, 'src/config'));
|
|
18
|
+
}
|
|
12
19
|
|
|
13
20
|
// Render Kafka Service with dynamic logger path
|
|
14
21
|
const kafkaServiceFileName = `kafkaService.${langExt}`;
|
|
@@ -16,7 +23,7 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
|
|
|
16
23
|
// Render Kafka Service Spec
|
|
17
24
|
const kafkaSpecFileName = `kafkaService.spec.${langExt}`;
|
|
18
25
|
const kafkaSpecTemplate = path.join(targetDir, 'src', 'services', `${kafkaSpecFileName}.ejs`);
|
|
19
|
-
|
|
26
|
+
|
|
20
27
|
if (await fs.pathExists(kafkaServiceTemplate)) {
|
|
21
28
|
let serviceLoggerPath, serviceConfigPath;
|
|
22
29
|
if (language === 'TypeScript') {
|
|
@@ -39,10 +46,9 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
|
|
|
39
46
|
specConfigPath = architecture === 'Clean Architecture' ? '@/infrastructure/config/kafka' : '@/config/kafka';
|
|
40
47
|
specServicePath = architecture === 'Clean Architecture' ? '@/infrastructure/messaging/kafkaClient' : '@/services/kafkaService';
|
|
41
48
|
} else {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
specServicePath = architecture === 'Clean Architecture' ? '@/infrastructure/messaging/kafkaClient' : '@/services/kafkaService';
|
|
49
|
+
specLoggerPath = architecture === 'Clean Architecture' ? '../../infrastructure/log/logger' : '../utils/logger';
|
|
50
|
+
specConfigPath = architecture === 'Clean Architecture' ? '../../infrastructure/config/kafka' : '../config/kafka';
|
|
51
|
+
specServicePath = architecture === 'Clean Architecture' ? '../../infrastructure/messaging/kafkaClient' : '../services/kafkaService';
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
const specContent = ejs.render(await fs.readFile(kafkaSpecTemplate, 'utf-8'), { ...config, loggerPath: specLoggerPath, configPath: specConfigPath, servicePath: specServicePath });
|
|
@@ -64,7 +70,6 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
|
|
|
64
70
|
await fs.ensureDir(path.dirname(specTarget));
|
|
65
71
|
await fs.writeFile(specTarget, specContent);
|
|
66
72
|
|
|
67
|
-
// Remove the template from src in targetDir to avoid double processing by processAllTests
|
|
68
73
|
const targetSpecTemplate = path.join(targetDir, 'src', 'config', `${kafkaConfigSpecFileName}.ejs`);
|
|
69
74
|
if (await fs.pathExists(targetSpecTemplate)) {
|
|
70
75
|
await fs.remove(targetSpecTemplate);
|
|
@@ -79,14 +84,12 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
|
|
|
79
84
|
|
|
80
85
|
const serviceExt = language === 'TypeScript' ? 'ts' : 'js';
|
|
81
86
|
|
|
82
|
-
// Move Service to Infrastructure/Messaging
|
|
83
87
|
await fs.move(
|
|
84
88
|
path.join(targetDir, `src/services/kafkaService.${serviceExt}`),
|
|
85
89
|
path.join(targetDir, `src/infrastructure/messaging/kafkaClient.${serviceExt}`),
|
|
86
90
|
{ overwrite: true }
|
|
87
91
|
);
|
|
88
92
|
|
|
89
|
-
// Move Spec to Tests/Infrastructure/Messaging
|
|
90
93
|
if (await fs.pathExists(path.join(targetDir, `src/services/kafkaService.spec.${serviceExt}`))) {
|
|
91
94
|
await fs.move(
|
|
92
95
|
path.join(targetDir, `src/services/kafkaService.spec.${serviceExt}`),
|
|
@@ -95,8 +98,6 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
|
|
|
95
98
|
);
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
// Move Config to Infrastructure/Config
|
|
99
|
-
// Note: Check if config path exists before moving, though copy above should have put it there
|
|
100
101
|
if (await fs.pathExists(path.join(targetDir, `src/config/kafka.${serviceExt}`))) {
|
|
101
102
|
await fs.move(
|
|
102
103
|
path.join(targetDir, `src/config/kafka.${serviceExt}`),
|
|
@@ -105,18 +106,68 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
|
|
|
105
106
|
);
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
// Cleanup old services folder
|
|
109
109
|
await fs.remove(path.join(targetDir, 'src/services'));
|
|
110
|
+
|
|
111
|
+
// Messaging Infrastructure Enhancement
|
|
112
|
+
const messagingDir = path.join(targetDir, 'src/interfaces/messaging');
|
|
113
|
+
await fs.ensureDir(path.join(messagingDir, 'consumers/instances'));
|
|
114
|
+
await fs.ensureDir(path.join(messagingDir, 'schemas'));
|
|
115
|
+
|
|
116
|
+
const loggerPath = language === 'TypeScript' ? '@/infrastructure/log/logger' : '../../infrastructure/log/logger';
|
|
117
|
+
const messagingTemplates = [
|
|
118
|
+
{ src: 'baseConsumer', dest: 'interfaces/messaging/baseConsumer' },
|
|
119
|
+
{ src: 'userEventSchema', dest: 'interfaces/messaging/schemas/userEventSchema' },
|
|
120
|
+
{ src: 'welcomeEmailConsumer', dest: 'interfaces/messaging/consumers/instances/welcomeEmailConsumer' }
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
for (const t of messagingTemplates) {
|
|
124
|
+
const templateSource = path.join(templatesDir, 'common', 'kafka', langExt, 'messaging', `${t.src}.${langExt}.ejs`);
|
|
125
|
+
if (await fs.pathExists(templateSource)) {
|
|
126
|
+
const content = ejs.render(await fs.readFile(templateSource, 'utf-8'), { ...config, loggerPath });
|
|
127
|
+
await fs.writeFile(path.join(targetDir, 'src', `${t.dest}.${langExt}`), content);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Render Specs for messaging components
|
|
131
|
+
const specTemplateSource = path.join(templatesDir, 'common', 'kafka', langExt, 'messaging', `${t.src}.spec.${langExt}.ejs`);
|
|
132
|
+
if (await fs.pathExists(specTemplateSource)) {
|
|
133
|
+
const specContent = ejs.render(await fs.readFile(specTemplateSource, 'utf-8'), { ...config, loggerPath });
|
|
134
|
+
const specDest = path.join(targetDir, 'tests', `${t.dest}.spec.${langExt}`);
|
|
135
|
+
await fs.ensureDir(path.dirname(specDest));
|
|
136
|
+
await fs.writeFile(specDest, specContent);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
110
139
|
|
|
111
|
-
// Remove REST-specific folders (Interfaces) - Note: routes is kept for health endpoint
|
|
112
|
-
await fs.remove(path.join(targetDir, 'src/interfaces/controllers'));
|
|
113
|
-
await fs.remove(path.join(targetDir, 'tests/interfaces/controllers'));
|
|
114
|
-
|
|
115
|
-
// Original logic removed src/config entirely, but now we use it for Zod env validation in TS.
|
|
116
|
-
// We will no longer delete it.
|
|
117
140
|
} else if (architecture === 'MVC') {
|
|
118
141
|
const serviceExt = language === 'TypeScript' ? 'ts' : 'js';
|
|
119
|
-
|
|
142
|
+
|
|
143
|
+
const messagingDir = path.join(targetDir, 'src/messaging');
|
|
144
|
+
await fs.ensureDir(path.join(messagingDir, 'consumers/instances'));
|
|
145
|
+
await fs.ensureDir(path.join(messagingDir, 'schemas'));
|
|
146
|
+
|
|
147
|
+
const loggerPath = language === 'TypeScript' ? '@/utils/logger' : '../utils/logger';
|
|
148
|
+
const messagingTemplates = [
|
|
149
|
+
{ src: 'baseConsumer', dest: 'messaging/baseConsumer' },
|
|
150
|
+
{ src: 'userEventSchema', dest: 'messaging/schemas/userEventSchema' },
|
|
151
|
+
{ src: 'welcomeEmailConsumer', dest: 'messaging/consumers/instances/welcomeEmailConsumer' }
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
for (const t of messagingTemplates) {
|
|
155
|
+
const templateSource = path.join(templatesDir, 'common', 'kafka', langExt, 'messaging', `${t.src}.${langExt}.ejs`);
|
|
156
|
+
if (await fs.pathExists(templateSource)) {
|
|
157
|
+
const content = ejs.render(await fs.readFile(templateSource, 'utf-8'), { ...config, loggerPath });
|
|
158
|
+
await fs.writeFile(path.join(targetDir, 'src', `${t.dest}.${langExt}`), content);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Render Specs for messaging components
|
|
162
|
+
const specTemplateSource = path.join(templatesDir, 'common', 'kafka', langExt, 'messaging', `${t.src}.spec.${langExt}.ejs`);
|
|
163
|
+
if (await fs.pathExists(specTemplateSource)) {
|
|
164
|
+
const specContent = ejs.render(await fs.readFile(specTemplateSource, 'utf-8'), { ...config, loggerPath });
|
|
165
|
+
const specDest = path.join(targetDir, 'tests', `${t.dest}.spec.${langExt}`);
|
|
166
|
+
await fs.ensureDir(path.dirname(specDest));
|
|
167
|
+
await fs.writeFile(specDest, specContent);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
120
171
|
if (await fs.pathExists(path.join(targetDir, `src/services/kafkaService.spec.${serviceExt}`))) {
|
|
121
172
|
await fs.ensureDir(path.join(targetDir, 'tests/services'));
|
|
122
173
|
await fs.move(
|
|
@@ -126,11 +177,6 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
|
|
|
126
177
|
);
|
|
127
178
|
}
|
|
128
179
|
|
|
129
|
-
if (!config.viewEngine || config.viewEngine === 'None') {
|
|
130
|
-
// MVC Cleanup for Kafka Worker (No views) - Note: routes is kept for health endpoint
|
|
131
|
-
await fs.remove(path.join(targetDir, 'src/controllers'));
|
|
132
|
-
await fs.remove(path.join(targetDir, 'tests/controllers'));
|
|
133
|
-
}
|
|
134
180
|
}
|
|
135
181
|
};
|
|
136
182
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodejs-quickstart-structure",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "The ultimate nodejs quickstart structure CLI to scaffold Node.js microservices with MVC or Clean Architecture",
|
|
6
6
|
"main": "bin/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"nodejs-quickstart": "./bin/index.js"
|
|
@@ -16,12 +16,17 @@
|
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"nodejs",
|
|
19
|
+
"node",
|
|
20
|
+
"quickstart",
|
|
21
|
+
"structure",
|
|
19
22
|
"cli",
|
|
20
23
|
"scaffold",
|
|
21
24
|
"mvc",
|
|
22
25
|
"clean-architecture",
|
|
23
26
|
"microservices",
|
|
24
|
-
"backend"
|
|
27
|
+
"backend",
|
|
28
|
+
"generator",
|
|
29
|
+
"boilerplate"
|
|
25
30
|
],
|
|
26
31
|
"author": "Pau Dang <[EMAIL_ADDRESS]>",
|
|
27
32
|
"repository": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const startServer = require('./infrastructure/webserver/server');
|
|
2
2
|
const logger = require('./infrastructure/log/logger');
|
|
3
3
|
<% if (communication === 'Kafka') { -%>
|
|
4
|
-
const { connectKafka
|
|
4
|
+
const { connectKafka } = require('./infrastructure/messaging/kafkaClient');
|
|
5
5
|
<% } -%>
|
|
6
6
|
|
|
7
7
|
<%_ if (database !== 'None') { -%>
|
|
@@ -24,7 +24,6 @@ const syncDatabase = async () => {
|
|
|
24
24
|
// Connect Kafka
|
|
25
25
|
connectKafka().then(async () => {
|
|
26
26
|
logger.info('Kafka connected');
|
|
27
|
-
await sendMessage('test-topic', 'Hello Kafka from Clean Arch JS!');
|
|
28
27
|
}).catch(err => {
|
|
29
28
|
logger.error('Failed to connect to Kafka:', err);
|
|
30
29
|
});
|
|
@@ -45,7 +44,6 @@ startServer();
|
|
|
45
44
|
// Connect Kafka
|
|
46
45
|
connectKafka().then(async () => {
|
|
47
46
|
logger.info('Kafka connected');
|
|
48
|
-
await sendMessage('test-topic', 'Hello Kafka from Clean Arch JS!');
|
|
49
47
|
}).catch(err => {
|
|
50
48
|
logger.error('Failed to connect to Kafka:', err);
|
|
51
49
|
});
|
|
@@ -4,7 +4,7 @@ const logger = require('../log/logger');
|
|
|
4
4
|
const morgan = require('morgan');
|
|
5
5
|
const { errorMiddleware } = require('./middleware/errorMiddleware');
|
|
6
6
|
const healthRoutes = require('../../interfaces/routes/healthRoute');
|
|
7
|
-
<%_ if (communication === 'REST APIs') { -%>
|
|
7
|
+
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
8
8
|
const apiRoutes = require('../../interfaces/routes/api');
|
|
9
9
|
const swaggerUi = require('swagger-ui-express');
|
|
10
10
|
const swaggerSpecs = require('./swagger');
|
|
@@ -29,10 +29,10 @@ const startServer = async () => {
|
|
|
29
29
|
app.use(cors());
|
|
30
30
|
app.use(express.json());
|
|
31
31
|
app.use(morgan('combined', { stream: { write: message => logger.info(message.trim()) } }));
|
|
32
|
-
<%_ if (communication === 'REST APIs') { -%>
|
|
32
|
+
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
33
33
|
app.use('/api', apiRoutes);
|
|
34
34
|
<%_ } -%>
|
|
35
|
-
<%_ if (communication === 'REST APIs') { -%>
|
|
35
|
+
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
36
36
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
|
|
37
37
|
<%_ } -%>
|
|
38
38
|
<%_ if (communication === 'GraphQL') { -%>
|
|
@@ -71,13 +71,14 @@ const startServer = async () => {
|
|
|
71
71
|
const server = app.listen(port, () => {
|
|
72
72
|
logger.info(`Server running on port ${port}`);
|
|
73
73
|
<%_ if (communication === 'Kafka') { -%>
|
|
74
|
-
const { connectKafka
|
|
75
|
-
connectKafka()
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
const { connectKafka } = require('../../infrastructure/messaging/kafkaClient');
|
|
75
|
+
connectKafka()
|
|
76
|
+
.then(async () => {
|
|
77
|
+
logger.info('Kafka connected');
|
|
78
|
+
})
|
|
79
|
+
.catch(err => {
|
|
80
|
+
logger.error('Failed to connect to Kafka after retries:', err.message);
|
|
81
|
+
});
|
|
81
82
|
<%_ } -%>
|
|
82
83
|
});
|
|
83
84
|
|
|
@@ -26,7 +26,15 @@ class UserController {
|
|
|
26
26
|
async createUser(data) {
|
|
27
27
|
const { name, email } = data;
|
|
28
28
|
try {
|
|
29
|
-
|
|
29
|
+
const user = await this.createUserUseCase.execute(name, email);
|
|
30
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
31
|
+
const { sendMessage } = require('../../infrastructure/messaging/kafkaClient');
|
|
32
|
+
await sendMessage('user-topic', JSON.stringify({
|
|
33
|
+
action: 'USER_CREATED',
|
|
34
|
+
payload: { id: user.id || user._id, email: user.email }
|
|
35
|
+
}));
|
|
36
|
+
<%_ } -%>
|
|
37
|
+
return user;
|
|
30
38
|
} catch (error) {
|
|
31
39
|
logger.error('Error creating user:', error);
|
|
32
40
|
throw error;
|
|
@@ -47,6 +55,13 @@ class UserController {
|
|
|
47
55
|
const { name, email } = req.body;
|
|
48
56
|
try {
|
|
49
57
|
const user = await this.createUserUseCase.execute(name, email);
|
|
58
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
59
|
+
const { sendMessage } = require('../../infrastructure/messaging/kafkaClient');
|
|
60
|
+
await sendMessage('user-topic', JSON.stringify({
|
|
61
|
+
action: 'USER_CREATED',
|
|
62
|
+
payload: { id: user.id || user._id, email: user.email }
|
|
63
|
+
}));
|
|
64
|
+
<%_ } -%>
|
|
50
65
|
res.status(HTTP_STATUS.CREATED).json(user);
|
|
51
66
|
} catch (error) {
|
|
52
67
|
logger.error('Error creating user:', error);
|