novatec-cli 1.0.1 → 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 +146 -64
- package/lib/create.js +312 -212
- package/lib/prompts.js +187 -260
- package/lib/utils.js +22 -9
- package/package.json +2 -2
package/lib/create.js
CHANGED
|
@@ -1,278 +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
|
-
|
|
11
|
-
|
|
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
|
+
}
|
|
12
22
|
|
|
13
|
-
|
|
23
|
+
// ── MAIN ──────────────────────────────────────────────────────────────────────
|
|
24
|
+
export async function runCreate(nameArg, options) {
|
|
25
|
+
const startTime = Date.now();
|
|
14
26
|
const config = await askProjectOptions(nameArg, options);
|
|
15
27
|
const projectPath = path.resolve(process.cwd(), config.name);
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
throw new Error(`La carpeta "${config.name}" ya existe en este directorio.`);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
console.log(chalk.cyan(`\n📁 Creando proyecto en: ${chalk.bold(projectPath)}\n`));
|
|
29
|
+
// Asegurar carpeta
|
|
22
30
|
await fse.ensureDir(projectPath);
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
await generateReadme(config, projectPath);
|
|
32
|
+
const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
|
|
33
|
+
const isNode = !isPython && config.backend !== 'spring' && config.backend !== 'gin';
|
|
27
34
|
|
|
35
|
+
// Calcular pasos totales
|
|
28
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);
|
|
29
66
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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);
|
|
37
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
|
+
}
|
|
38
112
|
if (extras.includes('git') && isInstalled('git')) {
|
|
39
|
-
|
|
113
|
+
tick('Git init...');
|
|
114
|
+
try { run('git init', projectPath, 'git init'); } catch {}
|
|
40
115
|
await fse.writeFile(path.join(projectPath, '.gitignore'), GITIGNORE);
|
|
41
116
|
}
|
|
42
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
|
+
|
|
43
141
|
// Resumen final
|
|
44
|
-
const novatecGradient = gradient.default(['#00C6FF', '#0072FF', '#7B2FF7']);
|
|
45
142
|
const backendCmd = {
|
|
46
|
-
express: 'npm run dev',
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
hono: 'npm run dev',
|
|
50
|
-
fastapi: 'uvicorn main:app --reload',
|
|
51
|
-
django: 'python manage.py runserver',
|
|
52
|
-
flask: 'python run.py',
|
|
53
|
-
spring: 'mvn spring-boot:run',
|
|
54
|
-
deno: 'deno task dev',
|
|
55
|
-
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',
|
|
56
146
|
};
|
|
57
147
|
|
|
58
148
|
console.log(
|
|
59
|
-
boxen
|
|
60
|
-
|
|
61
|
-
chalk.
|
|
62
|
-
chalk.
|
|
63
|
-
chalk.
|
|
64
|
-
chalk.
|
|
65
|
-
chalk.
|
|
66
|
-
chalk.
|
|
67
|
-
chalk.white(
|
|
68
|
-
chalk.
|
|
69
|
-
|
|
70
|
-
chalk.white(` cd ${config.name}\\backend\n`) +
|
|
71
|
-
chalk.cyan(` ${backendCmd[config.backend] || 'npm start'}`),
|
|
72
|
-
{
|
|
73
|
-
padding: 1,
|
|
74
|
-
margin: { top: 1, left: 2 },
|
|
75
|
-
borderStyle: 'double',
|
|
76
|
-
borderColor: 'cyan',
|
|
77
|
-
}
|
|
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 }
|
|
78
160
|
)
|
|
79
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);
|
|
220
|
+
}
|
|
221
|
+
|
|
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 {}
|
|
80
278
|
}
|
|
81
279
|
|
|
82
|
-
// ──
|
|
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 ────────────────────────────────────────────────────────────────────
|
|
83
314
|
async function setupEslint(config, projectPath) {
|
|
84
315
|
const frontendPath = path.join(projectPath, 'frontend');
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
"parserOptions": { "ecmaVersion": "latest", "sourceType": "module" },
|
|
89
|
-
"rules": { "no-unused-vars": "warn", "no-console": "off" }
|
|
90
|
-
}\n`;
|
|
91
|
-
const prettierConfig = `{
|
|
92
|
-
"semi": true,
|
|
93
|
-
"singleQuote": true,
|
|
94
|
-
"tabWidth": 2,
|
|
95
|
-
"trailingComma": "es5"
|
|
96
|
-
}\n`;
|
|
97
|
-
|
|
98
|
-
await fse.writeFile(path.join(frontendPath, '.eslintrc.json'), eslintConfig);
|
|
99
|
-
await fse.writeFile(path.join(frontendPath, '.prettierrc'), prettierConfig);
|
|
100
|
-
run('npm install -D eslint prettier eslint-config-prettier', frontendPath, 'Instalando ESLint + Prettier');
|
|
101
|
-
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 {}
|
|
102
319
|
}
|
|
103
320
|
|
|
104
|
-
// ── Husky
|
|
321
|
+
// ── Husky ─────────────────────────────────────────────────────────────────────
|
|
105
322
|
async function setupHusky(config, projectPath) {
|
|
106
323
|
const frontendPath = path.join(projectPath, 'frontend');
|
|
107
324
|
try {
|
|
108
|
-
|
|
109
|
-
|
|
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'));
|
|
110
328
|
await fse.writeFile(path.join(frontendPath, '.husky', 'pre-commit'), '#!/bin/sh\nnpx lint-staged\n');
|
|
111
|
-
|
|
112
|
-
const pkg = await fse.readJSON(pkgPath);
|
|
113
|
-
pkg['lint-staged'] = { '*.{js,jsx,ts,tsx,vue}': ['eslint --fix', 'prettier --write'] };
|
|
114
|
-
await fse.writeJSON(pkgPath, pkg, { spaces: 2 });
|
|
115
|
-
console.log(chalk.green('✔ Husky + lint-staged configurados'));
|
|
116
|
-
} catch { console.log(chalk.yellow('⚠ Husky requiere git init primero')); }
|
|
329
|
+
} catch {}
|
|
117
330
|
}
|
|
118
331
|
|
|
119
332
|
// ── Testing ───────────────────────────────────────────────────────────────────
|
|
120
333
|
async function setupTesting(config, projectPath) {
|
|
121
334
|
const frontendPath = path.join(projectPath, 'frontend');
|
|
122
|
-
const isReactLike = ['react', 'next', 'solid', 'remix', 'qwik'].includes(config.frontend);
|
|
123
|
-
|
|
124
|
-
run('npm install -D vitest @vitest/ui', frontendPath, 'Instalando Vitest');
|
|
125
|
-
|
|
126
335
|
const testDir = path.join(frontendPath, 'src', '__tests__');
|
|
127
336
|
await fse.ensureDir(testDir);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
? `import { describe, it, expect } from 'vitest';\n\ndescribe('App', () => {\n it('should work', () => {\n expect(1 + 1).toBe(2);\n });\n});\n`
|
|
131
|
-
: `import { describe, it, expect } from 'vitest';\n\ndescribe('App', () => {\n it('should work', () => {\n expect(true).toBe(true);\n });\n});\n`;
|
|
132
|
-
|
|
133
|
-
await fse.writeFile(path.join(testDir, 'app.test.js'), sampleTest);
|
|
134
|
-
|
|
135
|
-
const pkgPath = path.join(frontendPath, 'package.json');
|
|
136
|
-
const pkg = await fse.readJSON(pkgPath);
|
|
137
|
-
pkg.scripts = { ...pkg.scripts, test: 'vitest', 'test:ui': 'vitest --ui' };
|
|
138
|
-
await fse.writeJSON(pkgPath, pkg, { spaces: 2 });
|
|
139
|
-
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 {}
|
|
140
339
|
}
|
|
141
340
|
|
|
142
|
-
// ── GitHub Actions
|
|
341
|
+
// ── GitHub Actions ────────────────────────────────────────────────────────────
|
|
143
342
|
async function setupCI(config, projectPath) {
|
|
144
343
|
const ciDir = path.join(projectPath, '.github', 'workflows');
|
|
145
344
|
await fse.ensureDir(ciDir);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
on:
|
|
150
|
-
push:
|
|
151
|
-
branches: [main, develop]
|
|
152
|
-
pull_request:
|
|
153
|
-
branches: [main]
|
|
154
|
-
|
|
155
|
-
jobs:
|
|
156
|
-
frontend:
|
|
157
|
-
runs-on: ubuntu-latest
|
|
158
|
-
defaults:
|
|
159
|
-
run:
|
|
160
|
-
working-directory: frontend
|
|
161
|
-
steps:
|
|
162
|
-
- uses: actions/checkout@v4
|
|
163
|
-
- uses: actions/setup-node@v4
|
|
164
|
-
with:
|
|
165
|
-
node-version: 20
|
|
166
|
-
cache: npm
|
|
167
|
-
cache-dependency-path: frontend/package-lock.json
|
|
168
|
-
- run: npm ci
|
|
169
|
-
- run: npm run build
|
|
170
|
-
${config.extras.includes('testing') ? ' - run: npm test\n' : ''}
|
|
171
|
-
backend:
|
|
172
|
-
runs-on: ubuntu-latest
|
|
173
|
-
defaults:
|
|
174
|
-
run:
|
|
175
|
-
working-directory: backend
|
|
176
|
-
steps:
|
|
177
|
-
- uses: actions/checkout@v4
|
|
178
|
-
${['fastapi', 'django', 'flask'].includes(config.backend) ? ` - uses: actions/setup-python@v5
|
|
179
|
-
with:
|
|
180
|
-
python-version: '3.11'
|
|
181
|
-
- run: pip install -r requirements.txt` : ` - uses: actions/setup-node@v4
|
|
182
|
-
with:
|
|
183
|
-
node-version: 20
|
|
184
|
-
- run: npm ci`}
|
|
185
|
-
`;
|
|
186
|
-
await fse.writeFile(path.join(ciDir, 'ci.yml'), ci);
|
|
187
|
-
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'}`);
|
|
188
347
|
}
|
|
189
348
|
|
|
190
349
|
// ── Docker ────────────────────────────────────────────────────────────────────
|
|
191
350
|
async function generateDocker(config, projectPath) {
|
|
192
351
|
const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
|
|
193
|
-
const backendPort = isPython ?
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
?
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
- "${backendPort}:${backendPort}"
|
|
200
|
-
|
|
201
|
-
- ./backend:/app
|
|
202
|
-
environment:
|
|
203
|
-
- DEBUG=True`
|
|
204
|
-
: ` backend:
|
|
205
|
-
build: ./backend
|
|
206
|
-
ports:
|
|
207
|
-
- "${backendPort}:${backendPort}"
|
|
208
|
-
volumes:
|
|
209
|
-
- ./backend:/app
|
|
210
|
-
- /app/node_modules
|
|
211
|
-
environment:
|
|
212
|
-
- NODE_ENV=development`;
|
|
213
|
-
|
|
214
|
-
const compose = `version: '3.9'
|
|
215
|
-
services:
|
|
216
|
-
frontend:
|
|
217
|
-
build: ./frontend
|
|
218
|
-
ports:
|
|
219
|
-
- "5173:5173"
|
|
220
|
-
volumes:
|
|
221
|
-
- ./frontend:/app
|
|
222
|
-
- /app/node_modules
|
|
223
|
-
environment:
|
|
224
|
-
- NODE_ENV=development
|
|
225
|
-
${backendService}
|
|
226
|
-
db:
|
|
227
|
-
image: postgres:16-alpine
|
|
228
|
-
environment:
|
|
229
|
-
POSTGRES_DB: mydb
|
|
230
|
-
POSTGRES_USER: user
|
|
231
|
-
POSTGRES_PASSWORD: password
|
|
232
|
-
ports:
|
|
233
|
-
- "5432:5432"
|
|
234
|
-
volumes:
|
|
235
|
-
- pgdata:/var/lib/postgresql/data
|
|
236
|
-
|
|
237
|
-
volumes:
|
|
238
|
-
pgdata:
|
|
239
|
-
`;
|
|
240
|
-
await fse.writeFile(path.join(projectPath, 'docker-compose.yml'), compose);
|
|
241
|
-
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
|
+
);
|
|
242
360
|
}
|
|
243
361
|
|
|
244
|
-
// ──
|
|
362
|
+
// ── .env ──────────────────────────────────────────────────────────────────────
|
|
245
363
|
async function generateEnvFiles(config, projectPath) {
|
|
246
364
|
await fse.writeFile(path.join(projectPath, 'frontend', '.env.example'), 'VITE_API_URL=http://localhost:3001\n');
|
|
247
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' : '';
|
|
248
370
|
if (!isPython) {
|
|
249
|
-
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
|
+
);
|
|
250
374
|
}
|
|
251
|
-
console.log(chalk.green('✔ Archivos .env.example generados'));
|
|
252
375
|
}
|
|
253
376
|
|
|
254
|
-
const EDITORCONFIG = `root = true
|
|
255
|
-
|
|
256
|
-
indent_style = space
|
|
257
|
-
indent_size = 2
|
|
258
|
-
end_of_line = lf
|
|
259
|
-
charset = utf-8
|
|
260
|
-
trim_trailing_whitespace = true
|
|
261
|
-
insert_final_newline = true
|
|
262
|
-
`;
|
|
263
|
-
|
|
264
|
-
const GITIGNORE = `node_modules/
|
|
265
|
-
dist/
|
|
266
|
-
.next/
|
|
267
|
-
.nuxt/
|
|
268
|
-
.svelte-kit/
|
|
269
|
-
.env
|
|
270
|
-
*.log
|
|
271
|
-
__pycache__/
|
|
272
|
-
.venv/
|
|
273
|
-
*.pyc
|
|
274
|
-
target/
|
|
275
|
-
*.class
|
|
276
|
-
`;
|
|
277
|
-
|
|
278
|
-
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`;
|