slicejs-cli 3.6.3 → 3.6.5

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