create-backlist 6.0.0 → 6.0.3
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/bin/backlist.js +227 -0
- package/package.json +10 -4
- package/src/analyzer.js +245 -91
- package/src/db/prisma.ts +4 -0
- package/src/generators/dotnet.js +120 -94
- package/src/generators/java.js +157 -109
- package/src/generators/node.js +262 -85
- package/src/generators/template.js +38 -2
- package/src/scanner/index.js +99 -0
- package/src/templates/dotnet/partials/Controller.cs.ejs +7 -14
- package/src/templates/dotnet/partials/Dto.cs.ejs +8 -0
- package/src/templates/java-spring/partials/ApplicationSeeder.java.ejs +7 -2
- package/src/templates/java-spring/partials/AuthController.java.ejs +23 -10
- package/src/templates/java-spring/partials/Controller.java.ejs +17 -6
- package/src/templates/java-spring/partials/Dockerfile.ejs +6 -1
- package/src/templates/java-spring/partials/Entity.java.ejs +15 -5
- package/src/templates/java-spring/partials/JwtAuthFilter.java.ejs +30 -7
- package/src/templates/java-spring/partials/JwtService.java.ejs +38 -10
- package/src/templates/java-spring/partials/Repository.java.ejs +10 -1
- package/src/templates/java-spring/partials/Service.java.ejs +45 -7
- package/src/templates/java-spring/partials/User.java.ejs +17 -4
- package/src/templates/java-spring/partials/UserDetailsServiceImpl.java.ejs +10 -4
- package/src/templates/java-spring/partials/UserRepository.java.ejs +8 -0
- package/src/templates/java-spring/partials/docker-compose.yml.ejs +16 -8
- package/src/templates/node-ts-express/base/server.ts +12 -5
- package/src/templates/node-ts-express/base/tsconfig.json +13 -3
- package/src/templates/node-ts-express/partials/ApiDocs.ts.ejs +17 -7
- package/src/templates/node-ts-express/partials/App.test.ts.ejs +27 -27
- package/src/templates/node-ts-express/partials/Auth.controller.ts.ejs +56 -62
- package/src/templates/node-ts-express/partials/Auth.middleware.ts.ejs +21 -10
- package/src/templates/node-ts-express/partials/Controller.ts.ejs +40 -40
- package/src/templates/node-ts-express/partials/DbContext.cs.ejs +3 -3
- package/src/templates/node-ts-express/partials/Dockerfile.ejs +9 -11
- package/src/templates/node-ts-express/partials/Model.cs.ejs +25 -7
- package/src/templates/node-ts-express/partials/Model.ts.ejs +20 -12
- package/src/templates/node-ts-express/partials/PrismaController.ts.ejs +72 -55
- package/src/templates/node-ts-express/partials/PrismaSchema.prisma.ejs +27 -12
- package/src/templates/node-ts-express/partials/README.md.ejs +9 -12
- package/src/templates/node-ts-express/partials/Seeder.ts.ejs +44 -64
- package/src/templates/node-ts-express/partials/docker-compose.yml.ejs +31 -16
- package/src/templates/node-ts-express/partials/package.json.ejs +3 -1
- package/src/templates/node-ts-express/partials/prismaClient.ts.ejs +4 -0
- package/src/templates/node-ts-express/partials/routes.ts.ejs +35 -24
- package/src/utils.js +19 -4
- package/bin/index.js +0 -141
package/src/generators/node.js
CHANGED
|
@@ -7,27 +7,70 @@ const { analyzeFrontend } = require('../analyzer');
|
|
|
7
7
|
const { renderAndWrite, getTemplatePath } = require('./template');
|
|
8
8
|
|
|
9
9
|
async function generateNodeProject(options) {
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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', {
|
|
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(
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 &
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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:
|
|
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(
|
|
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:
|
|
259
|
+
// --- Step 8: Extra Features ---
|
|
129
260
|
if (extraFeatures.includes('docker')) {
|
|
130
261
|
console.log(chalk.blue(' -> Generating Docker files...'));
|
|
131
|
-
await renderAndWrite(
|
|
132
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
148
|
-
await renderAndWrite(
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
327
|
+
swaggerInjector = `\nimport { setupSwagger } from './utils/swagger';\nsetupSwagger(app);\n`;
|
|
160
328
|
}
|
|
329
|
+
|
|
330
|
+
let authRoutesInjector = '';
|
|
161
331
|
if (addAuth) {
|
|
162
|
-
|
|
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(
|
|
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 &
|
|
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:
|
|
353
|
+
|
|
354
|
+
// --- Step 11: Final Files (.env.example) ---
|
|
182
355
|
let envContent = `PORT=${port}\n`;
|
|
356
|
+
|
|
183
357
|
if (dbType === 'mongoose') {
|
|
184
|
-
|
|
358
|
+
envContent += `MONGO_URI=mongodb://127.0.0.1:27017/${projectName}\n`;
|
|
185
359
|
} else if (dbType === 'prisma') {
|
|
186
|
-
|
|
360
|
+
envContent += `DATABASE_URL="postgresql://user:password@localhost:5432/${projectName}?schema=public"\n`;
|
|
187
361
|
}
|
|
188
|
-
|
|
189
|
-
if (
|
|
190
|
-
|
|
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 });
|
|
9
|
-
|
|
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("
|
|
6
|
+
[Route("[controller]")]
|
|
7
7
|
public class <%= controllerName %>Controller : ControllerBase
|
|
8
8
|
{
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
}
|