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.
@@ -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
- console.log(chalk.blue(' -> Analyzing frontend for Python (FastAPI) backend...'));
13
- const endpoints = await analyzeFrontend(frontendSrcDir);
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, { name: ep.controllerName, fields: Object.entries(ep.schemaFields).map(([key, type]) => ({ name: key, type })) });
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.blue(' -> Scaffolding Python (FastAPI) project structure...'));
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
- await fs.ensureDir(appDir);
34
- await fs.ensureDir(coreDir);
35
- await fs.ensureDir(dbDir);
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 controllers = Array.from(modelsToGenerate.keys());
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 renderAndWrite(getTemplatePath('python-fastapi/main.py.ejs'), path.join(projectDir, 'app', 'main.py'), { projectName, controllers });
43
- await renderAndWrite(getTemplatePath('python-fastapi/requirements.txt.ejs'), path.join(projectDir, 'requirements.txt'), {});
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
- await renderAndWrite(getTemplatePath('python-fastapi/app/core/config.py.ejs'), path.join(coreDir, 'config.py'), { projectName });
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
- await renderAndWrite(getTemplatePath('python-fastapi/app/db.py.ejs'), path.join(appDir, 'db.py'), {});
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
- await renderAndWrite(getTemplatePath('python-fastapi/app/models/user.py.ejs'), path.join(modelsDir, 'user.py'), {});
51
- await renderAndWrite(getTemplatePath('python-fastapi/app/schemas/user.py.ejs'), path.join(schemasDir, 'user.py'), {});
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
- await renderAndWrite(getTemplatePath('python-fastapi/app/routers/auth.py.ejs'), path.join(routesDir, 'auth.py'), {});
136
+ console.log(chalk.gray(` All Python files generated. ${step3()}`));
54
137
 
55
- for (const [modelName, modelData] of modelsToGenerate.entries()) {
56
- if (modelName.toLowerCase() !== 'user') {
57
- await renderAndWrite(getTemplatePath('python-fastapi/app/routers/model_routes.py.ejs'), path.join(routesDir, `${modelName.toLowerCase()}_routes.py`), { modelName, schema: modelData });
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
- console.log(chalk.magenta(' -> Setting up virtual environment and installing dependencies...'));
62
- await execa('python', ['-m', 'venv', 'venv'], { cwd: projectDir });
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' ? path.join('venv', 'Scripts', 'pip') : path.join('venv', 'bin', 'pip');
65
- await execa(path.join(projectDir, pipPath), ['install', '-r', 'requirements.txt'], { cwd: projectDir });
147
+ const pipPath = process.platform === 'win32'
148
+ ? path.join('venv', 'Scripts', 'pip')
149
+ : path.join('venv', 'bin', 'pip');
66
150
 
67
- await renderAndWrite(getTemplatePath('python-fastapi/Dockerfile.ejs'), path.join(projectDir, 'Dockerfile'), {});
68
- await renderAndWrite(getTemplatePath('python-fastapi/docker-compose.yml.ejs'), path.join(projectDir, 'docker-compose.yml'), { projectName });
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
- const envContent = `DATABASE_URL="postgresql://postgres:password@db:5432/${projectName}"\nJWT_SECRET="a_very_secret_key_change_this"`;
71
- await fs.writeFile(path.join(projectDir, '.env'), envContent);
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.green(' -> Python (FastAPI) backend generation is complete!'));
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 will be available at http://localhost:8000 and docs at http://localhost:8000/docs'));
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}' command not found. Please ensure Python and venv are installed and in your system's PATH.`);
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 fs.readFile(templatePath, 'utf-8');
12
- const code = ejs.render(tpl, data || {}, { filename: templatePath }); // filename helps with EJS errors
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
  }