slicejs-cli 2.9.5 → 3.0.2

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.
@@ -5,12 +5,12 @@ import crypto from 'crypto';
5
5
  import { parse } from '@babel/parser';
6
6
  import traverse from '@babel/traverse';
7
7
  import { minify as terserMinify } from 'terser';
8
- import { getSrcPath, getComponentsJsPath, getDistPath } from '../PathHelper.js';
8
+ import { getSrcPath, getComponentsJsPath, getDistPath, getConfigPath } from '../PathHelper.js';
9
9
 
10
10
  export default class BundleGenerator {
11
11
  constructor(moduleUrl, analysisData, options = {}) {
12
12
  this.moduleUrl = moduleUrl;
13
- this.analysisData = analysisData;
13
+ this.analysisData = analysisData || { components: [], routes: [], metrics: {} };
14
14
  this.srcPath = getSrcPath(moduleUrl);
15
15
  this.distPath = getDistPath(moduleUrl);
16
16
  this.output = options.output || 'src';
@@ -22,12 +22,17 @@ export default class BundleGenerator {
22
22
  minify: !!options.minify,
23
23
  obfuscate: !!options.obfuscate
24
24
  };
25
+ this.format = 'v2';
26
+ this.sliceConfig = this.resolveSliceConfig();
27
+ this.loadingPolicy = this.resolveLoadingPolicy();
25
28
 
26
29
  // Configuration
27
30
  this.config = {
28
31
  maxCriticalSize: 50 * 1024, // 50KB
29
32
  maxCriticalComponents: 15,
30
33
  minSharedUsage: 3, // Minimum routes to be considered "shared"
34
+ maxRouteBundleSize: 120 * 1024,
35
+ maxRouteRequests: 12,
31
36
  strategy: 'hybrid' // 'global', 'hybrid', 'per-route'
32
37
  };
33
38
 
@@ -41,6 +46,27 @@ export default class BundleGenerator {
41
46
  };
42
47
  }
43
48
 
49
+ resolveSliceConfig() {
50
+ if (this.analysisData?.sliceConfig && typeof this.analysisData.sliceConfig === 'object') {
51
+ return this.analysisData.sliceConfig;
52
+ }
53
+
54
+ try {
55
+ const configPath = getConfigPath(this.moduleUrl);
56
+ if (fs.existsSync(configPath)) {
57
+ return fs.readJsonSync(configPath);
58
+ }
59
+ } catch (error) {
60
+ console.warn('Warning: Could not read sliceConfig.json for loading policy:', error.message);
61
+ }
62
+
63
+ return {};
64
+ }
65
+
66
+ resolveLoadingPolicy() {
67
+ return this.sliceConfig?.loading?.enabled ? 'enabled' : 'disabled';
68
+ }
69
+
44
70
  /**
45
71
  * Computes deterministic integrity hash for bundle metadata.
46
72
  * @param {Array} components
@@ -154,6 +180,7 @@ export default class BundleGenerator {
154
180
  // Filter critical candidates
155
181
  const candidates = components
156
182
  .filter(comp => {
183
+ if (!this.isComponentAllowedByLoadingPolicy(comp)) return false;
157
184
  // Shared components (used in 3+ routes)
158
185
  const isShared = comp.routes.size >= this.config.minSharedUsage;
159
186
 
@@ -173,8 +200,8 @@ export default class BundleGenerator {
173
200
  return priorityB - priorityA;
174
201
  });
175
202
 
176
- const loadingComponent = components.find((comp) => comp.name === 'Loading');
177
- if (loadingComponent && !candidates.includes(loadingComponent)) {
203
+ const loadingComponent = components.find((comp) => comp.name === 'Loading' && this.isComponentAllowedByLoadingPolicy(comp));
204
+ if (this.loadingPolicy === 'enabled' && loadingComponent && !candidates.includes(loadingComponent)) {
178
205
  candidates.unshift(loadingComponent);
179
206
  }
180
207
 
@@ -203,6 +230,11 @@ export default class BundleGenerator {
203
230
  }
204
231
  }
205
232
 
233
+ if (this.loadingPolicy === 'disabled') {
234
+ this.bundles.critical.components = this.bundles.critical.components.filter((comp) => comp.name !== 'Loading');
235
+ this.bundles.critical.size = this.bundles.critical.components.reduce((sum, comp) => sum + comp.size, 0);
236
+ }
237
+
206
238
  console.log(`✓ Critical bundle: ${this.bundles.critical.components.length} components, ${(this.bundles.critical.size / 1024).toFixed(1)} KB`);
207
239
  }
208
240
 
@@ -217,6 +249,12 @@ export default class BundleGenerator {
217
249
  } else {
218
250
  this.assignPerRouteBundles(criticalNames);
219
251
  }
252
+
253
+ this.extractSharedComponents(criticalNames);
254
+ this.rebalanceBundlesByBudget(this.bundles.routes, {
255
+ maxBundleSize: this.config.maxRouteBundleSize,
256
+ maxRequests: this.config.maxRouteRequests
257
+ });
220
258
  }
221
259
 
222
260
  /**
@@ -240,7 +278,7 @@ export default class BundleGenerator {
240
278
 
241
279
  // Filter those already in critical
242
280
  const uniqueComponents = Array.from(allComponents).filter(comp =>
243
- !criticalNames.has(comp.name)
281
+ !criticalNames.has(comp.name) && this.isComponentAllowedByLoadingPolicy(comp)
244
282
  );
245
283
 
246
284
  if (uniqueComponents.length === 0) continue;
@@ -250,7 +288,7 @@ export default class BundleGenerator {
250
288
 
251
289
  this.bundles.routes[routeKey] = {
252
290
  path: routePath,
253
- components: uniqueComponents,
291
+ components: this.sortComponentsByName(uniqueComponents),
254
292
  size: totalSize,
255
293
  file: `slice-bundle.${routeKey}.js`
256
294
  };
@@ -313,7 +351,7 @@ export default class BundleGenerator {
313
351
 
314
352
  // Filter those already in critical
315
353
  const uniqueComponents = Array.from(allComponents).filter(comp =>
316
- !criticalNames.has(comp.name)
354
+ !criticalNames.has(comp.name) && this.isComponentAllowedByLoadingPolicy(comp)
317
355
  );
318
356
 
319
357
  if (uniqueComponents.length > 0) {
@@ -321,7 +359,7 @@ export default class BundleGenerator {
321
359
 
322
360
  this.bundles.routes[groupKey] = {
323
361
  paths: groupData.routes,
324
- components: uniqueComponents,
362
+ components: this.sortComponentsByName(uniqueComponents),
325
363
  size: totalSize,
326
364
  file: `slice-bundle.${this.routeToFileName(groupKey)}.js`
327
365
  };
@@ -368,7 +406,7 @@ export default class BundleGenerator {
368
406
 
369
407
  // Filter those already in critical
370
408
  const uniqueComponents = Array.from(allComponents).filter(comp =>
371
- !criticalNames.has(comp.name)
409
+ !criticalNames.has(comp.name) && this.isComponentAllowedByLoadingPolicy(comp)
372
410
  );
373
411
 
374
412
  if (uniqueComponents.length === 0) continue;
@@ -378,7 +416,7 @@ export default class BundleGenerator {
378
416
 
379
417
  this.bundles.routes[category] = {
380
418
  paths: routePaths,
381
- components: uniqueComponents,
419
+ components: this.sortComponentsByName(uniqueComponents),
382
420
  size: totalSize,
383
421
  file: `slice-bundle.${this.routeToFileName(category)}.js`
384
422
  };
@@ -387,6 +425,225 @@ export default class BundleGenerator {
387
425
  }
388
426
  }
389
427
 
428
+ isComponentAllowedByLoadingPolicy(component) {
429
+ if (!component) return false;
430
+ if (this.loadingPolicy === 'disabled' && component.name === 'Loading') {
431
+ return false;
432
+ }
433
+ return true;
434
+ }
435
+
436
+ sortComponentsByName(components) {
437
+ return [...components].sort((a, b) => a.name.localeCompare(b.name));
438
+ }
439
+
440
+ dedupeComponentsByName(components = []) {
441
+ const byName = new Map();
442
+ for (const component of components) {
443
+ if (!component?.name) continue;
444
+ if (!byName.has(component.name)) {
445
+ byName.set(component.name, component);
446
+ }
447
+ }
448
+ return this.sortComponentsByName(Array.from(byName.values()));
449
+ }
450
+
451
+ getBundlePaths(bundle = {}) {
452
+ const raw = Array.isArray(bundle.paths)
453
+ ? bundle.paths
454
+ : Array.isArray(bundle.path)
455
+ ? bundle.path
456
+ : bundle.path
457
+ ? [bundle.path]
458
+ : [];
459
+
460
+ return Array.from(new Set(raw.filter(Boolean))).sort((a, b) => a.localeCompare(b));
461
+ }
462
+
463
+ setBundlePaths(bundle, paths = []) {
464
+ const mergedPaths = Array.from(new Set((paths || []).filter(Boolean))).sort((a, b) => a.localeCompare(b));
465
+ if (mergedPaths.length === 0) {
466
+ delete bundle.path;
467
+ delete bundle.paths;
468
+ return;
469
+ }
470
+ if (mergedPaths.length === 1) {
471
+ bundle.path = mergedPaths[0];
472
+ delete bundle.paths;
473
+ return;
474
+ }
475
+ bundle.paths = mergedPaths;
476
+ delete bundle.path;
477
+ }
478
+
479
+ mergeBundleDependencies(...dependencyLists) {
480
+ const merged = [];
481
+ const append = (dep) => {
482
+ if (!dep || merged.includes(dep)) return;
483
+ if (dep === 'critical') {
484
+ merged.unshift(dep);
485
+ return;
486
+ }
487
+ merged.push(dep);
488
+ };
489
+
490
+ dependencyLists
491
+ .flat()
492
+ .forEach(append);
493
+
494
+ if (!merged.includes('critical')) {
495
+ merged.unshift('critical');
496
+ }
497
+
498
+ const rest = merged
499
+ .filter((dep) => dep !== 'critical')
500
+ .sort((a, b) => a.localeCompare(b));
501
+ return ['critical', ...rest];
502
+ }
503
+
504
+ extractSharedComponents(criticalNames) {
505
+ const usage = new Map();
506
+
507
+ for (const bundle of Object.values(this.bundles.routes)) {
508
+ const seenInBundle = new Set();
509
+ for (const component of this.dedupeComponentsByName(bundle.components || [])) {
510
+ if (criticalNames.has(component.name)) continue;
511
+ if (!this.isComponentAllowedByLoadingPolicy(component)) continue;
512
+ if (seenInBundle.has(component.name)) continue;
513
+ seenInBundle.add(component.name);
514
+ if (!usage.has(component.name)) {
515
+ usage.set(component.name, { component, count: 0 });
516
+ }
517
+ usage.get(component.name).count += 1;
518
+ }
519
+ }
520
+
521
+ const sharedComponents = Array.from(usage.values())
522
+ .filter((entry) => entry.count >= this.config.minSharedUsage)
523
+ .map((entry) => entry.component);
524
+
525
+ if (sharedComponents.length === 0) {
526
+ return;
527
+ }
528
+
529
+ const sharedSet = new Set(sharedComponents.map((component) => component.name));
530
+ const orderedShared = this.sortComponentsByName(sharedComponents);
531
+
532
+ for (const bundle of Object.values(this.bundles.routes)) {
533
+ const original = this.dedupeComponentsByName(bundle.components || []);
534
+ const filtered = original.filter((component) => !sharedSet.has(component.name));
535
+ const removedShared = original.length - filtered.length;
536
+ bundle.components = this.sortComponentsByName(filtered);
537
+ bundle.size = bundle.components.reduce((sum, component) => sum + component.size, 0);
538
+ if (removedShared > 0) {
539
+ bundle.dependencies = this.mergeBundleDependencies(bundle.dependencies || [], ['shared-core']);
540
+ }
541
+ }
542
+
543
+ this.bundles.routes['shared-core'] = {
544
+ paths: [],
545
+ components: orderedShared,
546
+ size: orderedShared.reduce((sum, component) => sum + component.size, 0),
547
+ file: `slice-bundle.${this.routeToFileName('shared-core')}.js`
548
+ };
549
+
550
+ for (const [key, bundle] of Object.entries(this.bundles.routes)) {
551
+ if (key === 'shared-core') continue;
552
+ if ((bundle.components || []).length === 0) {
553
+ delete this.bundles.routes[key];
554
+ }
555
+ }
556
+ }
557
+
558
+ rebalanceBundlesByBudget(bundles, limits = {}) {
559
+ const maxBundleSize = limits.maxBundleSize || this.config.maxRouteBundleSize;
560
+ const maxRequests = limits.maxRequests || this.config.maxRouteRequests;
561
+ const orderedEntries = Object.entries(bundles)
562
+ .sort(([a], [b]) => a.localeCompare(b));
563
+ const rebalanced = {};
564
+
565
+ for (const [key, bundle] of orderedEntries) {
566
+ const sortedComponents = this.dedupeComponentsByName(bundle.components || []);
567
+ const totalSize = sortedComponents.reduce((sum, component) => sum + component.size, 0);
568
+ if (totalSize <= maxBundleSize || sortedComponents.length <= 1) {
569
+ rebalanced[key] = {
570
+ ...bundle,
571
+ components: sortedComponents,
572
+ size: totalSize,
573
+ file: `slice-bundle.${this.routeToFileName(key)}.js`
574
+ };
575
+ continue;
576
+ }
577
+
578
+ let partIndex = 1;
579
+ let currentChunk = [];
580
+ let currentSize = 0;
581
+
582
+ for (const component of sortedComponents) {
583
+ const nextSize = currentSize + component.size;
584
+ const shouldFlush = currentChunk.length > 0 && nextSize > maxBundleSize;
585
+
586
+ if (shouldFlush) {
587
+ const partKey = `${key}--p${partIndex}`;
588
+ rebalanced[partKey] = {
589
+ ...bundle,
590
+ components: currentChunk,
591
+ size: currentSize,
592
+ file: `slice-bundle.${this.routeToFileName(partKey)}.js`
593
+ };
594
+ partIndex += 1;
595
+ currentChunk = [];
596
+ currentSize = 0;
597
+ }
598
+
599
+ currentChunk.push(component);
600
+ currentSize += component.size;
601
+ }
602
+
603
+ if (currentChunk.length > 0) {
604
+ const partKey = `${key}--p${partIndex}`;
605
+ rebalanced[partKey] = {
606
+ ...bundle,
607
+ components: currentChunk,
608
+ size: currentSize,
609
+ file: `slice-bundle.${this.routeToFileName(partKey)}.js`
610
+ };
611
+ }
612
+ }
613
+
614
+ const keys = Object.keys(rebalanced).sort((a, b) => a.localeCompare(b));
615
+ while (keys.length > maxRequests) {
616
+ const lastKey = keys.pop();
617
+ const targetKey = keys[keys.length - 1];
618
+ if (!lastKey || !targetKey) break;
619
+ const mergedComponents = this.dedupeComponentsByName([
620
+ ...(rebalanced[targetKey].components || []),
621
+ ...(rebalanced[lastKey].components || [])
622
+ ]);
623
+ const mergedPaths = [
624
+ ...this.getBundlePaths(rebalanced[targetKey]),
625
+ ...this.getBundlePaths(rebalanced[lastKey])
626
+ ];
627
+ const mergedDependencies = this.mergeBundleDependencies(
628
+ rebalanced[targetKey].dependencies || [],
629
+ rebalanced[lastKey].dependencies || []
630
+ );
631
+
632
+ rebalanced[targetKey].components = mergedComponents;
633
+ rebalanced[targetKey].size = mergedComponents.reduce((sum, component) => sum + component.size, 0);
634
+ rebalanced[targetKey].dependencies = mergedDependencies;
635
+ this.setBundlePaths(rebalanced[targetKey], mergedPaths);
636
+ delete rebalanced[lastKey];
637
+ }
638
+
639
+ Object.keys(bundles).forEach((key) => delete bundles[key]);
640
+ for (const [key, bundle] of Object.entries(rebalanced).sort(([a], [b]) => a.localeCompare(b))) {
641
+ bundles[key] = bundle;
642
+ }
643
+
644
+ return bundles;
645
+ }
646
+
390
647
  /**
391
648
  * Categorizes a route path for grouping, considering MultiRoute context
392
649
  */
@@ -674,15 +931,14 @@ export default class BundleGenerator {
674
931
  * Generates the content of a bundle
675
932
  */
676
933
  async generateBundleContent(components, type, routePath, bundleKey, fileName) {
677
- const componentsData = {};
934
+ const bundleComponents = [];
935
+ const uniqueComponents = this.dedupeComponentsByName(components || []);
678
936
 
679
- for (const comp of components) {
937
+ for (const comp of uniqueComponents) {
680
938
  const fileBaseName = comp.fileName || comp.name;
681
939
  const jsPath = path.join(comp.path, `${fileBaseName}.js`);
682
940
  const jsContent = await fs.readFile(jsPath, 'utf-8');
683
941
 
684
- const dependencyContents = await this.buildDependencyContents(jsContent, comp.path);
685
-
686
942
  let htmlContent = null;
687
943
  let cssContent = null;
688
944
 
@@ -697,36 +953,130 @@ export default class BundleGenerator {
697
953
  cssContent = await fs.readFile(cssPath, 'utf-8');
698
954
  }
699
955
 
700
- const componentKey = comp.isFramework ? `Framework/Structural/${comp.name}` : comp.name;
701
- componentsData[componentKey] = {
956
+ bundleComponents.push({
702
957
  name: comp.name,
703
958
  category: comp.category,
704
959
  categoryType: comp.categoryType,
705
- isFramework: !!comp.isFramework,
706
960
  js: this.cleanJavaScript(jsContent, comp.name),
707
- externalDependencies: dependencyContents, // Files imported with import statements
708
- componentDependencies: Array.from(comp.dependencies), // Other components this one depends on
709
961
  html: htmlContent,
710
962
  css: cssContent,
963
+ externalDependencies: await this.buildDependencyContents(jsContent, comp.path),
711
964
  size: comp.size
712
- };
965
+ });
713
966
  }
714
967
 
968
+ return this.generateBundleFileContent(fileName, type, this.sortComponentsByName(bundleComponents), routePath);
969
+ }
970
+
971
+ classFactoryName(componentName) {
972
+ return `SLICE_CLASS_FACTORY_${this.toSafeIdentifier(componentName)}`;
973
+ }
974
+
975
+ indentCodeBlock(code, spaces = 2) {
976
+ const indentation = ' '.repeat(spaces);
977
+ return String(code)
978
+ .split('\n')
979
+ .map((line) => `${indentation}${line}`)
980
+ .join('\n');
981
+ }
982
+
983
+ generateBundleFileContent(fileName, type, components, routePath = null) {
984
+ const uniqueComponents = this.dedupeComponentsByName(components || []);
985
+ const bundleKey = type === 'critical'
986
+ ? 'critical'
987
+ : type === 'framework'
988
+ ? 'framework'
989
+ : this.routeToFileName(routePath || fileName.replace('slice-bundle.', '').replace('.js', ''));
990
+
991
+ const dependencyModuleBlock = this.buildV2DependencyModuleBlock(uniqueComponents);
992
+
993
+ const classFactoryDefinitions = uniqueComponents
994
+ .map((component) => {
995
+ const factoryName = this.classFactoryName(component.name);
996
+ const dependencyBindings = this.buildDependencyBindings(component.externalDependencies || {});
997
+ const body = component.js && component.js.trim()
998
+ ? component.js
999
+ : `return window.${component.name};`;
1000
+ const bodyWithBindings = dependencyBindings
1001
+ ? `${dependencyBindings}\n${body}`
1002
+ : body;
1003
+ return `const ${factoryName} = () => {\n${this.indentCodeBlock(bodyWithBindings, 2)}\n};`;
1004
+ })
1005
+ .join('\n\n');
1006
+
1007
+ const templateDeclarations = uniqueComponents
1008
+ .map((component) => {
1009
+ const templateVarName = `__templateElement_${this.toSafeIdentifier(component.name)}`;
1010
+ return `const ${templateVarName} = document.createElement('template');\n${templateVarName}.innerHTML = ${JSON.stringify(component.html || '')};`;
1011
+ })
1012
+ .join('\n');
1013
+
1014
+ const classRegistrations = uniqueComponents
1015
+ .map((component) => {
1016
+ const componentName = JSON.stringify(component.name);
1017
+ return ` if (!controller.classes.has(${componentName})) {\n controller.classes.set(${componentName}, ${this.classFactoryName(component.name)}());\n }`;
1018
+ })
1019
+ .join('\n');
1020
+
1021
+ const templateRegistrations = uniqueComponents
1022
+ .map((component) => {
1023
+ const componentName = JSON.stringify(component.name);
1024
+ const templateVarName = `__templateElement_${this.toSafeIdentifier(component.name)}`;
1025
+ return ` if (!controller.templates.has(${componentName})) {\n controller.templates.set(${componentName}, ${templateVarName});\n }`;
1026
+ })
1027
+ .join('\n');
1028
+
1029
+ const cssRegistrationInit = uniqueComponents.length
1030
+ ? ` if (!stylesManager.__sliceRegisteredComponentStyles) {\n stylesManager.__sliceRegisteredComponentStyles = new Set();\n }`
1031
+ : '';
1032
+
1033
+ const cssRegistrations = uniqueComponents
1034
+ .map((component) => {
1035
+ const componentName = JSON.stringify(component.name);
1036
+ return ` if (!stylesManager.__sliceRegisteredComponentStyles.has(${componentName})) {\n stylesManager.registerComponentStyles(${componentName}, ${JSON.stringify(component.css || '')});\n stylesManager.__sliceRegisteredComponentStyles.add(${componentName});\n }`;
1037
+ })
1038
+ .join('\n');
1039
+
1040
+ const categoryRegistrations = uniqueComponents
1041
+ .map((component) => {
1042
+ const componentName = JSON.stringify(component.name);
1043
+ return ` if (!controller.componentCategories.has(${componentName})) {\n controller.componentCategories.set(${componentName}, ${JSON.stringify(component.category)});\n }`;
1044
+ })
1045
+ .join('\n');
1046
+
715
1047
  const metadata = {
716
- version: '2.0.0',
717
- type,
718
- route: routePath,
1048
+ version: '2',
719
1049
  bundleKey,
720
- file: fileName,
721
- generated: new Date().toISOString(),
722
- totalSize: components.reduce((sum, c) => sum + c.size, 0),
723
- componentCount: components.length,
724
- strategy: this.config.strategy,
725
- minified: this.options.minify,
726
- obfuscated: this.options.obfuscate
1050
+ type,
1051
+ routes: routePath ? [routePath] : [],
1052
+ componentCount: uniqueComponents.length
727
1053
  };
728
1054
 
729
- return this.formatBundleFile(componentsData, metadata);
1055
+ return `export const SLICE_BUNDLE_META = ${JSON.stringify(metadata, null, 2)};\n\n${dependencyModuleBlock}\n\n${classFactoryDefinitions}\n\n${templateDeclarations}\n\nexport async function registerAll(controller, stylesManager) {\n${classRegistrations}\n${templateRegistrations}\n${cssRegistrationInit}${cssRegistrationInit ? '\n' : ''}${cssRegistrations}\n${categoryRegistrations}\n}\n`;
1056
+ }
1057
+
1058
+ buildV2DependencyModuleBlock(components) {
1059
+ const modules = new Map();
1060
+ for (const component of components || []) {
1061
+ const externalDependencies = component.externalDependencies || {};
1062
+ for (const [moduleName, entry] of Object.entries(externalDependencies)) {
1063
+ if (modules.has(moduleName)) continue;
1064
+ const content = typeof entry === 'string' ? entry : entry?.content;
1065
+ if (!content) continue;
1066
+ modules.set(moduleName, { name: moduleName, content });
1067
+ }
1068
+ }
1069
+
1070
+ const lines = ['const SLICE_BUNDLE_DEPENDENCIES = {};'];
1071
+ Array.from(modules.values()).forEach((module, index) => {
1072
+ const exportVar = `__sliceDepExports${index}`;
1073
+ const transformedContent = this.transformDependencyContent(module.content, exportVar, module.name);
1074
+ lines.push(`const ${exportVar} = {};`);
1075
+ lines.push(transformedContent.trim());
1076
+ lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
1077
+ });
1078
+
1079
+ return lines.join('\n');
730
1080
  }
731
1081
 
732
1082
  async buildDependencyContents(jsContent, componentPath) {
@@ -979,8 +1329,11 @@ if (window.slice && window.slice.controller) {
979
1329
  * Generates the bundle configuration
980
1330
  */
981
1331
  generateBundleConfig(frameworkBundle = null) {
1332
+ const metrics = this.analysisData.metrics || {};
982
1333
  const config = {
983
1334
  version: '2.0.0',
1335
+ format: this.format,
1336
+ loadingPolicy: this.loadingPolicy,
984
1337
  strategy: this.config.strategy,
985
1338
  minified: this.options.minify,
986
1339
  obfuscated: this.options.obfuscate,
@@ -988,11 +1341,11 @@ if (window.slice && window.slice.controller) {
988
1341
  generated: new Date().toISOString(),
989
1342
 
990
1343
  stats: {
991
- totalComponents: this.analysisData.metrics.totalComponents,
992
- totalRoutes: this.analysisData.metrics.totalRoutes,
1344
+ totalComponents: metrics.totalComponents || 0,
1345
+ totalRoutes: metrics.totalRoutes || 0,
993
1346
  sharedComponents: this.bundles.critical.components.length,
994
- sharedPercentage: this.analysisData.metrics.sharedPercentage,
995
- totalSize: this.analysisData.metrics.totalSize,
1347
+ sharedPercentage: metrics.sharedPercentage || 0,
1348
+ totalSize: metrics.totalSize || 0,
996
1349
  criticalSize: this.bundles.critical.size
997
1350
  },
998
1351
 
@@ -1020,6 +1373,7 @@ if (window.slice && window.slice.controller) {
1020
1373
  const routeIdentifier = Array.isArray(bundle.path || bundle.paths)
1021
1374
  ? key
1022
1375
  : (bundle.path || bundle.paths || key);
1376
+ const dependencies = this.mergeBundleDependencies(bundle.dependencies || []);
1023
1377
 
1024
1378
  config.bundles.routes[key] = {
1025
1379
  path: bundle.path || bundle.paths || key, // Support both single path and array of paths, fallback to key
@@ -1028,7 +1382,7 @@ if (window.slice && window.slice.controller) {
1028
1382
  hash: bundle.hash || null,
1029
1383
  integrity: bundle.integrity || null,
1030
1384
  components: bundle.components.map(c => c.name),
1031
- dependencies: ['critical']
1385
+ dependencies
1032
1386
  };
1033
1387
 
1034
1388
  const paths = Array.isArray(config.bundles.routes[key].path)
@@ -1039,6 +1393,11 @@ if (window.slice && window.slice.controller) {
1039
1393
  if (!config.routeBundles[routePath]) {
1040
1394
  config.routeBundles[routePath] = ['critical'];
1041
1395
  }
1396
+ for (const dependency of dependencies.filter((dep) => dep !== 'critical')) {
1397
+ if (!config.routeBundles[routePath].includes(dependency)) {
1398
+ config.routeBundles[routePath].push(dependency);
1399
+ }
1400
+ }
1042
1401
  if (!config.routeBundles[routePath].includes(key)) {
1043
1402
  config.routeBundles[routePath].push(key);
1044
1403
  }
@@ -13,7 +13,7 @@ import fs from "fs-extra";
13
13
 
14
14
  const execAsync = promisify(exec);
15
15
 
16
- class UpdateManager {
16
+ export class UpdateManager {
17
17
  constructor() {
18
18
  this.packagesToUpdate = [];
19
19
  }
@@ -46,7 +46,9 @@ class UpdateManager {
46
46
  /**
47
47
  * Check for available updates and return structured info
48
48
  */
49
- async checkForUpdates() {
49
+ async checkForUpdates(options = {}) {
50
+ const { silentErrors = false } = options;
51
+
50
52
  try {
51
53
  const updateInfo = await versionChecker.checkForUpdates(true); // Silent mode
52
54
 
@@ -82,11 +84,25 @@ class UpdateManager {
82
84
  allCurrent: updateInfo.cli.status === 'current' && updateInfo.framework.status === 'current'
83
85
  };
84
86
  } catch (error) {
85
- Print.error(`Checking for updates: ${error.message}`);
87
+ if (!silentErrors) {
88
+ Print.error(`Checking for updates: ${error.message}`);
89
+ }
86
90
  return null;
87
91
  }
88
92
  }
89
93
 
94
+ async notifyAvailableUpdates() {
95
+ const updateInfo = await this.checkForUpdates({ silentErrors: true });
96
+
97
+ if (!updateInfo || !updateInfo.hasUpdates) {
98
+ return false;
99
+ }
100
+
101
+ this.displayUpdates(updateInfo);
102
+ Print.info("Run 'slice update' to install updates when convenient.");
103
+ return true;
104
+ }
105
+
90
106
  /**
91
107
  * Display available updates in a formatted way
92
108
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slicejs-cli",
3
- "version": "2.9.5",
3
+ "version": "3.0.2",
4
4
  "description": "Command client for developing web applications with Slice.js framework",
5
5
  "main": "client.js",
6
6
  "bin": {