slicejs-cli 2.8.6 → 2.9.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.
@@ -1,679 +1,859 @@
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
- routeGroups: this.routeGroups,
49
- metrics
50
- };
51
- }
52
-
53
- /**
54
- * Loads component configuration from components.js
55
- */
56
- async loadComponentsConfig() {
57
- const componentsConfigPath = path.join(this.componentsPath, 'components.js');
58
-
59
- if (!await fs.pathExists(componentsConfigPath)) {
60
- throw new Error('components.js not found');
61
- }
62
-
63
- // Read and parse components.js
64
- const content = await fs.readFile(componentsConfigPath, 'utf-8');
65
-
66
- // Extract configuration using simple regex - look for the components object
67
- const configMatch = content.match(/const components\s*=\s*({[\s\S]*?});/);
68
- if (!configMatch) {
69
- throw new Error('Could not parse components.js');
70
- }
71
-
72
- // Evaluate safely (in production use a more robust parser)
73
- const config = eval(`(${configMatch[1]})`);
74
-
75
- // Group components by category
76
- const categoryMap = new Map();
77
-
78
- // Build category map from component assignments
79
- for (const [componentName, categoryName] of Object.entries(config)) {
80
- if (!categoryMap.has(categoryName)) {
81
- categoryMap.set(categoryName, []);
82
- }
83
- categoryMap.get(categoryName).push(componentName);
84
- }
85
-
86
- // Process each category
87
- for (const [categoryName, componentList] of categoryMap) {
88
- // Determine category type based on category name
89
- let categoryType = 'Visual'; // default
90
- if (categoryName === 'Service') categoryType = 'Service';
91
- if (categoryName === 'AppComponents') categoryType = 'Visual'; // AppComponents are visual
92
-
93
- // Find category path
94
- const categoryPath = path.join(this.componentsPath, categoryName);
95
-
96
- if (await fs.pathExists(categoryPath)) {
97
- const files = await fs.readdir(categoryPath);
98
-
99
- for (const file of files) {
100
- const componentPath = path.join(categoryPath, file);
101
- const stat = await fs.stat(componentPath);
102
-
103
- if (stat.isDirectory() && componentList.includes(file)) {
104
- this.components.set(file, {
105
- name: file,
106
- category: categoryName,
107
- categoryType: categoryType,
108
- path: componentPath,
109
- dependencies: new Set(),
110
- usedBy: new Set(),
111
- routes: new Set(),
112
- size: 0
113
- });
114
- }
115
- }
116
- }
117
- }
118
- }
119
-
120
- /**
121
- * Analyzes each component's files
122
- */
123
- async analyzeComponents() {
124
- for (const [name, component] of this.components) {
125
- const jsFile = path.join(component.path, `${name}.js`);
126
-
127
- if (!await fs.pathExists(jsFile)) continue;
128
-
129
- // Read JavaScript file
130
- const content = await fs.readFile(jsFile, 'utf-8');
131
-
132
- // Calculate size
133
- component.size = await this.calculateComponentSize(component.path);
134
-
135
- // Parse and extract dependencies
136
- component.dependencies = await this.extractDependencies(content, jsFile);
137
- }
138
- }
139
-
140
- /**
141
- * Extracts dependencies from a component file
142
- */
143
- async extractDependencies(code, componentFilePath = null) {
144
- const dependencies = new Set();
145
-
146
- const resolveRoutesArray = (node, scope) => {
147
- if (!node) return null;
148
-
149
- if (node.type === 'ArrayExpression') {
150
- return node;
151
- }
152
-
153
- if (node.type === 'ObjectExpression') {
154
- const routesProp = node.properties.find(p => p.key?.name === 'routes');
155
- if (routesProp?.value) {
156
- return resolveRoutesArray(routesProp.value, scope);
157
- }
158
- }
159
-
160
- if (node.type === 'Identifier' && scope) {
161
- const binding = scope.getBinding(node.name);
162
- if (!binding) return null;
163
- const bindingNode = binding.path?.node;
164
-
165
- if (bindingNode?.type === 'VariableDeclarator') {
166
- const init = bindingNode.init;
167
- if (init?.type === 'ArrayExpression') {
168
- return init;
169
- }
170
-
171
- if (init?.type === 'Identifier') {
172
- return resolveRoutesArray(init, binding.path.scope);
173
- }
174
-
175
- if (init?.type === 'ObjectExpression') {
176
- return resolveRoutesArray(init, binding.path.scope);
177
- }
178
-
179
- if (init?.type === 'MemberExpression') {
180
- return resolveRoutesArray(init, binding.path.scope);
181
- }
182
- }
183
-
184
- if (bindingNode?.type === 'ImportSpecifier' || bindingNode?.type === 'ImportDefaultSpecifier') {
185
- const parent = binding.path.parentPath?.node;
186
- if (parent?.type === 'ImportDeclaration') {
187
- const importedName = bindingNode.type === 'ImportDefaultSpecifier'
188
- ? 'default'
189
- : bindingNode.imported?.name;
190
- const importedNode = resolveImportedValue(parent.source.value, importedName, componentFilePath);
191
- return resolveRoutesArray(importedNode, null);
192
- }
193
- }
194
- }
195
-
196
- if (node.type === 'MemberExpression' && scope) {
197
- const objectNode = resolveObjectExpression(node.object, scope);
198
- if (objectNode) {
199
- const propName = node.property?.name || node.property?.value;
200
- if (propName) {
201
- const prop = objectNode.properties.find(p => p.key?.name === propName || p.key?.value === propName);
202
- if (prop?.value) {
203
- return resolveRoutesArray(prop.value, scope);
204
- }
205
- }
206
- }
207
- }
208
-
209
- return null;
210
- };
211
-
212
- const resolveObjectExpression = (node, scope) => {
213
- if (!node) return null;
214
- if (node.type === 'ObjectExpression') return node;
215
-
216
- if (node.type === 'Identifier' && scope) {
217
- const binding = scope.getBinding(node.name);
218
- const bindingNode = binding?.path?.node;
219
- if (bindingNode?.type === 'VariableDeclarator') {
220
- const init = bindingNode.init;
221
- if (init?.type === 'ObjectExpression') {
222
- return init;
223
- }
224
- if (init?.type === 'Identifier') {
225
- return resolveObjectExpression(init, binding.path.scope);
226
- }
227
- }
228
-
229
- if (bindingNode?.type === 'ImportSpecifier' || bindingNode?.type === 'ImportDefaultSpecifier') {
230
- const parent = binding.path.parentPath?.node;
231
- if (parent?.type === 'ImportDeclaration') {
232
- const importedName = bindingNode.type === 'ImportDefaultSpecifier'
233
- ? 'default'
234
- : bindingNode.imported?.name;
235
- const importedNode = resolveImportedValue(parent.source.value, importedName, componentFilePath);
236
- return resolveObjectExpression(importedNode, null);
237
- }
238
- }
239
- }
240
-
241
- return null;
242
- };
243
-
244
- const resolveStringValue = (node, scope) => {
245
- if (!node) return null;
246
- if (node.type === 'StringLiteral') return node.value;
247
- if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
248
- return node.quasis.map(q => q.value.cooked).join('');
249
- }
250
-
251
- if (node.type === 'Identifier' && scope) {
252
- const binding = scope.getBinding(node.name);
253
- const bindingNode = binding?.path?.node;
254
- if (bindingNode?.type === 'VariableDeclarator') {
255
- return resolveStringValue(bindingNode.init, binding.path.scope);
256
- }
257
-
258
- if (bindingNode?.type === 'ImportSpecifier' || bindingNode?.type === 'ImportDefaultSpecifier') {
259
- const parent = binding.path.parentPath?.node;
260
- if (parent?.type === 'ImportDeclaration') {
261
- const importedName = bindingNode.type === 'ImportDefaultSpecifier'
262
- ? 'default'
263
- : bindingNode.imported?.name;
264
- const importedNode = resolveImportedValue(parent.source.value, importedName, componentFilePath);
265
- return resolveStringValue(importedNode, null);
266
- }
267
- }
268
- }
269
-
270
- if (node.type === 'MemberExpression' && scope) {
271
- const objectNode = resolveObjectExpression(node.object, scope);
272
- if (objectNode) {
273
- const propName = node.property?.name || node.property?.value;
274
- if (propName) {
275
- const prop = objectNode.properties.find(p => p.key?.name === propName || p.key?.value === propName);
276
- if (prop?.value) {
277
- return resolveStringValue(prop.value, scope);
278
- }
279
- }
280
- }
281
- }
282
-
283
- return null;
284
- };
285
-
286
- const resolveImportedValue = (importPath, importedName, fromFilePath) => {
287
- if (!fromFilePath) return null;
288
-
289
- const baseDir = path.dirname(fromFilePath);
290
- const resolvedPath = resolveImportPath(importPath, baseDir);
291
- if (!resolvedPath) {
292
- console.warn(`āš ļø Cannot resolve import for MultiRoute routes: ${importPath}`);
293
- return null;
294
- }
295
-
296
- const cacheKey = `${resolvedPath}:${importedName || 'default'}`;
297
- if (!resolveImportedValue.cache) {
298
- resolveImportedValue.cache = new Map();
299
- }
300
- if (resolveImportedValue.cache.has(cacheKey)) {
301
- return resolveImportedValue.cache.get(cacheKey);
302
- }
303
-
304
- try {
305
- const source = fs.readFileSync(resolvedPath, 'utf-8');
306
- const importAst = parse(source, {
307
- sourceType: 'module',
308
- plugins: ['jsx']
309
- });
310
-
311
- const topLevelBindings = new Map();
312
- for (const node of importAst.program.body) {
313
- if (node.type === 'VariableDeclaration') {
314
- node.declarations.forEach(decl => {
315
- if (decl.id?.type === 'Identifier') {
316
- topLevelBindings.set(decl.id.name, decl.init);
317
- }
318
- });
319
- }
320
- }
321
-
322
- let exportNode = null;
323
- for (const node of importAst.program.body) {
324
- if (node.type === 'ExportDefaultDeclaration' && importedName === 'default') {
325
- exportNode = node.declaration;
326
- break;
327
- }
328
-
329
- if (node.type === 'ExportNamedDeclaration') {
330
- if (node.declaration?.type === 'VariableDeclaration') {
331
- for (const decl of node.declaration.declarations) {
332
- if (decl.id?.name === importedName) {
333
- exportNode = decl.init;
334
- break;
335
- }
336
- }
337
- }
338
-
339
- if (!exportNode && node.specifiers?.length) {
340
- const specifier = node.specifiers.find(s => s.exported?.name === importedName);
341
- if (specifier && specifier.local?.name) {
342
- exportNode = topLevelBindings.get(specifier.local.name) || null;
343
- }
344
- }
345
- }
346
-
347
- if (exportNode) break;
348
- }
349
-
350
- if (exportNode?.type === 'Identifier') {
351
- exportNode = topLevelBindings.get(exportNode.name) || exportNode;
352
- }
353
-
354
- resolveImportedValue.cache.set(cacheKey, exportNode || null);
355
- return exportNode || null;
356
- } catch (error) {
357
- console.warn(`āš ļø Error resolving import ${importPath}: ${error.message}`);
358
- resolveImportedValue.cache.set(cacheKey, null);
359
- return null;
360
- }
361
- };
362
-
363
- const resolveImportPath = (importPath, baseDir) => {
364
- if (!importPath.startsWith('.')) return null;
365
-
366
- const resolvedBase = path.resolve(baseDir, importPath);
367
- const extensions = ['.js', '.mjs', '.cjs', '.json'];
368
-
369
- if (fs.existsSync(resolvedBase) && fs.statSync(resolvedBase).isFile()) {
370
- return resolvedBase;
371
- }
372
-
373
- if (!path.extname(resolvedBase)) {
374
- for (const ext of extensions) {
375
- const candidate = resolvedBase + ext;
376
- if (fs.existsSync(candidate)) {
377
- return candidate;
378
- }
379
- }
380
- }
381
-
382
- return null;
383
- };
384
-
385
- const addMultiRouteDependencies = (routesArrayNode, scope) => {
386
- if (!routesArrayNode || routesArrayNode.type !== 'ArrayExpression') return;
387
-
388
- routesArrayNode.elements.forEach(routeElement => {
389
- if (!routeElement) return;
390
-
391
- if (routeElement.type === 'SpreadElement') {
392
- const spreadArray = resolveRoutesArray(routeElement.argument, scope);
393
- addMultiRouteDependencies(spreadArray, scope);
394
- return;
395
- }
396
-
397
- const routeObject = resolveObjectExpression(routeElement, scope) || routeElement;
398
- if (routeObject?.type === 'ObjectExpression') {
399
- const componentProp = routeObject.properties.find(p => p.key?.name === 'component');
400
- if (componentProp?.value) {
401
- const componentName = resolveStringValue(componentProp.value, scope);
402
- if (componentName) {
403
- dependencies.add(componentName);
404
- }
405
- }
406
- }
407
- });
408
- };
409
-
410
- try {
411
- const ast = parse(code, {
412
- sourceType: 'module',
413
- plugins: ['jsx']
414
- });
415
-
416
- traverse.default(ast, {
417
- // Detect slice.build() calls
418
- CallExpression(path) {
419
- const { callee, arguments: args } = path.node;
420
-
421
- // slice.build('MultiRoute', { routes: [...] })
422
- if (
423
- callee.type === 'MemberExpression' &&
424
- callee.object.name === 'slice' &&
425
- callee.property.name === 'build' &&
426
- args[0]?.type === 'StringLiteral' &&
427
- args[0].value === 'MultiRoute' &&
428
- args[1]?.type === 'ObjectExpression'
429
- ) {
430
- // Add MultiRoute itself
431
- dependencies.add('MultiRoute');
432
-
433
- // Extract routes from MultiRoute props
434
- const routesProp = args[1].properties.find(p => p.key?.name === 'routes');
435
- if (routesProp) {
436
- const routesArrayNode = resolveRoutesArray(routesProp.value, path.scope);
437
- addMultiRouteDependencies(routesArrayNode);
438
- }
439
- }
440
- // Regular slice.build() calls
441
- else if (
442
- callee.type === 'MemberExpression' &&
443
- callee.object.name === 'slice' &&
444
- callee.property.name === 'build' &&
445
- args[0]?.type === 'StringLiteral'
446
- ) {
447
- dependencies.add(args[0].value);
448
- }
449
- },
450
-
451
- // Detect direct imports (less common but possible)
452
- ImportDeclaration(path) {
453
- const importPath = path.node.source.value;
454
- if (importPath.includes('/Components/')) {
455
- const componentName = importPath.split('/').pop();
456
- dependencies.add(componentName);
457
- }
458
- }
459
- });
460
- } catch (error) {
461
- console.warn(`āš ļø Error parsing component: ${error.message}`);
462
- }
463
-
464
- return dependencies;
465
- }
466
-
467
- /**
468
- * Analyzes the routes file and detects route groups
469
- */
470
- async analyzeRoutes() {
471
- if (!await fs.pathExists(this.routesPath)) {
472
- throw new Error('routes.js no encontrado');
473
- }
474
-
475
- const content = await fs.readFile(this.routesPath, 'utf-8');
476
-
477
- try {
478
- const ast = parse(content, {
479
- sourceType: 'module',
480
- plugins: ['jsx']
481
- });
482
-
483
- let currentRoute = null;
484
- const self = this; // Guardar referencia a la instancia
485
-
486
- traverse.default(ast, {
487
- ObjectExpression(path) {
488
- // Buscar objetos de ruta: { path: '/', component: 'HomePage' }
489
- const properties = path.node.properties;
490
- const pathProp = properties.find(p => p.key?.name === 'path');
491
- const componentProp = properties.find(p => p.key?.name === 'component');
492
-
493
- if (pathProp && componentProp) {
494
- const routePath = pathProp.value.value;
495
- const componentName = componentProp.value.value;
496
-
497
- currentRoute = {
498
- path: routePath,
499
- component: componentName,
500
- dependencies: new Set([componentName])
501
- };
502
-
503
- self.routes.set(routePath, currentRoute);
504
-
505
- // Marcar el componente como usado por esta ruta
506
- if (self.components.has(componentName)) {
507
- self.components.get(componentName).routes.add(routePath);
508
- }
509
- }
510
- }
511
- });
512
-
513
- // Detect and store route groups based on MultiRoute usage
514
- this.routeGroups = this.detectRouteGroups();
515
-
516
- } catch (error) {
517
- console.warn(`āš ļø Error parseando rutas: ${error.message}`);
518
- }
519
- }
520
-
521
- /**
522
- * Builds the complete dependency graph
523
- */
524
- buildDependencyGraph() {
525
- // Propagate transitive dependencies
526
- for (const [name, component] of this.components) {
527
- this.dependencyGraph.set(name, this.getAllDependencies(name, new Set()));
528
- }
529
-
530
- // Calculate usedBy (inverse dependencies)
531
- for (const [name, deps] of this.dependencyGraph) {
532
- for (const dep of deps) {
533
- if (this.components.has(dep)) {
534
- this.components.get(dep).usedBy.add(name);
535
- }
536
- }
537
- }
538
- }
539
-
540
- /**
541
- * Detects route groups based on MultiRoute usage
542
- */
543
- detectRouteGroups() {
544
- const routeGroups = new Map();
545
-
546
- for (const [componentName, component] of this.components) {
547
- // Check if component uses MultiRoute
548
- const hasMultiRoute = Array.from(component.dependencies).includes('MultiRoute');
549
-
550
- if (hasMultiRoute) {
551
- // Find all routes that point to this component
552
- const relatedRoutes = Array.from(this.routes.values())
553
- .filter(route => route.component === componentName);
554
-
555
- if (relatedRoutes.length > 1) {
556
- // Group these routes together
557
- const groupKey = `multiroute-${componentName}`;
558
- routeGroups.set(groupKey, {
559
- component: componentName,
560
- routes: relatedRoutes.map(r => r.path),
561
- type: 'multiroute'
562
- });
563
-
564
- // Mark component as multiroute handler
565
- component.isMultiRouteHandler = true;
566
- component.multiRoutePaths = relatedRoutes.map(r => r.path);
567
- }
568
- }
569
- }
570
-
571
- return routeGroups;
572
- }
573
-
574
- /**
575
- * Gets all dependencies of a component (recursive)
576
- */
577
- getAllDependencies(componentName, visited = new Set()) {
578
- if (visited.has(componentName)) return new Set();
579
- visited.add(componentName);
580
-
581
- const component = this.components.get(componentName);
582
- if (!component) return new Set();
583
-
584
- const allDeps = new Set(component.dependencies);
585
-
586
- for (const dep of component.dependencies) {
587
- const transitiveDeps = this.getAllDependencies(dep, visited);
588
- for (const transDep of transitiveDeps) {
589
- allDeps.add(transDep);
590
- }
591
- }
592
-
593
- return allDeps;
594
- }
595
-
596
- /**
597
- * Calculates the total size of a component (JS + HTML + CSS)
598
- */
599
- async calculateComponentSize(componentPath) {
600
- let totalSize = 0;
601
- const files = await fs.readdir(componentPath);
602
-
603
- for (const file of files) {
604
- const filePath = path.join(componentPath, file);
605
- const stat = await fs.stat(filePath);
606
-
607
- if (stat.isFile()) {
608
- totalSize += stat.size;
609
- }
610
- }
611
-
612
- return totalSize;
613
- }
614
-
615
- /**
616
- * Calculates project metrics
617
- */
618
- calculateMetrics() {
619
- const totalComponents = this.components.size;
620
- const totalRoutes = this.routes.size;
621
-
622
- // Shared components (used in multiple routes)
623
- const sharedComponents = Array.from(this.components.values())
624
- .filter(c => c.routes.size >= 2);
625
-
626
- // Total size
627
- const totalSize = Array.from(this.components.values())
628
- .reduce((sum, c) => sum + c.size, 0);
629
-
630
- // Components by category
631
- const byCategory = {};
632
- for (const comp of this.components.values()) {
633
- byCategory[comp.category] = (byCategory[comp.category] || 0) + 1;
634
- }
635
-
636
- // Top components by usage
637
- const topByUsage = Array.from(this.components.values())
638
- .sort((a, b) => b.routes.size - a.routes.size)
639
- .slice(0, 10)
640
- .map(c => ({
641
- name: c.name,
642
- routes: c.routes.size,
643
- size: c.size
644
- }));
645
-
646
- return {
647
- totalComponents,
648
- totalRoutes,
649
- sharedComponentsCount: sharedComponents.length,
650
- sharedPercentage: (sharedComponents.length / totalComponents * 100).toFixed(1),
651
- totalSize,
652
- averageSize: Math.round(totalSize / totalComponents),
653
- byCategory,
654
- topByUsage
655
- };
656
- }
657
-
658
- /**
659
- * Generates a visual report of the analysis
660
- */
661
- generateReport(metrics) {
662
- console.log('\nšŸ“Š PROJECT ANALYSIS\n');
663
- console.log(`Total components: ${metrics.totalComponents}`);
664
- console.log(`Total routes: ${metrics.totalRoutes}`);
665
- console.log(`Shared components: ${metrics.sharedComponentsCount} (${metrics.sharedPercentage}%)`);
666
- console.log(`Total size: ${(metrics.totalSize / 1024).toFixed(1)} KB`);
667
- console.log(`Average size: ${(metrics.averageSize / 1024).toFixed(1)} KB per component`);
668
-
669
- console.log('\nšŸ“¦ By category:');
670
- for (const [category, count] of Object.entries(metrics.byCategory)) {
671
- console.log(` ${category}: ${count} components`);
672
- }
673
-
674
- console.log('\nšŸ”„ Top 10 most used components:');
675
- metrics.topByUsage.forEach((comp, i) => {
676
- console.log(` ${i + 1}. ${comp.name} - ${comp.routes} routes - ${(comp.size / 1024).toFixed(1)} KB`);
677
- });
678
- }
679
- }
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.frameworkComponentsPath = path.join(
14
+ this.projectRoot,
15
+ 'node_modules',
16
+ 'slicejs-web-framework',
17
+ 'Slice',
18
+ 'Components',
19
+ 'Structural'
20
+ );
21
+ this.routesPath = getSrcPath(moduleUrl, 'routes.js');
22
+
23
+ // Analysis storage
24
+ this.components = new Map();
25
+ this.routes = new Map();
26
+ this.dependencyGraph = new Map();
27
+ }
28
+
29
+ /**
30
+ * Executes complete project analysis
31
+ */
32
+ async analyze() {
33
+ console.log('šŸ” Analyzing project...');
34
+
35
+ // 1. Load component configuration
36
+ await this.loadComponentsConfig();
37
+
38
+ // 2. Analyze component files
39
+ await this.analyzeComponents();
40
+ await this.analyzeFrameworkComponents();
41
+
42
+ // 3. Load and analyze routes
43
+ await this.analyzeRoutes();
44
+
45
+ // 4. Build dependency graph
46
+ this.buildDependencyGraph();
47
+
48
+ // 5. Calculate metrics
49
+ const metrics = this.calculateMetrics();
50
+
51
+ console.log('āœ… Analysis completed');
52
+
53
+ return {
54
+ components: Array.from(this.components.values()),
55
+ routes: Array.from(this.routes.values()),
56
+ dependencyGraph: this.dependencyGraph,
57
+ routeGroups: this.routeGroups,
58
+ metrics
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Loads component configuration from components.js
64
+ */
65
+ async loadComponentsConfig() {
66
+ const componentsConfigPath = path.join(this.componentsPath, 'components.js');
67
+
68
+ if (!await fs.pathExists(componentsConfigPath)) {
69
+ throw new Error('components.js not found');
70
+ }
71
+
72
+ // Read and parse components.js
73
+ const content = await fs.readFile(componentsConfigPath, 'utf-8');
74
+
75
+ const config = this.parseComponentsConfig(content);
76
+
77
+ // Group components by category
78
+ const categoryMap = new Map();
79
+
80
+ // Build category map from component assignments
81
+ for (const [componentName, categoryName] of Object.entries(config)) {
82
+ if (!categoryMap.has(categoryName)) {
83
+ categoryMap.set(categoryName, []);
84
+ }
85
+ categoryMap.get(categoryName).push(componentName);
86
+ }
87
+
88
+ // Process each category
89
+ for (const [categoryName, componentList] of categoryMap) {
90
+ // Determine category type based on category name
91
+ let categoryType = 'Visual'; // default
92
+ if (categoryName === 'Service') categoryType = 'Service';
93
+ if (categoryName === 'AppComponents') categoryType = 'Visual'; // AppComponents are visual
94
+
95
+ // Find category path
96
+ const categoryPath = path.join(this.componentsPath, categoryName);
97
+
98
+ if (await fs.pathExists(categoryPath)) {
99
+ const files = await fs.readdir(categoryPath);
100
+
101
+ for (const file of files) {
102
+ const componentPath = path.join(categoryPath, file);
103
+ const stat = await fs.stat(componentPath);
104
+
105
+ if (stat.isDirectory() && componentList.includes(file)) {
106
+ this.components.set(file, {
107
+ name: file,
108
+ category: categoryName,
109
+ categoryType: categoryType,
110
+ path: componentPath,
111
+ dependencies: new Set(),
112
+ usedBy: new Set(),
113
+ routes: new Set(),
114
+ size: 0
115
+ });
116
+ }
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Parses components.js safely using AST (no eval).
124
+ * @param {string} content
125
+ * @returns {Record<string, string>}
126
+ */
127
+ parseComponentsConfig(content) {
128
+ try {
129
+ const ast = parse(content, {
130
+ sourceType: 'module',
131
+ plugins: ['jsx']
132
+ });
133
+
134
+ let componentsNode = null;
135
+
136
+ traverse.default(ast, {
137
+ VariableDeclarator(path) {
138
+ if (path.node.id?.type === 'Identifier' && path.node.id.name === 'components') {
139
+ componentsNode = path.node.init;
140
+ path.stop();
141
+ }
142
+ }
143
+ });
144
+
145
+ if (!componentsNode || componentsNode.type !== 'ObjectExpression') {
146
+ throw new Error('components object not found');
147
+ }
148
+
149
+ const config = {};
150
+ for (const prop of componentsNode.properties) {
151
+ if (prop.type !== 'ObjectProperty') continue;
152
+
153
+ const key = this.extractStringValue(prop.key);
154
+ const value = this.extractStringValue(prop.value);
155
+
156
+ if (!key || !value) {
157
+ throw new Error('Invalid components entry');
158
+ }
159
+
160
+ config[key] = value;
161
+ }
162
+
163
+ return config;
164
+ } catch (error) {
165
+ throw new Error(`Could not parse components.js: ${error.message}`);
166
+ }
167
+ }
168
+
169
+
170
+ /**
171
+ * Extracts string values from AST nodes.
172
+ * @param {object} node
173
+ * @returns {string|null}
174
+ */
175
+ extractStringValue(node) {
176
+ if (!node) return null;
177
+
178
+ if (node.type === 'StringLiteral') {
179
+ return node.value;
180
+ }
181
+
182
+ if (node.type === 'Identifier') {
183
+ return node.name;
184
+ }
185
+
186
+ if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
187
+ return node.quasis.map((q) => q.value.cooked).join('');
188
+ }
189
+
190
+ return null;
191
+ }
192
+
193
+ /**
194
+ * Analyzes each component's files
195
+ */
196
+ async analyzeComponents() {
197
+ for (const [name, component] of this.components) {
198
+ const jsFile = path.join(component.path, `${name}.js`);
199
+
200
+ if (!await fs.pathExists(jsFile)) continue;
201
+
202
+ // Read JavaScript file
203
+ const content = await fs.readFile(jsFile, 'utf-8');
204
+
205
+ // Calculate size
206
+ component.size = await this.calculateComponentSize(component.path);
207
+
208
+ // Parse and extract dependencies
209
+ component.dependencies = await this.extractDependencies(content, jsFile);
210
+ }
211
+ }
212
+
213
+ async analyzeFrameworkComponents() {
214
+ if (!await fs.pathExists(this.frameworkComponentsPath)) {
215
+ return;
216
+ }
217
+ const frameworkConfigPath = path.join(this.projectRoot, 'src', 'sliceConfig.json');
218
+ let frameworkConfig = {};
219
+ try {
220
+ frameworkConfig = await fs.readJson(frameworkConfigPath);
221
+ } catch (error) {
222
+ console.warn('Warning: Could not read sliceConfig.json for framework bundling:', error.message);
223
+ }
224
+
225
+ const enabledStructural = this.getEnabledStructuralComponents(frameworkConfig);
226
+ const componentEntries = await fs.readdir(this.frameworkComponentsPath);
227
+
228
+ for (const componentName of componentEntries) {
229
+ if (!enabledStructural.has(componentName)) {
230
+ continue;
231
+ }
232
+
233
+ const componentPath = path.join(this.frameworkComponentsPath, componentName);
234
+ const componentStat = await fs.stat(componentPath);
235
+ if (!componentStat.isDirectory()) continue;
236
+
237
+ const key = `Framework/Structural/${componentName}`;
238
+ if (this.components.has(key)) continue;
239
+
240
+ this.components.set(key, {
241
+ name: componentName,
242
+ category: 'Framework/Structural',
243
+ categoryType: 'Visual',
244
+ path: componentPath,
245
+ dependencies: new Set(),
246
+ usedBy: new Set(),
247
+ routes: new Set(),
248
+ size: 0,
249
+ isFramework: true
250
+ });
251
+ }
252
+
253
+ if (enabledStructural.has('EventManagerDebugger')) {
254
+ this.addFrameworkFileComponent('EventManagerDebugger', 'EventManager');
255
+ }
256
+
257
+ if (enabledStructural.has('ContextManagerDebugger')) {
258
+ this.addFrameworkFileComponent('ContextManagerDebugger', 'ContextManager');
259
+ }
260
+
261
+ if (enabledStructural.has('ThemeManager')) {
262
+ this.addFrameworkFileComponent('ThemeManager', path.join('StylesManager', 'ThemeManager'));
263
+ }
264
+ }
265
+
266
+ addFrameworkFileComponent(componentName, folderName) {
267
+ const componentPath = path.join(this.frameworkComponentsPath, folderName);
268
+ const key = `Framework/Structural/${componentName}`;
269
+
270
+ if (this.components.has(key)) return;
271
+
272
+ this.components.set(key, {
273
+ name: componentName,
274
+ fileName: componentName,
275
+ category: 'Framework/Structural',
276
+ categoryType: 'Visual',
277
+ path: componentPath,
278
+ dependencies: new Set(),
279
+ usedBy: new Set(),
280
+ routes: new Set(),
281
+ size: 0,
282
+ isFramework: true
283
+ });
284
+ }
285
+
286
+ getEnabledStructuralComponents(config) {
287
+ const enabled = new Set(['Controller', 'Router', 'StylesManager']);
288
+
289
+ if (config?.logger?.enabled) {
290
+ enabled.add('Logger');
291
+ }
292
+
293
+ if (config?.debugger?.enabled) {
294
+ enabled.add('Debugger');
295
+ }
296
+
297
+ if (config?.events?.enabled) {
298
+ enabled.add('EventManager');
299
+ }
300
+
301
+ if (config?.events?.ui?.enabled) {
302
+ enabled.add('EventManagerDebugger');
303
+ }
304
+
305
+ if (config?.context?.enabled) {
306
+ enabled.add('ContextManager');
307
+ }
308
+
309
+ if (config?.context?.ui?.enabled) {
310
+ enabled.add('ContextManagerDebugger');
311
+ }
312
+
313
+ if (config?.themeManager?.enabled) {
314
+ enabled.add('ThemeManager');
315
+ }
316
+
317
+ return enabled;
318
+ }
319
+
320
+ /**
321
+ * Extracts dependencies from a component file
322
+ */
323
+ async extractDependencies(code, componentFilePath = null) {
324
+ const dependencies = new Set();
325
+
326
+ const resolveRoutesArray = (node, scope) => {
327
+ if (!node) return null;
328
+
329
+ if (node.type === 'ArrayExpression') {
330
+ return node;
331
+ }
332
+
333
+ if (node.type === 'ObjectExpression') {
334
+ const routesProp = node.properties.find(p => p.key?.name === 'routes');
335
+ if (routesProp?.value) {
336
+ return resolveRoutesArray(routesProp.value, scope);
337
+ }
338
+ }
339
+
340
+ if (node.type === 'Identifier' && scope) {
341
+ const binding = scope.getBinding(node.name);
342
+ if (!binding) return null;
343
+ const bindingNode = binding.path?.node;
344
+
345
+ if (bindingNode?.type === 'VariableDeclarator') {
346
+ const init = bindingNode.init;
347
+ if (init?.type === 'ArrayExpression') {
348
+ return init;
349
+ }
350
+
351
+ if (init?.type === 'Identifier') {
352
+ return resolveRoutesArray(init, binding.path.scope);
353
+ }
354
+
355
+ if (init?.type === 'ObjectExpression') {
356
+ return resolveRoutesArray(init, binding.path.scope);
357
+ }
358
+
359
+ if (init?.type === 'MemberExpression') {
360
+ return resolveRoutesArray(init, binding.path.scope);
361
+ }
362
+ }
363
+
364
+ if (bindingNode?.type === 'ImportSpecifier' || bindingNode?.type === 'ImportDefaultSpecifier') {
365
+ const parent = binding.path.parentPath?.node;
366
+ if (parent?.type === 'ImportDeclaration') {
367
+ const importedName = bindingNode.type === 'ImportDefaultSpecifier'
368
+ ? 'default'
369
+ : bindingNode.imported?.name;
370
+ const importedNode = resolveImportedValue(parent.source.value, importedName, componentFilePath);
371
+ return resolveRoutesArray(importedNode, null);
372
+ }
373
+ }
374
+ }
375
+
376
+ if (node.type === 'MemberExpression' && scope) {
377
+ const objectNode = resolveObjectExpression(node.object, scope);
378
+ if (objectNode) {
379
+ const propName = node.property?.name || node.property?.value;
380
+ if (propName) {
381
+ const prop = objectNode.properties.find(p => p.key?.name === propName || p.key?.value === propName);
382
+ if (prop?.value) {
383
+ return resolveRoutesArray(prop.value, scope);
384
+ }
385
+ }
386
+ }
387
+ }
388
+
389
+ return null;
390
+ };
391
+
392
+ const resolveObjectExpression = (node, scope) => {
393
+ if (!node) return null;
394
+ if (node.type === 'ObjectExpression') return node;
395
+
396
+ if (node.type === 'Identifier' && scope) {
397
+ const binding = scope.getBinding(node.name);
398
+ const bindingNode = binding?.path?.node;
399
+ if (bindingNode?.type === 'VariableDeclarator') {
400
+ const init = bindingNode.init;
401
+ if (init?.type === 'ObjectExpression') {
402
+ return init;
403
+ }
404
+ if (init?.type === 'Identifier') {
405
+ return resolveObjectExpression(init, binding.path.scope);
406
+ }
407
+ }
408
+
409
+ if (bindingNode?.type === 'ImportSpecifier' || bindingNode?.type === 'ImportDefaultSpecifier') {
410
+ const parent = binding.path.parentPath?.node;
411
+ if (parent?.type === 'ImportDeclaration') {
412
+ const importedName = bindingNode.type === 'ImportDefaultSpecifier'
413
+ ? 'default'
414
+ : bindingNode.imported?.name;
415
+ const importedNode = resolveImportedValue(parent.source.value, importedName, componentFilePath);
416
+ return resolveObjectExpression(importedNode, null);
417
+ }
418
+ }
419
+ }
420
+
421
+ return null;
422
+ };
423
+
424
+ const resolveStringValue = (node, scope) => {
425
+ if (!node) return null;
426
+ if (node.type === 'StringLiteral') return node.value;
427
+ if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
428
+ return node.quasis.map(q => q.value.cooked).join('');
429
+ }
430
+
431
+ if (node.type === 'Identifier' && scope) {
432
+ const binding = scope.getBinding(node.name);
433
+ const bindingNode = binding?.path?.node;
434
+ if (bindingNode?.type === 'VariableDeclarator') {
435
+ return resolveStringValue(bindingNode.init, binding.path.scope);
436
+ }
437
+
438
+ if (bindingNode?.type === 'ImportSpecifier' || bindingNode?.type === 'ImportDefaultSpecifier') {
439
+ const parent = binding.path.parentPath?.node;
440
+ if (parent?.type === 'ImportDeclaration') {
441
+ const importedName = bindingNode.type === 'ImportDefaultSpecifier'
442
+ ? 'default'
443
+ : bindingNode.imported?.name;
444
+ const importedNode = resolveImportedValue(parent.source.value, importedName, componentFilePath);
445
+ return resolveStringValue(importedNode, null);
446
+ }
447
+ }
448
+ }
449
+
450
+ if (node.type === 'MemberExpression' && scope) {
451
+ const objectNode = resolveObjectExpression(node.object, scope);
452
+ if (objectNode) {
453
+ const propName = node.property?.name || node.property?.value;
454
+ if (propName) {
455
+ const prop = objectNode.properties.find(p => p.key?.name === propName || p.key?.value === propName);
456
+ if (prop?.value) {
457
+ return resolveStringValue(prop.value, scope);
458
+ }
459
+ }
460
+ }
461
+ }
462
+
463
+ return null;
464
+ };
465
+
466
+ const resolveImportedValue = (importPath, importedName, fromFilePath) => {
467
+ if (!fromFilePath) return null;
468
+
469
+ const baseDir = path.dirname(fromFilePath);
470
+ const resolvedPath = resolveImportPath(importPath, baseDir);
471
+ if (!resolvedPath) {
472
+ console.warn(`āš ļø Cannot resolve import for MultiRoute routes: ${importPath}`);
473
+ return null;
474
+ }
475
+
476
+ const cacheKey = `${resolvedPath}:${importedName || 'default'}`;
477
+ if (!resolveImportedValue.cache) {
478
+ resolveImportedValue.cache = new Map();
479
+ }
480
+ if (resolveImportedValue.cache.has(cacheKey)) {
481
+ return resolveImportedValue.cache.get(cacheKey);
482
+ }
483
+
484
+ try {
485
+ const source = fs.readFileSync(resolvedPath, 'utf-8');
486
+ const importAst = parse(source, {
487
+ sourceType: 'module',
488
+ plugins: ['jsx']
489
+ });
490
+
491
+ const topLevelBindings = new Map();
492
+ for (const node of importAst.program.body) {
493
+ if (node.type === 'VariableDeclaration') {
494
+ node.declarations.forEach(decl => {
495
+ if (decl.id?.type === 'Identifier') {
496
+ topLevelBindings.set(decl.id.name, decl.init);
497
+ }
498
+ });
499
+ }
500
+ }
501
+
502
+ let exportNode = null;
503
+ for (const node of importAst.program.body) {
504
+ if (node.type === 'ExportDefaultDeclaration' && importedName === 'default') {
505
+ exportNode = node.declaration;
506
+ break;
507
+ }
508
+
509
+ if (node.type === 'ExportNamedDeclaration') {
510
+ if (node.declaration?.type === 'VariableDeclaration') {
511
+ for (const decl of node.declaration.declarations) {
512
+ if (decl.id?.name === importedName) {
513
+ exportNode = decl.init;
514
+ break;
515
+ }
516
+ }
517
+ }
518
+
519
+ if (!exportNode && node.specifiers?.length) {
520
+ const specifier = node.specifiers.find(s => s.exported?.name === importedName);
521
+ if (specifier && specifier.local?.name) {
522
+ exportNode = topLevelBindings.get(specifier.local.name) || null;
523
+ }
524
+ }
525
+ }
526
+
527
+ if (exportNode) break;
528
+ }
529
+
530
+ if (exportNode?.type === 'Identifier') {
531
+ exportNode = topLevelBindings.get(exportNode.name) || exportNode;
532
+ }
533
+
534
+ resolveImportedValue.cache.set(cacheKey, exportNode || null);
535
+ return exportNode || null;
536
+ } catch (error) {
537
+ console.warn(`āš ļø Error resolving import ${importPath}: ${error.message}`);
538
+ resolveImportedValue.cache.set(cacheKey, null);
539
+ return null;
540
+ }
541
+ };
542
+
543
+ const resolveImportPath = (importPath, baseDir) => {
544
+ if (!importPath.startsWith('.')) return null;
545
+
546
+ const resolvedBase = path.resolve(baseDir, importPath);
547
+ const extensions = ['.js', '.mjs', '.cjs', '.json'];
548
+
549
+ if (fs.existsSync(resolvedBase) && fs.statSync(resolvedBase).isFile()) {
550
+ return resolvedBase;
551
+ }
552
+
553
+ if (!path.extname(resolvedBase)) {
554
+ for (const ext of extensions) {
555
+ const candidate = resolvedBase + ext;
556
+ if (fs.existsSync(candidate)) {
557
+ return candidate;
558
+ }
559
+ }
560
+ }
561
+
562
+ return null;
563
+ };
564
+
565
+ const addMultiRouteDependencies = (routesArrayNode, scope) => {
566
+ if (!routesArrayNode || routesArrayNode.type !== 'ArrayExpression') return;
567
+
568
+ routesArrayNode.elements.forEach(routeElement => {
569
+ if (!routeElement) return;
570
+
571
+ if (routeElement.type === 'SpreadElement') {
572
+ const spreadArray = resolveRoutesArray(routeElement.argument, scope);
573
+ addMultiRouteDependencies(spreadArray, scope);
574
+ return;
575
+ }
576
+
577
+ const routeObject = resolveObjectExpression(routeElement, scope) || routeElement;
578
+ if (routeObject?.type === 'ObjectExpression') {
579
+ const componentProp = routeObject.properties.find(p => p.key?.name === 'component');
580
+ if (componentProp?.value) {
581
+ const componentName = resolveStringValue(componentProp.value, scope);
582
+ if (componentName) {
583
+ dependencies.add(componentName);
584
+ }
585
+ }
586
+ }
587
+ });
588
+ };
589
+
590
+ try {
591
+ const ast = parse(code, {
592
+ sourceType: 'module',
593
+ plugins: ['jsx']
594
+ });
595
+
596
+ traverse.default(ast, {
597
+ // Detect slice.build() calls
598
+ CallExpression(path) {
599
+ const { callee, arguments: args } = path.node;
600
+
601
+ // slice.build('MultiRoute', { routes: [...] })
602
+ if (
603
+ callee.type === 'MemberExpression' &&
604
+ callee.object.name === 'slice' &&
605
+ callee.property.name === 'build' &&
606
+ args[0]?.type === 'StringLiteral' &&
607
+ args[0].value === 'MultiRoute' &&
608
+ args[1]?.type === 'ObjectExpression'
609
+ ) {
610
+ // Add MultiRoute itself
611
+ dependencies.add('MultiRoute');
612
+
613
+ // Extract routes from MultiRoute props
614
+ const routesProp = args[1].properties.find(p => p.key?.name === 'routes');
615
+ if (routesProp) {
616
+ const routesArrayNode = resolveRoutesArray(routesProp.value, path.scope);
617
+ addMultiRouteDependencies(routesArrayNode);
618
+ }
619
+ }
620
+ // Regular slice.build() calls
621
+ else if (
622
+ callee.type === 'MemberExpression' &&
623
+ callee.object.name === 'slice' &&
624
+ callee.property.name === 'build' &&
625
+ args[0]?.type === 'StringLiteral'
626
+ ) {
627
+ dependencies.add(args[0].value);
628
+ }
629
+ },
630
+
631
+ // Detect direct imports (less common but possible)
632
+ ImportDeclaration(path) {
633
+ const importPath = path.node.source.value;
634
+ if (importPath.includes('/Components/')) {
635
+ const componentName = importPath.split('/').pop();
636
+ dependencies.add(componentName);
637
+ }
638
+ }
639
+ });
640
+ } catch (error) {
641
+ console.warn(`āš ļø Error parsing component: ${error.message}`);
642
+ }
643
+
644
+ return dependencies;
645
+ }
646
+
647
+ /**
648
+ * Analyzes the routes file and detects route groups
649
+ */
650
+ async analyzeRoutes() {
651
+ if (!await fs.pathExists(this.routesPath)) {
652
+ throw new Error('routes.js no encontrado');
653
+ }
654
+
655
+ const content = await fs.readFile(this.routesPath, 'utf-8');
656
+
657
+ try {
658
+ const ast = parse(content, {
659
+ sourceType: 'module',
660
+ plugins: ['jsx']
661
+ });
662
+
663
+ let currentRoute = null;
664
+ const self = this; // Guardar referencia a la instancia
665
+
666
+ traverse.default(ast, {
667
+ ObjectExpression(path) {
668
+ // Buscar objetos de ruta: { path: '/', component: 'HomePage' }
669
+ const properties = path.node.properties;
670
+ const pathProp = properties.find(p => p.key?.name === 'path');
671
+ const componentProp = properties.find(p => p.key?.name === 'component');
672
+
673
+ if (pathProp && componentProp) {
674
+ const routePath = pathProp.value.value;
675
+ const componentName = componentProp.value.value;
676
+
677
+ currentRoute = {
678
+ path: routePath,
679
+ component: componentName,
680
+ dependencies: new Set([componentName])
681
+ };
682
+
683
+ self.routes.set(routePath, currentRoute);
684
+
685
+ // Marcar el componente como usado por esta ruta
686
+ if (self.components.has(componentName)) {
687
+ self.components.get(componentName).routes.add(routePath);
688
+ }
689
+ }
690
+ }
691
+ });
692
+
693
+ // Detect and store route groups based on MultiRoute usage
694
+ this.routeGroups = this.detectRouteGroups();
695
+
696
+ } catch (error) {
697
+ console.warn(`āš ļø Error parseando rutas: ${error.message}`);
698
+ }
699
+ }
700
+
701
+ /**
702
+ * Builds the complete dependency graph
703
+ */
704
+ buildDependencyGraph() {
705
+ // Propagate transitive dependencies
706
+ for (const [name, component] of this.components) {
707
+ this.dependencyGraph.set(name, this.getAllDependencies(name, new Set()));
708
+ }
709
+
710
+ // Calculate usedBy (inverse dependencies)
711
+ for (const [name, deps] of this.dependencyGraph) {
712
+ for (const dep of deps) {
713
+ if (this.components.has(dep)) {
714
+ this.components.get(dep).usedBy.add(name);
715
+ }
716
+ }
717
+ }
718
+ }
719
+
720
+ /**
721
+ * Detects route groups based on MultiRoute usage
722
+ */
723
+ detectRouteGroups() {
724
+ const routeGroups = new Map();
725
+
726
+ for (const [componentName, component] of this.components) {
727
+ // Check if component uses MultiRoute
728
+ const hasMultiRoute = Array.from(component.dependencies).includes('MultiRoute');
729
+
730
+ if (hasMultiRoute) {
731
+ // Find all routes that point to this component
732
+ const relatedRoutes = Array.from(this.routes.values())
733
+ .filter(route => route.component === componentName);
734
+
735
+ if (relatedRoutes.length > 1) {
736
+ // Group these routes together
737
+ const groupKey = `multiroute-${componentName}`;
738
+ routeGroups.set(groupKey, {
739
+ component: componentName,
740
+ routes: relatedRoutes.map(r => r.path),
741
+ type: 'multiroute'
742
+ });
743
+
744
+ // Mark component as multiroute handler
745
+ component.isMultiRouteHandler = true;
746
+ component.multiRoutePaths = relatedRoutes.map(r => r.path);
747
+ }
748
+ }
749
+ }
750
+
751
+ return routeGroups;
752
+ }
753
+
754
+ /**
755
+ * Gets all dependencies of a component (recursive)
756
+ */
757
+ getAllDependencies(componentName, visited = new Set()) {
758
+ if (visited.has(componentName)) return new Set();
759
+ visited.add(componentName);
760
+
761
+ const component = this.components.get(componentName);
762
+ if (!component) return new Set();
763
+
764
+ const allDeps = new Set(component.dependencies);
765
+
766
+ for (const dep of component.dependencies) {
767
+ const transitiveDeps = this.getAllDependencies(dep, visited);
768
+ for (const transDep of transitiveDeps) {
769
+ allDeps.add(transDep);
770
+ }
771
+ }
772
+
773
+ return allDeps;
774
+ }
775
+
776
+ /**
777
+ * Calculates the total size of a component (JS + HTML + CSS)
778
+ */
779
+ async calculateComponentSize(componentPath) {
780
+ let totalSize = 0;
781
+ const files = await fs.readdir(componentPath);
782
+
783
+ for (const file of files) {
784
+ const filePath = path.join(componentPath, file);
785
+ const stat = await fs.stat(filePath);
786
+
787
+ if (stat.isFile()) {
788
+ totalSize += stat.size;
789
+ }
790
+ }
791
+
792
+ return totalSize;
793
+ }
794
+
795
+ /**
796
+ * Calculates project metrics
797
+ */
798
+ calculateMetrics() {
799
+ const totalComponents = this.components.size;
800
+ const totalRoutes = this.routes.size;
801
+
802
+ // Shared components (used in multiple routes)
803
+ const sharedComponents = Array.from(this.components.values())
804
+ .filter(c => c.routes.size >= 2);
805
+
806
+ // Total size
807
+ const totalSize = Array.from(this.components.values())
808
+ .reduce((sum, c) => sum + c.size, 0);
809
+
810
+ // Components by category
811
+ const byCategory = {};
812
+ for (const comp of this.components.values()) {
813
+ byCategory[comp.category] = (byCategory[comp.category] || 0) + 1;
814
+ }
815
+
816
+ // Top components by usage
817
+ const topByUsage = Array.from(this.components.values())
818
+ .sort((a, b) => b.routes.size - a.routes.size)
819
+ .slice(0, 10)
820
+ .map(c => ({
821
+ name: c.name,
822
+ routes: c.routes.size,
823
+ size: c.size
824
+ }));
825
+
826
+ return {
827
+ totalComponents,
828
+ totalRoutes,
829
+ sharedComponentsCount: sharedComponents.length,
830
+ sharedPercentage: (sharedComponents.length / totalComponents * 100).toFixed(1),
831
+ totalSize,
832
+ averageSize: Math.round(totalSize / totalComponents),
833
+ byCategory,
834
+ topByUsage
835
+ };
836
+ }
837
+
838
+ /**
839
+ * Generates a visual report of the analysis
840
+ */
841
+ generateReport(metrics) {
842
+ console.log('\nšŸ“Š PROJECT ANALYSIS\n');
843
+ console.log(`Total components: ${metrics.totalComponents}`);
844
+ console.log(`Total routes: ${metrics.totalRoutes}`);
845
+ console.log(`Shared components: ${metrics.sharedComponentsCount} (${metrics.sharedPercentage}%)`);
846
+ console.log(`Total size: ${(metrics.totalSize / 1024).toFixed(1)} KB`);
847
+ console.log(`Average size: ${(metrics.averageSize / 1024).toFixed(1)} KB per component`);
848
+
849
+ console.log('\nšŸ“¦ By category:');
850
+ for (const [category, count] of Object.entries(metrics.byCategory)) {
851
+ console.log(` ${category}: ${count} components`);
852
+ }
853
+
854
+ console.log('\nšŸ”„ Top 10 most used components:');
855
+ metrics.topByUsage.forEach((comp, i) => {
856
+ console.log(` ${i + 1}. ${comp.name} - ${comp.routes} routes - ${(comp.size / 1024).toFixed(1)} KB`);
857
+ });
858
+ }
859
+ }