create-backlist 6.0.0 → 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 (45) 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 +157 -109
  7. package/src/generators/node.js +262 -85
  8. package/src/generators/template.js +38 -2
  9. package/src/scanner/index.js +99 -0
  10. package/src/templates/dotnet/partials/Controller.cs.ejs +7 -14
  11. package/src/templates/dotnet/partials/Dto.cs.ejs +8 -0
  12. package/src/templates/java-spring/partials/ApplicationSeeder.java.ejs +7 -2
  13. package/src/templates/java-spring/partials/AuthController.java.ejs +23 -10
  14. package/src/templates/java-spring/partials/Controller.java.ejs +17 -6
  15. package/src/templates/java-spring/partials/Dockerfile.ejs +6 -1
  16. package/src/templates/java-spring/partials/Entity.java.ejs +15 -5
  17. package/src/templates/java-spring/partials/JwtAuthFilter.java.ejs +30 -7
  18. package/src/templates/java-spring/partials/JwtService.java.ejs +38 -10
  19. package/src/templates/java-spring/partials/Repository.java.ejs +10 -1
  20. package/src/templates/java-spring/partials/Service.java.ejs +45 -7
  21. package/src/templates/java-spring/partials/User.java.ejs +17 -4
  22. package/src/templates/java-spring/partials/UserDetailsServiceImpl.java.ejs +10 -4
  23. package/src/templates/java-spring/partials/UserRepository.java.ejs +8 -0
  24. package/src/templates/java-spring/partials/docker-compose.yml.ejs +16 -8
  25. package/src/templates/node-ts-express/base/server.ts +12 -5
  26. package/src/templates/node-ts-express/base/tsconfig.json +13 -3
  27. package/src/templates/node-ts-express/partials/ApiDocs.ts.ejs +17 -7
  28. package/src/templates/node-ts-express/partials/App.test.ts.ejs +27 -27
  29. package/src/templates/node-ts-express/partials/Auth.controller.ts.ejs +56 -62
  30. package/src/templates/node-ts-express/partials/Auth.middleware.ts.ejs +21 -10
  31. package/src/templates/node-ts-express/partials/Controller.ts.ejs +40 -40
  32. package/src/templates/node-ts-express/partials/DbContext.cs.ejs +3 -3
  33. package/src/templates/node-ts-express/partials/Dockerfile.ejs +9 -11
  34. package/src/templates/node-ts-express/partials/Model.cs.ejs +25 -7
  35. package/src/templates/node-ts-express/partials/Model.ts.ejs +20 -12
  36. package/src/templates/node-ts-express/partials/PrismaController.ts.ejs +72 -55
  37. package/src/templates/node-ts-express/partials/PrismaSchema.prisma.ejs +27 -12
  38. package/src/templates/node-ts-express/partials/README.md.ejs +9 -12
  39. package/src/templates/node-ts-express/partials/Seeder.ts.ejs +44 -64
  40. package/src/templates/node-ts-express/partials/docker-compose.yml.ejs +31 -16
  41. package/src/templates/node-ts-express/partials/package.json.ejs +3 -1
  42. package/src/templates/node-ts-express/partials/prismaClient.ts.ejs +4 -0
  43. package/src/templates/node-ts-express/partials/routes.ts.ejs +35 -24
  44. package/src/utils.js +19 -4
  45. 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
  }
@@ -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 || {}));
@@ -0,0 +1,99 @@
1
+ const path = require('path');
2
+ const fg = require('fast-glob');
3
+ const { Project, SyntaxKind } = require('ts-morph');
4
+ const fs = require('fs-extra');
5
+
6
+ function normalizeMethod(name) {
7
+ const m = String(name).toUpperCase();
8
+ return ['GET','POST','PUT','PATCH','DELETE'].includes(m) ? m : null;
9
+ }
10
+
11
+ // Very first-pass extractor: axios.<method>('url') OR fetch('url', { method: 'POST' })
12
+ async function scanFrontend({ frontendSrcDir }) {
13
+ const patterns = [
14
+ '**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'
15
+ ];
16
+
17
+ const files = await fg(patterns, {
18
+ cwd: frontendSrcDir,
19
+ absolute: true,
20
+ ignore: ['**/node_modules/**', '**/dist/**', '**/.next/**', '**/build/**']
21
+ });
22
+
23
+ const project = new Project({
24
+ tsConfigFilePath: fs.existsSync(path.join(frontendSrcDir, '../tsconfig.json'))
25
+ ? path.join(frontendSrcDir, '../tsconfig.json')
26
+ : undefined,
27
+ skipAddingFilesFromTsConfig: true
28
+ });
29
+
30
+ files.forEach(f => project.addSourceFileAtPathIfExists(f));
31
+
32
+ const endpoints = [];
33
+
34
+ for (const sf of project.getSourceFiles()) {
35
+ // axios.get('/x') | axios.post('/x')
36
+ const callExprs = sf.getDescendantsOfKind(SyntaxKind.CallExpression);
37
+
38
+ for (const call of callExprs) {
39
+ const expr = call.getExpression();
40
+
41
+ // axios.<method>(...)
42
+ if (expr.getKind() === SyntaxKind.PropertyAccessExpression) {
43
+ const pae = expr;
44
+ const method = normalizeMethod(pae.getName());
45
+ const target = pae.getExpression().getText(); // axios / api / client etc (basic)
46
+ const args = call.getArguments();
47
+ if (method && args.length >= 1 && args[0].getKind() === SyntaxKind.StringLiteral) {
48
+ const url = args[0].getText().slice(1, -1);
49
+ endpoints.push({
50
+ source: sf.getFilePath(),
51
+ kind: 'axios',
52
+ client: target,
53
+ method,
54
+ url
55
+ });
56
+ }
57
+ }
58
+
59
+ // fetch('/x', { method: 'POST' })
60
+ if (expr.getText() === 'fetch') {
61
+ const args = call.getArguments();
62
+ if (args.length >= 1 && args[0].getKind() === SyntaxKind.StringLiteral) {
63
+ const url = args[0].getText().slice(1, -1);
64
+ let method = 'GET';
65
+ if (args[1] && args[1].getKind() === SyntaxKind.ObjectLiteralExpression) {
66
+ const obj = args[1];
67
+ const methodProp = obj.getProperty('method');
68
+ if (methodProp && methodProp.getKind() === SyntaxKind.PropertyAssignment) {
69
+ const init = methodProp.getInitializer();
70
+ if (init && init.getKind() === SyntaxKind.StringLiteral) {
71
+ method = init.getText().slice(1, -1).toUpperCase();
72
+ }
73
+ }
74
+ }
75
+ endpoints.push({
76
+ source: sf.getFilePath(),
77
+ kind: 'fetch',
78
+ method,
79
+ url
80
+ });
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ return {
87
+ version: 1,
88
+ generatedAt: new Date().toISOString(),
89
+ frontendSrcDir,
90
+ endpoints
91
+ };
92
+ }
93
+
94
+ async function writeContracts(outFile, contracts) {
95
+ await fs.ensureDir(path.dirname(outFile));
96
+ await fs.writeJson(outFile, contracts, { spaces: 2 });
97
+ }
98
+
99
+ module.exports = { scanFrontend, writeContracts };
@@ -3,22 +3,15 @@ using Microsoft.AspNetCore.Mvc;
3
3
  namespace <%= projectName %>.Controllers;
4
4
 
5
5
  [ApiController]
6
- [Route("api/[controller]")]
6
+ [Route("[controller]")]
7
7
  public class <%= controllerName %>Controller : ControllerBase
8
8
  {
9
- // Endpoints for <%= controllerName %> auto-generated by Backlist
10
-
11
- <% endpoints.forEach(endpoint => { %>
12
- <%# Convert /api/users/{id} to just {id} for the route attribute %>
13
- <% const routePath = endpoint.path.replace(`/api/${controllerName.toLowerCase()}`, '').substring(1); %>
14
- /**
15
- * <%= endpoint.method.toUpperCase() %> <%= endpoint.path %>
16
- */
17
- [Http<%= endpoint.method.charAt(0) + endpoint.method.slice(1).toLowerCase() %>("<%- routePath %>")]
18
- public IActionResult AutoGenerated_<%= endpoint.method %>_<%= routePath.replace(/{|}/g, 'By_').replace(/[^a-zA-Z0-9_]/g, '') || 'Index' %>()
9
+ <% endpoints.forEach(ep => { -%>
10
+ [Http<%= ep.method.charAt(0) + ep.method.slice(1).toLowerCase() %>("<%= ep.route.replace(`/${controllerName.toLowerCase()}`, "") %>")]
11
+ public IActionResult <%= ep.actionName || (ep.method.toLowerCase() + controllerName) %>()
19
12
  {
20
- // TODO: Implement logic here. You can access route parameters like: public IActionResult Get(int id)
21
- return Ok(new { message = "Auto-generated response for <%= endpoint.method.toUpperCase() %> <%= endpoint.path %>" });
13
+ return Ok(new { message = "TODO: Implement <%= ep.method %> <%= ep.route %>" });
22
14
  }
23
- <% }); %>
15
+
16
+ <% }) -%>
24
17
  }
@@ -0,0 +1,8 @@
1
+ namespace <%= projectName %>.Models.DTOs;
2
+
3
+ public class <%= model.name %>
4
+ {
5
+ <% for (const [name, type] of Object.entries(model.fields || {})) { -%>
6
+ public <%= mapCSharpType(type) %> <%= pascal(name) %> { get; set; }
7
+ <% } -%>
8
+ }