create-backlist 5.0.7 → 6.0.2

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 (58) hide show
  1. package/bin/backlist.js +227 -0
  2. package/package.json +10 -4
  3. package/src/analyzer.js +210 -89
  4. package/src/db/prisma.ts +4 -0
  5. package/src/generators/dotnet.js +120 -94
  6. package/src/generators/java.js +205 -75
  7. package/src/generators/node.js +262 -85
  8. package/src/generators/python.js +54 -25
  9. package/src/generators/template.js +38 -2
  10. package/src/scanner/index.js +99 -0
  11. package/src/templates/dotnet/partials/Controller.cs.ejs +7 -14
  12. package/src/templates/dotnet/partials/Dto.cs.ejs +8 -0
  13. package/src/templates/java-spring/partials/ApplicationSeeder.java.ejs +30 -0
  14. package/src/templates/java-spring/partials/AuthController.java.ejs +62 -0
  15. package/src/templates/java-spring/partials/Controller.java.ejs +40 -50
  16. package/src/templates/java-spring/partials/Dockerfile.ejs +16 -0
  17. package/src/templates/java-spring/partials/Entity.java.ejs +16 -15
  18. package/src/templates/java-spring/partials/JwtAuthFilter.java.ejs +66 -0
  19. package/src/templates/java-spring/partials/JwtService.java.ejs +58 -0
  20. package/src/templates/java-spring/partials/Repository.java.ejs +9 -3
  21. package/src/templates/java-spring/partials/SecurityConfig.java.ejs +44 -0
  22. package/src/templates/java-spring/partials/Service.java.ejs +69 -0
  23. package/src/templates/java-spring/partials/User.java.ejs +33 -0
  24. package/src/templates/java-spring/partials/UserDetailsServiceImpl.java.ejs +33 -0
  25. package/src/templates/java-spring/partials/UserRepository.java.ejs +20 -0
  26. package/src/templates/java-spring/partials/docker-compose.yml.ejs +35 -0
  27. package/src/templates/node-ts-express/base/server.ts +12 -5
  28. package/src/templates/node-ts-express/base/tsconfig.json +13 -3
  29. package/src/templates/node-ts-express/partials/ApiDocs.ts.ejs +17 -7
  30. package/src/templates/node-ts-express/partials/App.test.ts.ejs +27 -27
  31. package/src/templates/node-ts-express/partials/Auth.controller.ts.ejs +56 -62
  32. package/src/templates/node-ts-express/partials/Auth.middleware.ts.ejs +21 -10
  33. package/src/templates/node-ts-express/partials/Controller.ts.ejs +40 -40
  34. package/src/templates/node-ts-express/partials/DbContext.cs.ejs +3 -3
  35. package/src/templates/node-ts-express/partials/Dockerfile.ejs +9 -11
  36. package/src/templates/node-ts-express/partials/Model.cs.ejs +25 -7
  37. package/src/templates/node-ts-express/partials/Model.ts.ejs +20 -12
  38. package/src/templates/node-ts-express/partials/PrismaController.ts.ejs +72 -55
  39. package/src/templates/node-ts-express/partials/PrismaSchema.prisma.ejs +27 -12
  40. package/src/templates/node-ts-express/partials/README.md.ejs +9 -12
  41. package/src/templates/node-ts-express/partials/Seeder.ts.ejs +44 -64
  42. package/src/templates/node-ts-express/partials/docker-compose.yml.ejs +31 -16
  43. package/src/templates/node-ts-express/partials/package.json.ejs +3 -1
  44. package/src/templates/node-ts-express/partials/prismaClient.ts.ejs +4 -0
  45. package/src/templates/node-ts-express/partials/routes.ts.ejs +35 -24
  46. package/src/templates/python-fastapi/Dockerfile.ejs +8 -0
  47. package/src/templates/python-fastapi/app/core/config.py.ejs +8 -0
  48. package/src/templates/python-fastapi/app/core/security.py.ejs +8 -0
  49. package/src/templates/python-fastapi/app/db.py.ejs +7 -0
  50. package/src/templates/python-fastapi/app/main.py.ejs +24 -0
  51. package/src/templates/python-fastapi/app/models/user.py.ejs +9 -0
  52. package/src/templates/python-fastapi/app/routers/auth.py.ejs +33 -0
  53. package/src/templates/python-fastapi/app/routers/model_routes.py.ejs +72 -0
  54. package/src/templates/python-fastapi/app/schemas/user.py.ejs +16 -0
  55. package/src/templates/python-fastapi/docker-compose.yml.ejs +19 -0
  56. package/src/templates/python-fastapi/requirements.txt.ejs +5 -1
  57. package/src/utils.js +19 -4
  58. package/bin/index.js +0 -141
@@ -7,27 +7,70 @@ const { analyzeFrontend } = require('../analyzer');
7
7
  const { renderAndWrite, getTemplatePath } = require('./template');
8
8
 
9
9
  async function generateNodeProject(options) {
10
- // v5.0: Destructure all new options
11
- const { projectDir, projectName, frontendSrcDir, dbType, addAuth, addSeeder, extraFeatures = [] } = options;
10
+ const {
11
+ projectDir,
12
+ projectName,
13
+ frontendSrcDir,
14
+ dbType,
15
+ addAuth,
16
+ addSeeder,
17
+ extraFeatures = [],
18
+ } = options;
19
+
12
20
  const port = 8000;
13
21
 
14
22
  try {
15
23
  // --- Step 1: Analyze Frontend ---
16
- console.log(chalk.blue(' -> Analyzing frontend for API endpoints...'));
24
+ console.log(chalk.blue(' -> Analyzing frontend for API endpoints (AST)...'));
17
25
  const endpoints = await analyzeFrontend(frontendSrcDir);
18
26
  if (endpoints.length > 0) console.log(chalk.green(` -> Found ${endpoints.length} endpoints.`));
19
27
  else console.log(chalk.yellow(' -> No API endpoints found. A basic project will be created.'));
20
28
 
21
- // --- Step 2: Identify Models to Generate ---
29
+ // Group endpoints by controller
30
+ const endpointsByController = new Map();
31
+ for (const ep of endpoints) {
32
+ const c = ep && ep.controllerName ? ep.controllerName : 'Default';
33
+ if (c === 'Default') continue;
34
+ if (!endpointsByController.has(c)) endpointsByController.set(c, []);
35
+ endpointsByController.get(c).push(ep);
36
+ }
37
+
38
+ // --- Step 2: Identify Models to Generate (merge fields per controller) ---
22
39
  const modelsToGenerate = new Map();
23
- endpoints.forEach(ep => {
24
- if (ep.schemaFields && ep.controllerName !== 'Default' && !modelsToGenerate.has(ep.controllerName)) {
25
- modelsToGenerate.set(ep.controllerName, { name: ep.controllerName, fields: Object.entries(ep.schemaFields).map(([key, type]) => ({ name: key, type, isUnique: key === 'email' })) });
40
+
41
+ for (const [controllerName, eps] of endpointsByController.entries()) {
42
+ const fieldMap = new Map();
43
+
44
+ for (const ep of eps) {
45
+ const fields = ep?.requestBody?.fields || ep?.schemaFields;
46
+ if (!fields) continue;
47
+
48
+ for (const [key, type] of Object.entries(fields)) {
49
+ fieldMap.set(key, { name: key, type, isUnique: key === 'email' });
50
+ }
51
+ }
52
+
53
+ if (fieldMap.size > 0) {
54
+ modelsToGenerate.set(controllerName, {
55
+ name: controllerName,
56
+ fields: Array.from(fieldMap.values()),
57
+ });
26
58
  }
27
- });
59
+ }
60
+
61
+ // Ensure User model if auth enabled
28
62
  if (addAuth && !modelsToGenerate.has('User')) {
29
63
  console.log(chalk.yellow(' -> Authentication requires a "User" model. Creating a default one.'));
30
- modelsToGenerate.set('User', { name: 'User', fields: [{ name: 'name', type: 'String' }, { name: 'email', type: 'String', isUnique: true }, { name: 'password', type: 'String' }] });
64
+ modelsToGenerate.set('User', {
65
+ name: 'User',
66
+ fields: [
67
+ { name: 'name', type: 'string' },
68
+ { name: 'email', type: 'string', isUnique: true },
69
+ { name: 'password', type: 'string' },
70
+ ],
71
+ });
72
+ // Also ensure controller exists so routes/controller can generate if frontend didn't call /api/users
73
+ if (!endpointsByController.has('User')) endpointsByController.set('User', []);
31
74
  }
32
75
 
33
76
  // --- Step 3: Base Scaffolding ---
@@ -36,28 +79,38 @@ async function generateNodeProject(options) {
36
79
  await fs.ensureDir(destSrcDir);
37
80
  await fs.copy(getTemplatePath('node-ts-express/base/server.ts'), path.join(destSrcDir, 'server.ts'));
38
81
  await fs.copy(getTemplatePath('node-ts-express/base/tsconfig.json'), path.join(projectDir, 'tsconfig.json'));
39
-
82
+
40
83
  // --- Step 4: Prepare and Write package.json ---
41
- const packageJsonContent = JSON.parse(await ejs.renderFile(getTemplatePath('node-ts-express/partials/package.json.ejs'), { projectName }));
42
-
43
- if (dbType === 'mongoose') packageJsonContent.dependencies['mongoose'] = '^7.6.3';
84
+ const packageJsonContent = JSON.parse(
85
+ await ejs.renderFile(getTemplatePath('node-ts-express/partials/package.json.ejs'), { projectName })
86
+ );
87
+
88
+ if (dbType === 'mongoose') {
89
+ packageJsonContent.dependencies['mongoose'] = '^7.6.3';
90
+ }
91
+
44
92
  if (dbType === 'prisma') {
45
93
  packageJsonContent.dependencies['@prisma/client'] = '^5.6.0';
46
94
  packageJsonContent.devDependencies['prisma'] = '^5.6.0';
47
- packageJsonContent.prisma = { seed: `ts-node ${addSeeder ? 'scripts/seeder.ts' : 'prisma/seed.ts'}` };
95
+ // prisma seed entry is fine, but if you do mongoose seeder only, don't point prisma seed to scripts/seeder.ts
96
+ packageJsonContent.prisma = { seed: `ts-node prisma/seed.ts` };
48
97
  }
98
+
49
99
  if (addAuth) {
50
100
  packageJsonContent.dependencies['jsonwebtoken'] = '^9.0.2';
51
101
  packageJsonContent.dependencies['bcryptjs'] = '^2.4.3';
52
102
  packageJsonContent.devDependencies['@types/jsonwebtoken'] = '^9.0.5';
53
103
  packageJsonContent.devDependencies['@types/bcryptjs'] = '^2.4.6';
54
104
  }
55
- if (addSeeder) {
105
+
106
+ // Seeder deps only if mongoose seeder enabled
107
+ if (addSeeder && dbType === 'mongoose') {
56
108
  packageJsonContent.devDependencies['@faker-js/faker'] = '^8.3.1';
57
109
  if (!packageJsonContent.dependencies['chalk']) packageJsonContent.dependencies['chalk'] = '^4.1.2';
58
110
  packageJsonContent.scripts['seed'] = `ts-node scripts/seeder.ts`;
59
111
  packageJsonContent.scripts['destroy'] = `ts-node scripts/seeder.ts -d`;
60
112
  }
113
+
61
114
  if (extraFeatures.includes('testing')) {
62
115
  packageJsonContent.devDependencies['jest'] = '^29.7.0';
63
116
  packageJsonContent.devDependencies['supertest'] = '^6.3.3';
@@ -66,131 +119,255 @@ async function generateNodeProject(options) {
66
119
  packageJsonContent.devDependencies['ts-jest'] = '^29.1.1';
67
120
  packageJsonContent.scripts['test'] = 'jest --detectOpenHandles --forceExit';
68
121
  }
122
+
69
123
  if (extraFeatures.includes('swagger')) {
70
124
  packageJsonContent.dependencies['swagger-ui-express'] = '^5.0.0';
71
125
  packageJsonContent.dependencies['swagger-jsdoc'] = '^6.2.8';
72
126
  packageJsonContent.devDependencies['@types/swagger-ui-express'] = '^4.1.6';
73
127
  }
128
+
74
129
  await fs.writeJson(path.join(projectDir, 'package.json'), packageJsonContent, { spaces: 2 });
75
-
76
- // --- Step 5: Generate DB-specific files & Controllers ---
130
+
131
+ // --- Step 5: Generate DB-specific files & Models ---
132
+ await fs.ensureDir(path.join(destSrcDir, 'controllers'));
133
+
77
134
  if (modelsToGenerate.size > 0) {
78
- await fs.ensureDir(path.join(destSrcDir, 'controllers'));
79
- if (dbType === 'mongoose') {
80
- console.log(chalk.blue(' -> Generating Mongoose models and controllers...'));
81
- await fs.ensureDir(path.join(destSrcDir, 'models'));
82
- for (const [modelName, modelData] of modelsToGenerate.entries()) {
83
- const schema = modelData.fields.reduce((acc, field) => { acc[field.name] = field.type; return acc; }, {});
84
- await renderAndWrite(getTemplatePath('node-ts-express/partials/Model.ts.ejs'), path.join(destSrcDir, 'models', `${modelName}.model.ts`), { modelName, schema, projectName });
85
- }
86
- } else if (dbType === 'prisma') {
87
- console.log(chalk.blue(' -> Generating Prisma schema...'));
88
- await fs.ensureDir(path.join(projectDir, 'prisma'));
89
- await renderAndWrite(getTemplatePath('node-ts-express/partials/PrismaSchema.prisma.ejs'), path.join(projectDir, 'prisma', 'schema.prisma'), { modelsToGenerate: Array.from(modelsToGenerate.values()) });
90
- }
91
- console.log(chalk.blue(' -> Generating controllers...'));
92
- for (const [modelName] of modelsToGenerate.entries()) {
93
- const templateFile = dbType === 'mongoose' ? 'Controller.ts.ejs' : 'PrismaController.ts.ejs';
94
- await renderAndWrite(getTemplatePath(`node-ts-express/partials/${templateFile}`), path.join(destSrcDir, 'controllers', `${modelName}.controller.ts`), { modelName, projectName });
135
+ if (dbType === 'mongoose') {
136
+ console.log(chalk.blue(' -> Generating Mongoose models...'));
137
+ await fs.ensureDir(path.join(destSrcDir, 'models'));
138
+
139
+ for (const [modelName, modelData] of modelsToGenerate.entries()) {
140
+ // normalize schema fields to simple types (string/number/boolean)
141
+ const schema = {};
142
+ for (const field of modelData.fields || []) {
143
+ const t = String(field.type || 'string').toLowerCase();
144
+ schema[field.name] = (t === 'number' || t === 'boolean') ? t : 'string';
145
+ }
146
+
147
+ await renderAndWrite(
148
+ getTemplatePath('node-ts-express/partials/Model.ts.ejs'),
149
+ path.join(destSrcDir, 'models', `${modelName}.model.ts`),
150
+ { modelName, schema, projectName }
151
+ );
95
152
  }
153
+ }
154
+
155
+ if (dbType === 'prisma') {
156
+ console.log(chalk.blue(' -> Generating Prisma schema + client...'));
157
+ await fs.ensureDir(path.join(projectDir, 'prisma'));
158
+
159
+ await renderAndWrite(
160
+ getTemplatePath('node-ts-express/partials/PrismaSchema.prisma.ejs'),
161
+ path.join(projectDir, 'prisma', 'schema.prisma'),
162
+ { modelsToGenerate: Array.from(modelsToGenerate.values()), projectName }
163
+ );
164
+
165
+ // Prisma client singleton
166
+ await fs.ensureDir(path.join(destSrcDir, 'db'));
167
+ await renderAndWrite(
168
+ getTemplatePath('node-ts-express/partials/prismaClient.ts.ejs'),
169
+ path.join(destSrcDir, 'db', 'prisma.ts'),
170
+ { projectName }
171
+ );
172
+ }
96
173
  }
97
-
98
- // --- Step 6: Generate Authentication Boilerplate ---
174
+
175
+ // --- Step 5b: Generate Controllers from Endpoints (AST-driven) ---
176
+ console.log(chalk.blue(' -> Generating controllers (from endpoints)...'));
177
+ for (const [controllerName, controllerEndpoints] of endpointsByController.entries()) {
178
+ const tpl =
179
+ dbType === 'prisma'
180
+ ? 'node-ts-express/partials/PrismaController.FromEndpoints.ts.ejs'
181
+ : 'node-ts-express/partials/Controller.FromEndpoints.ts.ejs';
182
+
183
+ await renderAndWrite(
184
+ getTemplatePath(tpl),
185
+ path.join(destSrcDir, 'controllers', `${controllerName}.controller.ts`),
186
+ { controllerName, endpoints: controllerEndpoints, projectName, dbType }
187
+ );
188
+ }
189
+
190
+ // --- Step 6: Authentication Boilerplate ---
99
191
  if (addAuth) {
100
- console.log(chalk.blue(' -> Generating authentication boilerplate...'));
101
- await fs.ensureDir(path.join(destSrcDir, 'routes'));
102
- await fs.ensureDir(path.join(destSrcDir, 'middleware'));
103
- await renderAndWrite(getTemplatePath('node-ts-express/partials/Auth.controller.ts.ejs'), path.join(destSrcDir, 'controllers', 'Auth.controller.ts'), { dbType, projectName });
104
- await renderAndWrite(getTemplatePath('node-ts-express/partials/Auth.routes.ts.ejs'), path.join(destSrcDir, 'routes', 'Auth.routes.ts'), { projectName });
105
- await renderAndWrite(getTemplatePath('node-ts-express/partials/Auth.middleware.ts.ejs'), path.join(destSrcDir, 'middleware', 'Auth.middleware.ts'), { projectName });
106
-
107
- if (dbType === 'mongoose') {
108
- const userModelPath = path.join(destSrcDir, 'models', 'User.model.ts');
109
- if (await fs.pathExists(userModelPath)) {
110
- let userModelContent = await fs.readFile(userModelPath, 'utf-8');
111
- if (!userModelContent.includes('bcryptjs')) {
112
- userModelContent = userModelContent.replace(`import mongoose, { Schema, Document } from 'mongoose';`, `import mongoose, { Schema, Document } from 'mongoose';\nimport bcrypt from 'bcryptjs';`);
113
- const preSaveHook = `\n// Hash password before saving\nUserSchema.pre('save', async function(next) {\n if (!this.isModified('password')) { return next(); }\n const salt = await bcrypt.genSalt(10);\n this.password = await bcrypt.hash(this.password, salt);\n next();\n});\n`;
114
- userModelContent = userModelContent.replace(`// Create and export the Model`, `${preSaveHook}\n// Create and export the Model`);
115
- await fs.writeFile(userModelPath, userModelContent);
116
- }
117
- }
192
+ console.log(chalk.blue(' -> Generating authentication boilerplate...'));
193
+ await fs.ensureDir(path.join(destSrcDir, 'routes'));
194
+ await fs.ensureDir(path.join(destSrcDir, 'middleware'));
195
+
196
+ await renderAndWrite(
197
+ getTemplatePath('node-ts-express/partials/Auth.controller.ts.ejs'),
198
+ path.join(destSrcDir, 'controllers', 'Auth.controller.ts'),
199
+ { dbType, projectName }
200
+ );
201
+
202
+ await renderAndWrite(
203
+ getTemplatePath('node-ts-express/partials/Auth.routes.ts.ejs'),
204
+ path.join(destSrcDir, 'routes', 'Auth.routes.ts'),
205
+ { projectName }
206
+ );
207
+
208
+ await renderAndWrite(
209
+ getTemplatePath('node-ts-express/partials/Auth.middleware.ts.ejs'),
210
+ path.join(destSrcDir, 'middleware', 'Auth.middleware.ts'),
211
+ { projectName }
212
+ );
213
+
214
+ // For mongoose: inject password hashing hook into User.model.ts if exists
215
+ if (dbType === 'mongoose') {
216
+ const userModelPath = path.join(destSrcDir, 'models', 'User.model.ts');
217
+ if (await fs.pathExists(userModelPath)) {
218
+ let userModelContent = await fs.readFile(userModelPath, 'utf-8');
219
+
220
+ if (!userModelContent.includes(`import bcrypt`)) {
221
+ userModelContent = userModelContent.replace(
222
+ `import mongoose, { Schema, Document } from 'mongoose';`,
223
+ `import mongoose, { Schema, Document } from 'mongoose';\nimport bcrypt from 'bcryptjs';`
224
+ );
225
+ }
226
+
227
+ if (!userModelContent.includes(`pre('save'`)) {
228
+ const preSaveHook =
229
+ `\n// Hash password before saving\n` +
230
+ `UserSchema.pre('save', async function(next) {\n` +
231
+ ` if (!this.isModified('password')) { return next(); }\n` +
232
+ ` const salt = await bcrypt.genSalt(10);\n` +
233
+ ` this.password = await bcrypt.hash(this.password, salt);\n` +
234
+ ` next();\n` +
235
+ `});\n`;
236
+
237
+ userModelContent = userModelContent.replace(
238
+ `// Create and export the Model`,
239
+ `${preSaveHook}\n// Create and export the Model`
240
+ );
241
+ }
242
+
243
+ await fs.writeFile(userModelPath, userModelContent);
118
244
  }
245
+ }
119
246
  }
120
247
 
121
- // --- Step 7: Generate Seeder Script ---
122
- if (addSeeder) {
123
- console.log(chalk.blue(' -> Generating database seeder script...'));
248
+ // --- Step 7: Seeder Script (mongoose only) ---
249
+ if (addSeeder && dbType === 'mongoose') {
250
+ console.log(chalk.blue(' -> Generating database seeder script (mongoose)...'));
124
251
  await fs.ensureDir(path.join(projectDir, 'scripts'));
125
- await renderAndWrite(getTemplatePath('node-ts-express/partials/Seeder.ts.ejs'), path.join(projectDir, 'scripts', 'seeder.ts'), { projectName });
252
+ await renderAndWrite(
253
+ getTemplatePath('node-ts-express/partials/Seeder.ts.ejs'),
254
+ path.join(projectDir, 'scripts', 'seeder.ts'),
255
+ { projectName }
256
+ );
126
257
  }
127
258
 
128
- // --- Step 8: Generate Extra Features ---
259
+ // --- Step 8: Extra Features ---
129
260
  if (extraFeatures.includes('docker')) {
130
261
  console.log(chalk.blue(' -> Generating Docker files...'));
131
- await renderAndWrite(getTemplatePath('node-ts-express/partials/Dockerfile.ejs'), path.join(projectDir, 'Dockerfile'), { dbType, port });
132
- await renderAndWrite(getTemplatePath('node-ts-express/partials/docker-compose.yml.ejs'), path.join(projectDir, 'docker-compose.yml'), { projectName, dbType, port });
262
+ await renderAndWrite(
263
+ getTemplatePath('node-ts-express/partials/Dockerfile.ejs'),
264
+ path.join(projectDir, 'Dockerfile'),
265
+ { dbType, port }
266
+ );
267
+ await renderAndWrite(
268
+ getTemplatePath('node-ts-express/partials/docker-compose.yml.ejs'),
269
+ path.join(projectDir, 'docker-compose.yml'),
270
+ { projectName, dbType, port }
271
+ );
133
272
  }
273
+
134
274
  if (extraFeatures.includes('swagger')) {
135
275
  console.log(chalk.blue(' -> Generating API documentation setup...'));
136
276
  await fs.ensureDir(path.join(destSrcDir, 'utils'));
137
- await renderAndWrite(getTemplatePath('node-ts-express/partials/ApiDocs.ts.ejs'), path.join(destSrcDir, 'utils', 'swagger.ts'), { projectName, port });
277
+ await renderAndWrite(
278
+ getTemplatePath('node-ts-express/partials/ApiDocs.ts.ejs'),
279
+ path.join(destSrcDir, 'utils', 'swagger.ts'),
280
+ { projectName, port, addAuth }
281
+ );
138
282
  }
283
+
139
284
  if (extraFeatures.includes('testing')) {
140
285
  console.log(chalk.blue(' -> Generating testing boilerplate...'));
141
- const jestConfig = `/** @type {import('ts-jest').JestConfigWithTsJest} */\nmodule.exports = {\n preset: 'ts-jest',\n testEnvironment: 'node',\n verbose: true,\n};`;
286
+ const jestConfig =
287
+ `/** @type {import('ts-jest').JestConfigWithTsJest} */\n` +
288
+ `module.exports = {\n` +
289
+ ` preset: 'ts-jest',\n` +
290
+ ` testEnvironment: 'node',\n` +
291
+ ` verbose: true,\n` +
292
+ `};\n`;
142
293
  await fs.writeFile(path.join(projectDir, 'jest.config.js'), jestConfig);
294
+
143
295
  await fs.ensureDir(path.join(projectDir, 'src', '__tests__'));
144
- await renderAndWrite(getTemplatePath('node-ts-express/partials/App.test.ts.ejs'), path.join(projectDir, 'src', '__tests__', 'api.test.ts'), { addAuth });
296
+ await renderAndWrite(
297
+ getTemplatePath('node-ts-express/partials/App.test.ts.ejs'),
298
+ path.join(projectDir, 'src', '__tests__', 'api.test.ts'),
299
+ { addAuth, endpoints }
300
+ );
145
301
  }
146
302
 
147
- // --- Step 9: Generate Main Route File & Inject Logic into Server ---
148
- await renderAndWrite(getTemplatePath('node-ts-express/partials/routes.ts.ejs'), path.join(destSrcDir, 'routes.ts'), { endpoints, addAuth, dbType });
149
-
303
+ // --- Step 9: Generate Main Route File & Inject into server.ts ---
304
+ await renderAndWrite(
305
+ getTemplatePath('node-ts-express/partials/routes.ts.ejs'),
306
+ path.join(destSrcDir, 'routes.ts'),
307
+ { endpoints, addAuth, dbType }
308
+ );
309
+
150
310
  let serverFileContent = await fs.readFile(path.join(destSrcDir, 'server.ts'), 'utf-8');
151
- let dbConnectionCode = '', swaggerInjector = '', authRoutesInjector = '';
152
311
 
312
+ // Mongoose db connect injection only; prisma uses src/db/prisma.ts
313
+ let dbConnectionCode = '';
153
314
  if (dbType === 'mongoose') {
154
- dbConnectionCode = `\n// --- Database Connection ---\nimport mongoose from 'mongoose';\nconst MONGO_URI = process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/${projectName}';\nmongoose.connect(MONGO_URI).then(() => console.log('MongoDB Connected...')).catch(err => console.error(err));\n// -------------------------\n`;
155
- } else if (dbType === 'prisma') {
156
- dbConnectionCode = `\nimport { PrismaClient } from '@prisma/client';\nexport const prisma = new PrismaClient();\n`;
315
+ dbConnectionCode =
316
+ `\n// --- Database Connection ---\n` +
317
+ `import mongoose from 'mongoose';\n` +
318
+ `const MONGO_URI = process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/${projectName}';\n` +
319
+ `mongoose.connect(MONGO_URI)\n` +
320
+ ` .then(() => console.log('MongoDB Connected...'))\n` +
321
+ ` .catch(err => console.error(err));\n` +
322
+ `// -------------------------\n`;
157
323
  }
324
+
325
+ let swaggerInjector = '';
158
326
  if (extraFeatures.includes('swagger')) {
159
- swaggerInjector = `\nimport { setupSwagger } from './utils/swagger';\nsetupSwagger(app);\n`;
327
+ swaggerInjector = `\nimport { setupSwagger } from './utils/swagger';\nsetupSwagger(app);\n`;
160
328
  }
329
+
330
+ let authRoutesInjector = '';
161
331
  if (addAuth) {
162
- authRoutesInjector = `import authRoutes from './routes/Auth.routes';\napp.use('/api/auth', authRoutes);\n\n`;
332
+ authRoutesInjector = `import authRoutes from './routes/Auth.routes';\napp.use('/api/auth', authRoutes);\n\n`;
163
333
  }
164
334
 
165
335
  serverFileContent = serverFileContent
166
- .replace("dotenv.config();", `dotenv.config();${dbConnectionCode}`)
167
- .replace('// INJECT:ROUTES', `${authRoutesInjector}import apiRoutes from './routes';\napp.use('/api', apiRoutes);`);
168
-
336
+ .replace('dotenv.config();', `dotenv.config();${dbConnectionCode}`)
337
+ .replace('// INJECT:ROUTES', `${authRoutesInjector}import apiRoutes from './routes';\napp.use('/api', apiRoutes);\n`);
338
+
339
+ // place swagger setup before listen
169
340
  const listenRegex = /(app\.listen\()/;
170
341
  serverFileContent = serverFileContent.replace(listenRegex, `${swaggerInjector}\n$1`);
342
+
171
343
  await fs.writeFile(path.join(destSrcDir, 'server.ts'), serverFileContent);
172
344
 
173
- // --- Step 10: Install Dependencies & Run Post-install Scripts ---
345
+ // --- Step 10: Install Dependencies & Post-install ---
174
346
  console.log(chalk.magenta(' -> Installing dependencies... This may take a moment.'));
175
347
  await execa('npm', ['install'], { cwd: projectDir });
348
+
176
349
  if (dbType === 'prisma') {
177
350
  console.log(chalk.blue(' -> Running `prisma generate`...'));
178
351
  await execa('npx', ['prisma', 'generate'], { cwd: projectDir });
179
352
  }
180
-
181
- // --- Step 11: Generate Final Files (.env.example) ---
353
+
354
+ // --- Step 11: Final Files (.env.example) ---
182
355
  let envContent = `PORT=${port}\n`;
356
+
183
357
  if (dbType === 'mongoose') {
184
- envContent += `MONGO_URI=mongodb://root:example@db:27017/${projectName}?authSource=admin\n`;
358
+ envContent += `MONGO_URI=mongodb://127.0.0.1:27017/${projectName}\n`;
185
359
  } else if (dbType === 'prisma') {
186
- envContent += `DATABASE_URL="postgresql://user:password@db:5432/${projectName}?schema=public"\n`;
360
+ envContent += `DATABASE_URL="postgresql://user:password@localhost:5432/${projectName}?schema=public"\n`;
187
361
  }
188
- if (addAuth) envContent += `JWT_SECRET=your_super_secret_jwt_key_12345\n`;
189
- if (extraFeatures.includes('docker')) {
190
- envContent += `\n# Docker-compose credentials (used in docker-compose.yml)\nDB_USER=user\nDB_PASSWORD=password\nDB_NAME=${projectName}`;
362
+
363
+ if (addAuth) envContent += `JWT_SECRET=change_me_long_secret_change_me_long_secret\n`;
364
+
365
+ if (extraFeatures.includes('docker') && dbType === 'prisma') {
366
+ envContent += `\n# Docker-compose credentials\nDB_USER=postgres\nDB_PASSWORD=password\nDB_NAME=${projectName}\n`;
191
367
  }
368
+
192
369
  await fs.writeFile(path.join(projectDir, '.env.example'), envContent);
193
-
370
+
194
371
  } catch (error) {
195
372
  throw error;
196
373
  }
@@ -19,52 +19,81 @@ async function generatePythonProject(options) {
19
19
  }
20
20
  });
21
21
 
22
- // --- Step 2: Scaffold Base Python Project ---
23
- console.log(chalk.blue(' -> Scaffolding Python (FastAPI) project...'));
22
+ // Add a default User model if none was detected but auth might be added later
23
+ if (!modelsToGenerate.has('User')) {
24
+ modelsToGenerate.set('User', { name: 'User', fields: [{ name: 'name', type: 'String' }, { name: 'email', type: 'String'}] });
25
+ }
26
+
27
+ // --- Step 2: Scaffold Base Python Project Directories ---
28
+ console.log(chalk.blue(' -> Scaffolding Python (FastAPI) project structure...'));
24
29
  const appDir = path.join(projectDir, 'app');
25
- const routesDir = path.join(appDir, 'routes');
30
+ const coreDir = path.join(appDir, 'core');
31
+ const dbDir = path.join(appDir, 'db'); // For DB connection
32
+ const modelsDir = path.join(appDir, 'models');
33
+ const schemasDir = path.join(appDir, 'schemas');
34
+ const routesDir = path.join(appDir, 'routers');
35
+
26
36
  await fs.ensureDir(appDir);
37
+ await fs.ensureDir(coreDir);
38
+ await fs.ensureDir(dbDir);
39
+ await fs.ensureDir(modelsDir);
40
+ await fs.ensureDir(schemasDir);
27
41
  await fs.ensureDir(routesDir);
28
42
 
29
- // --- Step 3: Generate Files from Templates ---
43
+ // --- Step 3: Generate All Python Files from Templates ---
30
44
  const controllers = Array.from(modelsToGenerate.keys());
31
45
 
32
- // Generate main.py
46
+ // Generate main application file
33
47
  await renderAndWrite(getTemplatePath('python-fastapi/main.py.ejs'), path.join(projectDir, 'app', 'main.py'), { projectName, controllers });
34
-
35
- // Generate requirements.txt
48
+ // Generate dependency file
36
49
  await renderAndWrite(getTemplatePath('python-fastapi/requirements.txt.ejs'), path.join(projectDir, 'requirements.txt'), {});
37
50
 
38
- // Generate route file for each model
51
+ // Generate core files (config, security)
52
+ await renderAndWrite(getTemplatePath('python-fastapi/app/core/config.py.ejs'), path.join(coreDir, 'config.py'), { projectName });
53
+ await renderAndWrite(getTemplatePath('python-fastapi/app/core/security.py.ejs'), path.join(coreDir, 'security.py'), {});
54
+
55
+ // Generate DB connection and base model
56
+ await renderAndWrite(getTemplatePath('python-fastapi/app/db.py.ejs'), path.join(appDir, 'db.py'), {});
57
+
58
+ // Generate model and schema files for User (for auth)
59
+ await renderAndWrite(getTemplatePath('python-fastapi/app/models/user.py.ejs'), path.join(modelsDir, 'user.py'), {});
60
+ await renderAndWrite(getTemplatePath('python-fastapi/app/schemas/user.py.ejs'), path.join(schemasDir, 'user.py'), {});
61
+
62
+ // Generate router for auth
63
+ await renderAndWrite(getTemplatePath('python-fastapi/app/routers/auth.py.ejs'), path.join(routesDir, 'auth.py'), {});
64
+
65
+ // Generate router for each detected model
39
66
  for (const [modelName, modelData] of modelsToGenerate.entries()) {
40
- await renderAndWrite(
41
- getTemplatePath('python-fastapi/routes.py.ejs'),
42
- path.join(routesDir, `${modelName.toLowerCase()}_routes.py`),
43
- { modelName, schema: modelData }
44
- );
67
+ if(modelName.toLowerCase() !== 'user') { // User model is handled separately
68
+ // In a full implementation, you'd have generic model/schema templates too
69
+ }
70
+ await renderAndWrite(getTemplatePath('python-fastapi/app/routers/model_routes.py.ejs'), path.join(routesDir, `${modelName.toLowerCase()}_routes.py`), { modelName, schema: modelData });
45
71
  }
46
72
 
47
73
  // --- Step 4: Setup Virtual Environment and Install Dependencies ---
48
74
  console.log(chalk.magenta(' -> Setting up virtual environment and installing dependencies...'));
49
- // Create a virtual environment
50
75
  await execa('python', ['-m', 'venv', 'venv'], { cwd: projectDir });
51
76
 
52
- // Determine the correct pip executable path based on OS
53
- const pipPath = process.platform === 'win32'
54
- ? path.join('venv', 'Scripts', 'pip')
55
- : path.join('venv', 'bin', 'pip');
56
-
57
- // Install dependencies using the virtual environment's pip
77
+ const pipPath = process.platform === 'win32' ? path.join('venv', 'Scripts', 'pip') : path.join('venv', 'bin', 'pip');
58
78
  await execa(path.join(projectDir, pipPath), ['install', '-r', 'requirements.txt'], { cwd: projectDir });
59
79
 
60
- console.log(chalk.green(' -> Python backend generation is complete!'));
61
- console.log(chalk.yellow('\nTo run your new Python backend:'));
62
- console.log(chalk.cyan(' 1. Activate the virtual environment: `source venv/bin/activate` (or `venv\\Scripts\\activate` on Windows)'));
63
- console.log(chalk.cyan(' 2. Start the server: `uvicorn app.main:app --reload`'));
80
+ // --- Step 5: Generate Docker and .env files ---
81
+ await renderAndWrite(getTemplatePath('python-fastapi/Dockerfile.ejs'), path.join(projectDir, 'Dockerfile'), {});
82
+ await renderAndWrite(getTemplatePath('python-fastapi/docker-compose.yml.ejs'), path.join(projectDir, 'docker-compose.yml'), { projectName });
83
+
84
+ const envContent = `DATABASE_URL="postgresql://postgres:password@db:5432/${projectName}"\nJWT_SECRET="a_very_secret_key_change_this"`;
85
+ await fs.writeFile(path.join(projectDir, '.env'), envContent);
86
+ await fs.writeFile(path.join(projectDir, '.env.example'), envContent);
87
+
88
+
89
+ console.log(chalk.green(' -> Python (FastAPI) backend generation is complete!'));
90
+ console.log(chalk.yellow('\nTo run your new Python backend with Docker:'));
91
+ console.log(chalk.cyan(' 1. Make sure Docker Desktop is running.'));
92
+ console.log(chalk.cyan(' 2. Run: `docker-compose up --build`'));
93
+ console.log(chalk.cyan(' 3. API will be available at http://localhost:8000 and docs at http://localhost:8000/docs'));
64
94
 
65
95
 
66
96
  } catch (error) {
67
- // Improve error message for command not found
68
97
  if (error.code === 'ENOENT') {
69
98
  throw new Error(`'${error.command}' command not found. Please ensure Python and venv are installed and in your system's PATH.`);
70
99
  }
@@ -2,11 +2,47 @@ const fs = require('fs-extra');
2
2
  const ejs = require('ejs');
3
3
  const path = require('path');
4
4
 
5
+ function pascalCase(str) {
6
+ return String(str || '')
7
+ .replace(/[-_]+(.)/g, (_, c) => c.toUpperCase())
8
+ .replace(/^\w/, c => c.toUpperCase())
9
+ .replace(/[^a-zA-Z0-9]/g, '');
10
+ }
11
+
12
+ function camelCase(str) {
13
+ const p = pascalCase(str);
14
+ return p ? p.charAt(0).toLowerCase() + p.slice(1) : '';
15
+ }
16
+
17
+ function mapTsType(t) {
18
+ const x = String(t || '').toLowerCase();
19
+ if (x === 'number' || x === 'int' || x === 'integer' || x === 'float' || x === 'double') return 'number';
20
+ if (x === 'boolean' || x === 'bool') return 'boolean';
21
+ return 'string';
22
+ }
23
+
24
+ function mapMongooseType(t) {
25
+ const x = String(t || '').toLowerCase();
26
+ if (x === 'number' || x === 'int' || x === 'integer' || x === 'float' || x === 'double') return 'Number';
27
+ if (x === 'boolean' || x === 'bool') return 'Boolean';
28
+ return 'String';
29
+ }
30
+
5
31
  async function renderAndWrite(templatePath, outPath, data) {
32
+ const helpers = { pascalCase, camelCase, mapTsType, mapMongooseType };
33
+
6
34
  try {
7
35
  const tpl = await fs.readFile(templatePath, 'utf-8');
8
- const code = ejs.render(tpl, data || {}, { filename: templatePath }); // filename helps with EJS errors
9
- await fs.outputFile(outPath, code);
36
+ const code = ejs.render(tpl, { ...(data || {}), ...helpers }, { filename: templatePath });
37
+
38
+ // avoid rewriting identical content (useful in watch mode)
39
+ const exists = await fs.pathExists(outPath);
40
+ if (exists) {
41
+ const current = await fs.readFile(outPath, 'utf-8');
42
+ if (current === code) return;
43
+ }
44
+
45
+ await fs.outputFile(outPath, code.endsWith('\n') ? code : code + '\n');
10
46
  } catch (err) {
11
47
  console.error('EJS render failed for:', templatePath);
12
48
  console.error('Data keys:', Object.keys(data || {}));