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.
- package/bin/backlist.js +227 -0
- package/package.json +10 -4
- package/src/analyzer.js +210 -89
- package/src/db/prisma.ts +4 -0
- package/src/generators/dotnet.js +120 -94
- package/src/generators/java.js +205 -75
- package/src/generators/node.js +262 -85
- package/src/generators/python.js +54 -25
- 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 +30 -0
- package/src/templates/java-spring/partials/AuthController.java.ejs +62 -0
- package/src/templates/java-spring/partials/Controller.java.ejs +40 -50
- package/src/templates/java-spring/partials/Dockerfile.ejs +16 -0
- package/src/templates/java-spring/partials/Entity.java.ejs +16 -15
- package/src/templates/java-spring/partials/JwtAuthFilter.java.ejs +66 -0
- package/src/templates/java-spring/partials/JwtService.java.ejs +58 -0
- package/src/templates/java-spring/partials/Repository.java.ejs +9 -3
- package/src/templates/java-spring/partials/SecurityConfig.java.ejs +44 -0
- package/src/templates/java-spring/partials/Service.java.ejs +69 -0
- package/src/templates/java-spring/partials/User.java.ejs +33 -0
- package/src/templates/java-spring/partials/UserDetailsServiceImpl.java.ejs +33 -0
- package/src/templates/java-spring/partials/UserRepository.java.ejs +20 -0
- package/src/templates/java-spring/partials/docker-compose.yml.ejs +35 -0
- 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/templates/python-fastapi/Dockerfile.ejs +8 -0
- package/src/templates/python-fastapi/app/core/config.py.ejs +8 -0
- package/src/templates/python-fastapi/app/core/security.py.ejs +8 -0
- package/src/templates/python-fastapi/app/db.py.ejs +7 -0
- package/src/templates/python-fastapi/app/main.py.ejs +24 -0
- package/src/templates/python-fastapi/app/models/user.py.ejs +9 -0
- package/src/templates/python-fastapi/app/routers/auth.py.ejs +33 -0
- package/src/templates/python-fastapi/app/routers/model_routes.py.ejs +72 -0
- package/src/templates/python-fastapi/app/schemas/user.py.ejs +16 -0
- package/src/templates/python-fastapi/docker-compose.yml.ejs +19 -0
- package/src/templates/python-fastapi/requirements.txt.ejs +5 -1
- package/src/utils.js +19 -4
- package/bin/index.js +0 -141
package/src/generators/dotnet.js
CHANGED
|
@@ -5,127 +5,153 @@ const path = require('path');
|
|
|
5
5
|
const { analyzeFrontend } = require('../analyzer');
|
|
6
6
|
const { renderAndWrite, getTemplatePath } = require('./template');
|
|
7
7
|
|
|
8
|
+
function groupByController(endpoints) {
|
|
9
|
+
const map = new Map();
|
|
10
|
+
for (const ep of endpoints) {
|
|
11
|
+
const c = ep.controllerName || 'Default';
|
|
12
|
+
if (!map.has(c)) map.set(c, []);
|
|
13
|
+
map.get(c).push(ep);
|
|
14
|
+
}
|
|
15
|
+
return map;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function collectDtoModels(endpoints) {
|
|
19
|
+
const models = new Map();
|
|
20
|
+
|
|
21
|
+
for (const ep of endpoints) {
|
|
22
|
+
if (ep.requestBody?.fields && ep.requestBody.modelName) {
|
|
23
|
+
models.set(ep.requestBody.modelName, { name: ep.requestBody.modelName, fields: ep.requestBody.fields });
|
|
24
|
+
}
|
|
25
|
+
if (ep.responseBody?.fields && ep.responseBody.modelName) {
|
|
26
|
+
models.set(ep.responseBody.modelName, { name: ep.responseBody.modelName, fields: ep.responseBody.fields });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return models;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function ensureProgramMarkers(programCsPath) {
|
|
34
|
+
let content = await fs.readFile(programCsPath, 'utf-8');
|
|
35
|
+
|
|
36
|
+
if (!content.includes('<backlist:usings>')) {
|
|
37
|
+
// add marker near top
|
|
38
|
+
content = content.replace(
|
|
39
|
+
/^/m,
|
|
40
|
+
`// <backlist:usings>\n// </backlist:usings>\n\n`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (!content.includes('<backlist:services>')) {
|
|
44
|
+
content = content.replace(
|
|
45
|
+
'builder.Services.AddControllers();',
|
|
46
|
+
`builder.Services.AddControllers();\n\n// <backlist:services>\n// </backlist:services>`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (!content.includes('<backlist:middleware>')) {
|
|
50
|
+
content = content.replace(
|
|
51
|
+
'var app = builder.Build();',
|
|
52
|
+
`var app = builder.Build();\n\n// <backlist:middleware>\n// </backlist:middleware>\n`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await fs.writeFile(programCsPath, content);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function insertBetweenMarkers(content, markerName, insertText) {
|
|
60
|
+
const start = `// <backlist:${markerName}>`;
|
|
61
|
+
const end = `// </backlist:${markerName}>`;
|
|
62
|
+
|
|
63
|
+
const s = content.indexOf(start);
|
|
64
|
+
const e = content.indexOf(end);
|
|
65
|
+
if (s === -1 || e === -1 || e < s) return content;
|
|
66
|
+
|
|
67
|
+
const before = content.slice(0, s + start.length);
|
|
68
|
+
const after = content.slice(e);
|
|
69
|
+
|
|
70
|
+
return `${before}\n${insertText}\n${after}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
8
73
|
async function generateDotnetProject(options) {
|
|
9
74
|
const { projectDir, projectName, frontendSrcDir } = options;
|
|
10
75
|
|
|
11
76
|
try {
|
|
12
|
-
|
|
13
|
-
console.log(chalk.blue(' -> Analyzing frontend for C# backend...'));
|
|
77
|
+
console.log(chalk.blue(' -> Analyzing frontend for C# backend (AST)...'));
|
|
14
78
|
const endpoints = await analyzeFrontend(frontendSrcDir);
|
|
15
|
-
const modelsToGenerate = new Map();
|
|
16
|
-
endpoints.forEach(ep => {
|
|
17
|
-
// For C#, we create a model if schemaFields exist for any endpoint related to a controller
|
|
18
|
-
if (ep.schemaFields && ep.controllerName !== 'Default' && !modelsToGenerate.has(ep.controllerName)) {
|
|
19
|
-
modelsToGenerate.set(ep.controllerName, {
|
|
20
|
-
name: ep.controllerName,
|
|
21
|
-
fields: Object.entries(ep.schemaFields).map(([key, type]) => ({ name: key, type }))
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
79
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
} else {
|
|
29
|
-
console.log(chalk.yellow(' -> No API calls with body data found. A basic API project will be created without models.'));
|
|
30
|
-
}
|
|
80
|
+
const byController = groupByController(endpoints);
|
|
81
|
+
const dtoModels = collectDtoModels(endpoints);
|
|
31
82
|
|
|
32
|
-
|
|
83
|
+
console.log(chalk.green(` -> Found ${endpoints.length} endpoints, ${dtoModels.size} DTO models, ${byController.size} controllers.`));
|
|
84
|
+
|
|
85
|
+
// Scaffold base project
|
|
33
86
|
console.log(chalk.blue(' -> Scaffolding .NET Core Web API project...'));
|
|
34
87
|
await execa('dotnet', ['new', 'webapi', '-n', projectName, '-o', projectDir, '--no-https']);
|
|
35
|
-
|
|
36
|
-
// --- Step 3: Add Required NuGet Packages ---
|
|
37
|
-
if (modelsToGenerate.size > 0) {
|
|
38
|
-
console.log(chalk.blue(' -> Adding NuGet packages (Entity Framework Core)...'));
|
|
39
|
-
const packages = [
|
|
40
|
-
'Microsoft.EntityFrameworkCore.Design',
|
|
41
|
-
'Microsoft.EntityFrameworkCore.InMemory' // Using InMemory for a simple, runnable setup
|
|
42
|
-
// For a real DB, a user would add: 'Npgsql.EntityFrameworkCore.PostgreSQL' or 'Microsoft.EntityFrameworkCore.SqlServer'
|
|
43
|
-
];
|
|
44
|
-
for (const pkg of packages) {
|
|
45
|
-
await execa('dotnet', ['add', 'package', pkg], { cwd: projectDir });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
88
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
path.join(modelsDir, `${modelName}.cs`),
|
|
61
|
-
{ projectName, modelName, model: modelData }
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
89
|
+
// Remove WeatherForecast
|
|
90
|
+
await fs.remove(path.join(projectDir, 'Controllers', 'WeatherForecastController.cs'));
|
|
91
|
+
await fs.remove(path.join(projectDir, 'WeatherForecast.cs'));
|
|
92
|
+
|
|
93
|
+
// Generate DTOs
|
|
94
|
+
if (dtoModels.size > 0) {
|
|
95
|
+
console.log(chalk.blue(' -> Generating DTO models...'));
|
|
96
|
+
const dtoDir = path.join(projectDir, 'Models', 'DTOs');
|
|
97
|
+
await fs.ensureDir(dtoDir);
|
|
98
|
+
|
|
99
|
+
for (const model of dtoModels.values()) {
|
|
65
100
|
await renderAndWrite(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
101
|
+
getTemplatePath('dotnet/partials/Dto.cs.ejs'),
|
|
102
|
+
path.join(dtoDir, `${model.name}.cs`),
|
|
103
|
+
{ projectName, model }
|
|
69
104
|
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Generate Controllers from endpoints (not CRUD stub)
|
|
109
|
+
console.log(chalk.blue(' -> Generating controllers from detected endpoints...'));
|
|
110
|
+
const controllersDir = path.join(projectDir, 'Controllers');
|
|
111
|
+
await fs.ensureDir(controllersDir);
|
|
112
|
+
|
|
113
|
+
for (const [controllerName, controllerEndpoints] of byController.entries()) {
|
|
114
|
+
if (controllerName === 'Default') continue;
|
|
115
|
+
|
|
116
|
+
await renderAndWrite(
|
|
117
|
+
getTemplatePath('dotnet/partials/Controller.FromEndpoints.cs.ejs'),
|
|
118
|
+
path.join(controllersDir, `${controllerName}Controller.cs`),
|
|
119
|
+
{ projectName, controllerName, endpoints: controllerEndpoints }
|
|
120
|
+
);
|
|
70
121
|
}
|
|
71
122
|
|
|
72
|
-
//
|
|
73
|
-
console.log(chalk.blue(' -> Configuring
|
|
123
|
+
// Program.cs markers + CORS insert (idempotent)
|
|
124
|
+
console.log(chalk.blue(' -> Configuring Program.cs (idempotent markers)...'));
|
|
74
125
|
const programCsPath = path.join(projectDir, 'Program.cs');
|
|
126
|
+
await ensureProgramMarkers(programCsPath);
|
|
127
|
+
|
|
75
128
|
let programCsContent = await fs.readFile(programCsPath, 'utf-8');
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
programCsContent = usingStatements + programCsContent;
|
|
79
|
-
|
|
80
|
-
let dbContextService = `// Configure the database context\nbuilder.Services.AddDbContext<ApplicationDbContext>(opt => opt.UseInMemoryDatabase("MyDb"));`;
|
|
81
|
-
programCsContent = programCsContent.replace('builder.Services.AddControllers();', `builder.Services.AddControllers();\n\n${dbContextService}`);
|
|
82
|
-
|
|
83
|
-
// Enable CORS to allow frontend communication
|
|
84
|
-
const corsPolicy = `
|
|
129
|
+
|
|
130
|
+
const corsBlock = `
|
|
85
131
|
builder.Services.AddCors(options =>
|
|
86
132
|
{
|
|
87
|
-
options.AddDefaultPolicy(
|
|
88
|
-
policy
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
});
|
|
94
|
-
});`;
|
|
95
|
-
programCsContent = programCsContent.replace('var app = builder.Build();', `${corsPolicy}\n\nvar app = builder.Build();\n\napp.UseCors();`);
|
|
133
|
+
options.AddDefaultPolicy(policy =>
|
|
134
|
+
policy.WithOrigins("http://localhost:3000", "http://localhost:5173")
|
|
135
|
+
.AllowAnyHeader()
|
|
136
|
+
.AllowAnyMethod()
|
|
137
|
+
);
|
|
138
|
+
});`.trim();
|
|
96
139
|
|
|
97
|
-
|
|
140
|
+
programCsContent = insertBetweenMarkers(programCsContent, 'services', corsBlock);
|
|
141
|
+
programCsContent = insertBetweenMarkers(programCsContent, 'middleware', 'app.UseCors();');
|
|
98
142
|
|
|
99
|
-
|
|
100
|
-
console.log(chalk.blue(' -> Generating controllers with CRUD logic...'));
|
|
101
|
-
await fs.remove(path.join(projectDir, 'Controllers', 'WeatherForecastController.cs'));
|
|
102
|
-
await fs.remove(path.join(projectDir, 'WeatherForecast.cs'));
|
|
103
|
-
|
|
104
|
-
const controllersToGenerate = new Set(Array.from(modelsToGenerate.keys()));
|
|
105
|
-
// Also add controllers for endpoints that didn't have a body but were detected
|
|
106
|
-
endpoints.forEach(ep => {
|
|
107
|
-
if (ep.controllerName !== 'Default') controllersToGenerate.add(ep.controllerName);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
for (const controllerName of controllersToGenerate) {
|
|
111
|
-
await renderAndWrite(
|
|
112
|
-
getTemplatePath('dotnet/partials/Controller.cs.ejs'),
|
|
113
|
-
path.join(projectDir, 'Controllers', `${controllerName}Controller.cs`),
|
|
114
|
-
{ projectName, controllerName }
|
|
115
|
-
);
|
|
116
|
-
}
|
|
143
|
+
await fs.writeFile(programCsPath, programCsContent);
|
|
117
144
|
|
|
118
|
-
//
|
|
145
|
+
// README
|
|
119
146
|
await renderAndWrite(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
147
|
+
getTemplatePath('dotnet/partials/README.md.ejs'),
|
|
148
|
+
path.join(projectDir, 'README.md'),
|
|
149
|
+
{ projectName }
|
|
123
150
|
);
|
|
124
151
|
|
|
125
152
|
console.log(chalk.green(' -> C# backend generation is complete!'));
|
|
126
153
|
|
|
127
154
|
} catch (error) {
|
|
128
|
-
// Re-throw the error to be caught by the main CLI handler
|
|
129
155
|
throw error;
|
|
130
156
|
}
|
|
131
157
|
}
|
package/src/generators/java.js
CHANGED
|
@@ -1,96 +1,226 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
|
-
const { execa } = require('execa');
|
|
3
2
|
const fs = require('fs-extra');
|
|
4
3
|
const path = require('path');
|
|
5
|
-
const axios = require('axios');
|
|
6
|
-
const unzipper = require('unzipper');
|
|
4
|
+
const axios = require('axios');
|
|
5
|
+
const unzipper = require('unzipper');
|
|
7
6
|
const { analyzeFrontend } = require('../analyzer');
|
|
8
7
|
const { renderAndWrite, getTemplatePath } = require('./template');
|
|
9
8
|
|
|
9
|
+
function sanitizeArtifactId(name) {
|
|
10
|
+
return String(name || 'backend')
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.replace(/[^a-z0-9\-]/g, '-')
|
|
13
|
+
.replace(/-+/g, '-');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function downloadInitializrZip({ groupId, artifactId, name, bootVersion, dependencies }) {
|
|
17
|
+
const params = new URLSearchParams({
|
|
18
|
+
type: 'maven-project',
|
|
19
|
+
language: 'java',
|
|
20
|
+
groupId,
|
|
21
|
+
artifactId,
|
|
22
|
+
name,
|
|
23
|
+
packageName: `${groupId}.${artifactId.replace(/-/g, '')}`,
|
|
24
|
+
dependencies: dependencies.join(','),
|
|
25
|
+
});
|
|
26
|
+
if (bootVersion) params.set('bootVersion', bootVersion);
|
|
27
|
+
|
|
28
|
+
const url = `https://start.spring.io/starter.zip?${params.toString()}`;
|
|
29
|
+
return axios.get(url, {
|
|
30
|
+
responseType: 'stream',
|
|
31
|
+
headers: { Accept: 'application/zip' }
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function extractZipStream(stream, dest) {
|
|
36
|
+
await new Promise((resolve, reject) => {
|
|
37
|
+
const out = stream.pipe(unzipper.Extract({ path: dest }));
|
|
38
|
+
out.on('close', resolve);
|
|
39
|
+
out.on('finish', resolve);
|
|
40
|
+
out.on('error', reject);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function groupByController(endpoints) {
|
|
45
|
+
const map = new Map();
|
|
46
|
+
for (const ep of endpoints || []) {
|
|
47
|
+
const c = ep.controllerName || 'Default';
|
|
48
|
+
if (!map.has(c)) map.set(c, []);
|
|
49
|
+
map.get(c).push(ep);
|
|
50
|
+
}
|
|
51
|
+
return map;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function collectModelsForJava(endpointsByController) {
|
|
55
|
+
// Produces one Entity per controller + DTOs per endpoint body/response if provided by analyzer.
|
|
56
|
+
const entities = new Map(); // controllerName -> {name, fields}
|
|
57
|
+
const dtos = new Map(); // dtoName -> {name, fields}
|
|
58
|
+
|
|
59
|
+
for (const [controllerName, eps] of endpointsByController.entries()) {
|
|
60
|
+
if (controllerName === 'Default') continue;
|
|
61
|
+
|
|
62
|
+
// Entity fields heuristic: merge requestBody fields across endpoints
|
|
63
|
+
const mergedFields = {};
|
|
64
|
+
for (const ep of eps) {
|
|
65
|
+
if (ep.requestBody?.fields) {
|
|
66
|
+
for (const [k, t] of Object.entries(ep.requestBody.fields)) mergedFields[k] = t;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (Object.keys(mergedFields).length > 0) {
|
|
71
|
+
entities.set(controllerName, { name: controllerName, fields: mergedFields });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// DTOs from analyzer if exists
|
|
75
|
+
for (const ep of eps) {
|
|
76
|
+
if (ep.requestBody?.modelName && ep.requestBody?.fields) {
|
|
77
|
+
dtos.set(ep.requestBody.modelName, { name: ep.requestBody.modelName, fields: ep.requestBody.fields });
|
|
78
|
+
}
|
|
79
|
+
if (ep.responseBody?.modelName && ep.responseBody?.fields) {
|
|
80
|
+
dtos.set(ep.responseBody.modelName, { name: ep.responseBody.modelName, fields: ep.responseBody.fields });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { entities, dtos };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function upsertApplicationProperties(projectDir, artifactId) {
|
|
89
|
+
const propsPath = path.join(projectDir, 'src', 'main', 'resources', 'application.properties');
|
|
90
|
+
if (!await fs.pathExists(propsPath)) return;
|
|
91
|
+
|
|
92
|
+
const start = '# <backlist:db>';
|
|
93
|
+
const end = '# </backlist:db>';
|
|
94
|
+
|
|
95
|
+
const dbProps = [
|
|
96
|
+
start,
|
|
97
|
+
`spring.datasource.url=jdbc:postgresql://localhost:5432/${artifactId}`,
|
|
98
|
+
`spring.datasource.username=postgres`,
|
|
99
|
+
`spring.datasource.password=password`,
|
|
100
|
+
`spring.jpa.hibernate.ddl-auto=update`,
|
|
101
|
+
`spring.jpa.show-sql=true`,
|
|
102
|
+
end,
|
|
103
|
+
''
|
|
104
|
+
].join('\n');
|
|
105
|
+
|
|
106
|
+
const current = await fs.readFile(propsPath, 'utf-8');
|
|
107
|
+
|
|
108
|
+
if (current.includes(start) && current.includes(end)) {
|
|
109
|
+
// replace existing block
|
|
110
|
+
const replaced = current.replace(new RegExp(`${start}[\\s\\S]*?${end}`), dbProps.trim());
|
|
111
|
+
await fs.writeFile(propsPath, replaced);
|
|
112
|
+
} else {
|
|
113
|
+
await fs.appendFile(propsPath, `\n\n${dbProps}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
10
117
|
async function generateJavaProject(options) {
|
|
11
118
|
const { projectDir, projectName, frontendSrcDir } = options;
|
|
12
|
-
const
|
|
119
|
+
const groupId = 'com.backlist.generated';
|
|
120
|
+
const artifactId = sanitizeArtifactId(projectName || 'backend');
|
|
121
|
+
const basePackage = `${groupId}.${artifactId.replace(/-/g, '')}`;
|
|
13
122
|
|
|
14
123
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
responseType: 'stream'
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
// --- Step 2: Unzip the Downloaded Project ---
|
|
29
|
-
console.log(chalk.blue(' -> Unzipping the Spring Boot project...'));
|
|
30
|
-
await new Promise((resolve, reject) => {
|
|
31
|
-
const stream = response.data.pipe(unzipper.Extract({ path: projectDir }));
|
|
32
|
-
stream.on('finish', () => {
|
|
33
|
-
console.log(chalk.gray(' -> Project unzipped successfully.'));
|
|
34
|
-
resolve();
|
|
124
|
+
console.log(chalk.blue(' -> Downloading base Spring Boot project from Initializr...'));
|
|
125
|
+
|
|
126
|
+
const deps = ['web', 'data-jpa', 'lombok', 'postgresql'];
|
|
127
|
+
|
|
128
|
+
let response;
|
|
129
|
+
try {
|
|
130
|
+
response = await downloadInitializrZip({
|
|
131
|
+
groupId, artifactId, name: projectName || 'backend',
|
|
132
|
+
bootVersion: '3.3.4',
|
|
133
|
+
dependencies: deps
|
|
35
134
|
});
|
|
36
|
-
|
|
37
|
-
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.log(chalk.yellow(' -> Retry without fixed bootVersion...'));
|
|
137
|
+
response = await downloadInitializrZip({
|
|
138
|
+
groupId, artifactId, name: projectName || 'backend',
|
|
139
|
+
bootVersion: '',
|
|
140
|
+
dependencies: deps
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log(chalk.blue(' -> Unzipping...'));
|
|
145
|
+
await extractZipStream(response.data, projectDir);
|
|
38
146
|
|
|
39
|
-
|
|
147
|
+
console.log(chalk.blue(' -> Analyzing frontend (AST) for endpoints/contracts...'));
|
|
40
148
|
const endpoints = await analyzeFrontend(frontendSrcDir);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
149
|
+
|
|
150
|
+
const endpointsByController = groupByController(endpoints);
|
|
151
|
+
const { entities, dtos } = collectModelsForJava(endpointsByController);
|
|
152
|
+
|
|
153
|
+
console.log(chalk.green(` -> Found ${Array.isArray(endpoints) ? endpoints.length : 0} endpoints`));
|
|
154
|
+
console.log(chalk.green(` -> Will generate ${entities.size} entities, ${dtos.size} DTOs, ${endpointsByController.size} controllers`));
|
|
155
|
+
|
|
156
|
+
// Compute java src root
|
|
157
|
+
const javaSrcRoot = path.join(
|
|
158
|
+
projectDir,
|
|
159
|
+
'src', 'main', 'java',
|
|
160
|
+
...groupId.split('.'),
|
|
161
|
+
artifactId.replace(/-/g, '')
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const entityDir = path.join(javaSrcRoot, 'model');
|
|
165
|
+
const dtoDir = path.join(javaSrcRoot, 'dto');
|
|
166
|
+
const repoDir = path.join(javaSrcRoot, 'repository');
|
|
167
|
+
const controllerDir = path.join(javaSrcRoot, 'controller');
|
|
168
|
+
|
|
169
|
+
await fs.ensureDir(entityDir);
|
|
170
|
+
await fs.ensureDir(dtoDir);
|
|
171
|
+
await fs.ensureDir(repoDir);
|
|
172
|
+
await fs.ensureDir(controllerDir);
|
|
173
|
+
|
|
174
|
+
// Entities + Repos
|
|
175
|
+
for (const ent of entities.values()) {
|
|
176
|
+
await renderAndWrite(
|
|
177
|
+
getTemplatePath('java-spring/partials/Entity.java.ejs'),
|
|
178
|
+
path.join(entityDir, `${ent.name}.java`),
|
|
179
|
+
{ basePackage, model: ent }
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
await renderAndWrite(
|
|
183
|
+
getTemplatePath('java-spring/partials/Repository.java.ejs'),
|
|
184
|
+
path.join(repoDir, `${ent.name}Repository.java`),
|
|
185
|
+
{ basePackage, entityName: ent.name }
|
|
186
|
+
);
|
|
70
187
|
}
|
|
71
188
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
189
|
+
// DTOs
|
|
190
|
+
for (const dto of dtos.values()) {
|
|
191
|
+
await renderAndWrite(
|
|
192
|
+
getTemplatePath('java-spring/partials/Dto.java.ejs'),
|
|
193
|
+
path.join(dtoDir, `${dto.name}.java`),
|
|
194
|
+
{ basePackage, dto }
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Controllers from endpoints
|
|
199
|
+
for (const [controllerName, eps] of endpointsByController.entries()) {
|
|
200
|
+
if (controllerName === 'Default') continue;
|
|
201
|
+
|
|
202
|
+
await renderAndWrite(
|
|
203
|
+
getTemplatePath('java-spring/partials/Controller.FromEndpoints.java.ejs'),
|
|
204
|
+
path.join(controllerDir, `${controllerName}Controller.java`),
|
|
205
|
+
{
|
|
206
|
+
basePackage,
|
|
207
|
+
controllerName,
|
|
208
|
+
endpoints: eps,
|
|
209
|
+
hasEntity: entities.has(controllerName)
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// application.properties idempotent update
|
|
215
|
+
await upsertApplicationProperties(projectDir, artifactId);
|
|
90
216
|
|
|
217
|
+
console.log(chalk.green(' -> Java (Spring Boot) backend generation is complete!'));
|
|
218
|
+
console.log(chalk.yellow('\nNext steps:'));
|
|
219
|
+
console.log(chalk.cyan(` cd ${path.basename(projectDir)}`));
|
|
220
|
+
console.log(chalk.cyan(' ./mvnw spring-boot:run'));
|
|
91
221
|
|
|
92
222
|
} catch (error) {
|
|
93
|
-
if (error.response) {
|
|
223
|
+
if (error.response && error.response.status) {
|
|
94
224
|
throw new Error(`Failed to download from Spring Initializr. Status: ${error.response.status}`);
|
|
95
225
|
}
|
|
96
226
|
throw error;
|