express-genix 4.5.2 → 4.6.1
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/README.md +18 -0
- package/index.js +29 -112
- package/lib/cleanup.js +7 -5
- package/lib/features.js +97 -9
- package/lib/generator.js +3 -2
- package/lib/scaffold.js +165 -0
- package/package.json +1 -1
- package/templates/controllers/authController.js.ejs +16 -7
- package/templates/core/docker-compose.yml.ejs +16 -3
- package/templates/core/env.ejs +1 -3
- package/templates/core/env.example.ejs +1 -3
- package/templates/core/server.js.ejs +6 -21
- package/templates/infra/Makefile.ejs +30 -0
- package/templates/infra/nginx.conf.ejs +25 -0
- package/templates/middleware/errorHandler.js.ejs +7 -1
- package/templates/middleware/validation.js.ejs +2 -2
- package/templates/migrations/create-users.js.ejs +5 -1
- package/templates/scaffold/controller.js.ejs +94 -0
- package/templates/scaffold/route.js.ejs +88 -0
- package/templates/scaffold/service.js.ejs +77 -0
- package/templates/services/authService.js.ejs +20 -2
- package/templates/services/userService.prisma.js.ejs +3 -2
- package/lib/ai-cli.js +0 -133
- package/templates/agents/graph.js.ejs +0 -84
- package/templates/config/ai.js.ejs +0 -47
- package/templates/controllers/aiController.js.ejs +0 -100
- package/templates/routes/aiRoutes.js.ejs +0 -117
- package/templates/services/aiService.js.ejs +0 -80
package/README.md
CHANGED
|
@@ -112,6 +112,7 @@ npm run dev
|
|
|
112
112
|
|
|
113
113
|
```bash
|
|
114
114
|
cd my-express-app
|
|
115
|
+
express-genix add infra # Adds Production-Ready Infra (Docker, CI/CD, Nginx, Makefile)
|
|
115
116
|
express-genix add docker # Adds Dockerfile, docker-compose.yml, .dockerignore
|
|
116
117
|
express-genix add cicd # Adds GitHub Actions CI workflow
|
|
117
118
|
express-genix add auth # Adds JWT auth, controllers, routes, middleware
|
|
@@ -119,6 +120,23 @@ express-genix add websocket # Adds Socket.io setup
|
|
|
119
120
|
express-genix add prisma # Adds Prisma schema, client config, migrations
|
|
120
121
|
```
|
|
121
122
|
|
|
123
|
+
## Interactive Resource Scaffolding
|
|
124
|
+
|
|
125
|
+
Generate a complete resource (Controller, Service, and Routes) inside an existing Express Genix project.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
cd my-express-app
|
|
129
|
+
express-genix generate product
|
|
130
|
+
# or use the alias
|
|
131
|
+
express-genix g product
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
This command automatically:
|
|
135
|
+
1. Creates `src/controllers/productController.js` (with CRUD operations)
|
|
136
|
+
2. Creates `src/services/productService.js` (with business logic structure)
|
|
137
|
+
3. Creates `src/routes/productRoutes.js` (with Swagger annotations and auth middleware if enabled)
|
|
138
|
+
4. **Auto-injects** the new route into `src/routes/index.js` (`router.use('/products', productRoutes);`)
|
|
139
|
+
|
|
122
140
|
### Generate from natural language (AI)
|
|
123
141
|
|
|
124
142
|
Requires `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` in your environment.
|
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const crypto = require('crypto');
|
|
|
8
8
|
const { execSync } = require('child_process');
|
|
9
9
|
const { runPostGenerationCleanup } = require('./lib/cleanup');
|
|
10
10
|
const { generateProject } = require('./lib/generator');
|
|
11
|
+
const { validateProjectName } = require('./lib/utils');
|
|
11
12
|
|
|
12
13
|
const pkg = require('./package.json');
|
|
13
14
|
const prompt = inquirer.createPromptModule();
|
|
@@ -70,13 +71,8 @@ async function main() {
|
|
|
70
71
|
message: 'Project name:',
|
|
71
72
|
default: 'my-express-app',
|
|
72
73
|
validate: (input) => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
if (input.length > 100) {
|
|
77
|
-
return 'Project name must be less than 100 characters';
|
|
78
|
-
}
|
|
79
|
-
return true;
|
|
74
|
+
const error = validateProjectName(input);
|
|
75
|
+
return error ? error : true;
|
|
80
76
|
},
|
|
81
77
|
},
|
|
82
78
|
{
|
|
@@ -107,7 +103,7 @@ async function main() {
|
|
|
107
103
|
{ name: 'JWT Authentication', value: 'auth', checked: true },
|
|
108
104
|
{ name: 'Rate Limiting', value: 'rateLimit', checked: true },
|
|
109
105
|
{ name: 'Swagger/OpenAPI Docs', value: 'swagger', checked: true },
|
|
110
|
-
{ name: 'Redis Token Blacklist', value: 'redis' },
|
|
106
|
+
{ name: 'Redis Caching / Token Blacklist', value: 'redis' },
|
|
111
107
|
{ name: 'Docker & Docker Compose', value: 'docker', checked: true },
|
|
112
108
|
{ name: 'CI/CD (GitHub Actions)', value: 'cicd' },
|
|
113
109
|
{ name: 'WebSocket (Socket.io)', value: 'websocket' },
|
|
@@ -131,14 +127,17 @@ async function main() {
|
|
|
131
127
|
choices: [
|
|
132
128
|
{ name: 'Rate Limiting', value: 'rateLimit', checked: true },
|
|
133
129
|
{ name: 'Swagger/OpenAPI Docs', value: 'swagger', checked: true },
|
|
130
|
+
{ name: 'Redis Caching', value: 'redis' },
|
|
134
131
|
{ name: 'Docker & Docker Compose', value: 'docker', checked: true },
|
|
135
132
|
{ name: 'CI/CD (GitHub Actions)', value: 'cicd' },
|
|
136
133
|
{ name: 'WebSocket (Socket.io)', value: 'websocket' },
|
|
137
134
|
{ name: 'Request ID / Correlation ID', value: 'requestId', checked: true },
|
|
135
|
+
{ name: 'Email Service (Nodemailer)', value: 'email' },
|
|
138
136
|
{ name: 'File Uploads (Multer)', value: 'fileUpload' },
|
|
139
137
|
{ name: 'Audit Logging', value: 'auditLog' },
|
|
140
138
|
{ name: 'Prometheus Metrics', value: 'metrics' },
|
|
141
139
|
{ name: 'API Versioning (/api/v1)', value: 'apiVersioning' },
|
|
140
|
+
{ name: 'Background Jobs (BullMQ)', value: 'backgroundJobs' },
|
|
142
141
|
{ name: 'GraphQL (Apollo Server)', value: 'graphql' },
|
|
143
142
|
{ name: 'MCP Server (AI Agent Tools)', value: 'mcp' },
|
|
144
143
|
],
|
|
@@ -172,8 +171,8 @@ async function main() {
|
|
|
172
171
|
hasCicd: features.includes('cicd'),
|
|
173
172
|
hasWebsocket: features.includes('websocket'),
|
|
174
173
|
hasRequestId: features.includes('requestId'),
|
|
175
|
-
hasRedis: features.includes('redis')
|
|
176
|
-
hasEmail: features.includes('email')
|
|
174
|
+
hasRedis: features.includes('redis') || features.includes('backgroundJobs'),
|
|
175
|
+
hasEmail: features.includes('email'),
|
|
177
176
|
hasFileUpload: features.includes('fileUpload'),
|
|
178
177
|
hasSoftDelete: features.includes('softDelete') && answers.db !== 'none',
|
|
179
178
|
hasAuditLog: features.includes('auditLog'),
|
|
@@ -249,12 +248,17 @@ Configuration:
|
|
|
249
248
|
}
|
|
250
249
|
|
|
251
250
|
} catch (error) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
251
|
+
if (error.message.includes('Failed to install dependencies')) {
|
|
252
|
+
console.error(`\n⚠️ Project generated, but dependency installation failed.`);
|
|
253
|
+
console.error(` Please cd into ${config.projectName} and run "npm install" manually.`);
|
|
254
|
+
} else {
|
|
255
|
+
// Rollback: remove partial project directory on failure
|
|
256
|
+
if (fs.existsSync(projectDir)) {
|
|
257
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
258
|
+
console.error('\n🧹 Rolled back partial project directory.');
|
|
259
|
+
}
|
|
260
|
+
console.error(`\n❌ Failed to create project: ${error.message}`);
|
|
256
261
|
}
|
|
257
|
-
console.error(`\n❌ Failed to create project: ${error.message}`);
|
|
258
262
|
process.exit(1);
|
|
259
263
|
}
|
|
260
264
|
});
|
|
@@ -263,7 +267,7 @@ Configuration:
|
|
|
263
267
|
.command('add <feature>')
|
|
264
268
|
.description('Add a feature to an existing express-genix project')
|
|
265
269
|
.action(async (feature) => {
|
|
266
|
-
const supportedFeatures = ['auth', 'websocket', 'docker', 'cicd', 'prisma'];
|
|
270
|
+
const supportedFeatures = ['auth', 'websocket', 'docker', 'cicd', 'prisma', 'infra'];
|
|
267
271
|
if (!supportedFeatures.includes(feature)) {
|
|
268
272
|
console.error(`❌ Unknown feature: "${feature}"`);
|
|
269
273
|
console.log(`Supported features: ${supportedFeatures.join(', ')}`);
|
|
@@ -290,106 +294,19 @@ Configuration:
|
|
|
290
294
|
}
|
|
291
295
|
});
|
|
292
296
|
|
|
293
|
-
program
|
|
294
|
-
.command('ai <description>')
|
|
295
|
-
.description('Generate a project from a natural language description (requires OPENAI_API_KEY or ANTHROPIC_API_KEY)')
|
|
296
|
-
.option('--skip-install', 'Skip npm install')
|
|
297
|
-
.option('--skip-cleanup', 'Skip post-generation cleanup')
|
|
298
|
-
.action(async (description, options) => {
|
|
299
|
-
const { runAiCommand } = require('./lib/ai-cli');
|
|
300
|
-
const aiConfig = await runAiCommand(description);
|
|
301
|
-
|
|
302
|
-
const inquirerPrompt = inquirer.createPromptModule();
|
|
303
|
-
const aiFollowUp = await inquirerPrompt([
|
|
304
|
-
{
|
|
305
|
-
type: 'confirm',
|
|
306
|
-
name: 'confirmed',
|
|
307
|
-
message: 'Generate project with this configuration?',
|
|
308
|
-
default: true,
|
|
309
|
-
},
|
|
310
|
-
]);
|
|
311
|
-
|
|
312
|
-
if (!aiFollowUp.confirmed) {
|
|
313
|
-
console.log('Cancelled.');
|
|
314
|
-
process.exit(0);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
aiConfig.aiProvider = aiFollowUp.aiProvider || 'openai';
|
|
318
|
-
const features = aiConfig.features || [];
|
|
319
|
-
const config = {
|
|
320
|
-
projectName: aiConfig.projectName,
|
|
321
|
-
language: aiConfig.language || 'javascript',
|
|
322
|
-
db: aiConfig.db || 'none',
|
|
323
|
-
logger: aiConfig.logger || 'winston',
|
|
324
|
-
hasDatabase: aiConfig.db !== 'none',
|
|
325
|
-
isNoDatabase: aiConfig.db === 'none',
|
|
326
|
-
isPrisma: aiConfig.db === 'prisma',
|
|
327
|
-
isTypescript: aiConfig.language === 'typescript',
|
|
328
|
-
hasAuth: features.includes('auth') && aiConfig.db !== 'none',
|
|
329
|
-
hasRateLimit: features.includes('rateLimit'),
|
|
330
|
-
hasSwagger: features.includes('swagger'),
|
|
331
|
-
hasDocker: features.includes('docker'),
|
|
332
|
-
hasCicd: features.includes('cicd'),
|
|
333
|
-
hasWebsocket: features.includes('websocket'),
|
|
334
|
-
hasRequestId: features.includes('requestId'),
|
|
335
|
-
hasRedis: features.includes('redis') && features.includes('auth') && aiConfig.db !== 'none',
|
|
336
|
-
hasEmail: features.includes('email') && aiConfig.db !== 'none',
|
|
337
|
-
hasFileUpload: features.includes('fileUpload'),
|
|
338
|
-
hasSoftDelete: features.includes('softDelete') && aiConfig.db !== 'none',
|
|
339
|
-
hasAuditLog: features.includes('auditLog'),
|
|
340
|
-
hasMetrics: features.includes('metrics'),
|
|
341
|
-
hasApiVersioning: features.includes('apiVersioning'),
|
|
342
|
-
hasBackgroundJobs: features.includes('backgroundJobs'),
|
|
343
|
-
hasGraphQL: features.includes('graphql'),
|
|
344
|
-
hasMCP: features.includes('mcp'),
|
|
345
|
-
jwtSecret: generateSecret(),
|
|
346
|
-
jwtRefreshSecret: generateSecret(),
|
|
347
|
-
};
|
|
348
297
|
|
|
349
|
-
const projectDir = path.join(process.cwd(), config.projectName);
|
|
350
|
-
|
|
351
|
-
if (fs.existsSync(projectDir)) {
|
|
352
|
-
console.error(`\n❌ Directory "${config.projectName}" already exists!`);
|
|
353
|
-
process.exit(1);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const dbLabel = config.isNoDatabase ? 'no database' : config.db;
|
|
357
|
-
console.log(`\n🚀 Creating ${config.projectName} (${config.language}, ${dbLabel})...\n`);
|
|
358
298
|
|
|
299
|
+
program
|
|
300
|
+
.command('generate <resource>')
|
|
301
|
+
.alias('g')
|
|
302
|
+
.description('Generate a new resource (controller, service, routes)')
|
|
303
|
+
.action(async (resource) => {
|
|
359
304
|
try {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
await runPostGenerationCleanup(projectDir, config);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
try {
|
|
367
|
-
execSync('git init', { cwd: projectDir, stdio: 'pipe' });
|
|
368
|
-
execSync('git add -A', { cwd: projectDir, stdio: 'pipe' });
|
|
369
|
-
execSync('git commit -m "Initial commit from express-genix ai"', {
|
|
370
|
-
cwd: projectDir,
|
|
371
|
-
stdio: 'pipe',
|
|
372
|
-
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' },
|
|
373
|
-
});
|
|
374
|
-
} catch {
|
|
375
|
-
// Git not available
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
console.log(`\n✅ Project ${config.projectName} created from AI description!\n`);
|
|
379
|
-
console.log(` cd ${config.projectName}`);
|
|
380
|
-
console.log(' npm run dev\n');
|
|
381
|
-
|
|
382
|
-
// Prompt to install Coda VS Code extension
|
|
383
|
-
if (config.hasMCP) {
|
|
384
|
-
await promptCodaExtension();
|
|
385
|
-
}
|
|
386
|
-
|
|
305
|
+
const { generateResource } = require('./lib/scaffold');
|
|
306
|
+
await generateResource(resource, process.cwd());
|
|
307
|
+
console.log(`\n🎉 Resource "${resource}" generated successfully!`);
|
|
387
308
|
} catch (error) {
|
|
388
|
-
|
|
389
|
-
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
390
|
-
console.error('\n🧹 Rolled back partial project directory.');
|
|
391
|
-
}
|
|
392
|
-
console.error(`\n❌ Failed to create project: ${error.message}`);
|
|
309
|
+
console.error(`\n❌ Failed to generate resource: ${error.message}`);
|
|
393
310
|
process.exit(1);
|
|
394
311
|
}
|
|
395
312
|
});
|
package/lib/cleanup.js
CHANGED
|
@@ -6,23 +6,25 @@ const runPostGenerationCleanup = async (projectDir) => {
|
|
|
6
6
|
try {
|
|
7
7
|
// Step 1: Run eslint --fix
|
|
8
8
|
try {
|
|
9
|
-
execSync(
|
|
9
|
+
execSync('npx eslint . --fix', { cwd: projectDir, stdio: 'pipe' });
|
|
10
10
|
console.log('✅ ESLint auto-fixes applied');
|
|
11
|
-
} catch {
|
|
11
|
+
} catch (error) {
|
|
12
12
|
console.log('⚠️ Some ESLint issues may need manual fixing');
|
|
13
|
+
if (error.stderr) console.log(error.stderr.toString());
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
// Step 2: Format with Prettier
|
|
16
17
|
try {
|
|
17
|
-
execSync(
|
|
18
|
+
execSync('npx prettier --write .', { cwd: projectDir, stdio: 'pipe' });
|
|
18
19
|
console.log('✅ Code formatted with Prettier');
|
|
19
|
-
} catch {
|
|
20
|
+
} catch (error) {
|
|
20
21
|
console.warn('⚠️ Prettier formatting encountered issues');
|
|
22
|
+
if (error.stderr) console.warn(error.stderr.toString());
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
// Step 3: Validate
|
|
24
26
|
try {
|
|
25
|
-
execSync(
|
|
27
|
+
execSync('npx eslint .', { cwd: projectDir, encoding: 'utf8', stdio: 'pipe' });
|
|
26
28
|
console.log('✅ All linting checks passed');
|
|
27
29
|
} catch (error) {
|
|
28
30
|
const stdout = error.stdout || '';
|
package/lib/features.js
CHANGED
|
@@ -11,6 +11,7 @@ const addFeature = async (feature, projectDir) => {
|
|
|
11
11
|
const handlers = {
|
|
12
12
|
docker: () => addDocker(projectDir, templatesDir, pkg),
|
|
13
13
|
cicd: () => addCicd(projectDir, templatesDir, pkg),
|
|
14
|
+
infra: () => addInfra(projectDir, templatesDir, pkg),
|
|
14
15
|
auth: () => addAuth(projectDir, templatesDir, pkg),
|
|
15
16
|
websocket: () => addWebsocket(projectDir, templatesDir, pkg),
|
|
16
17
|
prisma: () => addPrisma(projectDir, templatesDir, pkg),
|
|
@@ -62,6 +63,47 @@ const addCicd = (projectDir, templatesDir, pkg) => {
|
|
|
62
63
|
console.log(' Created .github/workflows/ci.yml');
|
|
63
64
|
};
|
|
64
65
|
|
|
66
|
+
const addInfra = (projectDir, templatesDir, pkg) => {
|
|
67
|
+
const config = inferConfig(projectDir, pkg);
|
|
68
|
+
config.hasNginx = true; // Enable Nginx in docker-compose
|
|
69
|
+
|
|
70
|
+
// 1. Add Docker files
|
|
71
|
+
const dockerFiles = [
|
|
72
|
+
{ template: 'core/Dockerfile.ejs', output: 'Dockerfile' },
|
|
73
|
+
{ template: 'core/docker-compose.yml.ejs', output: 'docker-compose.yml' },
|
|
74
|
+
{ template: 'core/dockerignore.ejs', output: '.dockerignore' },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
for (const file of dockerFiles) {
|
|
78
|
+
const content = renderTemplate(templatesDir, file.template, config);
|
|
79
|
+
fs.writeFileSync(path.join(projectDir, file.output), content);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 2. Add CI/CD files
|
|
83
|
+
const workflowDir = path.join(projectDir, '.github', 'workflows');
|
|
84
|
+
fs.mkdirSync(workflowDir, { recursive: true });
|
|
85
|
+
const ciContent = renderTemplate(templatesDir, 'cicd/github-actions.yml.ejs', config);
|
|
86
|
+
fs.writeFileSync(path.join(workflowDir, 'ci.yml'), ciContent);
|
|
87
|
+
|
|
88
|
+
// 3. Add Infra specific files (Makefile, Nginx)
|
|
89
|
+
const infraFiles = [
|
|
90
|
+
{ template: 'infra/Makefile.ejs', output: 'Makefile' },
|
|
91
|
+
{ template: 'infra/nginx.conf.ejs', output: 'nginx/nginx.conf' },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
for (const file of infraFiles) {
|
|
95
|
+
const outputPath = path.join(projectDir, file.output);
|
|
96
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
97
|
+
const content = renderTemplate(templatesDir, file.template, config);
|
|
98
|
+
fs.writeFileSync(outputPath, content);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(' ✅ Production-Ready Infrastructure added!');
|
|
102
|
+
console.log(' Created Dockerfile, docker-compose.yml, .dockerignore');
|
|
103
|
+
console.log(' Created .github/workflows/ci.yml');
|
|
104
|
+
console.log(' Created Makefile and nginx/nginx.conf');
|
|
105
|
+
};
|
|
106
|
+
|
|
65
107
|
const addAuth = (projectDir, templatesDir, pkg) => {
|
|
66
108
|
const config = inferConfig(projectDir, pkg);
|
|
67
109
|
|
|
@@ -79,10 +121,13 @@ const addAuth = (projectDir, templatesDir, pkg) => {
|
|
|
79
121
|
const files = [
|
|
80
122
|
{ template: 'controllers/authController.js.ejs', output: `src/controllers/authController.${ext}` },
|
|
81
123
|
{ template: 'controllers/userController.js.ejs', output: `src/controllers/userController.${ext}` },
|
|
124
|
+
{ template: 'controllers/adminController.js.ejs', output: `src/controllers/adminController.${ext}` },
|
|
82
125
|
{ template: 'middleware/auth.js.ejs', output: `src/middleware/auth.${ext}` },
|
|
83
126
|
{ template: 'middleware/validation.js.ejs', output: `src/middleware/validation.${ext}` },
|
|
127
|
+
{ template: 'middleware/rbac.js.ejs', output: `src/middleware/rbac.${ext}` },
|
|
84
128
|
{ template: 'routes/authRoutes.js.ejs', output: `src/routes/authRoutes.${ext}` },
|
|
85
129
|
{ template: 'routes/userRoutes.js.ejs', output: `src/routes/userRoutes.${ext}` },
|
|
130
|
+
{ template: 'routes/adminRoutes.js.ejs', output: `src/routes/adminRoutes.${ext}` },
|
|
86
131
|
{ template: 'services/authService.js.ejs', output: `src/services/authService.${ext}` },
|
|
87
132
|
{ template: 'utils/validators.js.ejs', output: `src/utils/validators.${ext}` },
|
|
88
133
|
];
|
|
@@ -93,12 +138,14 @@ const addAuth = (projectDir, templatesDir, pkg) => {
|
|
|
93
138
|
} else if (config.db === 'mongodb') {
|
|
94
139
|
files.push(
|
|
95
140
|
{ template: 'services/userService.mongodb.js.ejs', output: `src/services/userService.${ext}` },
|
|
96
|
-
{ template: 'models/User.mongo.js.ejs', output: `src/models/User.${ext}` }
|
|
141
|
+
{ template: 'models/User.mongo.js.ejs', output: `src/models/User.${ext}` },
|
|
142
|
+
{ template: 'models/index.mongo.js.ejs', output: `src/models/index.${ext}` }
|
|
97
143
|
);
|
|
98
144
|
} else if (config.db === 'postgresql') {
|
|
99
145
|
files.push(
|
|
100
146
|
{ template: 'services/userService.postgres.js.ejs', output: `src/services/userService.${ext}` },
|
|
101
|
-
{ template: 'models/User.postgres.js.ejs', output: `src/models/User.${ext}` }
|
|
147
|
+
{ template: 'models/User.postgres.js.ejs', output: `src/models/User.${ext}` },
|
|
148
|
+
{ template: 'models/index.postgres.js.ejs', output: `src/models/index.${ext}` }
|
|
102
149
|
);
|
|
103
150
|
}
|
|
104
151
|
|
|
@@ -109,8 +156,28 @@ const addAuth = (projectDir, templatesDir, pkg) => {
|
|
|
109
156
|
fs.writeFileSync(outputPath, content);
|
|
110
157
|
}
|
|
111
158
|
|
|
159
|
+
// Update routes/index.js
|
|
160
|
+
const routesIndexPath = path.join(projectDir, `src/routes/index.${ext}`);
|
|
161
|
+
if (fs.existsSync(routesIndexPath)) {
|
|
162
|
+
let routesContent = fs.readFileSync(routesIndexPath, 'utf8');
|
|
163
|
+
|
|
164
|
+
// Add imports if not present
|
|
165
|
+
if (!routesContent.includes('authRoutes')) {
|
|
166
|
+
const imports = `const authRoutes = require('./authRoutes');\nconst userRoutes = require('./userRoutes');\nconst adminRoutes = require('./adminRoutes');\n`;
|
|
167
|
+
routesContent = routesContent.replace(/(const express = require\('express'\);)/, `$1\n${imports}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Add routes if not present
|
|
171
|
+
if (!routesContent.includes('/auth')) {
|
|
172
|
+
const routes = `router.use('/auth', authRoutes);\nrouter.use('/users', userRoutes);\nrouter.use('/admin', adminRoutes);\n`;
|
|
173
|
+
routesContent = routesContent.replace(/(const router = express\.Router\(\);)/, `$1\n\n${routes}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fs.writeFileSync(routesIndexPath, routesContent);
|
|
177
|
+
}
|
|
178
|
+
|
|
112
179
|
// Add auth dependencies
|
|
113
|
-
const depsToAdd = ['jsonwebtoken', 'bcryptjs', 'validator'];
|
|
180
|
+
const depsToAdd = ['jsonwebtoken', 'bcryptjs', 'validator', 'zod'];
|
|
114
181
|
const missing = depsToAdd.filter((dep) => !(pkg.dependencies && pkg.dependencies[dep]));
|
|
115
182
|
if (missing.length > 0) {
|
|
116
183
|
console.log(` Installing: ${missing.join(', ')}`);
|
|
@@ -142,13 +209,20 @@ const addWebsocket = (projectDir, templatesDir, pkg) => {
|
|
|
142
209
|
execSync('npm install socket.io', { cwd: projectDir, stdio: 'pipe' });
|
|
143
210
|
}
|
|
144
211
|
|
|
212
|
+
// Update server.js
|
|
213
|
+
const serverPath = path.join(projectDir, `src/server.${ext}`);
|
|
214
|
+
if (fs.existsSync(serverPath)) {
|
|
215
|
+
let serverContent = fs.readFileSync(serverPath, 'utf8');
|
|
216
|
+
|
|
217
|
+
if (!serverContent.includes('setupWebSocket')) {
|
|
218
|
+
serverContent = serverContent.replace(/(const app = require\('\.\/app'\);)/, `$1\nconst { setupWebSocket } = require('./config/websocket');\nconst http = require('http');`);
|
|
219
|
+
serverContent = serverContent.replace(/const server = app\.listen\(port, \(\) => \{/, `const httpServer = http.createServer(app);\n const io = setupWebSocket(httpServer);\n app.set('io', io);\n\n const server = httpServer.listen(port, () => {`);
|
|
220
|
+
fs.writeFileSync(serverPath, serverContent);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
145
224
|
console.log(' Created src/config/websocket.' + ext);
|
|
146
|
-
console.log('
|
|
147
|
-
console.log(' const http = require("http");');
|
|
148
|
-
console.log(' const { setupWebSocket } = require("./config/websocket");');
|
|
149
|
-
console.log(' const httpServer = http.createServer(app);');
|
|
150
|
-
console.log(' setupWebSocket(httpServer);');
|
|
151
|
-
console.log(' httpServer.listen(port);');
|
|
225
|
+
console.log(' ✅ WebSocket setup injected into src/server.' + ext);
|
|
152
226
|
};
|
|
153
227
|
|
|
154
228
|
const addPrisma = (projectDir, templatesDir, pkg) => {
|
|
@@ -200,9 +274,23 @@ const addPrisma = (projectDir, templatesDir, pkg) => {
|
|
|
200
274
|
console.log(' ⚠️ Run "npx prisma generate" manually');
|
|
201
275
|
}
|
|
202
276
|
|
|
277
|
+
// Update server.js
|
|
278
|
+
const serverPath = path.join(projectDir, `src/server.${ext}`);
|
|
279
|
+
if (fs.existsSync(serverPath)) {
|
|
280
|
+
let serverContent = fs.readFileSync(serverPath, 'utf8');
|
|
281
|
+
if (!serverContent.includes('db.connect()')) {
|
|
282
|
+
serverContent = serverContent.replace(/(const app = require\('\.\/app'\);)/, `$1\nconst db = require('./config/database');`);
|
|
283
|
+
serverContent = serverContent.replace(/(try \{)/, `$1\n await db.connect();`);
|
|
284
|
+
serverContent = serverContent.replace(/(server\.close\(async \(\) => \{)/, `$1\n await db.disconnect();`);
|
|
285
|
+
fs.writeFileSync(serverPath, serverContent);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
203
289
|
console.log(' Created prisma/schema.prisma and src/config/database.' + ext);
|
|
290
|
+
console.log(' ✅ Database connection injected into src/server.' + ext);
|
|
204
291
|
console.log(' ⚠️ Add DATABASE_URL to your .env file');
|
|
205
292
|
console.log(' ⚠️ Run "npx prisma migrate dev --name init" to create tables');
|
|
293
|
+
console.log(' ⚠️ Update your health check endpoint in src/app.' + ext + ' to check Prisma connection');
|
|
206
294
|
};
|
|
207
295
|
|
|
208
296
|
const inferConfig = (projectDir, pkg) => {
|
package/lib/generator.js
CHANGED
|
@@ -371,11 +371,12 @@ const generateFiles = async (config, projectDir) => {
|
|
|
371
371
|
const installDependencies = (projectDir) => {
|
|
372
372
|
try {
|
|
373
373
|
console.log('📦 Installing dependencies...');
|
|
374
|
-
execSync(
|
|
374
|
+
execSync('npm install', { cwd: projectDir, stdio: 'inherit' });
|
|
375
375
|
} catch (error) {
|
|
376
376
|
console.error('Failed to install dependencies:', error.message);
|
|
377
|
+
if (error.stderr) console.error(error.stderr.toString());
|
|
377
378
|
console.log('Try running "npm install" manually in the project directory');
|
|
378
|
-
throw
|
|
379
|
+
throw new Error('Failed to install dependencies');
|
|
379
380
|
}
|
|
380
381
|
};
|
|
381
382
|
|
package/lib/scaffold.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const ejs = require('ejs');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Capitalize first letter
|
|
7
|
+
*/
|
|
8
|
+
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Convert to camelCase
|
|
12
|
+
*/
|
|
13
|
+
const toCamelCase = (str) => {
|
|
14
|
+
return str.replace(/[-_](.)/g, (_, c) => c.toUpperCase());
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Convert to PascalCase
|
|
19
|
+
*/
|
|
20
|
+
const toPascalCase = (str) => {
|
|
21
|
+
const camel = toCamelCase(str);
|
|
22
|
+
return capitalize(camel);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Simple pluralize (adds 's' if not ending in 's')
|
|
27
|
+
*/
|
|
28
|
+
const pluralize = (str) => {
|
|
29
|
+
if (str.endsWith('s')) return str;
|
|
30
|
+
if (str.endsWith('y')) return str.slice(0, -1) + 'ies';
|
|
31
|
+
return str + 's';
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate a resource (controller, service, route)
|
|
36
|
+
*/
|
|
37
|
+
const generateResource = async (resourceName, projectDir) => {
|
|
38
|
+
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
39
|
+
|
|
40
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
41
|
+
throw new Error('No package.json found. Run this command from your express-genix project root.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
45
|
+
|
|
46
|
+
// Check if it's an express-genix project (or at least has the structure)
|
|
47
|
+
const srcDir = path.join(projectDir, 'src');
|
|
48
|
+
if (!fs.existsSync(srcDir)) {
|
|
49
|
+
throw new Error('No "src" directory found. Are you in an express-genix project?');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Determine features from package.json dependencies
|
|
53
|
+
const hasAuth = !!pkg.dependencies['jsonwebtoken'];
|
|
54
|
+
|
|
55
|
+
const nameCamel = toCamelCase(resourceName);
|
|
56
|
+
const namePascal = toPascalCase(resourceName);
|
|
57
|
+
const namePlural = pluralize(nameCamel);
|
|
58
|
+
|
|
59
|
+
const templateData = {
|
|
60
|
+
resourceName: resourceName.toLowerCase(),
|
|
61
|
+
resourceNameCamel: nameCamel,
|
|
62
|
+
resourceNamePascal: namePascal,
|
|
63
|
+
resourceNamePlural: namePlural,
|
|
64
|
+
hasAuth,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const templatesDir = path.join(__dirname, '..', 'templates', 'scaffold');
|
|
68
|
+
|
|
69
|
+
const filesToGenerate = [
|
|
70
|
+
{
|
|
71
|
+
template: 'controller.js.ejs',
|
|
72
|
+
dest: path.join(srcDir, 'controllers', `${nameCamel}Controller.js`),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
template: 'service.js.ejs',
|
|
76
|
+
dest: path.join(srcDir, 'services', `${nameCamel}Service.js`),
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
template: 'route.js.ejs',
|
|
80
|
+
dest: path.join(srcDir, 'routes', `${nameCamel}Routes.js`),
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
console.log(`\nGenerating resource: ${namePascal}...`);
|
|
85
|
+
|
|
86
|
+
for (const file of filesToGenerate) {
|
|
87
|
+
if (fs.existsSync(file.dest)) {
|
|
88
|
+
console.log(`⚠️ Skipping ${path.basename(file.dest)} (already exists)`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const templatePath = path.join(templatesDir, file.template);
|
|
93
|
+
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
|
94
|
+
const rendered = ejs.render(templateContent, templateData);
|
|
95
|
+
|
|
96
|
+
// Ensure directory exists
|
|
97
|
+
fs.mkdirSync(path.dirname(file.dest), { recursive: true });
|
|
98
|
+
|
|
99
|
+
fs.writeFileSync(file.dest, rendered);
|
|
100
|
+
console.log(`✅ Created ${path.relative(projectDir, file.dest)}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Inject route into src/routes/index.js
|
|
104
|
+
injectRoute(srcDir, nameCamel, namePlural);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Inject the new route into src/routes/index.js
|
|
109
|
+
*/
|
|
110
|
+
const injectRoute = (srcDir, nameCamel, namePlural) => {
|
|
111
|
+
const routesIndexPath = path.join(srcDir, 'routes', 'index.js');
|
|
112
|
+
|
|
113
|
+
if (!fs.existsSync(routesIndexPath)) {
|
|
114
|
+
console.log(`⚠️ Could not find src/routes/index.js to inject the route.`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let content = fs.readFileSync(routesIndexPath, 'utf8');
|
|
119
|
+
|
|
120
|
+
const requireStatement = `const ${nameCamel}Routes = require('./${nameCamel}Routes');`;
|
|
121
|
+
const useStatement = `router.use('/${namePlural}', ${nameCamel}Routes);`;
|
|
122
|
+
|
|
123
|
+
if (content.includes(requireStatement) || content.includes(useStatement)) {
|
|
124
|
+
console.log(`⚠️ Route already injected in src/routes/index.js`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Find the last require statement
|
|
129
|
+
const requireRegex = /const .* = require\('.*'\);/g;
|
|
130
|
+
let lastRequireMatch;
|
|
131
|
+
let match;
|
|
132
|
+
while ((match = requireRegex.exec(content)) !== null) {
|
|
133
|
+
lastRequireMatch = match;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (lastRequireMatch) {
|
|
137
|
+
const insertPos = lastRequireMatch.index + lastRequireMatch[0].length;
|
|
138
|
+
content = content.slice(0, insertPos) + '\n' + requireStatement + content.slice(insertPos);
|
|
139
|
+
} else {
|
|
140
|
+
// Fallback: put it after express require
|
|
141
|
+
content = content.replace(/const express = require\('express'\);/, `const express = require('express');\n${requireStatement}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Find the last router.use statement
|
|
145
|
+
const useRegex = /router\.use\('.*', .*\);/g;
|
|
146
|
+
let lastUseMatch;
|
|
147
|
+
while ((match = useRegex.exec(content)) !== null) {
|
|
148
|
+
lastUseMatch = match;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (lastUseMatch) {
|
|
152
|
+
const insertPos = lastUseMatch.index + lastUseMatch[0].length;
|
|
153
|
+
content = content.slice(0, insertPos) + '\n' + useStatement + content.slice(insertPos);
|
|
154
|
+
} else {
|
|
155
|
+
// Fallback: put it before router.get('/', ...)
|
|
156
|
+
content = content.replace(/router\.get\('\/'/, `${useStatement}\n\nrouter.get('/'`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fs.writeFileSync(routesIndexPath, content);
|
|
160
|
+
console.log(`✅ Injected /${namePlural} route into src/routes/index.js`);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
generateResource,
|
|
165
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "express-genix",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.1",
|
|
4
4
|
"description": "Production-grade CLI to generate Express apps with JWT, RBAC, GraphQL, TypeScript, Prisma, MongoDB, PostgreSQL, file uploads, email, background jobs, and more",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -112,20 +112,29 @@ const forgotPassword = async (req, res, next) => {
|
|
|
112
112
|
const user = await userService.findByEmail(email);
|
|
113
113
|
|
|
114
114
|
// Always return success to prevent email enumeration
|
|
115
|
+
const responseMessage = 'If an account with that email exists, a reset link has been sent.';
|
|
116
|
+
|
|
115
117
|
if (!user) {
|
|
116
|
-
return success(res, { message:
|
|
118
|
+
return success(res, { message: responseMessage });
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
|
|
121
|
+
// Process asynchronously to prevent timing attacks
|
|
122
|
+
(async () => {
|
|
123
|
+
try {
|
|
124
|
+
const resetToken = await authService.generateResetToken(email);
|
|
120
125
|
<% if (hasEmail) { %>
|
|
121
|
-
|
|
126
|
+
await emailService.sendPasswordResetEmail(email, resetToken);
|
|
122
127
|
<% } else { %>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
// TODO: Send email with reset link
|
|
129
|
+
// For development, log the token:
|
|
130
|
+
console.log(`Password reset token for ${email}: ${resetToken}`);
|
|
126
131
|
<% } %>
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error('Error in forgotPassword background task:', err);
|
|
134
|
+
}
|
|
135
|
+
})();
|
|
127
136
|
|
|
128
|
-
return success(res, { message:
|
|
137
|
+
return success(res, { message: responseMessage });
|
|
129
138
|
} catch (error) {
|
|
130
139
|
next(error);
|
|
131
140
|
}
|