slicejs-cli 2.7.5 → 2.7.7

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
@@ -18,6 +18,7 @@ import { exec } from "child_process";
18
18
  import { promisify } from "util";
19
19
  import validations from "./commands/Validations.js";
20
20
  import Print from "./commands/Print.js";
21
+ import bundle, { cleanBundles, bundleInfo } from './commands/bundle/bundle.js';
21
22
 
22
23
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
24
 
@@ -137,12 +138,21 @@ sliceClient
137
138
  .description("Start development server")
138
139
  .option("-p, --port <port>", "Port for development server", 3000)
139
140
  .option("-w, --watch", "Enable watch mode for file changes")
141
+ .option("-b, --bundled", "Generate bundles before starting server")
140
142
  .action(async (options) => {
141
143
  await runWithVersionCheck(async () => {
144
+ // Si se solicita bundles, generarlos primero
145
+ if (options.bundled) {
146
+ Print.info("Generating bundles before starting server...");
147
+ await bundle({ verbose: false });
148
+ Print.newLine();
149
+ }
150
+
142
151
  await startServer({
143
- mode: 'development',
152
+ mode: options.bundled ? 'bundled' : 'development',
144
153
  port: parseInt(options.port),
145
- watch: options.watch
154
+ watch: options.watch,
155
+ bundled: options.bundled
146
156
  });
147
157
  });
148
158
  });
@@ -153,12 +163,21 @@ sliceClient
153
163
  .description("Start development server (alias for dev)")
154
164
  .option("-p, --port <port>", "Port for server", 3000)
155
165
  .option("-w, --watch", "Enable watch mode for file changes")
166
+ .option("-b, --bundled", "Generate bundles before starting server")
156
167
  .action(async (options) => {
157
168
  await runWithVersionCheck(async () => {
169
+ // Si se solicita bundles, generarlos primero
170
+ if (options.bundled) {
171
+ Print.info("Generating bundles before starting server...");
172
+ await bundle({ verbose: false });
173
+ Print.newLine();
174
+ }
175
+
158
176
  await startServer({
159
- mode: 'development',
177
+ mode: options.bundled ? 'bundled' : 'development',
160
178
  port: parseInt(options.port),
161
- watch: options.watch
179
+ watch: options.watch,
180
+ bundled: options.bundled
162
181
  });
163
182
  });
164
183
  });
@@ -434,13 +453,35 @@ sliceClient
434
453
  subcommandTerm: (cmd) => cmd.name() + ' ' + cmd.usage()
435
454
  });
436
455
 
456
+ sliceClient
457
+ .command('bundle')
458
+ .description('Generate production bundles for optimal loading')
459
+ .option('-a, --analyze', 'Only analyze without generating bundles')
460
+ .option('-v, --verbose', 'Show detailed information')
461
+ .action(bundle);
462
+
463
+ // Subcomando: limpiar bundles
464
+ sliceClient
465
+ .command('bundle:clean')
466
+ .description('Remove all generated bundles')
467
+ .action(cleanBundles);
468
+
469
+ // Subcomando: información
470
+ sliceClient
471
+ .command('bundle:info')
472
+ .description('Show information about generated bundles')
473
+ .action(bundleInfo);
474
+
475
+
437
476
  // Custom help - SIMPLIFICADO para development only
438
477
  sliceClient.addHelpText('after', `
439
478
  Common Usage Examples:
440
479
  slice init - Initialize new Slice.js project
441
480
  slice dev - Start development server
442
481
  slice start - Start development server (same as dev)
443
- slice get Button Card Input - Install Visual components from registry
482
+ slice dev --bundled - Generate bundles then start server
483
+ slice start --bundled - Same as above (bundle -> start)
484
+ slice get Button Card Input - Install Visual components from registry
444
485
  slice get FetchManager -s - Install Service component from registry
445
486
  slice browse - Browse all available components
446
487
  slice sync - Update local components to latest versions
@@ -456,9 +497,10 @@ Command Categories:
456
497
  • version, update, doctor - Maintenance commands
457
498
 
458
499
  Development Workflow:
459
- • slice init - Initialize project
460
- • slice dev - Start development server (serves from /src)
461
- • slice start - Alternative to dev command
500
+ • slice init - Initialize project
501
+ • slice dev - Start development server (serves from /src)
502
+ • slice start - Alternative to dev command
503
+ • slice dev --bundled - Start with bundles (bundle -> start)
462
504
 
463
505
  Note: Production builds are disabled. Use development mode for all workflows.
464
506
 
@@ -0,0 +1,226 @@
1
+ // cli/commands/bundle/bundle.js
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+ import DependencyAnalyzer from '../utils/bundling/DependencyAnalyzer.js';
5
+ import BundleGenerator from '../utils/bundling/BundleGenerator.js';
6
+ import Print from '..//Print.js';
7
+
8
+ /**
9
+ * Main bundling command
10
+ */
11
+ export default async function bundle(options = {}) {
12
+ const startTime = Date.now();
13
+ const projectRoot = process.cwd();
14
+
15
+ try {
16
+ Print.title('📦 Slice.js Bundle Generator');
17
+ Print.newLine();
18
+
19
+ // Validate that it's a Slice.js project
20
+ await validateProject(projectRoot);
21
+
22
+ // Phase 1: Analysis
23
+ Print.buildProgress('Analyzing project...');
24
+ const analyzer = new DependencyAnalyzer(import.meta.url);
25
+ const analysisData = await analyzer.analyze();
26
+
27
+ // Show report if in verbose mode
28
+ if (options.verbose || options.analyze) {
29
+ Print.newLine();
30
+ analyzer.generateReport(analysisData.metrics);
31
+ Print.newLine();
32
+ }
33
+
34
+ // If only analysis is requested, finish here
35
+ if (options.analyze) {
36
+ Print.success('Analysis completed');
37
+ return;
38
+ }
39
+
40
+ // Phase 2: Bundle generation
41
+ Print.buildProgress('Generating bundles...');
42
+ const generator = new BundleGenerator(import.meta.url, analysisData);
43
+ const result = await generator.generate();
44
+
45
+ // Phase 3: Save configuration
46
+ Print.buildProgress('Saving configuration...');
47
+ await generator.saveBundleConfig(result.config);
48
+
49
+ // Phase 4: Summary
50
+ Print.newLine();
51
+ printSummary(result, startTime);
52
+
53
+ // Show next step
54
+ Print.newLine();
55
+ Print.info('💡 Next step:');
56
+ console.log(' Update your Slice.js to use the generated bundles');
57
+ console.log(' Bundles will load automatically in production\n');
58
+
59
+ } catch (error) {
60
+ Print.error('Error generating bundles:', error.message);
61
+ console.error('\n📍 Complete stack trace:');
62
+ console.error(error.stack);
63
+
64
+ if (error.code) {
65
+ console.error('\n📍 Error code:', error.code);
66
+ }
67
+
68
+ process.exit(1);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Validates that the project has the correct structure
74
+ */
75
+ async function validateProject(projectRoot) {
76
+ const requiredPaths = [
77
+ 'src/Components/components.js',
78
+ 'src/routes.js'
79
+ ];
80
+
81
+ for (const reqPath of requiredPaths) {
82
+ const fullPath = path.join(projectRoot, reqPath);
83
+ if (!await fs.pathExists(fullPath)) {
84
+ throw new Error(`Required file not found: ${reqPath}`);
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Prints summary of generated bundles
91
+ */
92
+ function printSummary(result, startTime) {
93
+ const { bundles, config, files } = result;
94
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
95
+
96
+ Print.success(`Bundles generated in ${duration}s\n`);
97
+
98
+ // Critical bundle
99
+ if (bundles.critical.components.length > 0) {
100
+ Print.info('📦 Critical Bundle:');
101
+ console.log(` Components: ${bundles.critical.components.length}`);
102
+ console.log(` Size: ${(bundles.critical.size / 1024).toFixed(1)} KB`);
103
+ console.log(` File: ${bundles.critical.file}\n`);
104
+ }
105
+
106
+ // Route bundles
107
+ const routeCount = Object.keys(bundles.routes).length;
108
+ if (routeCount > 0) {
109
+ Print.info(`📦 Route Bundles (${routeCount}):`);
110
+
111
+ for (const [key, bundle] of Object.entries(bundles.routes)) {
112
+ console.log(` ${key}:`);
113
+ console.log(` Route: ${bundle.path}`);
114
+ console.log(` Components: ${bundle.components.length}`);
115
+ console.log(` Size: ${(bundle.size / 1024).toFixed(1)} KB`);
116
+ console.log(` File: ${bundle.file}`);
117
+ }
118
+ Print.newLine();
119
+ }
120
+
121
+ // Global statistics
122
+ Print.info('📊 Statistics:');
123
+ console.log(` Strategy: ${config.strategy}`);
124
+ console.log(` Total components: ${config.stats.totalComponents}`);
125
+ console.log(` Shared components: ${config.stats.sharedComponents}`);
126
+ console.log(` Generated files: ${files.length}`);
127
+
128
+ // Calculate estimated improvement
129
+ const beforeRequests = config.stats.totalComponents;
130
+ const afterRequests = files.length;
131
+ const improvement = Math.round((1 - afterRequests / beforeRequests) * 100);
132
+
133
+ console.log(` Request reduction: ${improvement}% (${beforeRequests} → ${afterRequests})`);
134
+ }
135
+
136
+ /**
137
+ * Subcommand: Clean bundles
138
+ */
139
+ export async function cleanBundles() {
140
+ const projectRoot = process.cwd();
141
+ const srcPath = path.join(projectRoot, 'src');
142
+
143
+ try {
144
+ Print.title('🧹 Cleaning bundles...');
145
+
146
+ const files = await fs.readdir(srcPath);
147
+ const bundleFiles = files.filter(f => f.startsWith('slice-bundle.'));
148
+
149
+ if (bundleFiles.length === 0) {
150
+ Print.warning('No bundles found to clean');
151
+ return;
152
+ }
153
+
154
+ for (const file of bundleFiles) {
155
+ await fs.remove(path.join(srcPath, file));
156
+ console.log(` ✓ Deleted: ${file}`);
157
+ }
158
+
159
+ // Remove config
160
+ const configPath = path.join(srcPath, 'bundle.config.json');
161
+ if (await fs.pathExists(configPath)) {
162
+ await fs.remove(configPath);
163
+ console.log(` ✓ Deleted: bundle.config.json`);
164
+ }
165
+
166
+ Print.newLine();
167
+ Print.success(`${bundleFiles.length} files deleted`);
168
+
169
+ } catch (error) {
170
+ Print.error('Error cleaning bundles:', error.message);
171
+ process.exit(1);
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Subcommand: Bundle information
177
+ */
178
+ export async function bundleInfo() {
179
+ const projectRoot = process.cwd();
180
+ const configPath = path.join(projectRoot, 'src/bundle.config.json');
181
+
182
+ try {
183
+ if (!await fs.pathExists(configPath)) {
184
+ Print.warning('Bundle configuration not found');
185
+ Print.info('Run "slice bundle" to generate bundles');
186
+ return;
187
+ }
188
+
189
+ const config = await fs.readJson(configPath);
190
+
191
+ Print.title('📦 Bundle Information');
192
+ Print.newLine();
193
+
194
+ Print.info('Configuration:');
195
+ console.log(` Version: ${config.version}`);
196
+ console.log(` Strategy: ${config.strategy}`);
197
+ console.log(` Generated: ${new Date(config.generated).toLocaleString()}`);
198
+ Print.newLine();
199
+
200
+ Print.info('Statistics:');
201
+ console.log(` Total components: ${config.stats.totalComponents}`);
202
+ console.log(` Total routes: ${config.stats.totalRoutes}`);
203
+ console.log(` Shared components: ${config.stats.sharedComponents} (${config.stats.sharedPercentage}%)`);
204
+ console.log(` Total size: ${(config.stats.totalSize / 1024).toFixed(1)} KB`);
205
+ Print.newLine();
206
+
207
+ Print.info('Bundles:');
208
+
209
+ // Critical
210
+ if (config.bundles.critical) {
211
+ console.log(` Critical: ${config.bundles.critical.components.length} components, ${(config.bundles.critical.size / 1024).toFixed(1)} KB`);
212
+ }
213
+
214
+ // Routes
215
+ const routeCount = Object.keys(config.bundles.routes).length;
216
+ console.log(` Routes: ${routeCount} bundles`);
217
+
218
+ for (const [key, bundle] of Object.entries(config.bundles.routes)) {
219
+ console.log(` ${key}: ${bundle.components.length} components, ${(bundle.size / 1024).toFixed(1)} KB`);
220
+ }
221
+
222
+ } catch (error) {
223
+ Print.error('Error reading information:', error.message);
224
+ process.exit(1);
225
+ }
226
+ }
@@ -86,6 +86,8 @@ function startNodeServer(port, mode) {
86
86
  const args = [apiIndexPath];
87
87
  if (mode === 'production') {
88
88
  args.push('--production');
89
+ } else if (mode === 'bundled') {
90
+ args.push('--bundled');
89
91
  } else {
90
92
  args.push('--development');
91
93
  }
@@ -171,7 +173,7 @@ export default async function startServer(options = {}) {
171
173
  const config = loadConfig();
172
174
  const defaultPort = config?.server?.port || 3000;
173
175
 
174
- const { mode = 'development', port = defaultPort, watch = false } = options;
176
+ const { mode = 'development', port = defaultPort, watch = false, bundled = false } = options;
175
177
 
176
178
  try {
177
179
  Print.title(`🚀 Starting Slice.js ${mode} server...`);
@@ -190,7 +192,7 @@ export default async function startServer(options = {}) {
190
192
  throw new Error(
191
193
  `Port ${port} is already in use. Please:\n` +
192
194
  ` 1. Stop the process using port ${port}, or\n` +
193
- ` 2. Use a different port: slice ${mode === 'development' ? 'dev' : 'start'} -p <port>`
195
+ ` 2. Use a different port: slice ${mode === 'development' ? 'dev' : mode === 'bundled' ? 'start --bundled' : 'start'} -p <port>`
194
196
  );
195
197
  }
196
198
 
@@ -203,6 +205,8 @@ export default async function startServer(options = {}) {
203
205
  throw new Error('No production build found. Run "slice build" first.');
204
206
  }
205
207
  Print.info('Production mode: serving optimized files from /dist');
208
+ } else if (mode === 'bundled') {
209
+ Print.info('Bundled mode: serving with generated bundles for optimized loading');
206
210
  } else {
207
211
  Print.info('Development mode: serving files from /src with hot reload');
208
212
  }
@@ -0,0 +1,389 @@
1
+ // cli/utils/bundling/BundleGenerator.js
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+ import { getSrcPath, getComponentsJsPath } from '../PathHelper.js';
6
+
7
+ export default class BundleGenerator {
8
+ constructor(moduleUrl, analysisData) {
9
+ this.moduleUrl = moduleUrl;
10
+ this.analysisData = analysisData;
11
+ this.srcPath = getSrcPath(moduleUrl);
12
+ this.componentsPath = path.dirname(getComponentsJsPath(moduleUrl));
13
+
14
+ // Configuration
15
+ this.config = {
16
+ maxCriticalSize: 50 * 1024, // 50KB
17
+ maxCriticalComponents: 15,
18
+ minSharedUsage: 3, // Minimum routes to be considered "shared"
19
+ strategy: 'hybrid' // 'global', 'hybrid', 'per-route'
20
+ };
21
+
22
+ this.bundles = {
23
+ critical: {
24
+ components: [],
25
+ size: 0,
26
+ file: 'slice-bundle.critical.js'
27
+ },
28
+ routes: {}
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Generates all bundles
34
+ */
35
+ async generate() {
36
+ console.log('🔨 Generating bundles...');
37
+
38
+ // 1. Determine optimal strategy
39
+ this.determineStrategy();
40
+
41
+ // 2. Identify critical components
42
+ this.identifyCriticalComponents();
43
+
44
+ // 3. Assign components to routes
45
+ this.assignRouteComponents();
46
+
47
+ // 4. Generate bundle files
48
+ const files = await this.generateBundleFiles();
49
+
50
+ // 5. Generate configuration
51
+ const config = this.generateBundleConfig();
52
+
53
+ console.log('✅ Bundles generated successfully');
54
+
55
+ return {
56
+ bundles: this.bundles,
57
+ config,
58
+ files
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Determines the optimal bundling strategy
64
+ */
65
+ determineStrategy() {
66
+ const { metrics } = this.analysisData;
67
+ const { totalComponents, sharedPercentage } = metrics;
68
+
69
+ // Strategy based on size and usage pattern
70
+ if (totalComponents < 20 || sharedPercentage > 60) {
71
+ this.config.strategy = 'global';
72
+ console.log('📦 Strategy: Global Bundle (small project or highly shared)');
73
+ } else if (totalComponents < 60) {
74
+ this.config.strategy = 'hybrid';
75
+ console.log('📦 Strategy: Hybrid (critical + per route)');
76
+ } else {
77
+ this.config.strategy = 'per-route';
78
+ console.log('📦 Strategy: Per Route (large project)');
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Identifies critical components for the initial bundle
84
+ */
85
+ identifyCriticalComponents() {
86
+ const { components } = this.analysisData;
87
+
88
+ // Filter critical candidates
89
+ const candidates = components
90
+ .filter(comp => {
91
+ // Shared components (used in 3+ routes)
92
+ const isShared = comp.routes.size >= this.config.minSharedUsage;
93
+
94
+ // Structural components (Navbar, Footer, etc.)
95
+ const isStructural = comp.categoryType === 'Structural' ||
96
+ ['Navbar', 'Footer', 'Layout'].includes(comp.name);
97
+
98
+ // Small and highly used components
99
+ const isSmallAndUseful = comp.size < 5000 && comp.routes.size >= 2;
100
+
101
+ return isShared || isStructural || isSmallAndUseful;
102
+ })
103
+ .sort((a, b) => {
104
+ // Prioritize by: (usage * 10) - size
105
+ const priorityA = (a.routes.size * 10) - (a.size / 1000);
106
+ const priorityB = (b.routes.size * 10) - (b.size / 1000);
107
+ return priorityB - priorityA;
108
+ });
109
+
110
+ // Fill critical bundle up to limit
111
+ for (const comp of candidates) {
112
+ const wouldExceedSize = this.bundles.critical.size + comp.size > this.config.maxCriticalSize;
113
+ const wouldExceedCount = this.bundles.critical.components.length >= this.config.maxCriticalComponents;
114
+
115
+ if (wouldExceedSize || wouldExceedCount) break;
116
+
117
+ this.bundles.critical.components.push(comp);
118
+ this.bundles.critical.size += comp.size;
119
+ }
120
+
121
+ console.log(`✓ Critical bundle: ${this.bundles.critical.components.length} components, ${(this.bundles.critical.size / 1024).toFixed(1)} KB`);
122
+ }
123
+
124
+ /**
125
+ * Assigns remaining components to route bundles
126
+ */
127
+ assignRouteComponents() {
128
+ const criticalNames = new Set(this.bundles.critical.components.map(c => c.name));
129
+
130
+ for (const [routePath, route] of this.analysisData.routes) {
131
+ // Get all route dependencies
132
+ const routeComponents = this.getRouteComponents(route.component);
133
+
134
+ // Filter those already in critical
135
+ const uniqueComponents = routeComponents.filter(comp =>
136
+ !criticalNames.has(comp.name)
137
+ );
138
+
139
+ if (uniqueComponents.length === 0) continue;
140
+
141
+ const routeKey = this.routeToFileName(routePath);
142
+ const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
143
+
144
+ this.bundles.routes[routeKey] = {
145
+ path: routePath,
146
+ components: uniqueComponents,
147
+ size: totalSize,
148
+ file: `slice-bundle.${routeKey}.js`
149
+ };
150
+
151
+ console.log(`✓ Bundle ${routeKey}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB`);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Gets all components needed for a route
157
+ */
158
+ getRouteComponents(componentName) {
159
+ const result = [];
160
+ const visited = new Set();
161
+
162
+ const traverse = (name) => {
163
+ if (visited.has(name)) return;
164
+ visited.add(name);
165
+
166
+ const component = this.analysisData.components.find(c => c.name === name);
167
+ if (!component) return;
168
+
169
+ result.push(component);
170
+
171
+ // Add dependencies recursively
172
+ for (const dep of component.dependencies) {
173
+ traverse(dep);
174
+ }
175
+ };
176
+
177
+ traverse(componentName);
178
+ return result;
179
+ }
180
+
181
+ /**
182
+ * Generates the physical bundle files
183
+ */
184
+ async generateBundleFiles() {
185
+ const files = [];
186
+
187
+ // 1. Critical bundle
188
+ if (this.bundles.critical.components.length > 0) {
189
+ const criticalFile = await this.createBundleFile(
190
+ this.bundles.critical.components,
191
+ 'critical',
192
+ null
193
+ );
194
+ files.push(criticalFile);
195
+ }
196
+
197
+ // 2. Route bundles
198
+ for (const [routeKey, bundle] of Object.entries(this.bundles.routes)) {
199
+ const routeFile = await this.createBundleFile(
200
+ bundle.components,
201
+ 'route',
202
+ bundle.path
203
+ );
204
+ files.push(routeFile);
205
+ }
206
+
207
+ return files;
208
+ }
209
+
210
+ /**
211
+ * Creates a bundle file
212
+ */
213
+ async createBundleFile(components, type, routePath) {
214
+ const routeKey = routePath ? this.routeToFileName(routePath) : 'critical';
215
+ const fileName = `slice-bundle.${routeKey}.js`;
216
+ const filePath = path.join(this.srcPath, fileName);
217
+
218
+ const bundleContent = await this.generateBundleContent(
219
+ components,
220
+ type,
221
+ routePath
222
+ );
223
+
224
+ await fs.writeFile(filePath, bundleContent, 'utf-8');
225
+
226
+ const hash = crypto.createHash('md5').update(bundleContent).digest('hex').substring(0, 12);
227
+
228
+ return {
229
+ name: routeKey,
230
+ file: fileName,
231
+ path: filePath,
232
+ size: Buffer.byteLength(bundleContent, 'utf-8'),
233
+ hash,
234
+ componentCount: components.length
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Generates the content of a bundle
240
+ */
241
+ async generateBundleContent(components, type, routePath) {
242
+ const componentsData = {};
243
+
244
+ for (const comp of components) {
245
+ const jsContent = await fs.readFile(
246
+ path.join(comp.path, `${comp.name}.js`),
247
+ 'utf-8'
248
+ );
249
+
250
+ let htmlContent = null;
251
+ let cssContent = null;
252
+
253
+ const htmlPath = path.join(comp.path, `${comp.name}.html`);
254
+ const cssPath = path.join(comp.path, `${comp.name}.css`);
255
+
256
+ if (await fs.pathExists(htmlPath)) {
257
+ htmlContent = await fs.readFile(htmlPath, 'utf-8');
258
+ }
259
+
260
+ if (await fs.pathExists(cssPath)) {
261
+ cssContent = await fs.readFile(cssPath, 'utf-8');
262
+ }
263
+
264
+ componentsData[comp.name] = {
265
+ name: comp.name,
266
+ category: comp.category,
267
+ categoryType: comp.categoryType,
268
+ js: this.cleanJavaScript(jsContent),
269
+ html: htmlContent,
270
+ css: cssContent,
271
+ size: comp.size,
272
+ dependencies: Array.from(comp.dependencies)
273
+ };
274
+ }
275
+
276
+ const metadata = {
277
+ version: '2.0.0',
278
+ type,
279
+ route: routePath,
280
+ generated: new Date().toISOString(),
281
+ totalSize: components.reduce((sum, c) => sum + c.size, 0),
282
+ componentCount: components.length,
283
+ strategy: this.config.strategy
284
+ };
285
+
286
+ return this.formatBundleFile(componentsData, metadata);
287
+ }
288
+
289
+ /**
290
+ * Cleans JavaScript code by removing imports/exports
291
+ */
292
+ cleanJavaScript(code) {
293
+ // Remove export default
294
+ code = code.replace(/export\s+default\s+/g, '');
295
+
296
+ // Remove imports (components will already be available)
297
+ code = code.replace(/import\s+.*?from\s+['"].*?['"];?\s*/g, '');
298
+
299
+ return code;
300
+ }
301
+
302
+ /**
303
+ * Formats the bundle file
304
+ */
305
+ formatBundleFile(componentsData, metadata) {
306
+ return `/**
307
+ * Slice.js Bundle
308
+ * Type: ${metadata.type}
309
+ * Generated: ${metadata.generated}
310
+ * Strategy: ${metadata.strategy}
311
+ * Components: ${metadata.componentCount}
312
+ * Total Size: ${(metadata.totalSize / 1024).toFixed(1)} KB
313
+ */
314
+
315
+ export const SLICE_BUNDLE = {
316
+ metadata: ${JSON.stringify(metadata, null, 2)},
317
+
318
+ components: ${JSON.stringify(componentsData, null, 2)}
319
+ };
320
+
321
+ // Auto-registration of components
322
+ if (window.slice && window.slice.controller) {
323
+ slice.controller.registerBundle(SLICE_BUNDLE);
324
+ }
325
+ `;
326
+ }
327
+
328
+ /**
329
+ * Generates the bundle configuration
330
+ */
331
+ generateBundleConfig() {
332
+ const config = {
333
+ version: '2.0.0',
334
+ strategy: this.config.strategy,
335
+ generated: new Date().toISOString(),
336
+
337
+ stats: {
338
+ totalComponents: this.analysisData.metrics.totalComponents,
339
+ totalRoutes: this.analysisData.metrics.totalRoutes,
340
+ sharedComponents: this.bundles.critical.components.length,
341
+ sharedPercentage: this.analysisData.metrics.sharedPercentage,
342
+ totalSize: this.analysisData.metrics.totalSize,
343
+ criticalSize: this.bundles.critical.size
344
+ },
345
+
346
+ bundles: {
347
+ critical: {
348
+ file: this.bundles.critical.file,
349
+ size: this.bundles.critical.size,
350
+ components: this.bundles.critical.components.map(c => c.name)
351
+ },
352
+ routes: {}
353
+ }
354
+ };
355
+
356
+ for (const [key, bundle] of Object.entries(this.bundles.routes)) {
357
+ config.bundles.routes[key] = {
358
+ path: bundle.path,
359
+ file: bundle.file,
360
+ size: bundle.size,
361
+ components: bundle.components.map(c => c.name),
362
+ dependencies: ['critical']
363
+ };
364
+ }
365
+
366
+ return config;
367
+ }
368
+
369
+ /**
370
+ * Converts a route to filename
371
+ */
372
+ routeToFileName(routePath) {
373
+ if (routePath === '/') return 'home';
374
+ return routePath
375
+ .replace(/^\//, '')
376
+ .replace(/\//g, '-')
377
+ .replace(/[^a-zA-Z0-9-]/g, '')
378
+ .toLowerCase();
379
+ }
380
+
381
+ /**
382
+ * Saves the configuration to file
383
+ */
384
+ async saveBundleConfig(config) {
385
+ const configPath = path.join(this.srcPath, 'bundle.config.json');
386
+ await fs.writeJson(configPath, config, { spaces: 2 });
387
+ console.log(`✓ Configuration saved to ${configPath}`);
388
+ }
389
+ }
@@ -0,0 +1,340 @@
1
+ // cli/utils/bundling/DependencyAnalyzer.js
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { parse } from '@babel/parser';
5
+ import traverse from '@babel/traverse';
6
+ import { getSrcPath, getComponentsJsPath, getProjectRoot } from '../PathHelper.js';
7
+
8
+ export default class DependencyAnalyzer {
9
+ constructor(moduleUrl) {
10
+ this.moduleUrl = moduleUrl;
11
+ this.projectRoot = getProjectRoot(moduleUrl);
12
+ this.componentsPath = path.dirname(getComponentsJsPath(moduleUrl));
13
+ this.routesPath = getSrcPath(moduleUrl, 'routes.js');
14
+
15
+ // Analysis storage
16
+ this.components = new Map();
17
+ this.routes = new Map();
18
+ this.dependencyGraph = new Map();
19
+ }
20
+
21
+ /**
22
+ * Executes complete project analysis
23
+ */
24
+ async analyze() {
25
+ console.log('🔍 Analyzing project...');
26
+
27
+ // 1. Load component configuration
28
+ await this.loadComponentsConfig();
29
+
30
+ // 2. Analyze component files
31
+ await this.analyzeComponents();
32
+
33
+ // 3. Load and analyze routes
34
+ await this.analyzeRoutes();
35
+
36
+ // 4. Build dependency graph
37
+ this.buildDependencyGraph();
38
+
39
+ // 5. Calculate metrics
40
+ const metrics = this.calculateMetrics();
41
+
42
+ console.log('✅ Analysis completed');
43
+
44
+ return {
45
+ components: Array.from(this.components.values()),
46
+ routes: Array.from(this.routes.values()),
47
+ dependencyGraph: this.dependencyGraph,
48
+ metrics
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Loads component configuration from components.js
54
+ */
55
+ async loadComponentsConfig() {
56
+ const componentsConfigPath = path.join(this.componentsPath, 'components.js');
57
+
58
+ if (!await fs.pathExists(componentsConfigPath)) {
59
+ throw new Error('components.js not found');
60
+ }
61
+
62
+ // Read and parse components.js
63
+ const content = await fs.readFile(componentsConfigPath, 'utf-8');
64
+
65
+ // Extract configuration using simple regex
66
+ const configMatch = content.match(/export default\s*({[\s\S]*})/);
67
+ if (!configMatch) {
68
+ throw new Error('Could not parse components.js');
69
+ }
70
+
71
+ // Evaluate safely (in production use a more robust parser)
72
+ const config = eval(`(${configMatch[1]})`);
73
+
74
+ // Save category metadata
75
+ for (const [category, info] of Object.entries(config)) {
76
+ const categoryPath = path.join(this.componentsPath, info.path.replace('../', ''));
77
+
78
+ if (await fs.pathExists(categoryPath)) {
79
+ const files = await fs.readdir(categoryPath);
80
+
81
+ for (const file of files) {
82
+ const componentPath = path.join(categoryPath, file);
83
+ const stat = await fs.stat(componentPath);
84
+
85
+ if (stat.isDirectory()) {
86
+ this.components.set(file, {
87
+ name: file,
88
+ category,
89
+ categoryType: info.type,
90
+ path: componentPath,
91
+ dependencies: new Set(),
92
+ usedBy: new Set(),
93
+ routes: new Set(),
94
+ size: 0
95
+ });
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Analyzes each component's files
104
+ */
105
+ async analyzeComponents() {
106
+ for (const [name, component] of this.components) {
107
+ const jsFile = path.join(component.path, `${name}.js`);
108
+
109
+ if (!await fs.pathExists(jsFile)) continue;
110
+
111
+ // Read JavaScript file
112
+ const content = await fs.readFile(jsFile, 'utf-8');
113
+
114
+ // Calculate size
115
+ component.size = await this.calculateComponentSize(component.path);
116
+
117
+ // Parse and extract dependencies
118
+ component.dependencies = await this.extractDependencies(content);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Extracts dependencies from a component file
124
+ */
125
+ async extractDependencies(code) {
126
+ const dependencies = new Set();
127
+
128
+ try {
129
+ const ast = parse(code, {
130
+ sourceType: 'module',
131
+ plugins: ['jsx']
132
+ });
133
+
134
+ traverse.default(ast, {
135
+ // Detect slice.build() calls
136
+ CallExpression(path) {
137
+ const { callee, arguments: args } = path.node;
138
+
139
+ // slice.build('ComponentName', ...)
140
+ if (
141
+ callee.type === 'MemberExpression' &&
142
+ callee.object.name === 'slice' &&
143
+ callee.property.name === 'build' &&
144
+ args[0]?.type === 'StringLiteral'
145
+ ) {
146
+ dependencies.add(args[0].value);
147
+ }
148
+ },
149
+
150
+ // Detect direct imports (less common but possible)
151
+ ImportDeclaration(path) {
152
+ const importPath = path.node.source.value;
153
+ if (importPath.includes('/Components/')) {
154
+ const componentName = importPath.split('/').pop();
155
+ dependencies.add(componentName);
156
+ }
157
+ }
158
+ });
159
+ } catch (error) {
160
+ console.warn(`⚠️ Error parsing component: ${error.message}`);
161
+ }
162
+
163
+ return dependencies;
164
+ }
165
+
166
+ /**
167
+ * Analyzes the routes file
168
+ */
169
+ async analyzeRoutes() {
170
+ if (!await fs.pathExists(this.routesPath)) {
171
+ throw new Error('routes.js no encontrado');
172
+ }
173
+
174
+ const content = await fs.readFile(this.routesPath, 'utf-8');
175
+
176
+ try {
177
+ const ast = parse(content, {
178
+ sourceType: 'module',
179
+ plugins: ['jsx']
180
+ });
181
+
182
+ let currentRoute = null;
183
+ const self = this; // Guardar referencia a la instancia
184
+
185
+ traverse.default(ast, {
186
+ ObjectExpression(path) {
187
+ // Buscar objetos de ruta: { path: '/', component: 'HomePage' }
188
+ const properties = path.node.properties;
189
+ const pathProp = properties.find(p => p.key?.name === 'path');
190
+ const componentProp = properties.find(p => p.key?.name === 'component');
191
+
192
+ if (pathProp && componentProp) {
193
+ const routePath = pathProp.value.value;
194
+ const componentName = componentProp.value.value;
195
+
196
+ currentRoute = {
197
+ path: routePath,
198
+ component: componentName,
199
+ dependencies: new Set([componentName])
200
+ };
201
+
202
+ self.routes.set(routePath, currentRoute);
203
+
204
+ // Marcar el componente como usado por esta ruta
205
+ if (self.components.has(componentName)) {
206
+ self.components.get(componentName).routes.add(routePath);
207
+ }
208
+ }
209
+ }
210
+ });
211
+ } catch (error) {
212
+ console.warn(`⚠️ Error parseando rutas: ${error.message}`);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Builds the complete dependency graph
218
+ */
219
+ buildDependencyGraph() {
220
+ // Propagate transitive dependencies
221
+ for (const [name, component] of this.components) {
222
+ this.dependencyGraph.set(name, this.getAllDependencies(name, new Set()));
223
+ }
224
+
225
+ // Calculate usedBy (inverse dependencies)
226
+ for (const [name, deps] of this.dependencyGraph) {
227
+ for (const dep of deps) {
228
+ if (this.components.has(dep)) {
229
+ this.components.get(dep).usedBy.add(name);
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Gets all dependencies of a component (recursive)
237
+ */
238
+ getAllDependencies(componentName, visited = new Set()) {
239
+ if (visited.has(componentName)) return new Set();
240
+ visited.add(componentName);
241
+
242
+ const component = this.components.get(componentName);
243
+ if (!component) return new Set();
244
+
245
+ const allDeps = new Set(component.dependencies);
246
+
247
+ for (const dep of component.dependencies) {
248
+ const transitiveDeps = this.getAllDependencies(dep, visited);
249
+ for (const transDep of transitiveDeps) {
250
+ allDeps.add(transDep);
251
+ }
252
+ }
253
+
254
+ return allDeps;
255
+ }
256
+
257
+ /**
258
+ * Calculates the total size of a component (JS + HTML + CSS)
259
+ */
260
+ async calculateComponentSize(componentPath) {
261
+ let totalSize = 0;
262
+ const files = await fs.readdir(componentPath);
263
+
264
+ for (const file of files) {
265
+ const filePath = path.join(componentPath, file);
266
+ const stat = await fs.stat(filePath);
267
+
268
+ if (stat.isFile()) {
269
+ totalSize += stat.size;
270
+ }
271
+ }
272
+
273
+ return totalSize;
274
+ }
275
+
276
+ /**
277
+ * Calculates project metrics
278
+ */
279
+ calculateMetrics() {
280
+ const totalComponents = this.components.size;
281
+ const totalRoutes = this.routes.size;
282
+
283
+ // Shared components (used in multiple routes)
284
+ const sharedComponents = Array.from(this.components.values())
285
+ .filter(c => c.routes.size >= 2);
286
+
287
+ // Total size
288
+ const totalSize = Array.from(this.components.values())
289
+ .reduce((sum, c) => sum + c.size, 0);
290
+
291
+ // Components by category
292
+ const byCategory = {};
293
+ for (const comp of this.components.values()) {
294
+ byCategory[comp.category] = (byCategory[comp.category] || 0) + 1;
295
+ }
296
+
297
+ // Top components by usage
298
+ const topByUsage = Array.from(this.components.values())
299
+ .sort((a, b) => b.routes.size - a.routes.size)
300
+ .slice(0, 10)
301
+ .map(c => ({
302
+ name: c.name,
303
+ routes: c.routes.size,
304
+ size: c.size
305
+ }));
306
+
307
+ return {
308
+ totalComponents,
309
+ totalRoutes,
310
+ sharedComponentsCount: sharedComponents.length,
311
+ sharedPercentage: (sharedComponents.length / totalComponents * 100).toFixed(1),
312
+ totalSize,
313
+ averageSize: Math.round(totalSize / totalComponents),
314
+ byCategory,
315
+ topByUsage
316
+ };
317
+ }
318
+
319
+ /**
320
+ * Generates a visual report of the analysis
321
+ */
322
+ generateReport(metrics) {
323
+ console.log('\n📊 PROJECT ANALYSIS\n');
324
+ console.log(`Total components: ${metrics.totalComponents}`);
325
+ console.log(`Total routes: ${metrics.totalRoutes}`);
326
+ console.log(`Shared components: ${metrics.sharedComponentsCount} (${metrics.sharedPercentage}%)`);
327
+ console.log(`Total size: ${(metrics.totalSize / 1024).toFixed(1)} KB`);
328
+ console.log(`Average size: ${(metrics.averageSize / 1024).toFixed(1)} KB per component`);
329
+
330
+ console.log('\n📦 By category:');
331
+ for (const [category, count] of Object.entries(metrics.byCategory)) {
332
+ console.log(` ${category}: ${count} components`);
333
+ }
334
+
335
+ console.log('\n🔥 Top 10 most used components:');
336
+ metrics.topByUsage.forEach((comp, i) => {
337
+ console.log(` ${i + 1}. ${comp.name} - ${comp.routes} routes - ${(comp.size / 1024).toFixed(1)} KB`);
338
+ });
339
+ }
340
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slicejs-cli",
3
- "version": "2.7.5",
3
+ "version": "2.7.7",
4
4
  "description": "Command client for developing web applications with Slice.js framework",
5
5
  "main": "client.js",
6
6
  "bin": {
@@ -39,5 +39,9 @@
39
39
  "ora": "^8.2.0",
40
40
  "slicejs-web-framework": "latest",
41
41
  "terser": "^5.43.1"
42
+ },
43
+ "devDependencies": {
44
+ "@babel/parser": "^7.28.5",
45
+ "@babel/traverse": "^7.28.5"
42
46
  }
43
47
  }
package/refactor.md ADDED
@@ -0,0 +1,271 @@
1
+ # Plan de Refactorización del CLI para MCP
2
+
3
+ ## Objetivo
4
+ Separar la lógica de negocio de la presentación CLI para permitir importación directa desde el MCP server, manteniendo **100% de compatibilidad** con el CLI actual.
5
+
6
+ ---
7
+
8
+ ## Estructura Propuesta
9
+
10
+ ```
11
+ commands/
12
+ ├── createComponent/
13
+ │ ├── createComponent.js # CLI wrapper (presentación)
14
+ │ ├── core.js # ⭐ NUEVO - Lógica pura exportable
15
+ │ └── VisualComponentTemplate.js
16
+
17
+ ├── listComponents/
18
+ │ ├── listComponents.js # CLI wrapper (presentación)
19
+ │ └── core.js # ⭐ NUEVO - Lógica pura exportable
20
+
21
+ ├── deleteComponent/
22
+ │ ├── deleteComponent.js # CLI wrapper (presentación)
23
+ │ └── core.js # ⭐ NUEVO - Lógica pura exportable
24
+
25
+ ├── getComponent/
26
+ │ ├── getComponent.js # CLI wrapper (presentación)
27
+ │ └── core.js # ⭐ NUEVO - Lógica pura exportable
28
+
29
+ └── init/
30
+ ├── init.js # CLI wrapper (presentación)
31
+ └── core.js # ⭐ NUEVO - Lógica pura exportable
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Principios de Refactorización
37
+
38
+ ### 1. Separación de Responsabilidades
39
+ - **core.js**: Lógica pura sin dependencias de CLI (Print, chalk, Table, inquirer)
40
+ - **[comando].js**: Solo presentación CLI, usa funciones del core
41
+
42
+ ### 2. Funciones Puras en Core
43
+ - Reciben parámetros explícitos (no usan import.meta.url implícitamente)
44
+ - Retornan objetos con estructura `{ success: boolean, data?, error? }`
45
+ - No imprimen a consola directamente
46
+ - Son agnósticas del entorno (CLI vs MCP)
47
+
48
+ ### 3. Compatibilidad Total
49
+ - Los archivos CLI actuales se mantienen funcionales
50
+ - Misma API de comandos
51
+ - Mismo comportamiento observable
52
+ - No romper ningún test existente
53
+
54
+ ---
55
+
56
+ ## Pasos de Refactorización por Comando
57
+
58
+ ### Fase 1: List Components
59
+
60
+ #### Paso 1.1: Análisis del Código Actual
61
+ - Identificar toda la lógica de negocio en `listComponents.js`
62
+ - Separar mentalmente: lógica vs presentación
63
+ - Funciones a extraer: `loadConfig`, `getComponents`, `countComponentFiles`, `listComponentDirectories`
64
+
65
+ #### Paso 1.2: Crear core.js
66
+ - Crear archivo `commands/listComponents/core.js`
67
+ - Extraer funciones de lógica pura
68
+ - Modificar para que acepten `projectPath` como parámetro opcional
69
+ - Retornar objetos estructurados en lugar de imprimir
70
+
71
+ #### Paso 1.3: Adaptar listComponents.js
72
+ - Importar funciones desde `core.js`
73
+ - Mantener solo la lógica de presentación (Print, Table, chalk)
74
+ - Re-exportar funciones del core: `export { getComponents, ... } from './core.js'`
75
+
76
+ #### Paso 1.4: Verificación
77
+ - Ejecutar `slice list` - debe funcionar idéntico
78
+ - Probar importación directa: `import { getComponents } from './commands/listComponents/core.js'`
79
+ - Verificar que retorna datos correctos sin CLI
80
+
81
+ ---
82
+
83
+ ### Fase 2: Create Component
84
+
85
+ #### Paso 2.1: Análisis
86
+ - Identificar lógica: validaciones, generación de templates, creación de archivos
87
+ - Identificar presentación: mensajes de error, comandos de ejemplo
88
+
89
+ #### Paso 2.2: Crear core.js
90
+ - Crear `commands/createComponent/core.js`
91
+ - Extraer: `validateComponentName`, `validateCategory`, `generateTemplate`, `createComponentFiles`
92
+ - Cada función retorna `{ success, data, error }`
93
+
94
+ #### Paso 2.3: Adaptar createComponent.js
95
+ - Importar funciones del core
96
+ - Mantener solo los Print y mensajes de ayuda
97
+ - Re-exportar funciones del core
98
+
99
+ #### Paso 2.4: Verificación
100
+ - Ejecutar `slice component create` - debe funcionar igual
101
+ - Probar importación directa y creación programática
102
+
103
+ ---
104
+
105
+ ### Fase 3: Delete Component
106
+
107
+ #### Paso 3.1: Análisis
108
+ - Identificar lógica: validaciones, verificación de existencia, eliminación de archivos
109
+ - Identificar presentación: mensajes de confirmación, errores
110
+
111
+ #### Paso 3.2: Crear core.js
112
+ - Crear `commands/deleteComponent/core.js`
113
+ - Extraer: `deleteComponentFiles` (con validaciones integradas)
114
+
115
+ #### Paso 3.3: Adaptar deleteComponent.js
116
+ - Importar del core
117
+ - Mantener presentación CLI
118
+ - Re-exportar funciones
119
+
120
+ #### Paso 3.4: Verificación
121
+ - Ejecutar `slice component delete`
122
+ - Verificar funcionamiento idéntico
123
+
124
+ ---
125
+
126
+ ### Fase 4: Get Component (Registry)
127
+
128
+ #### Paso 4.1: Análisis
129
+ - Identificar lógica: descarga de registry, instalación de componentes, actualización
130
+ - Es el más complejo - tiene clase ComponentRegistry
131
+
132
+ #### Paso 4.2: Crear core.js
133
+ - Crear `commands/getComponent/core.js`
134
+ - Extraer clase `ComponentRegistry` limpia (sin inquirer, sin Print)
135
+ - Métodos retornan objetos estructurados
136
+
137
+ #### Paso 4.3: Adaptar getComponent.js
138
+ - Importar ComponentRegistry del core
139
+ - Envolver con lógica interactiva (inquirer) solo en CLI
140
+ - Mantener funciones: `getComponents`, `listComponents`, `syncComponents`
141
+
142
+ #### Paso 4.4: Verificación
143
+ - Probar: `slice get Button`, `slice browse`, `slice sync`
144
+ - Verificar descarga y registro correctos
145
+
146
+ ---
147
+
148
+ ### Fase 5: Init Project
149
+
150
+ #### Paso 5.1: Análisis
151
+ - Identificar lógica: copia de estructuras, configuración de package.json
152
+ - Separar spinners y mensajes visuales
153
+
154
+ #### Paso 5.2: Crear core.js
155
+ - Crear `commands/init/core.js`
156
+ - Extraer: funciones de scaffolding, configuración
157
+
158
+ #### Paso 5.3: Adaptar init.js
159
+ - Importar del core
160
+ - Mantener spinners y presentación
161
+ - Re-exportar funciones
162
+
163
+ #### Paso 5.4: Verificación
164
+ - Ejecutar `slice init` en proyecto nuevo
165
+ - Verificar estructura completa creada
166
+
167
+ ---
168
+
169
+ ## Patrón de Estructura de Funciones Core
170
+
171
+ ### Firma de Función Estándar
172
+ ```
173
+ function operationName({ param1, param2, projectPath = null }) {
174
+ // Validaciones
175
+ // Lógica de negocio
176
+ return { success: boolean, data?, error? }
177
+ }
178
+ ```
179
+
180
+ ### Objeto de Retorno Estándar
181
+ ```javascript
182
+ // Éxito
183
+ {
184
+ success: true,
185
+ data: { ... },
186
+ // Campos adicionales específicos
187
+ }
188
+
189
+ // Error
190
+ {
191
+ success: false,
192
+ error: "mensaje descriptivo"
193
+ }
194
+ ```
195
+
196
+ ---
197
+
198
+ ## Estrategia de Dependencias
199
+
200
+ ### Dependencias Permitidas en core.js
201
+ - ✅ `fs`, `fs-extra`
202
+ - ✅ `path`
203
+ - ✅ `Validations.js` (lógica pura)
204
+ - ✅ Helpers de PathHelper (refactorizar para aceptar projectPath)
205
+
206
+ ### Dependencias NO Permitidas en core.js
207
+ - ❌ `Print.js` (específico de CLI)
208
+ - ❌ `chalk` (presentación)
209
+ - ❌ `cli-table3` (presentación)
210
+ - ❌ `inquirer` (interacción CLI)
211
+ - ❌ `ora` (spinners CLI)
212
+
213
+ ---
214
+
215
+ ## Refactorización de PathHelper
216
+
217
+ ### Problema Actual
218
+ PathHelper usa `import.meta.url` internamente, asumiendo que se llama desde dentro del CLI
219
+
220
+ ### Solución
221
+ Crear versiones alternativas que acepten `projectPath`:
222
+
223
+ ```
224
+ // Versión CLI (actual)
225
+ getSrcPath(import.meta.url, ...paths)
226
+
227
+ // Versión core (nueva)
228
+ getProjectSrcPath(projectPath, ...paths)
229
+ ```
230
+
231
+ O mejor: modificar funciones existentes para aceptar ambos modos:
232
+ ```
233
+ getSrcPath(importMetaUrlOrProjectPath, ...paths)
234
+ ```
235
+
236
+ ---
237
+
238
+ ## Testing de la Refactorización
239
+
240
+ ### Test 1: CLI Functionality
241
+ Para cada comando refactorizado:
242
+ - Ejecutar comando CLI
243
+ - Verificar output idéntico al anterior
244
+ - Verificar archivos creados/modificados
245
+
246
+ ### Test 2: Importación Directa
247
+ ```javascript
248
+ // test-mcp-import.js
249
+ import { getComponents } from './commands/listComponents/core.js';
250
+
251
+ const result = getComponents('/ruta/proyecto');
252
+ console.log(result); // Debe retornar objeto con componentes
253
+ ```
254
+
255
+ ### Test 3: Sin Dependencias CLI
256
+ Verificar que core.js no tiene imports de Print, chalk, etc:
257
+ ```bash
258
+ grep -r "from.*Print" commands/*/core.js # debe estar vacío
259
+ grep -r "from.*chalk" commands/*/core.js # debe estar vacío
260
+ ```
261
+
262
+ ---
263
+
264
+ ## Orden de Implementación Recomendado
265
+
266
+ 1. **listComponents** (más simple, establece patrón)
267
+ 2. **createComponent** (complejidad media)
268
+ 3. **deleteComponent** (similar a create)
269
+ 4. **getComponent** (más complejo, tiene clase)
270
+ 5. **init** (el más complejo)
271
+