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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-backlist",
3
- "version": "10.0.9",
3
+ "version": "10.1.1",
4
4
  "description": "An advanced, multi-language backend generator based on frontend analysis. Smart Freemium SaaS CLI with Live QA.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- console.log(chalk.blue(' -> Analyzing frontend for C# backend...'));
13
- const endpoints = await analyzeFrontend(frontendSrcDir);
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/controllers to generate.`));
61
+ console.log(chalk.green(` -> Identified ${modelsToGenerate.size} models. ${chalk.gray(step1())}`));
26
62
  } else {
27
- console.log(chalk.yellow(' -> No API calls with body data found. A basic API project will be created without models.'));
63
+ console.log(chalk.yellow(` -> No models found. Basic API project will be created. ${chalk.gray(step1())}`));
28
64
  }
29
65
 
30
- console.log(chalk.blue(' -> Scaffolding .NET Core Web API project...'));
31
- await execa('dotnet', ['new', 'webapi', '-n', projectName, '-o', projectDir, '--no-https']);
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
- console.log(chalk.blue(' -> Adding NuGet packages (Entity Framework Core)...'));
35
- const packages = [
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
- await fs.ensureDir(modelsDir);
49
- await fs.ensureDir(dataDir);
50
-
51
- for (const [modelName, modelData] of modelsToGenerate.entries()) {
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
- await renderAndWrite(
60
- getTemplatePath('dotnet/partials/DbContext.cs.ejs'),
61
- path.join(dataDir, 'ApplicationDbContext.cs'),
62
- { projectName, modelsToGenerate: Array.from(modelsToGenerate.values()) }
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
- console.log(chalk.blue(' -> Configuring services in Program.cs...'));
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
- let usingStatements = 'using Microsoft.EntityFrameworkCore;\nusing ' + projectName + '.Data;\n';
71
- programCsContent = usingStatements + programCsContent;
126
+ programCsContent =
127
+ `using Microsoft.EntityFrameworkCore;\nusing ${projectName}.Data;\n` + programCsContent;
72
128
 
73
- let dbContextService = '// Configure the database context\nbuilder.Services.AddDbContext<ApplicationDbContext>(opt => opt.UseInMemoryDatabase("MyDb"));';
74
- programCsContent = programCsContent.replace('builder.Services.AddControllers();', `builder.Services.AddControllers();\n\n${dbContextService}`);
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
- policy =>
81
- {
82
- policy.WithOrigins("http://localhost:3000", "http://localhost:5173")
83
- .AllowAnyHeader()
84
- .AllowAnyMethod();
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('var app = builder.Build();', `${corsPolicy}\n\nvar app = builder.Build();\n\napp.UseCors();`);
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
- console.log(chalk.blue(' -> Generating controllers with CRUD logic...'));
92
- await fs.remove(path.join(projectDir, 'Controllers', 'WeatherForecastController.cs'));
93
- await fs.remove(path.join(projectDir, 'WeatherForecast.cs'));
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
- for (const controllerName of controllersToGenerate) {
101
- const controllerEndpoints = endpoints.filter(
102
- ep => ep.controllerName === controllerName
103
- );
104
- await renderAndWrite(
105
- getTemplatePath('dotnet/partials/Controller.cs.ejs'),
106
- path.join(projectDir, 'Controllers', `${controllerName}Controller.cs`),
107
- { projectName, controllerName, endpoints: controllerEndpoints }
108
- );
109
- }
110
-
111
- console.log(chalk.blue(' -> Generating Docker files...'));
112
- await renderAndWrite(
113
- getTemplatePath('dotnet/partials/Dockerfile.ejs'),
114
- path.join(projectDir, 'Dockerfile'),
115
- { projectName }
116
- );
117
- await renderAndWrite(
118
- getTemplatePath('dotnet/partials/docker-compose.yml.ejs'),
119
- path.join(projectDir, 'docker-compose.yml'),
120
- { projectName }
121
- );
122
-
123
- await renderAndWrite(
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
+ }
@@ -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
- language: "java",
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 url = `https://start.spring.io/starter.zip?${params.toString()}`;
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 || !ep.controllerName || ep.controllerName === "Default") return;
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
- console.log(chalk.blue(" -> Contacting Spring Initializr to download a base Spring Boot project..."));
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
- console.log(chalk.blue(" -> Unzipping the Spring Boot project..."));
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
- await fs.ensureDir(javaSrcRoot);
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
- if (modelsToGenerate.size > 0) {
175
- console.log(chalk.blue(" -> Generating Java entities, repositories, services, and controllers..."));
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
- const entityDir = path.join(javaSrcRoot, "model");
178
- const repoDir = path.join(javaSrcRoot, "repository");
179
- const serviceDir = path.join(javaSrcRoot, "service");
180
- const controllerDir = path.join(javaSrcRoot, "controller");
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
- await fs.ensureDir(entityDir);
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
- await renderAndWrite(
199
- getTemplatePath("java-spring/partials/Entity.java.ejs"),
200
- path.join(entityDir, `${model.name}.java`),
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
- await appendApplicationProperties(projectDir, artifactId);
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
- console.log(chalk.blue(" -> Generating Docker files..."));
229
- await renderAndWrite(
230
- getTemplatePath("java-spring/partials/Dockerfile.ejs"),
231
- path.join(projectDir, "Dockerfile"),
232
- { projectName, artifactId }
233
- );
234
- await renderAndWrite(
235
- getTemplatePath("java-spring/partials/docker-compose.yml.ejs"),
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 && error.response && error.response.status) {
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
+ }