slicejs-cli 2.7.6 → 2.7.8

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.
@@ -19,6 +19,10 @@ export default async function bundle(options = {}) {
19
19
  // Validate that it's a Slice.js project
20
20
  await validateProject(projectRoot);
21
21
 
22
+ // Create default bundle config if needed
23
+ const bundleGenerator = new BundleGenerator(import.meta.url, null);
24
+ await bundleGenerator.createDefaultBundleConfig();
25
+
22
26
  // Phase 1: Analysis
23
27
  Print.buildProgress('Analyzing project...');
24
28
  const analyzer = new DependencyAnalyzer(import.meta.url);
@@ -110,7 +114,8 @@ function printSummary(result, startTime) {
110
114
 
111
115
  for (const [key, bundle] of Object.entries(bundles.routes)) {
112
116
  console.log(` ${key}:`);
113
- console.log(` Route: ${bundle.path}`);
117
+ const routeDisplay = Array.isArray(bundle.path) ? `${bundle.path.length} routes` : (bundle.path || key);
118
+ console.log(` Route: ${routeDisplay}`);
114
119
  console.log(` Components: ${bundle.components.length}`);
115
120
  console.log(` Size: ${(bundle.size / 1024).toFixed(1)} KB`);
116
121
  console.log(` File: ${bundle.file}`);
@@ -9,6 +9,7 @@ export default class BundleGenerator {
9
9
  this.moduleUrl = moduleUrl;
10
10
  this.analysisData = analysisData;
11
11
  this.srcPath = getSrcPath(moduleUrl);
12
+ this.bundlesPath = path.join(this.srcPath, 'bundles');
12
13
  this.componentsPath = path.dirname(getComponentsJsPath(moduleUrl));
13
14
 
14
15
  // Configuration
@@ -35,6 +36,9 @@ export default class BundleGenerator {
35
36
  async generate() {
36
37
  console.log('🔨 Generating bundles...');
37
38
 
39
+ // 0. Create bundles directory
40
+ await fs.ensureDir(this.bundlesPath);
41
+
38
42
  // 1. Determine optimal strategy
39
43
  this.determineStrategy();
40
44
 
@@ -70,9 +74,9 @@ export default class BundleGenerator {
70
74
  if (totalComponents < 20 || sharedPercentage > 60) {
71
75
  this.config.strategy = 'global';
72
76
  console.log('📦 Strategy: Global Bundle (small project or highly shared)');
73
- } else if (totalComponents < 60) {
77
+ } else if (totalComponents < 100) {
74
78
  this.config.strategy = 'hybrid';
75
- console.log('📦 Strategy: Hybrid (critical + per route)');
79
+ console.log('📦 Strategy: Hybrid (critical + grouped routes)');
76
80
  } else {
77
81
  this.config.strategy = 'per-route';
78
82
  console.log('📦 Strategy: Per Route (large project)');
@@ -95,8 +99,8 @@ export default class BundleGenerator {
95
99
  const isStructural = comp.categoryType === 'Structural' ||
96
100
  ['Navbar', 'Footer', 'Layout'].includes(comp.name);
97
101
 
98
- // Small and highly used components
99
- const isSmallAndUseful = comp.size < 5000 && comp.routes.size >= 2;
102
+ // Small and highly used components (only if used in 3+ routes)
103
+ const isSmallAndUseful = comp.size < 2000 && comp.routes.size >= 3;
100
104
 
101
105
  return isShared || isStructural || isSmallAndUseful;
102
106
  })
@@ -109,13 +113,27 @@ export default class BundleGenerator {
109
113
 
110
114
  // Fill critical bundle up to limit
111
115
  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;
116
+ const dependencies = this.getComponentDependencies(comp);
117
+ const totalSize = comp.size + dependencies.reduce((sum, dep) => sum + dep.size, 0);
118
+ const totalCount = 1 + dependencies.length;
119
+
120
+ const wouldExceedSize = this.bundles.critical.size + totalSize > this.config.maxCriticalSize;
121
+ const wouldExceedCount = this.bundles.critical.components.length + totalCount > this.config.maxCriticalComponents;
122
+
123
+ if (wouldExceedSize || wouldExceedCount) continue;
114
124
 
115
- if (wouldExceedSize || wouldExceedCount) break;
125
+ // Add component and its dependencies
126
+ if (!this.bundles.critical.components.find(c => c.name === comp.name)) {
127
+ this.bundles.critical.components.push(comp);
128
+ this.bundles.critical.size += comp.size;
129
+ }
116
130
 
117
- this.bundles.critical.components.push(comp);
118
- this.bundles.critical.size += comp.size;
131
+ for (const dep of dependencies) {
132
+ if (!this.bundles.critical.components.find(c => c.name === dep.name)) {
133
+ this.bundles.critical.components.push(dep);
134
+ this.bundles.critical.size += dep.size;
135
+ }
136
+ }
119
137
  }
120
138
 
121
139
  console.log(`✓ Critical bundle: ${this.bundles.critical.components.length} components, ${(this.bundles.critical.size / 1024).toFixed(1)} KB`);
@@ -127,12 +145,34 @@ export default class BundleGenerator {
127
145
  assignRouteComponents() {
128
146
  const criticalNames = new Set(this.bundles.critical.components.map(c => c.name));
129
147
 
130
- for (const [routePath, route] of this.analysisData.routes) {
148
+ if (this.config.strategy === 'hybrid') {
149
+ this.assignHybridBundles(criticalNames);
150
+ } else {
151
+ this.assignPerRouteBundles(criticalNames);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Assigns components to per-route bundles
157
+ */
158
+ assignPerRouteBundles(criticalNames) {
159
+ for (const route of this.analysisData.routes) {
160
+ const routePath = route.path;
131
161
  // Get all route dependencies
132
162
  const routeComponents = this.getRouteComponents(route.component);
133
163
 
164
+ // Include dependencies for all route components
165
+ const allComponents = new Set();
166
+ for (const comp of routeComponents) {
167
+ allComponents.add(comp);
168
+ const dependencies = this.getComponentDependencies(comp);
169
+ for (const dep of dependencies) {
170
+ allComponents.add(dep);
171
+ }
172
+ }
173
+
134
174
  // Filter those already in critical
135
- const uniqueComponents = routeComponents.filter(comp =>
175
+ const uniqueComponents = Array.from(allComponents).filter(comp =>
136
176
  !criticalNames.has(comp.name)
137
177
  );
138
178
 
@@ -152,6 +192,105 @@ export default class BundleGenerator {
152
192
  }
153
193
  }
154
194
 
195
+ /**
196
+ * Gets all component dependencies transitively
197
+ */
198
+ getComponentDependencies(component, visited = new Set()) {
199
+ if (visited.has(component.name)) return [];
200
+ visited.add(component.name);
201
+
202
+ const dependencies = [];
203
+
204
+ // Add direct dependencies
205
+ for (const depName of component.dependencies) {
206
+ const depComp = this.analysisData.components.find(c => c.name === depName);
207
+ if (depComp && !visited.has(depName)) {
208
+ dependencies.push(depComp);
209
+ // Add transitive dependencies
210
+ dependencies.push(...this.getComponentDependencies(depComp, visited));
211
+ }
212
+ }
213
+
214
+ return dependencies;
215
+ }
216
+
217
+ /**
218
+ * Assigns components to hybrid bundles (grouped by category)
219
+ */
220
+ assignHybridBundles(criticalNames) {
221
+ const routeGroups = new Map();
222
+
223
+ // Group routes by category
224
+ for (const route of this.analysisData.routes) {
225
+ const category = this.categorizeRoute(route.path);
226
+ if (!routeGroups.has(category)) {
227
+ routeGroups.set(category, []);
228
+ }
229
+ routeGroups.get(category).push(route);
230
+ }
231
+
232
+ // Create bundles for each group
233
+ for (const [category, routes] of routeGroups) {
234
+ const allComponents = new Set();
235
+
236
+ // Collect all unique components for this category (including dependencies)
237
+ for (const route of routes) {
238
+ const routeComponents = this.getRouteComponents(route.component);
239
+ for (const comp of routeComponents) {
240
+ allComponents.add(comp);
241
+ // Add transitive dependencies
242
+ const dependencies = this.getComponentDependencies(comp);
243
+ for (const dep of dependencies) {
244
+ allComponents.add(dep);
245
+ }
246
+ }
247
+ }
248
+
249
+ // Filter those already in critical
250
+ const uniqueComponents = Array.from(allComponents).filter(comp =>
251
+ !criticalNames.has(comp.name)
252
+ );
253
+
254
+ if (uniqueComponents.length === 0) continue;
255
+
256
+ const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
257
+ const routePaths = routes.map(r => r.path);
258
+
259
+ this.bundles.routes[category] = {
260
+ paths: routePaths,
261
+ components: uniqueComponents,
262
+ size: totalSize,
263
+ file: `slice-bundle.${category}.js`
264
+ };
265
+
266
+ console.log(`✓ Bundle ${category}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB (${routes.length} routes)`);
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Categorizes a route path for grouping
272
+ */
273
+ categorizeRoute(routePath) {
274
+ const path = routePath.toLowerCase();
275
+
276
+ if (path === '/' || path === '/home') return 'home';
277
+ if (path.includes('docum') || path.includes('documentation')) return 'documentation';
278
+ if (path.includes('component') || path.includes('visual') || path.includes('card') ||
279
+ path.includes('button') || path.includes('input') || path.includes('switch') ||
280
+ path.includes('checkbox') || path.includes('select') || path.includes('details') ||
281
+ path.includes('grid') || path.includes('loading') || path.includes('layout') ||
282
+ path.includes('navbar') || path.includes('treeview') || path.includes('multiroute')) return 'components';
283
+ if (path.includes('theme') || path.includes('slice') || path.includes('config')) return 'configuration';
284
+ if (path.includes('routing') || path.includes('guard')) return 'routing';
285
+ if (path.includes('service') || path.includes('command')) return 'services';
286
+ if (path.includes('structural') || path.includes('lifecycle') || path.includes('static') ||
287
+ path.includes('build')) return 'advanced';
288
+ if (path.includes('playground') || path.includes('creator')) return 'tools';
289
+ if (path.includes('about') || path.includes('404')) return 'misc';
290
+
291
+ return 'general';
292
+ }
293
+
155
294
  /**
156
295
  * Gets all components needed for a route
157
296
  */
@@ -199,7 +338,7 @@ export default class BundleGenerator {
199
338
  const routeFile = await this.createBundleFile(
200
339
  bundle.components,
201
340
  'route',
202
- bundle.path
341
+ bundle.path || routeKey // Use routeKey as fallback for hybrid bundles
203
342
  );
204
343
  files.push(routeFile);
205
344
  }
@@ -213,7 +352,7 @@ export default class BundleGenerator {
213
352
  async createBundleFile(components, type, routePath) {
214
353
  const routeKey = routePath ? this.routeToFileName(routePath) : 'critical';
215
354
  const fileName = `slice-bundle.${routeKey}.js`;
216
- const filePath = path.join(this.srcPath, fileName);
355
+ const filePath = path.join(this.bundlesPath, fileName);
217
356
 
218
357
  const bundleContent = await this.generateBundleContent(
219
358
  components,
@@ -235,6 +374,50 @@ export default class BundleGenerator {
235
374
  };
236
375
  }
237
376
 
377
+ /**
378
+ * Analyzes dependencies of a JavaScript file using simple regex
379
+ */
380
+ analyzeDependencies(jsContent, componentPath) {
381
+ const dependencies = [];
382
+
383
+ try {
384
+ // Simple regex to find import statements
385
+ const importRegex = /import\s+.*?\s+from\s+['"`]([^'"`]+)['"`]/g;
386
+ let match;
387
+
388
+ while ((match = importRegex.exec(jsContent)) !== null) {
389
+ const importPath = match[1];
390
+
391
+ // Only process relative imports (starting with ./ or ../)
392
+ if (importPath.startsWith('./') || importPath.startsWith('../')) {
393
+ // Resolve the absolute path
394
+ const resolvedPath = path.resolve(componentPath, importPath);
395
+
396
+ // If no extension, try common extensions
397
+ let finalPath = resolvedPath;
398
+ const ext = path.extname(resolvedPath);
399
+ if (!ext) {
400
+ const extensions = ['.js', '.json', '.mjs'];
401
+ for (const ext of extensions) {
402
+ if (fs.existsSync(resolvedPath + ext)) {
403
+ finalPath = resolvedPath + ext;
404
+ break;
405
+ }
406
+ }
407
+ }
408
+
409
+ if (fs.existsSync(finalPath)) {
410
+ dependencies.push(finalPath);
411
+ }
412
+ }
413
+ }
414
+ } catch (error) {
415
+ console.warn(`Warning: Could not analyze dependencies for ${componentPath}:`, error.message);
416
+ }
417
+
418
+ return dependencies;
419
+ }
420
+
238
421
  /**
239
422
  * Generates the content of a bundle
240
423
  */
@@ -242,10 +425,23 @@ export default class BundleGenerator {
242
425
  const componentsData = {};
243
426
 
244
427
  for (const comp of components) {
245
- const jsContent = await fs.readFile(
246
- path.join(comp.path, `${comp.name}.js`),
247
- 'utf-8'
248
- );
428
+ const jsPath = path.join(comp.path, `${comp.name}.js`);
429
+ const jsContent = await fs.readFile(jsPath, 'utf-8');
430
+
431
+ // Analyze dependencies
432
+ const dependencies = this.analyzeDependencies(jsContent, comp.path);
433
+ const dependencyContents = {};
434
+
435
+ // Read all dependency files
436
+ for (const depPath of dependencies) {
437
+ try {
438
+ const depContent = await fs.readFile(depPath, 'utf-8');
439
+ const depName = path.basename(depPath, path.extname(depPath));
440
+ dependencyContents[depName] = depContent;
441
+ } catch (error) {
442
+ console.warn(`Warning: Could not read dependency ${depPath}:`, error.message);
443
+ }
444
+ }
249
445
 
250
446
  let htmlContent = null;
251
447
  let cssContent = null;
@@ -266,10 +462,11 @@ export default class BundleGenerator {
266
462
  category: comp.category,
267
463
  categoryType: comp.categoryType,
268
464
  js: this.cleanJavaScript(jsContent),
465
+ externalDependencies: dependencyContents, // Files imported with import statements
466
+ componentDependencies: Array.from(comp.dependencies), // Other components this one depends on
269
467
  html: htmlContent,
270
468
  css: cssContent,
271
- size: comp.size,
272
- dependencies: Array.from(comp.dependencies)
469
+ size: comp.size
273
470
  };
274
471
  }
275
472
 
@@ -355,7 +552,7 @@ if (window.slice && window.slice.controller) {
355
552
 
356
553
  for (const [key, bundle] of Object.entries(this.bundles.routes)) {
357
554
  config.bundles.routes[key] = {
358
- path: bundle.path,
555
+ path: bundle.path || bundle.paths || key, // Support both single path and array of paths, fallback to key
359
556
  file: bundle.file,
360
557
  size: bundle.size,
361
558
  components: bundle.components.map(c => c.name),
@@ -382,8 +579,76 @@ if (window.slice && window.slice.controller) {
382
579
  * Saves the configuration to file
383
580
  */
384
581
  async saveBundleConfig(config) {
385
- const configPath = path.join(this.srcPath, 'bundle.config.json');
582
+ // Ensure bundles directory exists
583
+ await fs.ensureDir(this.bundlesPath);
584
+
585
+ // Save JSON config
586
+ const configPath = path.join(this.bundlesPath, 'bundle.config.json');
386
587
  await fs.writeJson(configPath, config, { spaces: 2 });
588
+
589
+ // Generate JavaScript module for direct import
590
+ const jsConfigPath = path.join(this.bundlesPath, 'bundle.config.js');
591
+ const jsConfig = this.generateBundleConfigJS(config);
592
+ await fs.writeFile(jsConfigPath, jsConfig, 'utf-8');
593
+
387
594
  console.log(`✓ Configuration saved to ${configPath}`);
595
+ console.log(`✓ JavaScript config generated: ${jsConfigPath}`);
596
+ }
597
+
598
+ /**
599
+ * Creates a default bundle config file if none exists
600
+ */
601
+ async createDefaultBundleConfig() {
602
+ const defaultConfigPath = path.join(this.srcPath, 'bundles', 'bundle.config.js');
603
+
604
+ // Only create if it doesn't exist
605
+ if (await fs.pathExists(defaultConfigPath)) {
606
+ return;
607
+ }
608
+
609
+ await fs.ensureDir(path.dirname(defaultConfigPath));
610
+
611
+ const defaultConfig = `/**
612
+ * Slice.js Bundle Configuration
613
+ * Default empty configuration - no bundles available
614
+ * Run 'slice bundle' to generate optimized bundles
615
+ */
616
+
617
+ // No bundles available - using individual component loading
618
+ export const SLICE_BUNDLE_CONFIG = null;
619
+
620
+ // No auto-initialization needed for default config
621
+ `;
622
+
623
+ await fs.writeFile(defaultConfigPath, defaultConfig, 'utf-8');
624
+ console.log(`✓ Default bundle config created: ${defaultConfigPath}`);
625
+ }
626
+
627
+ /**
628
+ * Generates JavaScript module for direct import
629
+ */
630
+ generateBundleConfigJS(config) {
631
+ return `/**
632
+ * Slice.js Bundle Configuration
633
+ * Generated: ${new Date().toISOString()}
634
+ * Strategy: ${config.strategy}
635
+ */
636
+
637
+ // Direct bundle configuration (no fetch required)
638
+ export const SLICE_BUNDLE_CONFIG = ${JSON.stringify(config, null, 2)};
639
+
640
+ // Auto-initialization if slice is available
641
+ if (typeof window !== 'undefined' && window.slice && window.slice.controller) {
642
+ window.slice.controller.bundleConfig = SLICE_BUNDLE_CONFIG;
643
+
644
+ // Load critical bundle automatically
645
+ if (SLICE_BUNDLE_CONFIG.bundles.critical && !window.slice.controller.criticalBundleLoaded) {
646
+ import('./slice-bundle.critical.js').catch(err =>
647
+ console.warn('Failed to load critical bundle:', err)
648
+ );
649
+ window.slice.controller.criticalBundleLoaded = true;
650
+ }
651
+ }
652
+ `;
388
653
  }
389
654
  }
@@ -62,8 +62,8 @@ export default class DependencyAnalyzer {
62
62
  // Read and parse components.js
63
63
  const content = await fs.readFile(componentsConfigPath, 'utf-8');
64
64
 
65
- // Extract configuration using simple regex
66
- const configMatch = content.match(/export default\s*({[\s\S]*})/);
65
+ // Extract configuration using simple regex - look for the components object
66
+ const configMatch = content.match(/const components\s*=\s*({[\s\S]*?});/);
67
67
  if (!configMatch) {
68
68
  throw new Error('Could not parse components.js');
69
69
  }
@@ -71,9 +71,26 @@ export default class DependencyAnalyzer {
71
71
  // Evaluate safely (in production use a more robust parser)
72
72
  const config = eval(`(${configMatch[1]})`);
73
73
 
74
- // Save category metadata
75
- for (const [category, info] of Object.entries(config)) {
76
- const categoryPath = path.join(this.componentsPath, info.path.replace('../', ''));
74
+ // Group components by category
75
+ const categoryMap = new Map();
76
+
77
+ // Build category map from component assignments
78
+ for (const [componentName, categoryName] of Object.entries(config)) {
79
+ if (!categoryMap.has(categoryName)) {
80
+ categoryMap.set(categoryName, []);
81
+ }
82
+ categoryMap.get(categoryName).push(componentName);
83
+ }
84
+
85
+ // Process each category
86
+ for (const [categoryName, componentList] of categoryMap) {
87
+ // Determine category type based on category name
88
+ let categoryType = 'Visual'; // default
89
+ if (categoryName === 'Service') categoryType = 'Service';
90
+ if (categoryName === 'AppComponents') categoryType = 'Visual'; // AppComponents are visual
91
+
92
+ // Find category path
93
+ const categoryPath = path.join(this.componentsPath, categoryName);
77
94
 
78
95
  if (await fs.pathExists(categoryPath)) {
79
96
  const files = await fs.readdir(categoryPath);
@@ -82,11 +99,11 @@ export default class DependencyAnalyzer {
82
99
  const componentPath = path.join(categoryPath, file);
83
100
  const stat = await fs.stat(componentPath);
84
101
 
85
- if (stat.isDirectory()) {
102
+ if (stat.isDirectory() && componentList.includes(file)) {
86
103
  this.components.set(file, {
87
104
  name: file,
88
- category,
89
- categoryType: info.type,
105
+ category: categoryName,
106
+ categoryType: categoryType,
90
107
  path: componentPath,
91
108
  dependencies: new Set(),
92
109
  usedBy: new Set(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slicejs-cli",
3
- "version": "2.7.6",
3
+ "version": "2.7.8",
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
  }