slicejs-cli 2.1.10 → 2.2.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/client.js CHANGED
@@ -6,6 +6,8 @@ import createComponent from "./commands/createComponent/createComponent.js";
6
6
  import listComponents from "./commands/listComponents/listComponents.js";
7
7
  import deleteComponent from "./commands/deleteComponent/deleteComponent.js";
8
8
  import getComponent, { listComponents as listRemoteComponents, syncComponents } from "./commands/getComponent/getComponent.js";
9
+ import buildProduction, { buildCommand, serveProductionBuild } from "./commands/buildProduction/buildProduction.js";
10
+ import startServer from "./commands/startServer/startServer.js";
9
11
  import versionChecker from "./commands/utils/versionChecker.js";
10
12
  import fs from "fs";
11
13
  import path from "path";
@@ -73,6 +75,80 @@ sliceClient
73
75
  await versionChecker.showVersionInfo();
74
76
  });
75
77
 
78
+ // BUILD COMMAND
79
+ sliceClient
80
+ .command("build")
81
+ .description("Build project for production")
82
+ .option("--serve", "Start production server after build")
83
+ .option("--preview", "Start preview server after build")
84
+ .option("--analyze", "Analyze build without building")
85
+ .option("--skip-clean", "Skip cleaning previous build")
86
+ .option("-p, --port <port>", "Port for preview/serve server", 3001)
87
+ .action(async (options) => {
88
+ await runWithVersionCheck(async () => {
89
+ const success = await buildCommand({
90
+ serve: options.serve,
91
+ preview: options.preview,
92
+ analyze: options.analyze,
93
+ skipClean: options.skipClean,
94
+ port: parseInt(options.port)
95
+ });
96
+
97
+ if (!success) {
98
+ process.exit(1);
99
+ }
100
+ });
101
+ });
102
+
103
+ // START COMMAND (PRODUCTION)
104
+ sliceClient
105
+ .command("start")
106
+ .description("Start production server (requires build first)")
107
+ .option("-p, --port <port>", "Port for production server", 3000)
108
+ .option("--build", "Build for production before starting")
109
+ .action(async (options) => {
110
+ await runWithVersionCheck(async () => {
111
+ const distDir = path.join(__dirname, "../../dist");
112
+
113
+ // Verificar si existe build de producción
114
+ if (!fs.existsSync(distDir) && !options.build) {
115
+ Print.error("No production build found");
116
+ Print.info("Run 'slice build' first or use 'slice start --build'");
117
+ return false;
118
+ }
119
+
120
+ // Si se solicita build, construir primero
121
+ if (options.build) {
122
+ Print.info("Building for production...");
123
+ const buildSuccess = await buildProduction();
124
+ if (!buildSuccess) {
125
+ Print.error("Build failed, cannot start production server");
126
+ return false;
127
+ }
128
+ }
129
+
130
+ // Iniciar servidor de producción
131
+ await startServer({
132
+ mode: 'production',
133
+ port: parseInt(options.port)
134
+ });
135
+ });
136
+ });
137
+
138
+ // DEV COMMAND (DEVELOPMENT)
139
+ sliceClient
140
+ .command("dev")
141
+ .description("Start development server")
142
+ .option("-p, --port <port>", "Port for development server", 3000)
143
+ .action(async (options) => {
144
+ await runWithVersionCheck(async () => {
145
+ await startServer({
146
+ mode: 'development',
147
+ port: parseInt(options.port)
148
+ });
149
+ });
150
+ });
151
+
76
152
  // COMPONENT COMMAND GROUP - For local component management
77
153
  const componentCommand = sliceClient.command("component").alias("comp").description("Manage local project components");
78
154
 
@@ -286,18 +362,26 @@ sliceClient
286
362
  sliceClient.addHelpText('after', `
287
363
  Common Usage Examples:
288
364
  slice init - Initialize new Slice.js project
365
+ slice dev - Start development server
366
+ slice build - Build for production
367
+ slice start - Start production server
289
368
  slice get Button Card Input - Install Visual components from registry
290
369
  slice get FetchManager -s - Install Service component from registry
291
370
  slice browse - Browse all available components
292
371
  slice sync - Update local components to latest versions
293
372
  slice component create - Create new local component
294
- slice update - Check for CLI/framework updates
295
373
 
296
374
  Command Categories:
297
- • init, version, update - Project setup and maintenance
375
+ • init, dev, build, start - Project lifecycle
298
376
  • get, browse, sync - Quick registry shortcuts
299
377
  • component <cmd> - Local component management
300
378
  • registry <cmd> - Official repository operations
379
+ • version, update - Maintenance commands
380
+
381
+ Development vs Production:
382
+ • slice dev - Development server (serves from /src)
383
+ • slice build - Create optimized /dist build
384
+ • slice start - Production server (serves from /dist)
301
385
 
302
386
  More info: https://slice-js-docs.vercel.app/
303
387
  `);
@@ -307,6 +391,7 @@ if (!process.argv.slice(2).length) {
307
391
  program.outputHelp();
308
392
  Print.newLine();
309
393
  Print.info("Start with: slice init");
394
+ Print.commandExample("Development", "slice dev");
310
395
  Print.commandExample("View available components", "slice browse");
311
396
  }
312
397
 
package/commands/Print.js CHANGED
@@ -90,4 +90,28 @@ export default class Print {
90
90
  }
91
91
  Print.separator();
92
92
  }
93
+
94
+ // Método para mostrar resultados de minificación
95
+ static minificationResult(filename, originalSize, minifiedSize, savingsPercent) {
96
+ const originalKB = (originalSize / 1024).toFixed(1);
97
+ const minifiedKB = (minifiedSize / 1024).toFixed(1);
98
+
99
+ console.log('\x1b[32m', ` ✅ ${filename}`, '\x1b[0m');
100
+ console.log('\x1b[90m', ` ${originalKB}KB → ${minifiedKB}KB (${savingsPercent}% saved)`, '\x1b[0m');
101
+ }
102
+
103
+ // Método para mostrar progreso de build
104
+ static buildProgress(message) {
105
+ console.log('\x1b[36m', `🔄 ${message}`, '\x1b[0m');
106
+ }
107
+
108
+ // Método para mostrar estadísticas de servidor
109
+ static serverStats(mode, port, directory) {
110
+ Print.newLine();
111
+ console.log('\x1b[35m', `🌐 Server Configuration:`, '\x1b[0m');
112
+ console.log('\x1b[90m', ` Mode: ${mode}`, '\x1b[0m');
113
+ console.log('\x1b[90m', ` Port: ${port}`, '\x1b[0m');
114
+ console.log('\x1b[90m', ` Serving: /${directory}`, '\x1b[0m');
115
+ Print.newLine();
116
+ }
93
117
  }
@@ -0,0 +1,493 @@
1
+ // commands/buildProduction/buildProduction.js
2
+
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { minify as terserMinify } from 'terser';
7
+ import CleanCSS from 'clean-css';
8
+ import htmlMinifier from 'html-minifier-terser';
9
+ import Print from '../Print.js';
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+
13
+ /**
14
+ * Opciones de minificación para diferentes tipos de archivos
15
+ */
16
+ const getMinificationOptions = () => ({
17
+ js: {
18
+ compress: {
19
+ dead_code: true,
20
+ drop_console: true, // Remover console.log en producción
21
+ drop_debugger: true,
22
+ pure_funcs: ['console.log', 'console.info', 'console.warn'],
23
+ passes: 2
24
+ },
25
+ mangle: {
26
+ toplevel: true,
27
+ reserved: ['Slice', 'Controller', 'StylesManager'] // Preservar clases principales
28
+ },
29
+ output: {
30
+ comments: false,
31
+ beautify: false
32
+ },
33
+ toplevel: true
34
+ },
35
+ css: {
36
+ level: 2, // Optimización agresiva
37
+ returnPromise: false
38
+ },
39
+ html: {
40
+ collapseWhitespace: true,
41
+ removeComments: true,
42
+ removeRedundantAttributes: true,
43
+ removeEmptyAttributes: true,
44
+ minifyCSS: true,
45
+ minifyJS: true,
46
+ useShortDoctype: true,
47
+ removeAttributeQuotes: true,
48
+ removeOptionalTags: true
49
+ }
50
+ });
51
+
52
+ /**
53
+ * Minifica un archivo JavaScript
54
+ */
55
+ async function minifyJavaScript(content, filename) {
56
+ try {
57
+ const options = getMinificationOptions().js;
58
+ const result = await terserMinify(content, options);
59
+
60
+ if (result.error) {
61
+ throw result.error;
62
+ }
63
+
64
+ const originalSize = Buffer.byteLength(content, 'utf8');
65
+ const minifiedSize = Buffer.byteLength(result.code, 'utf8');
66
+ const savings = ((originalSize - minifiedSize) / originalSize * 100).toFixed(1);
67
+
68
+ Print.minificationResult(filename, originalSize, minifiedSize, savings);
69
+
70
+ return result.code;
71
+ } catch (error) {
72
+ Print.error(`Error minifying ${filename}: ${error.message}`);
73
+ throw error;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Minifica un archivo CSS
79
+ */
80
+ async function minifyCSS(content, filename) {
81
+ try {
82
+ const cleanCSS = new CleanCSS(getMinificationOptions().css);
83
+ const result = cleanCSS.minify(content);
84
+
85
+ if (result.errors && result.errors.length > 0) {
86
+ throw new Error(result.errors.join(', '));
87
+ }
88
+
89
+ const originalSize = Buffer.byteLength(content, 'utf8');
90
+ const minifiedSize = Buffer.byteLength(result.styles, 'utf8');
91
+ const savings = ((originalSize - minifiedSize) / originalSize * 100).toFixed(1);
92
+
93
+ Print.minificationResult(filename, originalSize, minifiedSize, savings);
94
+
95
+ return result.styles;
96
+ } catch (error) {
97
+ Print.error(`Error minifying ${filename}: ${error.message}`);
98
+ throw error;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Minifica un archivo HTML
104
+ */
105
+ async function minifyHTML(content, filename) {
106
+ try {
107
+ const result = await htmlMinifier.minify(content, getMinificationOptions().html);
108
+
109
+ const originalSize = Buffer.byteLength(content, 'utf8');
110
+ const minifiedSize = Buffer.byteLength(result, 'utf8');
111
+ const savings = ((originalSize - minifiedSize) / originalSize * 100).toFixed(1);
112
+
113
+ Print.minificationResult(filename, originalSize, minifiedSize, savings);
114
+
115
+ return result;
116
+ } catch (error) {
117
+ Print.error(`Error minifying ${filename}: ${error.message}`);
118
+ throw error;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Procesa un archivo según su extensión
124
+ */
125
+ async function processFile(srcPath, destPath, relativePath) {
126
+ try {
127
+ const content = await fs.readFile(srcPath, 'utf8');
128
+ const ext = path.extname(srcPath).toLowerCase();
129
+ let processedContent = content;
130
+
131
+ switch (ext) {
132
+ case '.js':
133
+ processedContent = await minifyJavaScript(content, relativePath);
134
+ break;
135
+ case '.css':
136
+ processedContent = await minifyCSS(content, relativePath);
137
+ break;
138
+ case '.html':
139
+ processedContent = await minifyHTML(content, relativePath);
140
+ break;
141
+ default:
142
+ // Para otros archivos (JSON, etc.), solo copiar
143
+ await fs.copy(srcPath, destPath);
144
+ return;
145
+ }
146
+
147
+ await fs.writeFile(destPath, processedContent, 'utf8');
148
+
149
+ } catch (error) {
150
+ Print.error(`Error processing ${relativePath}: ${error.message}`);
151
+ throw error;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Copia y procesa recursivamente todos los archivos de src a dist
157
+ */
158
+ async function processDirectory(srcDir, distDir, baseSrcDir) {
159
+ const items = await fs.readdir(srcDir);
160
+
161
+ for (const item of items) {
162
+ const srcPath = path.join(srcDir, item);
163
+ const destPath = path.join(distDir, item);
164
+ const relativePath = path.relative(baseSrcDir, srcPath);
165
+
166
+ const stat = await fs.stat(srcPath);
167
+
168
+ if (stat.isDirectory()) {
169
+ await fs.ensureDir(destPath);
170
+ await processDirectory(srcPath, destPath, baseSrcDir);
171
+ } else {
172
+ const ext = path.extname(srcPath).toLowerCase();
173
+
174
+ // Procesar archivos que pueden ser minificados
175
+ if (['.js', '.css', '.html'].includes(ext)) {
176
+ await processFile(srcPath, destPath, relativePath);
177
+ } else {
178
+ // Copiar otros archivos sin modificar
179
+ await fs.copy(srcPath, destPath);
180
+ }
181
+ }
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Crea un bundle optimizado del archivo principal Slice.js
187
+ */
188
+ async function createOptimizedBundle() {
189
+ try {
190
+ Print.info('Creating optimized Slice.js bundle...');
191
+
192
+ const slicePath = path.join(__dirname, '../../../../src/Slice/Slice.js');
193
+ const distSlicePath = path.join(__dirname, '../../../../dist/Slice/Slice.js');
194
+
195
+ if (!await fs.pathExists(slicePath)) {
196
+ Print.warning('Slice.js main file not found, skipping bundle optimization');
197
+ return;
198
+ }
199
+
200
+ const content = await fs.readFile(slicePath, 'utf8');
201
+ const minifiedContent = await minifyJavaScript(content, 'Slice/Slice.js');
202
+
203
+ await fs.ensureDir(path.dirname(distSlicePath));
204
+ await fs.writeFile(distSlicePath, minifiedContent, 'utf8');
205
+
206
+ Print.success('Optimized Slice.js bundle created');
207
+
208
+ } catch (error) {
209
+ Print.error(`Error creating optimized bundle: ${error.message}`);
210
+ throw error;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Copia sliceConfig.json sin modificaciones
216
+ */
217
+ async function copySliceConfig() {
218
+ try {
219
+ const srcConfigPath = path.join(__dirname, '../../../../src/sliceConfig.json');
220
+ const distConfigPath = path.join(__dirname, '../../../../dist/sliceConfig.json');
221
+
222
+ if (await fs.pathExists(srcConfigPath)) {
223
+ await fs.copy(srcConfigPath, distConfigPath);
224
+ Print.success('sliceConfig.json copied to dist');
225
+ } else {
226
+ Print.warning('sliceConfig.json not found in src, skipping copy');
227
+ }
228
+ } catch (error) {
229
+ Print.error(`Error copying sliceConfig.json: ${error.message}`);
230
+ throw error;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Genera estadísticas del build
236
+ */
237
+ async function generateBuildStats(srcDir, distDir) {
238
+ try {
239
+ Print.info('Generating build statistics...');
240
+
241
+ const calculateDirSize = async (dir) => {
242
+ let totalSize = 0;
243
+ const files = await fs.readdir(dir, { withFileTypes: true });
244
+
245
+ for (const file of files) {
246
+ const filePath = path.join(dir, file.name);
247
+ if (file.isDirectory()) {
248
+ totalSize += await calculateDirSize(filePath);
249
+ } else {
250
+ const stats = await fs.stat(filePath);
251
+ totalSize += stats.size;
252
+ }
253
+ }
254
+ return totalSize;
255
+ };
256
+
257
+ const srcSize = await calculateDirSize(srcDir);
258
+ const distSize = await calculateDirSize(distDir);
259
+ const savings = ((srcSize - distSize) / srcSize * 100).toFixed(1);
260
+
261
+ Print.newLine();
262
+ Print.title('📊 Build Statistics');
263
+ console.log(`📁 Source size: ${(srcSize / 1024).toFixed(1)} KB`);
264
+ console.log(`📦 Production size: ${(distSize / 1024).toFixed(1)} KB`);
265
+ console.log(`💾 Size reduction: ${savings}% saved`);
266
+
267
+ } catch (error) {
268
+ Print.warning(`Could not generate build statistics: ${error.message}`);
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Función principal de build para producción
274
+ */
275
+ export default async function buildProduction(options = {}) {
276
+ const startTime = Date.now();
277
+
278
+ try {
279
+ Print.title('🚀 Building Slice.js project for production...');
280
+ Print.newLine();
281
+
282
+ // Verificar que existe src
283
+ const srcDir = path.join(__dirname, '../../../../src');
284
+ const distDir = path.join(__dirname, '../../../../dist');
285
+
286
+ if (!await fs.pathExists(srcDir)) {
287
+ throw new Error('src directory not found. Run "slice init" first.');
288
+ }
289
+
290
+ // 1. Limpiar directorio dist
291
+ if (await fs.pathExists(distDir)) {
292
+ if (!options.skipClean) {
293
+ Print.info('Cleaning previous build...');
294
+ await fs.remove(distDir);
295
+ Print.success('Previous build cleaned');
296
+ }
297
+ }
298
+
299
+ await fs.ensureDir(distDir);
300
+
301
+ // 2. Copiar sliceConfig.json sin modificaciones
302
+ await copySliceConfig();
303
+
304
+ // 3. Procesar todos los archivos de src
305
+ Print.info('Processing and minifying source files...');
306
+ await processDirectory(srcDir, distDir, srcDir);
307
+ Print.success('All source files processed and optimized');
308
+
309
+ // 4. Crear bundle optimizado del archivo principal
310
+ await createOptimizedBundle();
311
+
312
+ // 5. Generar estadísticas
313
+ await generateBuildStats(srcDir, distDir);
314
+
315
+ // 6. Tiempo total
316
+ const buildTime = ((Date.now() - startTime) / 1000).toFixed(1);
317
+
318
+ Print.newLine();
319
+ Print.success(`✨ Production build completed in ${buildTime}s`);
320
+ Print.info('Your optimized project is ready in the /dist directory');
321
+ Print.newLine();
322
+ Print.info('Next steps:');
323
+ console.log(' • The same /api folder serves both development and production');
324
+ console.log(' • Update your Express server to serve from /dist instead of /src');
325
+ console.log(' • Deploy both /api and /dist directories to your hosting provider');
326
+ console.log(' • Use "slice build --serve" to preview the production build locally');
327
+
328
+ return true;
329
+
330
+ } catch (error) {
331
+ Print.error(`Build failed: ${error.message}`);
332
+ return false;
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Servidor de desarrollo para testing del build de producción
338
+ * Usa Express como el servidor principal pero sirviendo desde /dist
339
+ */
340
+ export async function serveProductionBuild(port = 3001) {
341
+ try {
342
+ const distDir = path.join(__dirname, '../../../../dist');
343
+
344
+ if (!await fs.pathExists(distDir)) {
345
+ throw new Error('No production build found. Run "slice build" first.');
346
+ }
347
+
348
+ Print.info(`Starting production build server on port ${port}...`);
349
+
350
+ // Implementar servidor estático simple que simula el comportamiento de la API
351
+ const express = await import('express');
352
+ const app = express.default();
353
+
354
+ // Servir archivos estáticos desde dist (equivalente a lo que hace la API con src)
355
+ app.use(express.default.static(distDir));
356
+
357
+ // SPA fallback - servir index.html para rutas no encontradas
358
+ app.get('*', (req, res) => {
359
+ const indexPath = path.join(distDir, 'index.html');
360
+ if (fs.existsSync(indexPath)) {
361
+ res.sendFile(indexPath);
362
+ } else {
363
+ res.status(404).send('Production build not found');
364
+ }
365
+ });
366
+
367
+ app.listen(port, () => {
368
+ Print.success(`Production build server running at http://localhost:${port}`);
369
+ Print.info('Press Ctrl+C to stop the server');
370
+ Print.info('This server simulates production environment using /dist files');
371
+ });
372
+
373
+ } catch (error) {
374
+ Print.error(`Error starting production server: ${error.message}`);
375
+ throw error;
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Comando build con opciones
381
+ */
382
+ export async function buildCommand(options = {}) {
383
+ // Verificar dependencias necesarias
384
+ if (!await checkBuildDependencies()) {
385
+ return false;
386
+ }
387
+
388
+ if (options.serve) {
389
+ // Solo servir build existente
390
+ await serveProductionBuild(options.port);
391
+ return true;
392
+ }
393
+
394
+ if (options.analyze) {
395
+ // Analizar build sin construir
396
+ await analyzeBuild();
397
+ return true;
398
+ }
399
+
400
+ // Build completo
401
+ const success = await buildProduction(options);
402
+
403
+ if (success && options.preview) {
404
+ Print.newLine();
405
+ Print.info('Starting preview server...');
406
+ setTimeout(() => {
407
+ serveProductionBuild(options.port);
408
+ }, 1000);
409
+ }
410
+
411
+ return success;
412
+ }
413
+
414
+ /**
415
+ * Verifica que las dependencias de build estén instaladas
416
+ */
417
+ async function checkBuildDependencies() {
418
+ try {
419
+ Print.info('Checking build dependencies...');
420
+
421
+ const packageJsonPath = path.join(__dirname, '../../../../package.json');
422
+
423
+ if (!await fs.pathExists(packageJsonPath)) {
424
+ throw new Error('package.json not found');
425
+ }
426
+
427
+ const packageJson = await fs.readJson(packageJsonPath);
428
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
429
+
430
+ const requiredDeps = ['terser', 'clean-css', 'html-minifier-terser'];
431
+ const missing = requiredDeps.filter(dep => !deps[dep]);
432
+
433
+ if (missing.length > 0) {
434
+ Print.error('Missing build dependencies:');
435
+ missing.forEach(dep => console.log(` • ${dep}`));
436
+ Print.newLine();
437
+ Print.info('Install missing dependencies:');
438
+ console.log(`npm install --save-dev ${missing.join(' ')}`);
439
+ return false;
440
+ }
441
+
442
+ Print.success('All build dependencies are installed');
443
+ return true;
444
+
445
+ } catch (error) {
446
+ Print.error(`Error checking dependencies: ${error.message}`);
447
+ return false;
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Analiza el tamaño y composición del build
453
+ */
454
+ async function analyzeBuild() {
455
+ try {
456
+ const distDir = path.join(__dirname, '../../../../dist');
457
+
458
+ if (!await fs.pathExists(distDir)) {
459
+ throw new Error('No production build found. Run "slice build" first.');
460
+ }
461
+
462
+ Print.title('📊 Build Analysis');
463
+ Print.newLine();
464
+
465
+ const analyzeDirectory = async (dir, prefix = '') => {
466
+ const items = await fs.readdir(dir);
467
+ let totalSize = 0;
468
+
469
+ for (const item of items) {
470
+ const itemPath = path.join(dir, item);
471
+ const stat = await fs.stat(itemPath);
472
+
473
+ if (stat.isDirectory()) {
474
+ const dirSize = await analyzeDirectory(itemPath, `${prefix}${item}/`);
475
+ totalSize += dirSize;
476
+ } else {
477
+ const size = (stat.size / 1024).toFixed(1);
478
+ console.log(`📄 ${prefix}${item}: ${size} KB`);
479
+ totalSize += stat.size;
480
+ }
481
+ }
482
+
483
+ return totalSize;
484
+ };
485
+
486
+ const totalSize = await analyzeDirectory(distDir);
487
+ Print.newLine();
488
+ Print.info(`Total build size: ${(totalSize / 1024).toFixed(1)} KB`);
489
+
490
+ } catch (error) {
491
+ Print.error(`Error analyzing build: ${error.message}`);
492
+ }
493
+ }