create-backlist 10.0.9 → 10.1.1
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/index.js +1411 -1060
- package/package.json +1 -1
- package/src/generators/dotnet.js +137 -81
- package/src/generators/java.js +118 -130
- package/src/generators/js.js +199 -207
- package/src/generators/nestjs.js +168 -155
- package/src/generators/node.js +212 -194
- package/src/generators/python.js +130 -45
- package/src/generators/template.js +47 -2
- package/src/qa/qa-engine.js +2320 -414
- package/src/templates/dotnet/partials/Controller.cs.ejs +264 -16
- package/src/templates/dotnet/partials/DbContext.cs.ejs +93 -3
- package/src/templates/dotnet/partials/Model.cs.ejs +192 -31
package/package.json
CHANGED
package/src/generators/dotnet.js
CHANGED
|
@@ -3,132 +3,188 @@ import { execa } from 'execa';
|
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { analyzeFrontend } from '../analyzer.js';
|
|
6
|
-
import { renderAndWrite, getTemplatePath } from './template.js';
|
|
6
|
+
import { renderAndWrite, renderAndWriteAll, getTemplatePath, preloadTemplates } from './template.js';
|
|
7
|
+
|
|
8
|
+
// ─── Retry with exponential backoff ───────────────────────────────────────────
|
|
9
|
+
async function withRetry(fn, { attempts = 3, baseDelay = 300, label = 'task' } = {}) {
|
|
10
|
+
let lastErr;
|
|
11
|
+
for (let i = 0; i < attempts; i++) {
|
|
12
|
+
try { return await fn(); } catch (err) {
|
|
13
|
+
lastErr = err;
|
|
14
|
+
if (i < attempts - 1) {
|
|
15
|
+
const delay = baseDelay * 2 ** i;
|
|
16
|
+
console.log(chalk.yellow(` [RETRY] ${label} (${i + 1}/${attempts}), retrying in ${delay}ms...`));
|
|
17
|
+
await new Promise(r => setTimeout(r, delay));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
throw lastErr;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function timer() {
|
|
25
|
+
const s = Date.now();
|
|
26
|
+
return () => `${((Date.now() - s) / 1000).toFixed(2)}s`;
|
|
27
|
+
}
|
|
7
28
|
|
|
8
29
|
export async function generateDotnetProject(options) {
|
|
9
30
|
const { projectDir, projectName, frontendSrcDir } = options;
|
|
31
|
+
const totalTimer = timer();
|
|
10
32
|
|
|
11
33
|
try {
|
|
12
|
-
|
|
13
|
-
const
|
|
34
|
+
// ── Step 1: Analyze + preload templates in parallel ────────────────────────
|
|
35
|
+
const step1 = timer();
|
|
36
|
+
console.log(chalk.blue(' -> [1] Analyzing frontend & preloading templates in parallel...'));
|
|
37
|
+
|
|
38
|
+
const [endpoints] = await Promise.all([
|
|
39
|
+
analyzeFrontend(frontendSrcDir),
|
|
40
|
+
preloadTemplates([
|
|
41
|
+
'dotnet/partials/Model.cs.ejs',
|
|
42
|
+
'dotnet/partials/DbContext.cs.ejs',
|
|
43
|
+
'dotnet/partials/Controller.cs.ejs',
|
|
44
|
+
'dotnet/partials/Dockerfile.ejs',
|
|
45
|
+
'dotnet/partials/docker-compose.yml.ejs',
|
|
46
|
+
'dotnet/partials/README.md.ejs',
|
|
47
|
+
]).catch(() => {}),
|
|
48
|
+
]);
|
|
49
|
+
|
|
14
50
|
const modelsToGenerate = new Map();
|
|
15
51
|
endpoints.forEach(ep => {
|
|
16
52
|
if (ep.schemaFields && ep.controllerName !== 'Default' && !modelsToGenerate.has(ep.controllerName)) {
|
|
17
53
|
modelsToGenerate.set(ep.controllerName, {
|
|
18
54
|
name: ep.controllerName,
|
|
19
|
-
fields: Object.entries(ep.schemaFields).map(([key, type]) => ({ name: key, type }))
|
|
55
|
+
fields: Object.entries(ep.schemaFields).map(([key, type]) => ({ name: key, type })),
|
|
20
56
|
});
|
|
21
57
|
}
|
|
22
58
|
});
|
|
23
59
|
|
|
24
60
|
if (modelsToGenerate.size > 0) {
|
|
25
|
-
console.log(chalk.green(` -> Identified ${modelsToGenerate.size} models
|
|
61
|
+
console.log(chalk.green(` -> Identified ${modelsToGenerate.size} models. ${chalk.gray(step1())}`));
|
|
26
62
|
} else {
|
|
27
|
-
console.log(chalk.yellow(
|
|
63
|
+
console.log(chalk.yellow(` -> No models found. Basic API project will be created. ${chalk.gray(step1())}`));
|
|
28
64
|
}
|
|
29
65
|
|
|
30
|
-
|
|
31
|
-
|
|
66
|
+
// ── Step 2: Scaffold .NET project ─────────────────────────────────────────
|
|
67
|
+
console.log(chalk.blue(' -> [2] Scaffolding .NET Core Web API project...'));
|
|
68
|
+
const step2 = timer();
|
|
69
|
+
await withRetry(
|
|
70
|
+
() => execa('dotnet', ['new', 'webapi', '-n', projectName, '-o', projectDir, '--no-https']),
|
|
71
|
+
{ attempts: 2, baseDelay: 500, label: 'dotnet new' }
|
|
72
|
+
);
|
|
73
|
+
console.log(chalk.gray(` Scaffold done. ${step2()}`));
|
|
32
74
|
|
|
75
|
+
// ── Step 3: Add NuGet packages + ensure dirs in parallel ──────────────────
|
|
33
76
|
if (modelsToGenerate.size > 0) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
'Microsoft.EntityFrameworkCore.Design',
|
|
37
|
-
'Microsoft.EntityFrameworkCore.InMemory'
|
|
38
|
-
];
|
|
39
|
-
for (const pkg of packages) {
|
|
40
|
-
await execa('dotnet', ['add', 'package', pkg], { cwd: projectDir });
|
|
41
|
-
}
|
|
42
|
-
}
|
|
77
|
+
const step3 = timer();
|
|
78
|
+
console.log(chalk.blue(' -> [3] Adding NuGet packages & preparing dirs in parallel...'));
|
|
43
79
|
|
|
44
|
-
if (modelsToGenerate.size > 0) {
|
|
45
|
-
console.log(chalk.blue(' -> Generating EF Core models and DbContext...'));
|
|
46
80
|
const modelsDir = path.join(projectDir, 'Models');
|
|
47
81
|
const dataDir = path.join(projectDir, 'Data');
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
await renderAndWrite(
|
|
53
|
-
getTemplatePath('dotnet/partials/Model.cs.ejs'),
|
|
54
|
-
path.join(modelsDir, `${modelName}.cs`),
|
|
55
|
-
{ projectName, modelName, model: modelData }
|
|
56
|
-
);
|
|
57
|
-
}
|
|
82
|
+
const packages = [
|
|
83
|
+
'Microsoft.EntityFrameworkCore.Design',
|
|
84
|
+
'Microsoft.EntityFrameworkCore.InMemory',
|
|
85
|
+
];
|
|
58
86
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
87
|
+
// Add packages in parallel + ensure dirs simultaneously
|
|
88
|
+
await Promise.all([
|
|
89
|
+
...packages.map(pkg =>
|
|
90
|
+
withRetry(
|
|
91
|
+
() => execa('dotnet', ['add', 'package', pkg], { cwd: projectDir }),
|
|
92
|
+
{ attempts: 3, baseDelay: 400, label: `add ${pkg}` }
|
|
93
|
+
)
|
|
94
|
+
),
|
|
95
|
+
fs.ensureDir(modelsDir),
|
|
96
|
+
fs.ensureDir(dataDir),
|
|
97
|
+
]);
|
|
98
|
+
console.log(chalk.gray(` NuGet + dirs done. ${step3()}`));
|
|
99
|
+
|
|
100
|
+
// ── Step 4: Generate models + DbContext in parallel ──────────────────────
|
|
101
|
+
const step4 = timer();
|
|
102
|
+
console.log(chalk.blue(` -> [4] Generating ${modelsToGenerate.size} EF Core model(s) + DbContext in parallel...`));
|
|
103
|
+
|
|
104
|
+
const modelTasks = Array.from(modelsToGenerate.entries()).map(([modelName, modelData]) => ({
|
|
105
|
+
templatePath: getTemplatePath('dotnet/partials/Model.cs.ejs'),
|
|
106
|
+
outPath: path.join(modelsDir, `${modelName}.cs`),
|
|
107
|
+
data: { projectName, modelName, model: modelData },
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
await Promise.all([
|
|
111
|
+
renderAndWriteAll(modelTasks),
|
|
112
|
+
renderAndWrite(
|
|
113
|
+
getTemplatePath('dotnet/partials/DbContext.cs.ejs'),
|
|
114
|
+
path.join(dataDir, 'ApplicationDbContext.cs'),
|
|
115
|
+
{ projectName, modelsToGenerate: Array.from(modelsToGenerate.values()) }
|
|
116
|
+
),
|
|
117
|
+
]);
|
|
118
|
+
console.log(chalk.gray(` Models + DbContext done. ${step4()}`));
|
|
64
119
|
}
|
|
65
120
|
|
|
66
|
-
|
|
121
|
+
// ── Step 5: Patch Program.cs ───────────────────────────────────────────────
|
|
122
|
+
console.log(chalk.blue(' -> [5] Configuring Program.cs...'));
|
|
67
123
|
const programCsPath = path.join(projectDir, 'Program.cs');
|
|
68
124
|
let programCsContent = await fs.readFile(programCsPath, 'utf-8');
|
|
69
125
|
|
|
70
|
-
|
|
71
|
-
|
|
126
|
+
programCsContent =
|
|
127
|
+
`using Microsoft.EntityFrameworkCore;\nusing ${projectName}.Data;\n` + programCsContent;
|
|
72
128
|
|
|
73
|
-
|
|
74
|
-
|
|
129
|
+
if (modelsToGenerate.size > 0) {
|
|
130
|
+
programCsContent = programCsContent.replace(
|
|
131
|
+
'builder.Services.AddControllers();',
|
|
132
|
+
`builder.Services.AddControllers();\n\n// Configure the database context\nbuilder.Services.AddDbContext<ApplicationDbContext>(opt => opt.UseInMemoryDatabase("MyDb"));`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
75
135
|
|
|
76
136
|
const corsPolicy = `
|
|
77
137
|
builder.Services.AddCors(options =>
|
|
78
138
|
{
|
|
79
|
-
options.AddDefaultPolicy(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
});
|
|
139
|
+
options.AddDefaultPolicy(policy =>
|
|
140
|
+
{
|
|
141
|
+
policy.WithOrigins("http://localhost:3000", "http://localhost:5173")
|
|
142
|
+
.AllowAnyHeader()
|
|
143
|
+
.AllowAnyMethod();
|
|
144
|
+
});
|
|
86
145
|
});`;
|
|
87
|
-
programCsContent = programCsContent.replace(
|
|
146
|
+
programCsContent = programCsContent.replace(
|
|
147
|
+
'var app = builder.Build();',
|
|
148
|
+
`${corsPolicy}\n\nvar app = builder.Build();\n\napp.UseCors();`
|
|
149
|
+
);
|
|
88
150
|
|
|
89
151
|
await fs.writeFile(programCsPath, programCsContent);
|
|
90
152
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
153
|
+
// ── Step 6: Generate controllers + remove boilerplate + Docker + README in parallel ──
|
|
154
|
+
const step6 = timer();
|
|
155
|
+
console.log(chalk.blue(' -> [6] Generating controllers, Docker files & README in parallel...'));
|
|
94
156
|
|
|
95
157
|
const controllersToGenerate = new Set(Array.from(modelsToGenerate.keys()));
|
|
96
158
|
endpoints.forEach(ep => {
|
|
97
159
|
if (ep.controllerName !== 'Default') controllersToGenerate.add(ep.controllerName);
|
|
98
160
|
});
|
|
99
161
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
getTemplatePath('dotnet/partials/docker-compose.yml.ejs'),
|
|
119
|
-
path.join(projectDir, '
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
getTemplatePath('dotnet/partials/README.md.ejs'),
|
|
125
|
-
path.join(projectDir, 'README.md'),
|
|
126
|
-
{ projectName }
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
console.log(chalk.green(' -> C# backend generation is complete!'));
|
|
162
|
+
const controllerTasks = Array.from(controllersToGenerate).map(controllerName => ({
|
|
163
|
+
templatePath: getTemplatePath('dotnet/partials/Controller.cs.ejs'),
|
|
164
|
+
outPath: path.join(projectDir, 'Controllers', `${controllerName}Controller.cs`),
|
|
165
|
+
data: {
|
|
166
|
+
projectName,
|
|
167
|
+
controllerName,
|
|
168
|
+
endpoints: endpoints.filter(ep => ep.controllerName === controllerName),
|
|
169
|
+
},
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
await Promise.all([
|
|
173
|
+
// Remove default boilerplate files
|
|
174
|
+
fs.remove(path.join(projectDir, 'Controllers', 'WeatherForecastController.cs')),
|
|
175
|
+
fs.remove(path.join(projectDir, 'WeatherForecast.cs')),
|
|
176
|
+
// Generate all controllers in parallel
|
|
177
|
+
renderAndWriteAll(controllerTasks),
|
|
178
|
+
// Docker + README in parallel
|
|
179
|
+
renderAndWrite(getTemplatePath('dotnet/partials/Dockerfile.ejs'), path.join(projectDir, 'Dockerfile'), { projectName }),
|
|
180
|
+
renderAndWrite(getTemplatePath('dotnet/partials/docker-compose.yml.ejs'), path.join(projectDir, 'docker-compose.yml'), { projectName }),
|
|
181
|
+
renderAndWrite(getTemplatePath('dotnet/partials/README.md.ejs'), path.join(projectDir, 'README.md'), { projectName }),
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
console.log(chalk.gray(` Controllers + Docker + README done. ${step6()}`));
|
|
185
|
+
console.log(chalk.green(` -> ✓ C# backend generation complete. Total: ${chalk.bold(totalTimer())}`));
|
|
130
186
|
|
|
131
187
|
} catch (error) {
|
|
132
188
|
throw error;
|
|
133
189
|
}
|
|
134
|
-
}
|
|
190
|
+
}
|
package/src/generators/java.js
CHANGED
|
@@ -5,7 +5,29 @@ import axios from "axios";
|
|
|
5
5
|
import unzipper from "unzipper";
|
|
6
6
|
|
|
7
7
|
import { analyzeFrontend } from "../analyzer.js";
|
|
8
|
-
import { renderAndWrite, getTemplatePath } from "./template.js";
|
|
8
|
+
import { renderAndWrite, renderAndWriteAll, getTemplatePath, preloadTemplates } from "./template.js";
|
|
9
|
+
|
|
10
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
function timer() {
|
|
13
|
+
const s = Date.now();
|
|
14
|
+
return () => `${((Date.now() - s) / 1000).toFixed(2)}s`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function withRetry(fn, { attempts = 3, baseDelay = 300, label = "task" } = {}) {
|
|
18
|
+
let lastErr;
|
|
19
|
+
for (let i = 0; i < attempts; i++) {
|
|
20
|
+
try { return await fn(); } catch (err) {
|
|
21
|
+
lastErr = err;
|
|
22
|
+
if (i < attempts - 1) {
|
|
23
|
+
const delay = baseDelay * 2 ** i;
|
|
24
|
+
console.log(chalk.yellow(` [RETRY] ${label} (${i + 1}/${attempts}), retrying in ${delay}ms...`));
|
|
25
|
+
await new Promise(r => setTimeout(r, delay));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
throw lastErr;
|
|
30
|
+
}
|
|
9
31
|
|
|
10
32
|
function sanitizeArtifactId(name) {
|
|
11
33
|
return String(name || "backend")
|
|
@@ -17,24 +39,18 @@ function sanitizeArtifactId(name) {
|
|
|
17
39
|
|
|
18
40
|
async function downloadInitializrZip({ groupId, artifactId, name, bootVersion, dependencies }) {
|
|
19
41
|
const params = new URLSearchParams({
|
|
20
|
-
type: "maven-project",
|
|
21
|
-
|
|
22
|
-
groupId,
|
|
23
|
-
artifactId,
|
|
24
|
-
name,
|
|
42
|
+
type: "maven-project", language: "java",
|
|
43
|
+
groupId, artifactId, name,
|
|
25
44
|
packageName: `${groupId}.${artifactId.replace(/-/g, "")}`,
|
|
26
45
|
dependencies: dependencies.join(","),
|
|
27
46
|
});
|
|
28
|
-
|
|
29
47
|
if (bootVersion) params.set("bootVersion", bootVersion);
|
|
30
48
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const res = await axios.get(url, {
|
|
49
|
+
const res = await axios.get(`https://start.spring.io/starter.zip?${params.toString()}`, {
|
|
34
50
|
responseType: "stream",
|
|
35
51
|
headers: { Accept: "application/zip" },
|
|
52
|
+
timeout: 30_000,
|
|
36
53
|
});
|
|
37
|
-
|
|
38
54
|
return res;
|
|
39
55
|
}
|
|
40
56
|
|
|
@@ -51,8 +67,7 @@ async function appendApplicationProperties(projectDir, artifactId) {
|
|
|
51
67
|
try {
|
|
52
68
|
const propsPath = path.join(projectDir, "src", "main", "resources", "application.properties");
|
|
53
69
|
const dbProps = [
|
|
54
|
-
"",
|
|
55
|
-
"",
|
|
70
|
+
"", "",
|
|
56
71
|
"# --- Auto-generated by create-backlist ---",
|
|
57
72
|
`spring.datasource.url=jdbc:postgresql://localhost:5432/${artifactId}`,
|
|
58
73
|
"spring.datasource.username=postgres",
|
|
@@ -60,7 +75,6 @@ async function appendApplicationProperties(projectDir, artifactId) {
|
|
|
60
75
|
"spring.jpa.hibernate.ddl-auto=update",
|
|
61
76
|
"spring.jpa.show-sql=true",
|
|
62
77
|
].join("\n");
|
|
63
|
-
|
|
64
78
|
await fs.appendFile(propsPath, dbProps);
|
|
65
79
|
} catch {
|
|
66
80
|
console.log(chalk.yellow(" -> Could not update application.properties (continuing)."));
|
|
@@ -69,88 +83,79 @@ async function appendApplicationProperties(projectDir, artifactId) {
|
|
|
69
83
|
|
|
70
84
|
function buildModelsFromEndpoints(endpoints) {
|
|
71
85
|
const modelsToGenerate = new Map();
|
|
72
|
-
|
|
73
86
|
(Array.isArray(endpoints) ? endpoints : []).forEach((ep) => {
|
|
74
|
-
if (!ep
|
|
75
|
-
|
|
87
|
+
if (!ep?.controllerName || ep.controllerName === "Default") return;
|
|
76
88
|
if (!modelsToGenerate.has(ep.controllerName)) {
|
|
77
|
-
modelsToGenerate.set(ep.controllerName, {
|
|
78
|
-
name: ep.controllerName,
|
|
79
|
-
fields: [],
|
|
80
|
-
endpoints: [],
|
|
81
|
-
});
|
|
89
|
+
modelsToGenerate.set(ep.controllerName, { name: ep.controllerName, fields: [], endpoints: [] });
|
|
82
90
|
}
|
|
83
|
-
|
|
84
91
|
const m = modelsToGenerate.get(ep.controllerName);
|
|
85
|
-
|
|
86
|
-
m.endpoints.push({
|
|
87
|
-
method: ep.method,
|
|
88
|
-
route: ep.route || ep.path,
|
|
89
|
-
actionName: ep.actionName,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
+
m.endpoints.push({ method: ep.method, route: ep.route || ep.path, actionName: ep.actionName });
|
|
92
93
|
if (ep.schemaFields) {
|
|
93
94
|
for (const [key, type] of Object.entries(ep.schemaFields)) {
|
|
94
|
-
if (!m.fields.find((f) => f.name === key)) {
|
|
95
|
-
m.fields.push({ name: key, type });
|
|
96
|
-
}
|
|
95
|
+
if (!m.fields.find((f) => f.name === key)) m.fields.push({ name: key, type });
|
|
97
96
|
}
|
|
98
97
|
}
|
|
99
98
|
});
|
|
100
|
-
|
|
101
99
|
return modelsToGenerate;
|
|
102
100
|
}
|
|
103
101
|
|
|
102
|
+
// ─── Main Generator ───────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
104
|
export async function generateJavaProject(options) {
|
|
105
105
|
const { projectDir, projectName, frontendSrcDir } = options;
|
|
106
106
|
|
|
107
107
|
const groupId = "com.backlist.generated";
|
|
108
108
|
const artifactId = sanitizeArtifactId(projectName || "backend");
|
|
109
109
|
const name = projectName || "backend";
|
|
110
|
+
const totalTimer = timer();
|
|
110
111
|
|
|
111
112
|
try {
|
|
112
|
-
|
|
113
|
+
// ── Step 1: Download Initializr zip + analyze frontend + preload templates in parallel ──
|
|
114
|
+
const step1 = timer();
|
|
115
|
+
console.log(chalk.blue(" -> [1] Downloading Spring Initializr zip & analyzing frontend in parallel..."));
|
|
113
116
|
|
|
114
117
|
const deps = ["web", "data-jpa", "lombok", "postgresql"];
|
|
115
|
-
let response;
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
response = await downloadInitializrZip({
|
|
119
|
-
groupId,
|
|
120
|
-
artifactId,
|
|
121
|
-
name,
|
|
122
|
-
bootVersion: "3.3.4",
|
|
123
|
-
dependencies: deps,
|
|
124
|
-
});
|
|
125
|
-
} catch {
|
|
126
|
-
console.log(chalk.yellow(" -> Initial attempt failed. Retrying with default Boot version..."));
|
|
127
|
-
try {
|
|
128
|
-
response = await downloadInitializrZip({
|
|
129
|
-
groupId,
|
|
130
|
-
artifactId,
|
|
131
|
-
name,
|
|
132
|
-
bootVersion: "",
|
|
133
|
-
dependencies: deps,
|
|
134
|
-
});
|
|
135
|
-
} catch {
|
|
136
|
-
console.log(chalk.yellow(" -> Second attempt failed. Retrying with minimal dependencies..."));
|
|
137
|
-
const fallbackDeps = ["web", "data-jpa", "lombok"];
|
|
138
|
-
response = await downloadInitializrZip({
|
|
139
|
-
groupId,
|
|
140
|
-
artifactId,
|
|
141
|
-
name,
|
|
142
|
-
bootVersion: "",
|
|
143
|
-
dependencies: fallbackDeps,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
118
|
|
|
148
|
-
|
|
119
|
+
const downloadWithFallback = () =>
|
|
120
|
+
withRetry(
|
|
121
|
+
async () => {
|
|
122
|
+
// Try with specific boot version first, fall back gracefully
|
|
123
|
+
const attempts = [
|
|
124
|
+
{ bootVersion: "3.3.4", dependencies: deps },
|
|
125
|
+
{ bootVersion: "", dependencies: deps },
|
|
126
|
+
{ bootVersion: "", dependencies: ["web", "data-jpa", "lombok"] },
|
|
127
|
+
];
|
|
128
|
+
for (const params of attempts) {
|
|
129
|
+
try {
|
|
130
|
+
return await downloadInitializrZip({ groupId, artifactId, name, ...params });
|
|
131
|
+
} catch { /* try next */ }
|
|
132
|
+
}
|
|
133
|
+
throw new Error("All Spring Initializr attempts failed.");
|
|
134
|
+
},
|
|
135
|
+
{ attempts: 2, baseDelay: 1000, label: "Spring Initializr download" }
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const [response, endpoints] = await Promise.all([
|
|
139
|
+
downloadWithFallback(),
|
|
140
|
+
analyzeFrontend(frontendSrcDir),
|
|
141
|
+
preloadTemplates([
|
|
142
|
+
"java-spring/partials/Entity.java.ejs",
|
|
143
|
+
"java-spring/partials/Repository.java.ejs",
|
|
144
|
+
"java-spring/partials/Service.java.ejs",
|
|
145
|
+
"java-spring/partials/Controller.java.ejs",
|
|
146
|
+
"java-spring/partials/Dockerfile.ejs",
|
|
147
|
+
"java-spring/partials/docker-compose.yml.ejs",
|
|
148
|
+
]).catch(() => {}),
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
console.log(chalk.gray(` Download + analysis done. ${step1()}`));
|
|
152
|
+
|
|
153
|
+
// ── Step 2: Extract zip + ensure project dir ───────────────────────────────
|
|
154
|
+
const step2 = timer();
|
|
155
|
+
console.log(chalk.blue(" -> [2] Unzipping Spring Boot project..."));
|
|
149
156
|
await fs.ensureDir(projectDir);
|
|
150
157
|
await extractZipStream(response.data, projectDir);
|
|
151
|
-
|
|
152
|
-
console.log(chalk.blue(" -> Analyzing frontend for API endpoints..."));
|
|
153
|
-
const endpoints = await analyzeFrontend(frontendSrcDir);
|
|
158
|
+
console.log(chalk.gray(` Unzip done. ${step2()}`));
|
|
154
159
|
|
|
155
160
|
if (!Array.isArray(endpoints) || endpoints.length === 0) {
|
|
156
161
|
console.log(chalk.yellow(" -> No endpoints found. Only base Spring project created."));
|
|
@@ -160,89 +165,72 @@ export async function generateJavaProject(options) {
|
|
|
160
165
|
|
|
161
166
|
const modelsToGenerate = buildModelsFromEndpoints(endpoints);
|
|
162
167
|
|
|
168
|
+
// ── Step 3: Prepare Java source directories in parallel ────────────────────
|
|
163
169
|
const javaSrcRoot = path.join(
|
|
164
|
-
projectDir,
|
|
165
|
-
"src",
|
|
166
|
-
"main",
|
|
167
|
-
"java",
|
|
170
|
+
projectDir, "src", "main", "java",
|
|
168
171
|
...groupId.split("."),
|
|
169
172
|
artifactId.replace(/-/g, "")
|
|
170
173
|
);
|
|
171
174
|
|
|
172
|
-
|
|
175
|
+
const entityDir = path.join(javaSrcRoot, "model");
|
|
176
|
+
const repoDir = path.join(javaSrcRoot, "repository");
|
|
177
|
+
const serviceDir = path.join(javaSrcRoot, "service");
|
|
178
|
+
const controllerDir = path.join(javaSrcRoot, "controller");
|
|
173
179
|
|
|
174
|
-
|
|
175
|
-
|
|
180
|
+
await Promise.all([
|
|
181
|
+
fs.ensureDir(javaSrcRoot),
|
|
182
|
+
fs.ensureDir(entityDir),
|
|
183
|
+
fs.ensureDir(repoDir),
|
|
184
|
+
fs.ensureDir(serviceDir),
|
|
185
|
+
fs.ensureDir(controllerDir),
|
|
186
|
+
]);
|
|
176
187
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
188
|
+
// ── Step 4: Generate all Java layers in parallel ───────────────────────────
|
|
189
|
+
if (modelsToGenerate.size > 0) {
|
|
190
|
+
const step4 = timer();
|
|
191
|
+
console.log(chalk.blue(` -> [4] Generating ${modelsToGenerate.size} Java entity/repo/service/controller sets in parallel...`));
|
|
181
192
|
|
|
182
|
-
|
|
183
|
-
await fs.ensureDir(repoDir);
|
|
184
|
-
await fs.ensureDir(serviceDir);
|
|
185
|
-
await fs.ensureDir(controllerDir);
|
|
193
|
+
const allTasks = [];
|
|
186
194
|
|
|
187
195
|
for (const model of modelsToGenerate.values()) {
|
|
188
196
|
const commonData = {
|
|
189
|
-
projectName,
|
|
190
|
-
groupId,
|
|
191
|
-
artifactId,
|
|
192
|
-
group: groupId,
|
|
193
|
-
model,
|
|
197
|
+
projectName, groupId, artifactId,
|
|
198
|
+
group: groupId, model,
|
|
194
199
|
modelName: model.name,
|
|
195
|
-
controllerName: model.name
|
|
200
|
+
controllerName: model.name,
|
|
196
201
|
};
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
getTemplatePath("java-spring/partials/
|
|
200
|
-
path.join(
|
|
201
|
-
commonData
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
await renderAndWrite(
|
|
205
|
-
getTemplatePath("java-spring/partials/Repository.java.ejs"),
|
|
206
|
-
path.join(repoDir, `${model.name}Repository.java`),
|
|
207
|
-
commonData
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
await renderAndWrite(
|
|
211
|
-
getTemplatePath("java-spring/partials/Service.java.ejs"),
|
|
212
|
-
path.join(serviceDir, `${model.name}Service.java`),
|
|
213
|
-
commonData
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
await renderAndWrite(
|
|
217
|
-
getTemplatePath("java-spring/partials/Controller.java.ejs"),
|
|
218
|
-
path.join(controllerDir, `${model.name}Controller.java`),
|
|
219
|
-
commonData
|
|
202
|
+
allTasks.push(
|
|
203
|
+
{ templatePath: getTemplatePath("java-spring/partials/Entity.java.ejs"), outPath: path.join(entityDir, `${model.name}.java`), data: commonData },
|
|
204
|
+
{ templatePath: getTemplatePath("java-spring/partials/Repository.java.ejs"), outPath: path.join(repoDir, `${model.name}Repository.java`), data: commonData },
|
|
205
|
+
{ templatePath: getTemplatePath("java-spring/partials/Service.java.ejs"), outPath: path.join(serviceDir, `${model.name}Service.java`), data: commonData },
|
|
206
|
+
{ templatePath: getTemplatePath("java-spring/partials/Controller.java.ejs"), outPath: path.join(controllerDir, `${model.name}Controller.java`), data: commonData }
|
|
220
207
|
);
|
|
221
208
|
}
|
|
209
|
+
|
|
210
|
+
await renderAndWriteAll(allTasks, 12); // high concurrency — mostly CPU-bound EJS
|
|
211
|
+
console.log(chalk.gray(` Java layers done. ${step4()}`));
|
|
222
212
|
} else {
|
|
223
213
|
console.log(chalk.yellow(" -> No models inferred. Skipping entity/controller generation."));
|
|
224
214
|
}
|
|
225
215
|
|
|
226
|
-
|
|
216
|
+
// ── Step 5: application.properties + Docker files in parallel ─────────────
|
|
217
|
+
const step5 = timer();
|
|
218
|
+
console.log(chalk.blue(" -> [5] Writing application.properties & Docker files in parallel..."));
|
|
227
219
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
getTemplatePath("java-spring/partials/Dockerfile.ejs"),
|
|
231
|
-
path.join(projectDir, "
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
path.join(projectDir, "docker-compose.yml"),
|
|
237
|
-
{ projectName, artifactId }
|
|
238
|
-
);
|
|
220
|
+
await Promise.all([
|
|
221
|
+
appendApplicationProperties(projectDir, artifactId),
|
|
222
|
+
renderAndWrite(getTemplatePath("java-spring/partials/Dockerfile.ejs"), path.join(projectDir, "Dockerfile"), { projectName, artifactId }),
|
|
223
|
+
renderAndWrite(getTemplatePath("java-spring/partials/docker-compose.yml.ejs"), path.join(projectDir, "docker-compose.yml"), { projectName, artifactId }),
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
console.log(chalk.gray(` Configs done. ${step5()}`));
|
|
227
|
+
console.log(chalk.green(` -> ✓ Java (Spring Boot) backend generation complete. Total: ${chalk.bold(totalTimer())}`));
|
|
239
228
|
|
|
240
|
-
console.log(chalk.green(" -> Java (Spring Boot) backend generation is complete!"));
|
|
241
229
|
} catch (error) {
|
|
242
|
-
if (error
|
|
230
|
+
if (error?.response?.status) {
|
|
243
231
|
console.error(chalk.red(` -> Initializr error status: ${error.response.status}`));
|
|
244
232
|
throw new Error(`Failed to download from Spring Initializr. Status: ${error.response.status}`);
|
|
245
233
|
}
|
|
246
234
|
throw error;
|
|
247
235
|
}
|
|
248
|
-
}
|
|
236
|
+
}
|