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.
- 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 +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/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,5 +1,4 @@
|
|
|
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
4
|
const axios = require('axios');
|
|
@@ -8,8 +7,10 @@ const { analyzeFrontend } = require('../analyzer');
|
|
|
8
7
|
const { renderAndWrite, getTemplatePath } = require('./template');
|
|
9
8
|
|
|
10
9
|
function sanitizeArtifactId(name) {
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
return String(name || 'backend')
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.replace(/[^a-z0-9\-]/g, '-')
|
|
13
|
+
.replace(/-+/g, '-');
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
async function downloadInitializrZip({ groupId, artifactId, name, bootVersion, dependencies }) {
|
|
@@ -19,20 +20,16 @@ async function downloadInitializrZip({ groupId, artifactId, name, bootVersion, d
|
|
|
19
20
|
groupId,
|
|
20
21
|
artifactId,
|
|
21
22
|
name,
|
|
22
|
-
packageName: `${groupId}.${artifactId.replace(/-/g, '')}`,
|
|
23
|
+
packageName: `${groupId}.${artifactId.replace(/-/g, '')}`,
|
|
23
24
|
dependencies: dependencies.join(','),
|
|
24
25
|
});
|
|
25
|
-
|
|
26
26
|
if (bootVersion) params.set('bootVersion', bootVersion);
|
|
27
27
|
|
|
28
28
|
const url = `https://start.spring.io/starter.zip?${params.toString()}`;
|
|
29
|
-
|
|
30
|
-
const res = await axios.get(url, {
|
|
29
|
+
return axios.get(url, {
|
|
31
30
|
responseType: 'stream',
|
|
32
31
|
headers: { Accept: 'application/zip' }
|
|
33
32
|
});
|
|
34
|
-
|
|
35
|
-
return res;
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
async function extractZipStream(stream, dest) {
|
|
@@ -44,135 +41,186 @@ async function extractZipStream(stream, dest) {
|
|
|
44
41
|
});
|
|
45
42
|
}
|
|
46
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
|
+
|
|
47
117
|
async function generateJavaProject(options) {
|
|
48
118
|
const { projectDir, projectName, frontendSrcDir } = options;
|
|
49
119
|
const groupId = 'com.backlist.generated';
|
|
50
120
|
const artifactId = sanitizeArtifactId(projectName || 'backend');
|
|
51
|
-
const
|
|
121
|
+
const basePackage = `${groupId}.${artifactId.replace(/-/g, '')}`;
|
|
52
122
|
|
|
53
123
|
try {
|
|
54
|
-
console.log(chalk.blue(' ->
|
|
124
|
+
console.log(chalk.blue(' -> Downloading base Spring Boot project from Initializr...'));
|
|
55
125
|
|
|
56
|
-
|
|
57
|
-
const deps = ['web', 'data-jpa', 'lombok', 'postgresql']; // valid Initializr ids
|
|
126
|
+
const deps = ['web', 'data-jpa', 'lombok', 'postgresql'];
|
|
58
127
|
|
|
59
128
|
let response;
|
|
60
129
|
try {
|
|
61
130
|
response = await downloadInitializrZip({
|
|
62
|
-
groupId,
|
|
63
|
-
|
|
64
|
-
name,
|
|
65
|
-
bootVersion: '3.3.4', // current stable; adjust as needed
|
|
131
|
+
groupId, artifactId, name: projectName || 'backend',
|
|
132
|
+
bootVersion: '3.3.4',
|
|
66
133
|
dependencies: deps
|
|
67
134
|
});
|
|
68
135
|
} catch (err) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
artifactId,
|
|
76
|
-
name,
|
|
77
|
-
bootVersion: '', // let Initializr pick latest
|
|
78
|
-
dependencies: deps
|
|
79
|
-
});
|
|
80
|
-
} catch {
|
|
81
|
-
console.log(chalk.yellow(' -> Second attempt failed. Retrying with minimal dependencies...'));
|
|
82
|
-
response = await downloadInitializrZip({
|
|
83
|
-
groupId,
|
|
84
|
-
artifactId,
|
|
85
|
-
name,
|
|
86
|
-
bootVersion: '',
|
|
87
|
-
dependencies: fallbackDeps
|
|
88
|
-
});
|
|
89
|
-
}
|
|
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
|
+
});
|
|
90
142
|
}
|
|
91
143
|
|
|
92
|
-
console.log(chalk.blue(' -> Unzipping
|
|
144
|
+
console.log(chalk.blue(' -> Unzipping...'));
|
|
93
145
|
await extractZipStream(response.data, projectDir);
|
|
94
146
|
|
|
95
|
-
|
|
147
|
+
console.log(chalk.blue(' -> Analyzing frontend (AST) for endpoints/contracts...'));
|
|
96
148
|
const endpoints = await analyzeFrontend(frontendSrcDir);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
path.join(controllerDir, `${modelName}Controller.java`),
|
|
136
|
-
{ group: groupId, projectName: artifactId.replace(/-/g, ''), controllerName: modelName, model: modelData }
|
|
137
|
-
);
|
|
138
|
-
}
|
|
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
|
+
);
|
|
139
187
|
}
|
|
140
188
|
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
`spring.datasource.password=password`,
|
|
149
|
-
`spring.jpa.hibernate.ddl-auto=update`,
|
|
150
|
-
`spring.jpa.show-sql=true`,
|
|
151
|
-
].join('\n');
|
|
152
|
-
await fs.appendFile(propsPath, dbProps);
|
|
153
|
-
} catch (e) {
|
|
154
|
-
console.log(chalk.yellow(' -> Could not update application.properties (continuing).'));
|
|
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
|
+
);
|
|
155
196
|
}
|
|
156
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);
|
|
216
|
+
|
|
157
217
|
console.log(chalk.green(' -> Java (Spring Boot) backend generation is complete!'));
|
|
158
218
|
console.log(chalk.yellow('\nNext steps:'));
|
|
159
219
|
console.log(chalk.cyan(` cd ${path.basename(projectDir)}`));
|
|
160
|
-
console.log(chalk.cyan(' ./mvnw spring-boot:run
|
|
220
|
+
console.log(chalk.cyan(' ./mvnw spring-boot:run'));
|
|
161
221
|
|
|
162
222
|
} catch (error) {
|
|
163
223
|
if (error.response && error.response.status) {
|
|
164
|
-
console.error(chalk.red(` -> Initializr error status: ${error.response.status}`));
|
|
165
|
-
if (error.response.data) {
|
|
166
|
-
try {
|
|
167
|
-
// Try read error text body for hints
|
|
168
|
-
const text = (await (async () => {
|
|
169
|
-
let buf = '';
|
|
170
|
-
for await (const chunk of error.response.data) buf += chunk.toString();
|
|
171
|
-
return buf;
|
|
172
|
-
})());
|
|
173
|
-
console.error(chalk.yellow(' -> Initializr response body:'), text);
|
|
174
|
-
} catch {}
|
|
175
|
-
}
|
|
176
224
|
throw new Error(`Failed to download from Spring Initializr. Status: ${error.response.status}`);
|
|
177
225
|
}
|
|
178
226
|
throw error;
|