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