express-genix 1.1.4 → 2.0.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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +204 -259
  3. package/index.js +229 -113
  4. package/lib/cleanup.js +41 -129
  5. package/lib/features.js +239 -0
  6. package/lib/generator.js +286 -204
  7. package/lib/utils.js +43 -91
  8. package/package.json +81 -63
  9. package/templates/cicd/github-actions.yml.ejs +70 -0
  10. package/templates/config/database.mongo.js.ejs +29 -33
  11. package/templates/config/database.postgres.js.ejs +41 -40
  12. package/templates/config/database.prisma.js.ejs +26 -0
  13. package/templates/config/redis.js.ejs +28 -0
  14. package/templates/config/schema.prisma.ejs +20 -0
  15. package/templates/config/swagger.js.ejs +30 -0
  16. package/templates/config/websocket.js.ejs +62 -0
  17. package/templates/controllers/authController.js.ejs +152 -129
  18. package/templates/controllers/exampleController.js.ejs +92 -152
  19. package/templates/controllers/userController.js.ejs +52 -60
  20. package/templates/core/Dockerfile.ejs +41 -31
  21. package/templates/core/README.md.ejs +191 -179
  22. package/templates/core/app.js.ejs +114 -64
  23. package/templates/core/docker-compose.yml.ejs +59 -47
  24. package/templates/core/dockerignore.ejs +7 -0
  25. package/templates/core/env.ejs +25 -19
  26. package/templates/core/env.example.ejs +26 -0
  27. package/templates/core/eslintrc.json.ejs +50 -20
  28. package/templates/core/gitignore.ejs +51 -51
  29. package/templates/core/healthcheck.js.ejs +24 -24
  30. package/templates/core/jest.config.js.ejs +19 -22
  31. package/templates/core/package.json.ejs +70 -33
  32. package/templates/core/prettierrc.json.ejs +11 -11
  33. package/templates/core/server.js.ejs +64 -48
  34. package/templates/core/tsconfig.json.ejs +19 -0
  35. package/templates/middleware/auth.js.ejs +80 -66
  36. package/templates/middleware/cache.js.ejs +67 -0
  37. package/templates/middleware/errorHandler.js.ejs +50 -46
  38. package/templates/middleware/requestId.js.ejs +9 -0
  39. package/templates/middleware/validation.js.ejs +109 -47
  40. package/templates/migrations/create-users.js.ejs +50 -0
  41. package/templates/migrations/seed-users.js.ejs +34 -0
  42. package/templates/migrations/sequelizerc.ejs +8 -0
  43. package/templates/models/User.mongo.js.ejs +29 -29
  44. package/templates/models/User.postgres.js.ejs +40 -40
  45. package/templates/models/index.mongo.js.ejs +7 -7
  46. package/templates/models/index.postgres.js.ejs +11 -11
  47. package/templates/routes/authRoutes.js.ejs +222 -13
  48. package/templates/routes/exampleRoutes.js.ejs +100 -12
  49. package/templates/routes/index.js.ejs +34 -24
  50. package/templates/routes/userRoutes.js.ejs +78 -15
  51. package/templates/services/authService.js.ejs +111 -35
  52. package/templates/services/exampleService.js.ejs +112 -112
  53. package/templates/services/userService.mongodb.js.ejs +33 -33
  54. package/templates/services/userService.postgres.js.ejs +30 -30
  55. package/templates/services/userService.prisma.js.ejs +36 -0
  56. package/templates/tests/auth.test.js.ejs +83 -66
  57. package/templates/tests/example.test.js.ejs +109 -112
  58. package/templates/tests/setup.js.ejs +11 -11
  59. package/templates/tests/users.test.js.ejs +42 -42
  60. package/templates/utils/envValidator.js.ejs +23 -0
  61. package/templates/utils/errors.js.ejs +12 -12
  62. package/templates/utils/logger.js.ejs +37 -28
  63. package/templates/utils/response.js.ejs +28 -0
  64. package/templates/utils/validators.js.ejs +34 -34
  65. package/templates/config/swagger.json.ejs +0 -194
  66. package/templates/core/index.js.ejs +0 -24
package/index.js CHANGED
@@ -1,114 +1,230 @@
1
- #!/usr/bin/env node
2
-
3
- const { program } = require('commander');
4
- const inquirer = require('inquirer');
5
- const path = require('path');
6
- const fs = require('fs');
7
- const { runPostGenerationCleanup } = require('./lib/cleanup');
8
- const { generateProject } = require('./lib/generator');
9
-
10
- // Create inquirer prompt module
11
- const prompt = inquirer.createPromptModule();
12
-
13
- async function main() {
14
- program
15
- .name('express-genix')
16
- .description('CLI to generate a production-grade Express boilerplate with automatic code cleanup')
17
- .version('1.1.0'); // Bumped for new features
18
-
19
- program
20
- .command('init')
21
- .description('Initialize a new Express project with automatic formatting and linting')
22
- .option('--skip-cleanup', 'Skip post-generation cleanup (for debugging)')
23
- .action(async (options) => {
24
- const answers = await prompt([
25
- {
26
- type: 'input',
27
- name: 'projectName',
28
- message: 'Project name:',
29
- default: 'my-express-app',
30
- validate: (input) => {
31
- if (!/^[a-zA-Z0-9-_]+$/.test(input)) {
32
- return 'Project name can only contain letters, numbers, hyphens, and underscores';
33
- }
34
- return true;
35
- },
36
- },
37
- {
38
- type: 'list',
39
- name: 'db',
40
- message: 'Choose a database:',
41
- choices: [
42
- { name: 'MongoDB (with Mongoose)', value: 'mongodb' },
43
- { name: 'PostgreSQL (with Sequelize)', value: 'postgresql' },
44
- { name: 'No Database (API without database)', value: 'none' },
45
- ],
46
- },
47
- ]);
48
-
49
- const config = {
50
- ...answers,
51
- hasDatabase: answers.db !== 'none',
52
- isNoDatabase: answers.db === 'none',
53
- };
54
-
55
- const projectDir = path.join(process.cwd(), config.projectName);
56
-
57
- if (fs.existsSync(projectDir)) {
58
- console.error(`Directory ${projectDir} already exists!`);
59
- process.exit(1);
60
- }
61
-
62
- console.log(`Creating ${config.projectName}${config.hasDatabase ? ` with ${config.db}` : ' without database'}...`);
63
-
64
- try {
65
- // Generate project files
66
- await generateProject(config, projectDir);
67
-
68
- console.log('Installing dependencies...');
69
-
70
- // Run post-generation cleanup
71
- if (!options.skipCleanup) {
72
- await runPostGenerationCleanup(projectDir);
73
- } else {
74
- console.log('Skipping post-generation cleanup');
75
- }
76
-
77
- console.log(`
78
- Project ${config.projectName} created successfully!
79
-
80
- ${options.skipCleanup ? 'Remember to run: npm run lint:fix && npm run format' : 'Code has been automatically formatted and cleaned!'}
81
-
82
- To get started:
83
- cd ${config.projectName}
84
- npm run dev
85
-
86
- API Documentation: http://localhost:3000/api-docs
87
- Health Check: http://localhost:3000/health
88
-
89
- Available scripts:
90
- npm run dev - Start development server
91
- npm start - Start production server
92
- npm test - Run tests
93
- npm run lint - Run ESLint
94
- npm run lint:fix - Fix ESLint issues
95
- npm run format - Format with Prettier
96
- npm run format:check - Check formatting
97
-
98
- Database: ${config.isNoDatabase ? 'None (API only)' : config.db}
99
- Authentication: ${config.isNoDatabase ? 'Simple token-based' : 'JWT with refresh tokens'}
100
- Security: Helmet, CORS, Rate limiting
101
- Monitoring: Health checks, logging
102
- Docker: Ready for containerization
103
- `);
104
-
105
- } catch (error) {
106
- console.error('Failed to create project:', error.message);
107
- process.exit(1);
108
- }
109
- });
110
-
111
- await program.parseAsync(process.argv);
112
- }
113
-
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const inquirer = require('inquirer');
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+ const crypto = require('crypto');
8
+ const { execSync } = require('child_process');
9
+ const { runPostGenerationCleanup } = require('./lib/cleanup');
10
+ const { generateProject } = require('./lib/generator');
11
+
12
+ const pkg = require('./package.json');
13
+ const prompt = inquirer.createPromptModule();
14
+
15
+ const generateSecret = (length = 64) => crypto.randomBytes(length).toString('hex');
16
+
17
+ async function main() {
18
+ program
19
+ .name('express-genix')
20
+ .description('Production-grade CLI to generate Express.js applications')
21
+ .version(pkg.version);
22
+
23
+ program
24
+ .command('init')
25
+ .description('Initialize a new Express project')
26
+ .option('--skip-cleanup', 'Skip post-generation cleanup (for debugging)')
27
+ .option('--skip-install', 'Skip npm install (for CI/testing)')
28
+ .action(async (options) => {
29
+ const answers = await prompt([
30
+ {
31
+ type: 'input',
32
+ name: 'projectName',
33
+ message: 'Project name:',
34
+ default: 'my-express-app',
35
+ validate: (input) => {
36
+ if (!/^[a-zA-Z0-9-_]+$/.test(input)) {
37
+ return 'Project name can only contain letters, numbers, hyphens, and underscores';
38
+ }
39
+ if (input.length > 100) {
40
+ return 'Project name must be less than 100 characters';
41
+ }
42
+ return true;
43
+ },
44
+ },
45
+ {
46
+ type: 'list',
47
+ name: 'language',
48
+ message: 'Language:',
49
+ choices: [
50
+ { name: 'JavaScript', value: 'javascript' },
51
+ { name: 'TypeScript', value: 'typescript' },
52
+ ],
53
+ },
54
+ {
55
+ type: 'list',
56
+ name: 'db',
57
+ message: 'Database:',
58
+ choices: [
59
+ { name: 'MongoDB (with Mongoose)', value: 'mongodb' },
60
+ { name: 'PostgreSQL (with Sequelize)', value: 'postgresql' },
61
+ { name: 'PostgreSQL (with Prisma)', value: 'prisma' },
62
+ { name: 'No Database (API without database)', value: 'none' },
63
+ ],
64
+ },
65
+ {
66
+ type: 'checkbox',
67
+ name: 'features',
68
+ message: 'Select additional features:',
69
+ choices: [
70
+ { name: 'JWT Authentication', value: 'auth', checked: true },
71
+ { name: 'Rate Limiting', value: 'rateLimit', checked: true },
72
+ { name: 'Swagger/OpenAPI Docs', value: 'swagger', checked: true },
73
+ { name: 'Redis Token Blacklist', value: 'redis' },
74
+ { name: 'Docker & Docker Compose', value: 'docker', checked: true },
75
+ { name: 'CI/CD (GitHub Actions)', value: 'cicd' },
76
+ { name: 'WebSocket (Socket.io)', value: 'websocket' },
77
+ { name: 'Request ID / Correlation ID', value: 'requestId', checked: true },
78
+ ],
79
+ when: (ans) => ans.db !== 'none',
80
+ },
81
+ {
82
+ type: 'checkbox',
83
+ name: 'features',
84
+ message: 'Select additional features:',
85
+ choices: [
86
+ { name: 'Rate Limiting', value: 'rateLimit', checked: true },
87
+ { name: 'Swagger/OpenAPI Docs', value: 'swagger', checked: true },
88
+ { name: 'Docker & Docker Compose', value: 'docker', checked: true },
89
+ { name: 'CI/CD (GitHub Actions)', value: 'cicd' },
90
+ { name: 'WebSocket (Socket.io)', value: 'websocket' },
91
+ { name: 'Request ID / Correlation ID', value: 'requestId', checked: true },
92
+ ],
93
+ when: (ans) => ans.db === 'none',
94
+ },
95
+ {
96
+ type: 'list',
97
+ name: 'logger',
98
+ message: 'Logging library:',
99
+ choices: [
100
+ { name: 'Winston (feature-rich, widely used)', value: 'winston' },
101
+ { name: 'Pino (fast, low overhead)', value: 'pino' },
102
+ ],
103
+ },
104
+ ]);
105
+
106
+ const features = answers.features || [];
107
+ const config = {
108
+ projectName: answers.projectName,
109
+ language: answers.language,
110
+ db: answers.db,
111
+ logger: answers.logger,
112
+ hasDatabase: answers.db !== 'none',
113
+ isNoDatabase: answers.db === 'none',
114
+ isPrisma: answers.db === 'prisma',
115
+ isTypescript: answers.language === 'typescript',
116
+ hasAuth: features.includes('auth') && answers.db !== 'none',
117
+ hasRateLimit: features.includes('rateLimit'),
118
+ hasSwagger: features.includes('swagger'),
119
+ hasDocker: features.includes('docker'),
120
+ hasCicd: features.includes('cicd'),
121
+ hasWebsocket: features.includes('websocket'),
122
+ hasRequestId: features.includes('requestId'),
123
+ hasRedis: features.includes('redis') && features.includes('auth') && answers.db !== 'none',
124
+ jwtSecret: generateSecret(),
125
+ jwtRefreshSecret: generateSecret(),
126
+ };
127
+
128
+ const projectDir = path.join(process.cwd(), config.projectName);
129
+
130
+ if (fs.existsSync(projectDir)) {
131
+ console.error(`\nāŒ Directory "${config.projectName}" already exists!`);
132
+ process.exit(1);
133
+ }
134
+
135
+ const dbLabel = config.isNoDatabase ? 'no database' : config.db;
136
+ console.log(`\nšŸš€ Creating ${config.projectName} (${config.language}, ${dbLabel})...\n`);
137
+
138
+ try {
139
+ await generateProject(config, projectDir, options);
140
+
141
+ if (!options.skipCleanup) {
142
+ await runPostGenerationCleanup(projectDir, config);
143
+ }
144
+
145
+ // Git init + initial commit
146
+ try {
147
+ execSync('git init', { cwd: projectDir, stdio: 'pipe' });
148
+ execSync('git add -A', { cwd: projectDir, stdio: 'pipe' });
149
+ execSync('git commit -m "Initial commit from express-genix"', {
150
+ cwd: projectDir,
151
+ stdio: 'pipe',
152
+ env: { ...process.env, GIT_COMMITTER_NAME: 'express-genix', GIT_COMMITTER_EMAIL: 'cli@express-genix.dev', GIT_AUTHOR_NAME: 'express-genix', GIT_AUTHOR_EMAIL: 'cli@express-genix.dev' },
153
+ });
154
+ console.log('šŸ“¦ Git repository initialized with initial commit');
155
+ } catch {
156
+ // Git not available — skip silently
157
+ }
158
+
159
+ const ext = config.isTypescript ? 'ts' : 'js';
160
+ console.log(`
161
+ āœ… Project ${config.projectName} created successfully!
162
+
163
+ To get started:
164
+ cd ${config.projectName}
165
+ npm run dev
166
+ ${config.hasSwagger ? `\nAPI Documentation: http://localhost:3000/api-docs` : ''}
167
+ Health Check: http://localhost:3000/health
168
+
169
+ Available scripts:
170
+ npm run dev Start development server
171
+ npm start Start production server
172
+ npm test Run tests
173
+ npm run lint Run ESLint
174
+ npm run lint:fix Fix ESLint issues
175
+ npm run format Format with Prettier${config.isTypescript ? '\n npm run build Compile TypeScript' : ''}
176
+
177
+ Configuration:
178
+ Language: ${config.isTypescript ? 'TypeScript' : 'JavaScript'}
179
+ Database: ${config.isNoDatabase ? 'None' : config.db}
180
+ Authentication: ${config.hasAuth ? 'JWT with refresh tokens' : 'None'}
181
+ Logger: ${config.logger}
182
+ Features: ${features.join(', ') || 'base'}
183
+ `);
184
+
185
+ } catch (error) {
186
+ // Rollback: remove partial project directory on failure
187
+ if (fs.existsSync(projectDir)) {
188
+ fs.rmSync(projectDir, { recursive: true, force: true });
189
+ console.error('\n🧹 Rolled back partial project directory.');
190
+ }
191
+ console.error(`\nāŒ Failed to create project: ${error.message}`);
192
+ process.exit(1);
193
+ }
194
+ });
195
+
196
+ program
197
+ .command('add <feature>')
198
+ .description('Add a feature to an existing express-genix project')
199
+ .action(async (feature) => {
200
+ const supportedFeatures = ['auth', 'websocket', 'docker', 'cicd', 'prisma'];
201
+ if (!supportedFeatures.includes(feature)) {
202
+ console.error(`āŒ Unknown feature: "${feature}"`);
203
+ console.log(`Supported features: ${supportedFeatures.join(', ')}`);
204
+ process.exit(1);
205
+ }
206
+
207
+ const projectDir = process.cwd();
208
+ const packageJsonPath = path.join(projectDir, 'package.json');
209
+
210
+ if (!fs.existsSync(packageJsonPath)) {
211
+ console.error('āŒ No package.json found. Run this command from your project root.');
212
+ process.exit(1);
213
+ }
214
+
215
+ console.log(`Adding ${feature} to project...`);
216
+
217
+ try {
218
+ const { addFeature } = require('./lib/features');
219
+ await addFeature(feature, projectDir);
220
+ console.log(`āœ… ${feature} added successfully!`);
221
+ } catch (error) {
222
+ console.error(`āŒ Failed to add feature: ${error.message}`);
223
+ process.exit(1);
224
+ }
225
+ });
226
+
227
+ await program.parseAsync(process.argv);
228
+ }
229
+
114
230
  main().catch(console.error);
package/lib/cleanup.js CHANGED
@@ -1,129 +1,41 @@
1
- const { execSync } = require('child_process');
2
- const fs = require('fs');
3
- const path = require('path');
4
-
5
- /**
6
- * Run post-generation cleanup to ensure zero linting errors
7
- */
8
- const runPostGenerationCleanup = async (projectDir) => {
9
- console.log('\nRunning post-generation cleanup...');
10
-
11
- try {
12
- // Step 1: Add missing dependencies and format scripts
13
- await addMissingDependencies(projectDir);
14
-
15
- // Step 2: Install Prettier and add to devDependencies
16
- await setupPrettier(projectDir);
17
-
18
- // Step 3: Run eslint --fix automatically
19
- await runEslintFix(projectDir);
20
-
21
- // Step 4: Format code with Prettier
22
- await formatWithPrettier(projectDir);
23
-
24
- // Step 5: Test generated output against linting rules
25
- await validateOutput(projectDir);
26
-
27
- console.log('Post-generation cleanup completed!');
28
-
29
- } catch (error) {
30
- console.warn('Some cleanup steps failed:', error.message);
31
- console.log('You can manually run: npm run lint:fix && npm run format');
32
- }
33
- };
34
-
35
- const addMissingDependencies = async (projectDir) => {
36
- console.log('Adding missing dependencies...');
37
-
38
- const packageJsonPath = path.join(projectDir, 'package.json');
39
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
40
-
41
- // Add missing validator dependency (only if not no-database)
42
- if (!packageJson.dependencies.validator) {
43
- packageJson.dependencies.validator = '^13.11.0';
44
- }
45
-
46
- // Add formatting and linting scripts
47
- packageJson.scripts['lint:fix'] = 'eslint . --fix';
48
- packageJson.scripts.format = 'prettier --write .';
49
- packageJson.scripts['format:check'] = 'prettier --check .';
50
-
51
- fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
52
- };
53
-
54
- const setupPrettier = async (projectDir) => {
55
- console.log('Installing and configuring Prettier...');
56
-
57
- // Install Prettier
58
- execSync(`cd "${projectDir}" && npm install --save-dev prettier`, { stdio: 'pipe' });
59
-
60
- // Create Prettier config
61
- const prettierConfig = {
62
- semi: true,
63
- trailingComma: 'es5',
64
- singleQuote: true,
65
- printWidth: 80,
66
- tabWidth: 2,
67
- useTabs: false,
68
- };
69
-
70
- fs.writeFileSync(
71
- path.join(projectDir, '.prettierrc'),
72
- JSON.stringify(prettierConfig, null, 2)
73
- );
74
-
75
- // Install any missing dependencies
76
- execSync(`cd "${projectDir}" && npm install`, { stdio: 'pipe' });
77
- };
78
-
79
- const runEslintFix = async (projectDir) => {
80
- console.log('Running eslint --fix...');
81
-
82
- try {
83
- execSync(`cd "${projectDir}" && npm run lint:fix`, { stdio: 'pipe' });
84
- console.log('ESLint auto-fixes applied');
85
- } catch (error) {
86
- console.log('Some ESLint issues need manual fixing');
87
- }
88
- };
89
-
90
- const formatWithPrettier = async (projectDir) => {
91
- console.log('Formatting code with Prettier...');
92
-
93
- try {
94
- execSync(`cd "${projectDir}" && npm run format`, { stdio: 'pipe' });
95
- console.log('Code formatted with Prettier');
96
- } catch (error) {
97
- console.warn('Prettier formatting encountered issues');
98
- }
99
- };
100
-
101
- const validateOutput = async (projectDir) => {
102
- console.log('Testing generated output against linting rules...');
103
-
104
- try {
105
- execSync(`cd "${projectDir}" && npm run lint`, {
106
- encoding: 'utf8',
107
- stdio: 'pipe'
108
- });
109
- console.log('All linting checks passed!');
110
- return true;
111
- } catch (error) {
112
- // Count remaining errors
113
- const stdout = error.stdout || '';
114
- const errorMatches = stdout.match(/(\d+) problems?/);
115
- const errorCount = errorMatches ? errorMatches[1] : 'some';
116
-
117
- console.log(`Validation complete: ${errorCount} issues remaining`);
118
-
119
- if (parseInt(errorCount) < 20) {
120
- console.log('Great improvement! Most issues have been resolved.');
121
- }
122
-
123
- return false;
124
- }
125
- };
126
-
127
- module.exports = {
128
- runPostGenerationCleanup,
129
- };
1
+ const { execSync } = require('child_process');
2
+
3
+ const runPostGenerationCleanup = async (projectDir) => {
4
+ console.log('\nšŸ”§ Running post-generation cleanup...');
5
+
6
+ try {
7
+ // Step 1: Run eslint --fix
8
+ try {
9
+ execSync(`cd "${projectDir}" && npx eslint . --fix`, { stdio: 'pipe' });
10
+ console.log('āœ… ESLint auto-fixes applied');
11
+ } catch {
12
+ console.log('āš ļø Some ESLint issues may need manual fixing');
13
+ }
14
+
15
+ // Step 2: Format with Prettier
16
+ try {
17
+ execSync(`cd "${projectDir}" && npx prettier --write .`, { stdio: 'pipe' });
18
+ console.log('āœ… Code formatted with Prettier');
19
+ } catch {
20
+ console.warn('āš ļø Prettier formatting encountered issues');
21
+ }
22
+
23
+ // Step 3: Validate
24
+ try {
25
+ execSync(`cd "${projectDir}" && npx eslint .`, { encoding: 'utf8', stdio: 'pipe' });
26
+ console.log('āœ… All linting checks passed');
27
+ } catch (error) {
28
+ const stdout = error.stdout || '';
29
+ const match = stdout.match(/(\d+) problems?/);
30
+ const count = match ? match[1] : 'some';
31
+ console.log(`āš ļø ${count} lint issues remaining (run "npm run lint:fix" to resolve)`);
32
+ }
33
+
34
+ console.log('āœ… Post-generation cleanup completed\n');
35
+ } catch (error) {
36
+ console.warn('āš ļø Some cleanup steps failed:', error.message);
37
+ console.log('You can manually run: npm run lint:fix && npm run format');
38
+ }
39
+ };
40
+
41
+ module.exports = { runPostGenerationCleanup };