novatec-cli 1.0.2 → 2.0.0
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 +145 -63
- package/lib/create.js +312 -211
- package/lib/prompts.js +187 -260
- package/package.json +1 -1
package/lib/create.js
CHANGED
|
@@ -1,277 +1,378 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import fse from 'fs-extra';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { execSync, spawnSync } from 'child_process';
|
|
4
6
|
import { askProjectOptions } from './prompts.js';
|
|
5
7
|
import { generateFrontend } from './generators/frontend.js';
|
|
6
8
|
import { generateBackend } from './generators/backend.js';
|
|
7
9
|
import { generateReadme } from './generators/readme.js';
|
|
8
10
|
import { run, isInstalled } from './utils.js';
|
|
11
|
+
import boxen from 'boxen';
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
// ── PROGRESO ──────────────────────────────────────────────────────────────────
|
|
14
|
+
function progressBar(current, total, label) {
|
|
15
|
+
const width = 30;
|
|
16
|
+
const filled = Math.round((current / total) * width);
|
|
17
|
+
const empty = width - filled;
|
|
18
|
+
const pct = Math.round((current / total) * 100);
|
|
19
|
+
const bar = chalk.white('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
20
|
+
process.stdout.write(`\r [${bar}] ${chalk.white(pct + '%')} ${chalk.gray(label)} `);
|
|
21
|
+
}
|
|
11
22
|
|
|
12
|
-
|
|
23
|
+
// ── MAIN ──────────────────────────────────────────────────────────────────────
|
|
24
|
+
export async function runCreate(nameArg, options) {
|
|
25
|
+
const startTime = Date.now();
|
|
13
26
|
const config = await askProjectOptions(nameArg, options);
|
|
14
27
|
const projectPath = path.resolve(process.cwd(), config.name);
|
|
15
28
|
|
|
16
|
-
|
|
17
|
-
throw new Error(`La carpeta "${config.name}" ya existe en este directorio.`);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
console.log(chalk.cyan(`\n📁 Creando proyecto en: ${chalk.bold(projectPath)}\n`));
|
|
29
|
+
// Asegurar carpeta
|
|
21
30
|
await fse.ensureDir(projectPath);
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
await generateReadme(config, projectPath);
|
|
32
|
+
const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
|
|
33
|
+
const isNode = !isPython && config.backend !== 'spring' && config.backend !== 'gin';
|
|
26
34
|
|
|
35
|
+
// Calcular pasos totales
|
|
27
36
|
const extras = config.extras || [];
|
|
37
|
+
const steps = [
|
|
38
|
+
'Generando frontend',
|
|
39
|
+
'Generando backend',
|
|
40
|
+
'Generando README',
|
|
41
|
+
extras.includes('env') && 'Archivos .env',
|
|
42
|
+
extras.includes('editorconfig') && '.editorconfig',
|
|
43
|
+
extras.includes('eslint') && 'ESLint + Prettier',
|
|
44
|
+
extras.includes('husky') && 'Husky',
|
|
45
|
+
extras.includes('testing') && 'Vitest',
|
|
46
|
+
extras.includes('ci') && 'GitHub Actions',
|
|
47
|
+
extras.includes('docker') && 'Docker',
|
|
48
|
+
extras.includes('auth') && 'Auth JWT',
|
|
49
|
+
extras.includes('tailwind') && 'Tailwind CSS',
|
|
50
|
+
extras.includes('git') && 'Git init',
|
|
51
|
+
config.frontendDeps?.length && 'Instalando deps frontend',
|
|
52
|
+
config.backendDeps?.length && 'Instalando deps backend',
|
|
53
|
+
].filter(Boolean);
|
|
54
|
+
|
|
55
|
+
console.log();
|
|
56
|
+
let step = 0;
|
|
57
|
+
|
|
58
|
+
const tick = (label) => {
|
|
59
|
+
step++;
|
|
60
|
+
progressBar(step, steps.length, label);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// 1. Frontend
|
|
64
|
+
tick('Generando frontend...');
|
|
65
|
+
await generateFrontend(config, projectPath);
|
|
28
66
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
67
|
+
// 2. Backend
|
|
68
|
+
tick('Generando backend...');
|
|
69
|
+
await generateBackend(config, projectPath);
|
|
70
|
+
|
|
71
|
+
// 3. README personalizado
|
|
72
|
+
tick('Generando README...');
|
|
73
|
+
await generateCustomReadme(config, projectPath);
|
|
36
74
|
|
|
75
|
+
// 4. Extras
|
|
76
|
+
if (extras.includes('env')) {
|
|
77
|
+
tick('Archivos .env...');
|
|
78
|
+
await generateEnvFiles(config, projectPath);
|
|
79
|
+
}
|
|
80
|
+
if (extras.includes('editorconfig')) {
|
|
81
|
+
tick('.editorconfig...');
|
|
82
|
+
await fse.writeFile(path.join(projectPath, '.editorconfig'), EDITORCONFIG);
|
|
83
|
+
}
|
|
84
|
+
if (extras.includes('eslint')) {
|
|
85
|
+
tick('ESLint + Prettier...');
|
|
86
|
+
await setupEslint(config, projectPath);
|
|
87
|
+
}
|
|
88
|
+
if (extras.includes('husky')) {
|
|
89
|
+
tick('Husky...');
|
|
90
|
+
await setupHusky(config, projectPath);
|
|
91
|
+
}
|
|
92
|
+
if (extras.includes('testing')) {
|
|
93
|
+
tick('Vitest...');
|
|
94
|
+
await setupTesting(config, projectPath);
|
|
95
|
+
}
|
|
96
|
+
if (extras.includes('ci')) {
|
|
97
|
+
tick('GitHub Actions...');
|
|
98
|
+
await setupCI(config, projectPath);
|
|
99
|
+
}
|
|
100
|
+
if (extras.includes('docker')) {
|
|
101
|
+
tick('Docker...');
|
|
102
|
+
await generateDocker(config, projectPath);
|
|
103
|
+
}
|
|
104
|
+
if (extras.includes('auth')) {
|
|
105
|
+
tick('Auth JWT...');
|
|
106
|
+
await setupAuth(config, projectPath);
|
|
107
|
+
}
|
|
108
|
+
if (extras.includes('tailwind')) {
|
|
109
|
+
tick('Tailwind CSS...');
|
|
110
|
+
await setupTailwind(config, projectPath);
|
|
111
|
+
}
|
|
37
112
|
if (extras.includes('git') && isInstalled('git')) {
|
|
38
|
-
|
|
113
|
+
tick('Git init...');
|
|
114
|
+
try { run('git init', projectPath, 'git init'); } catch {}
|
|
39
115
|
await fse.writeFile(path.join(projectPath, '.gitignore'), GITIGNORE);
|
|
40
116
|
}
|
|
41
117
|
|
|
118
|
+
// 5. Instalar dependencias automáticamente
|
|
119
|
+
if (config.frontendDeps?.length && isInstalled('npm')) {
|
|
120
|
+
tick('Instalando deps frontend...');
|
|
121
|
+
const frontendPath = path.join(projectPath, 'frontend');
|
|
122
|
+
try {
|
|
123
|
+
spawnSync('npm', ['install', ...config.frontendDeps], { cwd: frontendPath, shell: true, stdio: 'pipe' });
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
if (config.backendDeps?.length && isNode && isInstalled('npm')) {
|
|
127
|
+
tick('Instalando deps backend...');
|
|
128
|
+
const backendPath = path.join(projectPath, 'backend');
|
|
129
|
+
try {
|
|
130
|
+
spawnSync('npm', ['install', ...config.backendDeps], { cwd: backendPath, shell: true, stdio: 'pipe' });
|
|
131
|
+
} catch {}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Barra al 100%
|
|
135
|
+
progressBar(steps.length, steps.length, 'Completado');
|
|
136
|
+
console.log('\n');
|
|
137
|
+
|
|
138
|
+
// Tiempo total
|
|
139
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
140
|
+
|
|
42
141
|
// Resumen final
|
|
43
142
|
const backendCmd = {
|
|
44
|
-
express: 'npm run dev',
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
hono: 'npm run dev',
|
|
48
|
-
fastapi: 'uvicorn main:app --reload',
|
|
49
|
-
django: 'python manage.py runserver',
|
|
50
|
-
flask: 'python run.py',
|
|
51
|
-
spring: 'mvn spring-boot:run',
|
|
52
|
-
deno: 'deno task dev',
|
|
53
|
-
gin: 'go run main.go'
|
|
143
|
+
express: 'npm run dev', nestjs: 'npm run start:dev', fastify: 'npm run dev',
|
|
144
|
+
hono: 'npm run dev', fastapi: 'uvicorn main:app --reload', django: 'python manage.py runserver',
|
|
145
|
+
flask: 'python run.py', spring: 'mvn spring-boot:run', deno: 'deno task dev', gin: 'go run main.go',
|
|
54
146
|
};
|
|
55
147
|
|
|
56
|
-
// Resumen final
|
|
57
148
|
console.log(
|
|
58
|
-
boxen
|
|
59
|
-
chalk.bold.white(' ✔ PROYECTO
|
|
60
|
-
chalk.
|
|
61
|
-
chalk.
|
|
62
|
-
chalk.
|
|
63
|
-
chalk.gray('
|
|
64
|
-
chalk.
|
|
65
|
-
chalk.gray(
|
|
66
|
-
chalk.white('
|
|
67
|
-
chalk.white('
|
|
68
|
-
|
|
69
|
-
chalk.white(` ${backendCmd[config.backend] || 'npm start'}`),
|
|
70
|
-
{
|
|
71
|
-
padding: 1,
|
|
72
|
-
margin: { top: 1, left: 2 },
|
|
73
|
-
borderStyle: 'round',
|
|
74
|
-
borderColor: 'white',
|
|
75
|
-
dimBorder: true,
|
|
76
|
-
}
|
|
149
|
+
boxen(
|
|
150
|
+
chalk.bold.white(' ✔ PROYECTO LISTO\n\n') +
|
|
151
|
+
chalk.gray(' Nombre │ ') + chalk.white(config.name) + '\n' +
|
|
152
|
+
chalk.gray(' Frontend │ ') + chalk.white(config.frontend) + (config.typescript ? chalk.gray(' + TypeScript') : '') + '\n' +
|
|
153
|
+
chalk.gray(' Backend │ ') + chalk.white(config.backend) + '\n' +
|
|
154
|
+
chalk.gray(' Base datos │ ') + chalk.white(config.db || 'ninguna') + '\n' +
|
|
155
|
+
chalk.gray(' Tiempo │ ') + chalk.white(elapsed + 's') + '\n\n' +
|
|
156
|
+
chalk.gray(' ─────────────────────────────────────\n\n') +
|
|
157
|
+
chalk.white(' cd ' + config.name + '/frontend → npm run dev\n') +
|
|
158
|
+
chalk.white(' cd ' + config.name + '/backend → ' + (backendCmd[config.backend] || 'npm start')),
|
|
159
|
+
{ padding: 1, margin: { top: 1, left: 2 }, borderStyle: 'round', borderColor: 'white', dimBorder: true }
|
|
77
160
|
)
|
|
78
161
|
);
|
|
162
|
+
|
|
163
|
+
// Abrir VS Code si está instalado
|
|
164
|
+
if (isInstalled('code')) {
|
|
165
|
+
try {
|
|
166
|
+
spawnSync('code', [projectPath], { shell: true, stdio: 'ignore', detached: true });
|
|
167
|
+
console.log(' ' + chalk.gray('VS Code abierto automáticamente ✔'));
|
|
168
|
+
} catch {}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── README PERSONALIZADO ──────────────────────────────────────────────────────
|
|
175
|
+
async function generateCustomReadme(config, projectPath) {
|
|
176
|
+
const ts = config.typescript ? ' + TypeScript' : '';
|
|
177
|
+
const dbSection = config.db && config.db !== 'none'
|
|
178
|
+
? `\n## Base de datos\n\n**${config.db}** — Configura la conexión en \`backend/.env\`\n` : '';
|
|
179
|
+
|
|
180
|
+
const content = `# ${config.name}
|
|
181
|
+
|
|
182
|
+
> Proyecto generado con [NOVATEC CLI](https://www.npmjs.com/package/novatec-cli)
|
|
183
|
+
|
|
184
|
+
## Stack
|
|
185
|
+
|
|
186
|
+
| Capa | Tecnología |
|
|
187
|
+
|------|-----------|
|
|
188
|
+
| Frontend | **${config.frontend}**${ts} |
|
|
189
|
+
| Backend | **${config.backend}**${ts} |
|
|
190
|
+
| Base de datos | **${config.db || 'No configurada'}** |
|
|
191
|
+
${dbSection}
|
|
192
|
+
## Inicio rápido
|
|
193
|
+
|
|
194
|
+
\`\`\`bash
|
|
195
|
+
# Frontend
|
|
196
|
+
cd ${config.name}/frontend
|
|
197
|
+
npm install
|
|
198
|
+
npm run dev
|
|
199
|
+
|
|
200
|
+
# Backend
|
|
201
|
+
cd ${config.name}/backend
|
|
202
|
+
${['fastapi','django','flask'].includes(config.backend) ? 'pip install -r requirements.txt\n' : 'npm install\n'}\
|
|
203
|
+
${config.backend === 'fastapi' ? 'uvicorn main:app --reload' :
|
|
204
|
+
config.backend === 'django' ? 'python manage.py runserver' :
|
|
205
|
+
config.backend === 'flask' ? 'python run.py' :
|
|
206
|
+
config.backend === 'spring' ? 'mvn spring-boot:run' :
|
|
207
|
+
config.backend === 'gin' ? 'go run main.go' : 'npm run dev'}
|
|
208
|
+
\`\`\`
|
|
209
|
+
${config.extras?.includes('docker') ? '\n## Docker\n\n```bash\ndocker-compose up --build\n```\n' : ''}
|
|
210
|
+
## Generado con
|
|
211
|
+
|
|
212
|
+
\`\`\`
|
|
213
|
+
novatec create ${config.name}
|
|
214
|
+
\`\`\`
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
*Generado el ${new Date().toLocaleDateString('es-MX')} con NOVATEC CLI*
|
|
218
|
+
`;
|
|
219
|
+
await fse.writeFile(path.join(projectPath, 'README.md'), content);
|
|
79
220
|
}
|
|
80
221
|
|
|
81
|
-
// ──
|
|
222
|
+
// ── AUTH JWT ──────────────────────────────────────────────────────────────────
|
|
223
|
+
async function setupAuth(config, projectPath) {
|
|
224
|
+
const backendPath = path.join(projectPath, 'backend');
|
|
225
|
+
const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
|
|
226
|
+
|
|
227
|
+
if (isPython) return; // Django/FastAPI ya tienen su propio auth
|
|
228
|
+
|
|
229
|
+
const authDir = path.join(backendPath, 'src', 'auth');
|
|
230
|
+
await fse.ensureDir(authDir);
|
|
231
|
+
|
|
232
|
+
const middleware = `import jwt from 'jsonwebtoken';
|
|
233
|
+
|
|
234
|
+
export function authMiddleware(req, res, next) {
|
|
235
|
+
const token = req.headers.authorization?.split(' ')[1];
|
|
236
|
+
if (!token) return res.status(401).json({ message: 'Token requerido' });
|
|
237
|
+
try {
|
|
238
|
+
req.user = jwt.verify(token, process.env.JWT_SECRET || 'change_me');
|
|
239
|
+
next();
|
|
240
|
+
} catch {
|
|
241
|
+
res.status(401).json({ message: 'Token inválido' });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
`;
|
|
245
|
+
const authRoutes = `import { Router } from 'express';
|
|
246
|
+
import jwt from 'jsonwebtoken';
|
|
247
|
+
import bcrypt from 'bcryptjs';
|
|
248
|
+
|
|
249
|
+
const router = Router();
|
|
250
|
+
|
|
251
|
+
// POST /api/auth/register
|
|
252
|
+
router.post('/register', async (req, res) => {
|
|
253
|
+
const { email, password } = req.body;
|
|
254
|
+
const hash = await bcrypt.hash(password, 10);
|
|
255
|
+
// TODO: guardar usuario en BD
|
|
256
|
+
const token = jwt.sign({ email }, process.env.JWT_SECRET || 'change_me', { expiresIn: '7d' });
|
|
257
|
+
res.json({ token });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// POST /api/auth/login
|
|
261
|
+
router.post('/login', async (req, res) => {
|
|
262
|
+
const { email, password } = req.body;
|
|
263
|
+
// TODO: buscar usuario en BD y verificar hash
|
|
264
|
+
const token = jwt.sign({ email }, process.env.JWT_SECRET || 'change_me', { expiresIn: '7d' });
|
|
265
|
+
res.json({ token });
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
export default router;
|
|
269
|
+
`;
|
|
270
|
+
|
|
271
|
+
await fse.writeFile(path.join(authDir, 'middleware.js'), middleware);
|
|
272
|
+
await fse.writeFile(path.join(authDir, 'routes.js'), authRoutes);
|
|
273
|
+
|
|
274
|
+
// Instalar deps de auth
|
|
275
|
+
try {
|
|
276
|
+
spawnSync('npm', ['install', 'jsonwebtoken', 'bcryptjs'], { cwd: backendPath, shell: true, stdio: 'pipe' });
|
|
277
|
+
} catch {}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── TAILWIND AUTO ─────────────────────────────────────────────────────────────
|
|
281
|
+
async function setupTailwind(config, projectPath) {
|
|
282
|
+
const frontendPath = path.join(projectPath, 'frontend');
|
|
283
|
+
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
|
|
284
|
+
export default {
|
|
285
|
+
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx,vue,svelte}'],
|
|
286
|
+
theme: { extend: {} },
|
|
287
|
+
plugins: [],
|
|
288
|
+
}
|
|
289
|
+
`;
|
|
290
|
+
const postcssConfig = `export default {
|
|
291
|
+
plugins: { tailwindcss: {}, autoprefixer: {} },
|
|
292
|
+
}
|
|
293
|
+
`;
|
|
294
|
+
await fse.writeFile(path.join(frontendPath, 'tailwind.config.js'), tailwindConfig);
|
|
295
|
+
await fse.writeFile(path.join(frontendPath, 'postcss.config.js'), postcssConfig);
|
|
296
|
+
|
|
297
|
+
// Agregar imports al CSS principal
|
|
298
|
+
const cssPath = path.join(frontendPath, 'src', 'index.css');
|
|
299
|
+
const tailwindImports = `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n`;
|
|
300
|
+
if (await fse.pathExists(cssPath)) {
|
|
301
|
+
const existing = await fse.readFile(cssPath, 'utf8');
|
|
302
|
+
await fse.writeFile(cssPath, tailwindImports + existing);
|
|
303
|
+
} else {
|
|
304
|
+
await fse.ensureDir(path.join(frontendPath, 'src'));
|
|
305
|
+
await fse.writeFile(cssPath, tailwindImports);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
spawnSync('npm', ['install', '-D', 'tailwindcss', 'postcss', 'autoprefixer'], { cwd: frontendPath, shell: true, stdio: 'pipe' });
|
|
310
|
+
} catch {}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── ESLint ────────────────────────────────────────────────────────────────────
|
|
82
314
|
async function setupEslint(config, projectPath) {
|
|
83
315
|
const frontendPath = path.join(projectPath, 'frontend');
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"parserOptions": { "ecmaVersion": "latest", "sourceType": "module" },
|
|
88
|
-
"rules": { "no-unused-vars": "warn", "no-console": "off" }
|
|
89
|
-
}\n`;
|
|
90
|
-
const prettierConfig = `{
|
|
91
|
-
"semi": true,
|
|
92
|
-
"singleQuote": true,
|
|
93
|
-
"tabWidth": 2,
|
|
94
|
-
"trailingComma": "es5"
|
|
95
|
-
}\n`;
|
|
96
|
-
|
|
97
|
-
await fse.writeFile(path.join(frontendPath, '.eslintrc.json'), eslintConfig);
|
|
98
|
-
await fse.writeFile(path.join(frontendPath, '.prettierrc'), prettierConfig);
|
|
99
|
-
run('npm install -D eslint prettier eslint-config-prettier', frontendPath, 'Instalando ESLint + Prettier');
|
|
100
|
-
console.log(chalk.green('✔ ESLint + Prettier configurados'));
|
|
316
|
+
await fse.writeFile(path.join(frontendPath, '.eslintrc.json'), `{"env":{"browser":true,"es2021":true},"extends":["eslint:recommended"],"parserOptions":{"ecmaVersion":"latest","sourceType":"module"},"rules":{"no-unused-vars":"warn"}}\n`);
|
|
317
|
+
await fse.writeFile(path.join(frontendPath, '.prettierrc'), `{"semi":true,"singleQuote":true,"tabWidth":2,"trailingComma":"es5"}\n`);
|
|
318
|
+
try { spawnSync('npm', ['install', '-D', 'eslint', 'prettier'], { cwd: frontendPath, shell: true, stdio: 'pipe' }); } catch {}
|
|
101
319
|
}
|
|
102
320
|
|
|
103
|
-
// ── Husky
|
|
321
|
+
// ── Husky ─────────────────────────────────────────────────────────────────────
|
|
104
322
|
async function setupHusky(config, projectPath) {
|
|
105
323
|
const frontendPath = path.join(projectPath, 'frontend');
|
|
106
324
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
325
|
+
spawnSync('npm', ['install', '-D', 'husky', 'lint-staged'], { cwd: frontendPath, shell: true, stdio: 'pipe' });
|
|
326
|
+
spawnSync('npx', ['husky', 'init'], { cwd: frontendPath, shell: true, stdio: 'pipe' });
|
|
327
|
+
await fse.ensureDir(path.join(frontendPath, '.husky'));
|
|
109
328
|
await fse.writeFile(path.join(frontendPath, '.husky', 'pre-commit'), '#!/bin/sh\nnpx lint-staged\n');
|
|
110
|
-
|
|
111
|
-
const pkg = await fse.readJSON(pkgPath);
|
|
112
|
-
pkg['lint-staged'] = { '*.{js,jsx,ts,tsx,vue}': ['eslint --fix', 'prettier --write'] };
|
|
113
|
-
await fse.writeJSON(pkgPath, pkg, { spaces: 2 });
|
|
114
|
-
console.log(chalk.green('✔ Husky + lint-staged configurados'));
|
|
115
|
-
} catch { console.log(chalk.yellow('⚠ Husky requiere git init primero')); }
|
|
329
|
+
} catch {}
|
|
116
330
|
}
|
|
117
331
|
|
|
118
332
|
// ── Testing ───────────────────────────────────────────────────────────────────
|
|
119
333
|
async function setupTesting(config, projectPath) {
|
|
120
334
|
const frontendPath = path.join(projectPath, 'frontend');
|
|
121
|
-
const isReactLike = ['react', 'next', 'solid', 'remix', 'qwik'].includes(config.frontend);
|
|
122
|
-
|
|
123
|
-
run('npm install -D vitest @vitest/ui', frontendPath, 'Instalando Vitest');
|
|
124
|
-
|
|
125
335
|
const testDir = path.join(frontendPath, 'src', '__tests__');
|
|
126
336
|
await fse.ensureDir(testDir);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
? `import { describe, it, expect } from 'vitest';\n\ndescribe('App', () => {\n it('should work', () => {\n expect(1 + 1).toBe(2);\n });\n});\n`
|
|
130
|
-
: `import { describe, it, expect } from 'vitest';\n\ndescribe('App', () => {\n it('should work', () => {\n expect(true).toBe(true);\n });\n});\n`;
|
|
131
|
-
|
|
132
|
-
await fse.writeFile(path.join(testDir, 'app.test.js'), sampleTest);
|
|
133
|
-
|
|
134
|
-
const pkgPath = path.join(frontendPath, 'package.json');
|
|
135
|
-
const pkg = await fse.readJSON(pkgPath);
|
|
136
|
-
pkg.scripts = { ...pkg.scripts, test: 'vitest', 'test:ui': 'vitest --ui' };
|
|
137
|
-
await fse.writeJSON(pkgPath, pkg, { spaces: 2 });
|
|
138
|
-
console.log(chalk.green('✔ Vitest configurado con test de ejemplo'));
|
|
337
|
+
await fse.writeFile(path.join(testDir, 'app.test.js'), `import { describe, it, expect } from 'vitest';\n\ndescribe('App', () => {\n it('works', () => expect(1 + 1).toBe(2));\n});\n`);
|
|
338
|
+
try { spawnSync('npm', ['install', '-D', 'vitest'], { cwd: frontendPath, shell: true, stdio: 'pipe' }); } catch {}
|
|
139
339
|
}
|
|
140
340
|
|
|
141
|
-
// ── GitHub Actions
|
|
341
|
+
// ── GitHub Actions ────────────────────────────────────────────────────────────
|
|
142
342
|
async function setupCI(config, projectPath) {
|
|
143
343
|
const ciDir = path.join(projectPath, '.github', 'workflows');
|
|
144
344
|
await fse.ensureDir(ciDir);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
on:
|
|
149
|
-
push:
|
|
150
|
-
branches: [main, develop]
|
|
151
|
-
pull_request:
|
|
152
|
-
branches: [main]
|
|
153
|
-
|
|
154
|
-
jobs:
|
|
155
|
-
frontend:
|
|
156
|
-
runs-on: ubuntu-latest
|
|
157
|
-
defaults:
|
|
158
|
-
run:
|
|
159
|
-
working-directory: frontend
|
|
160
|
-
steps:
|
|
161
|
-
- uses: actions/checkout@v4
|
|
162
|
-
- uses: actions/setup-node@v4
|
|
163
|
-
with:
|
|
164
|
-
node-version: 20
|
|
165
|
-
cache: npm
|
|
166
|
-
cache-dependency-path: frontend/package-lock.json
|
|
167
|
-
- run: npm ci
|
|
168
|
-
- run: npm run build
|
|
169
|
-
${config.extras.includes('testing') ? ' - run: npm test\n' : ''}
|
|
170
|
-
backend:
|
|
171
|
-
runs-on: ubuntu-latest
|
|
172
|
-
defaults:
|
|
173
|
-
run:
|
|
174
|
-
working-directory: backend
|
|
175
|
-
steps:
|
|
176
|
-
- uses: actions/checkout@v4
|
|
177
|
-
${['fastapi', 'django', 'flask'].includes(config.backend) ? ` - uses: actions/setup-python@v5
|
|
178
|
-
with:
|
|
179
|
-
python-version: '3.11'
|
|
180
|
-
- run: pip install -r requirements.txt` : ` - uses: actions/setup-node@v4
|
|
181
|
-
with:
|
|
182
|
-
node-version: 20
|
|
183
|
-
- run: npm ci`}
|
|
184
|
-
`;
|
|
185
|
-
await fse.writeFile(path.join(ciDir, 'ci.yml'), ci);
|
|
186
|
-
console.log(chalk.green('✔ GitHub Actions CI configurado'));
|
|
345
|
+
const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
|
|
346
|
+
await fse.writeFile(path.join(ciDir, 'ci.yml'), `name: CI\non:\n push:\n branches: [main]\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 20\n - run: cd frontend && npm ci && npm run build\n${isPython ? '' : ' - run: cd backend && npm ci\n'}`);
|
|
187
347
|
}
|
|
188
348
|
|
|
189
349
|
// ── Docker ────────────────────────────────────────────────────────────────────
|
|
190
350
|
async function generateDocker(config, projectPath) {
|
|
191
351
|
const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
|
|
192
|
-
const backendPort = isPython ?
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
?
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
- "${backendPort}:${backendPort}"
|
|
199
|
-
|
|
200
|
-
- ./backend:/app
|
|
201
|
-
environment:
|
|
202
|
-
- DEBUG=True`
|
|
203
|
-
: ` backend:
|
|
204
|
-
build: ./backend
|
|
205
|
-
ports:
|
|
206
|
-
- "${backendPort}:${backendPort}"
|
|
207
|
-
volumes:
|
|
208
|
-
- ./backend:/app
|
|
209
|
-
- /app/node_modules
|
|
210
|
-
environment:
|
|
211
|
-
- NODE_ENV=development`;
|
|
212
|
-
|
|
213
|
-
const compose = `version: '3.9'
|
|
214
|
-
services:
|
|
215
|
-
frontend:
|
|
216
|
-
build: ./frontend
|
|
217
|
-
ports:
|
|
218
|
-
- "5173:5173"
|
|
219
|
-
volumes:
|
|
220
|
-
- ./frontend:/app
|
|
221
|
-
- /app/node_modules
|
|
222
|
-
environment:
|
|
223
|
-
- NODE_ENV=development
|
|
224
|
-
${backendService}
|
|
225
|
-
db:
|
|
226
|
-
image: postgres:16-alpine
|
|
227
|
-
environment:
|
|
228
|
-
POSTGRES_DB: mydb
|
|
229
|
-
POSTGRES_USER: user
|
|
230
|
-
POSTGRES_PASSWORD: password
|
|
231
|
-
ports:
|
|
232
|
-
- "5432:5432"
|
|
233
|
-
volumes:
|
|
234
|
-
- pgdata:/var/lib/postgresql/data
|
|
235
|
-
|
|
236
|
-
volumes:
|
|
237
|
-
pgdata:
|
|
238
|
-
`;
|
|
239
|
-
await fse.writeFile(path.join(projectPath, 'docker-compose.yml'), compose);
|
|
240
|
-
console.log(chalk.green('✔ docker-compose.yml generado (con PostgreSQL)'));
|
|
352
|
+
const backendPort = isPython ? '8000' : '3001';
|
|
353
|
+
const dbService = config.db === 'postgres' ? `\n db:\n image: postgres:16-alpine\n environment:\n POSTGRES_DB: mydb\n POSTGRES_USER: user\n POSTGRES_PASSWORD: password\n ports:\n - "5432:5432"\n volumes:\n - pgdata:/var/lib/postgresql/data\n\nvolumes:\n pgdata:` :
|
|
354
|
+
config.db === 'mongo' ? `\n db:\n image: mongo:7\n ports:\n - "27017:27017"\n volumes:\n - mongodata:/data/db\n\nvolumes:\n mongodata:` :
|
|
355
|
+
config.db === 'mysql' ? `\n db:\n image: mysql:8\n environment:\n MYSQL_DATABASE: mydb\n MYSQL_ROOT_PASSWORD: password\n ports:\n - "3306:3306"` : '';
|
|
356
|
+
|
|
357
|
+
await fse.writeFile(path.join(projectPath, 'docker-compose.yml'),
|
|
358
|
+
`version: '3.9'\nservices:\n frontend:\n build: ./frontend\n ports:\n - "5173:5173"\n backend:\n build: ./backend\n ports:\n - "${backendPort}:${backendPort}"\n env_file: ./backend/.env${dbService}\n`
|
|
359
|
+
);
|
|
241
360
|
}
|
|
242
361
|
|
|
243
|
-
// ──
|
|
362
|
+
// ── .env ──────────────────────────────────────────────────────────────────────
|
|
244
363
|
async function generateEnvFiles(config, projectPath) {
|
|
245
364
|
await fse.writeFile(path.join(projectPath, 'frontend', '.env.example'), 'VITE_API_URL=http://localhost:3001\n');
|
|
246
365
|
const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
|
|
366
|
+
const dbUrl = config.db === 'postgres' ? 'postgresql://user:password@localhost:5432/mydb' :
|
|
367
|
+
config.db === 'mysql' ? 'mysql://user:password@localhost:3306/mydb' :
|
|
368
|
+
config.db === 'mongo' ? 'mongodb://localhost:27017/mydb' :
|
|
369
|
+
config.db === 'sqlite' ? 'file:./dev.db' : '';
|
|
247
370
|
if (!isPython) {
|
|
248
|
-
await fse.writeFile(path.join(projectPath, 'backend', '.env.example'),
|
|
371
|
+
await fse.writeFile(path.join(projectPath, 'backend', '.env.example'),
|
|
372
|
+
`PORT=3001\nNODE_ENV=development\nJWT_SECRET=change_me_in_production\n${dbUrl ? 'DATABASE_URL=' + dbUrl + '\n' : ''}`
|
|
373
|
+
);
|
|
249
374
|
}
|
|
250
|
-
console.log(chalk.green('✔ Archivos .env.example generados'));
|
|
251
375
|
}
|
|
252
376
|
|
|
253
|
-
const EDITORCONFIG = `root = true
|
|
254
|
-
|
|
255
|
-
indent_style = space
|
|
256
|
-
indent_size = 2
|
|
257
|
-
end_of_line = lf
|
|
258
|
-
charset = utf-8
|
|
259
|
-
trim_trailing_whitespace = true
|
|
260
|
-
insert_final_newline = true
|
|
261
|
-
`;
|
|
262
|
-
|
|
263
|
-
const GITIGNORE = `node_modules/
|
|
264
|
-
dist/
|
|
265
|
-
.next/
|
|
266
|
-
.nuxt/
|
|
267
|
-
.svelte-kit/
|
|
268
|
-
.env
|
|
269
|
-
*.log
|
|
270
|
-
__pycache__/
|
|
271
|
-
.venv/
|
|
272
|
-
*.pyc
|
|
273
|
-
target/
|
|
274
|
-
*.class
|
|
275
|
-
`;
|
|
276
|
-
|
|
277
|
-
export { runCreate };
|
|
377
|
+
const EDITORCONFIG = `root = true\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n`;
|
|
378
|
+
const GITIGNORE = `node_modules/\ndist/\n.next/\n.nuxt/\n.svelte-kit/\n.env\n*.log\n__pycache__/\n.venv/\n*.pyc\ntarget/\n*.class\n.DS_Store\n`;
|