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/src/generators/python.js
CHANGED
|
@@ -3,18 +3,65 @@ 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
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function timer() {
|
|
11
|
+
const s = Date.now();
|
|
12
|
+
return () => `${((Date.now() - s) / 1000).toFixed(2)}s`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function withRetry(fn, { attempts = 3, baseDelay = 300, label = 'task' } = {}) {
|
|
16
|
+
let lastErr;
|
|
17
|
+
for (let i = 0; i < attempts; i++) {
|
|
18
|
+
try { return await fn(); } catch (err) {
|
|
19
|
+
lastErr = err;
|
|
20
|
+
if (i < attempts - 1) {
|
|
21
|
+
const delay = baseDelay * 2 ** i;
|
|
22
|
+
console.log(chalk.yellow(` [RETRY] ${label} (${i + 1}/${attempts}), retrying in ${delay}ms...`));
|
|
23
|
+
await new Promise(r => setTimeout(r, delay));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
throw lastErr;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── Main Generator ───────────────────────────────────────────────────────────
|
|
7
31
|
|
|
8
32
|
export async function generatePythonProject(options) {
|
|
9
33
|
const { projectDir, projectName, frontendSrcDir } = options;
|
|
34
|
+
const totalTimer = timer();
|
|
10
35
|
|
|
11
36
|
try {
|
|
12
|
-
|
|
13
|
-
const
|
|
37
|
+
// ── Step 1: Analyze + preload templates in parallel ────────────────────────
|
|
38
|
+
const step1 = timer();
|
|
39
|
+
console.log(chalk.blue(' -> [1] Analyzing frontend & preloading templates in parallel...'));
|
|
40
|
+
|
|
41
|
+
const [endpoints] = await Promise.all([
|
|
42
|
+
analyzeFrontend(frontendSrcDir),
|
|
43
|
+
preloadTemplates([
|
|
44
|
+
'python-fastapi/main.py.ejs',
|
|
45
|
+
'python-fastapi/requirements.txt.ejs',
|
|
46
|
+
'python-fastapi/app/core/config.py.ejs',
|
|
47
|
+
'python-fastapi/app/core/security.py.ejs',
|
|
48
|
+
'python-fastapi/app/db.py.ejs',
|
|
49
|
+
'python-fastapi/app/models/user.py.ejs',
|
|
50
|
+
'python-fastapi/app/schemas/user.py.ejs',
|
|
51
|
+
'python-fastapi/app/routers/auth.py.ejs',
|
|
52
|
+
'python-fastapi/app/routers/model_routes.py.ejs',
|
|
53
|
+
'python-fastapi/Dockerfile.ejs',
|
|
54
|
+
'python-fastapi/docker-compose.yml.ejs',
|
|
55
|
+
]).catch(() => {}),
|
|
56
|
+
]);
|
|
57
|
+
|
|
14
58
|
const modelsToGenerate = new Map();
|
|
15
59
|
endpoints.forEach(ep => {
|
|
16
60
|
if (ep.schemaFields && ep.controllerName !== 'Default' && !modelsToGenerate.has(ep.controllerName)) {
|
|
17
|
-
modelsToGenerate.set(ep.controllerName, {
|
|
61
|
+
modelsToGenerate.set(ep.controllerName, {
|
|
62
|
+
name: ep.controllerName,
|
|
63
|
+
fields: Object.entries(ep.schemaFields).map(([key, type]) => ({ name: key, type })),
|
|
64
|
+
});
|
|
18
65
|
}
|
|
19
66
|
});
|
|
20
67
|
|
|
@@ -22,65 +69,103 @@ export async function generatePythonProject(options) {
|
|
|
22
69
|
modelsToGenerate.set('User', { name: 'User', fields: [{ name: 'name', type: 'String' }, { name: 'email', type: 'String' }] });
|
|
23
70
|
}
|
|
24
71
|
|
|
25
|
-
console.log(chalk.
|
|
26
|
-
const appDir = path.join(projectDir, 'app');
|
|
27
|
-
const coreDir = path.join(appDir, 'core');
|
|
28
|
-
const dbDir = path.join(appDir, 'db');
|
|
29
|
-
const modelsDir = path.join(appDir, 'models');
|
|
30
|
-
const schemasDir = path.join(appDir, 'schemas');
|
|
31
|
-
const routesDir = path.join(appDir, 'routers');
|
|
72
|
+
console.log(chalk.green(` -> Found ${endpoints.length} endpoints, ${modelsToGenerate.size} model(s). ${chalk.gray(step1())}`));
|
|
32
73
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
await fs.ensureDir(modelsDir);
|
|
37
|
-
await fs.ensureDir(schemasDir);
|
|
38
|
-
await fs.ensureDir(routesDir);
|
|
74
|
+
// ── Step 2: Create all directories in parallel ─────────────────────────────
|
|
75
|
+
const step2 = timer();
|
|
76
|
+
console.log(chalk.blue(' -> [2] Creating project directories in parallel...'));
|
|
39
77
|
|
|
40
|
-
const
|
|
78
|
+
const appDir = path.join(projectDir, 'app');
|
|
79
|
+
const coreDir = path.join(appDir, 'core');
|
|
80
|
+
const modelsDir = path.join(appDir, 'models');
|
|
81
|
+
const schemasDir = path.join(appDir, 'schemas');
|
|
82
|
+
const routesDir = path.join(appDir, 'routers');
|
|
41
83
|
|
|
42
|
-
await
|
|
43
|
-
|
|
84
|
+
await Promise.all([
|
|
85
|
+
fs.ensureDir(appDir),
|
|
86
|
+
fs.ensureDir(coreDir),
|
|
87
|
+
fs.ensureDir(path.join(appDir, 'db')),
|
|
88
|
+
fs.ensureDir(modelsDir),
|
|
89
|
+
fs.ensureDir(schemasDir),
|
|
90
|
+
fs.ensureDir(routesDir),
|
|
91
|
+
]);
|
|
44
92
|
|
|
45
|
-
|
|
46
|
-
await renderAndWrite(getTemplatePath('python-fastapi/app/core/security.py.ejs'), path.join(coreDir, 'security.py'), {});
|
|
93
|
+
console.log(chalk.gray(` Dirs created. ${step2()}`));
|
|
47
94
|
|
|
48
|
-
|
|
95
|
+
// ── Step 3: Generate ALL Python files in parallel ─────────────────────────
|
|
96
|
+
const step3 = timer();
|
|
97
|
+
console.log(chalk.blue(' -> [3] Generating all Python source files in parallel...'));
|
|
98
|
+
|
|
99
|
+
const controllers = Array.from(modelsToGenerate.keys());
|
|
49
100
|
|
|
50
|
-
|
|
51
|
-
|
|
101
|
+
const renderTasks = [
|
|
102
|
+
// Top-level files
|
|
103
|
+
{ templatePath: getTemplatePath('python-fastapi/main.py.ejs'), outPath: path.join(appDir, 'main.py'), data: { projectName, controllers } },
|
|
104
|
+
{ templatePath: getTemplatePath('python-fastapi/requirements.txt.ejs'), outPath: path.join(projectDir, 'requirements.txt'), data: {} },
|
|
105
|
+
// Core
|
|
106
|
+
{ templatePath: getTemplatePath('python-fastapi/app/core/config.py.ejs'), outPath: path.join(coreDir, 'config.py'), data: { projectName } },
|
|
107
|
+
{ templatePath: getTemplatePath('python-fastapi/app/core/security.py.ejs'), outPath: path.join(coreDir, 'security.py'), data: {} },
|
|
108
|
+
// DB
|
|
109
|
+
{ templatePath: getTemplatePath('python-fastapi/app/db.py.ejs'), outPath: path.join(appDir, 'db.py'), data: {} },
|
|
110
|
+
// User model + schema + auth router (always present)
|
|
111
|
+
{ templatePath: getTemplatePath('python-fastapi/app/models/user.py.ejs'), outPath: path.join(modelsDir, 'user.py'), data: {} },
|
|
112
|
+
{ templatePath: getTemplatePath('python-fastapi/app/schemas/user.py.ejs'), outPath: path.join(schemasDir, 'user.py'), data: {} },
|
|
113
|
+
{ templatePath: getTemplatePath('python-fastapi/app/routers/auth.py.ejs'), outPath: path.join(routesDir, 'auth.py'), data: {} },
|
|
114
|
+
// Docker
|
|
115
|
+
{ templatePath: getTemplatePath('python-fastapi/Dockerfile.ejs'), outPath: path.join(projectDir, 'Dockerfile'), data: {} },
|
|
116
|
+
{ templatePath: getTemplatePath('python-fastapi/docker-compose.yml.ejs'), outPath: path.join(projectDir, 'docker-compose.yml'), data: { projectName } },
|
|
117
|
+
// Non-user model routes
|
|
118
|
+
...Array.from(modelsToGenerate.entries())
|
|
119
|
+
.filter(([modelName]) => modelName.toLowerCase() !== 'user')
|
|
120
|
+
.map(([modelName, modelData]) => ({
|
|
121
|
+
templatePath: getTemplatePath('python-fastapi/app/routers/model_routes.py.ejs'),
|
|
122
|
+
outPath: path.join(routesDir, `${modelName.toLowerCase()}_routes.py`),
|
|
123
|
+
data: { modelName, schema: modelData },
|
|
124
|
+
})),
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
await renderAndWriteAll(renderTasks, 12);
|
|
128
|
+
|
|
129
|
+
// .env files (no template needed, write directly)
|
|
130
|
+
const envContent = `DATABASE_URL="postgresql://postgres:password@db:5432/${projectName}"\nJWT_SECRET="a_very_secret_key_change_this"`;
|
|
131
|
+
await Promise.all([
|
|
132
|
+
fs.writeFile(path.join(projectDir, '.env'), envContent),
|
|
133
|
+
fs.writeFile(path.join(projectDir, '.env.example'), envContent),
|
|
134
|
+
]);
|
|
52
135
|
|
|
53
|
-
|
|
136
|
+
console.log(chalk.gray(` All Python files generated. ${step3()}`));
|
|
54
137
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
}
|
|
138
|
+
// ── Step 4: Create venv + install deps ────────────────────────────────────
|
|
139
|
+
const step4 = timer();
|
|
140
|
+
console.log(chalk.magenta(' -> [4] Setting up virtual environment...'));
|
|
60
141
|
|
|
61
|
-
|
|
62
|
-
|
|
142
|
+
await withRetry(
|
|
143
|
+
() => execa('python', ['-m', 'venv', 'venv'], { cwd: projectDir }),
|
|
144
|
+
{ attempts: 2, baseDelay: 500, label: 'python -m venv' }
|
|
145
|
+
);
|
|
63
146
|
|
|
64
|
-
const pipPath = process.platform === 'win32'
|
|
65
|
-
|
|
147
|
+
const pipPath = process.platform === 'win32'
|
|
148
|
+
? path.join('venv', 'Scripts', 'pip')
|
|
149
|
+
: path.join('venv', 'bin', 'pip');
|
|
66
150
|
|
|
67
|
-
|
|
68
|
-
await
|
|
151
|
+
console.log(chalk.magenta(' -> [4] Installing Python dependencies...'));
|
|
152
|
+
await withRetry(
|
|
153
|
+
() => execa(path.join(projectDir, pipPath), ['install', '-r', 'requirements.txt'], { cwd: projectDir }),
|
|
154
|
+
{ attempts: 2, baseDelay: 1000, label: 'pip install' }
|
|
155
|
+
);
|
|
69
156
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
await fs.writeFile(path.join(projectDir, '.env.example'), envContent);
|
|
157
|
+
console.log(chalk.gray(` venv + pip install done. ${step4()}`));
|
|
158
|
+
console.log(chalk.green(` -> ✓ Python (FastAPI) backend generation complete. Total: ${chalk.bold(totalTimer())}`));
|
|
73
159
|
|
|
74
|
-
console.log(chalk.
|
|
75
|
-
console.log(chalk.yellow('\nTo run your new Python backend with Docker:'));
|
|
160
|
+
console.log(chalk.yellow('\nTo run your Python backend with Docker:'));
|
|
76
161
|
console.log(chalk.cyan(' 1. Make sure Docker Desktop is running.'));
|
|
77
162
|
console.log(chalk.cyan(' 2. Run: `docker-compose up --build`'));
|
|
78
|
-
console.log(chalk.cyan(' 3. API
|
|
163
|
+
console.log(chalk.cyan(' 3. API: http://localhost:8000 | Docs: http://localhost:8000/docs'));
|
|
79
164
|
|
|
80
165
|
} catch (error) {
|
|
81
166
|
if (error.code === 'ENOENT') {
|
|
82
|
-
throw new Error(`'${error.command}'
|
|
167
|
+
throw new Error(`'${error.command}' not found. Ensure Python and venv are installed and in your PATH.`);
|
|
83
168
|
}
|
|
84
169
|
throw error;
|
|
85
170
|
}
|
|
86
|
-
}
|
|
171
|
+
}
|
|
@@ -6,10 +6,25 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
7
|
const __dirname = path.dirname(__filename);
|
|
8
8
|
|
|
9
|
+
// ─── Template Cache (avoids re-reading same file from disk) ───────────────────
|
|
10
|
+
const _templateCache = new Map();
|
|
11
|
+
|
|
12
|
+
async function readTemplateCached(templatePath) {
|
|
13
|
+
if (_templateCache.has(templatePath)) return _templateCache.get(templatePath);
|
|
14
|
+
const tpl = await fs.readFile(templatePath, 'utf-8');
|
|
15
|
+
_templateCache.set(templatePath, tpl);
|
|
16
|
+
return tpl;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function clearTemplateCache() {
|
|
20
|
+
_templateCache.clear();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ─── Core render + write (now cached) ─────────────────────────────────────────
|
|
9
24
|
export async function renderAndWrite(templatePath, outPath, data) {
|
|
10
25
|
try {
|
|
11
|
-
const tpl = await
|
|
12
|
-
const code = ejs.render(tpl, data || {}, { filename: templatePath });
|
|
26
|
+
const tpl = await readTemplateCached(templatePath);
|
|
27
|
+
const code = ejs.render(tpl, data || {}, { filename: templatePath });
|
|
13
28
|
await fs.outputFile(outPath, code);
|
|
14
29
|
} catch (err) {
|
|
15
30
|
console.error('EJS render failed for:', templatePath);
|
|
@@ -18,6 +33,36 @@ export async function renderAndWrite(templatePath, outPath, data) {
|
|
|
18
33
|
}
|
|
19
34
|
}
|
|
20
35
|
|
|
36
|
+
// ─── Parallel batch render: fire all templates simultaneously ─────────────────
|
|
37
|
+
/**
|
|
38
|
+
* @param {Array<{templatePath: string, outPath: string, data: object}>} tasks
|
|
39
|
+
* @param {number} concurrency max simultaneous writes (default 8)
|
|
40
|
+
*/
|
|
41
|
+
export async function renderAndWriteAll(tasks, concurrency = 8) {
|
|
42
|
+
const results = [];
|
|
43
|
+
for (let i = 0; i < tasks.length; i += concurrency) {
|
|
44
|
+
const batch = tasks.slice(i, i + concurrency);
|
|
45
|
+
const batchResults = await Promise.allSettled(
|
|
46
|
+
batch.map(({ templatePath, outPath, data }) =>
|
|
47
|
+
renderAndWrite(templatePath, outPath, data)
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
results.push(...batchResults);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const failed = results.filter(r => r.status === 'rejected');
|
|
54
|
+
if (failed.length > 0) {
|
|
55
|
+
failed.forEach(f => console.error(' [WARN] Render failed:', f.reason?.message));
|
|
56
|
+
}
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── Template path helper ──────────────────────────────────────────────────────
|
|
21
61
|
export function getTemplatePath(subpath) {
|
|
22
62
|
return path.join(__dirname, '..', 'templates', subpath);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Preload a set of templates into cache (call early in generation) ──────────
|
|
66
|
+
export async function preloadTemplates(subpaths) {
|
|
67
|
+
await Promise.all(subpaths.map(sp => readTemplateCached(getTemplatePath(sp))));
|
|
23
68
|
}
|