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