slicejs-cli 2.8.6 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,783 +1,1331 @@
1
- // cli/utils/bundling/BundleGenerator.js
2
- import fs from 'fs-extra';
3
- import path from 'path';
4
- import crypto from 'crypto';
5
- import { parse } from '@babel/parser';
6
- import traverse from '@babel/traverse';
7
- import { getSrcPath, getComponentsJsPath } from '../PathHelper.js';
8
-
9
- export default class BundleGenerator {
10
- constructor(moduleUrl, analysisData) {
11
- this.moduleUrl = moduleUrl;
12
- this.analysisData = analysisData;
13
- this.srcPath = getSrcPath(moduleUrl);
14
- this.bundlesPath = path.join(this.srcPath, 'bundles');
15
- this.componentsPath = path.dirname(getComponentsJsPath(moduleUrl));
16
-
17
- // Configuration
18
- this.config = {
19
- maxCriticalSize: 50 * 1024, // 50KB
20
- maxCriticalComponents: 15,
21
- minSharedUsage: 3, // Minimum routes to be considered "shared"
22
- strategy: 'hybrid' // 'global', 'hybrid', 'per-route'
23
- };
24
-
25
- this.bundles = {
26
- critical: {
27
- components: [],
28
- size: 0,
29
- file: 'slice-bundle.critical.js'
30
- },
31
- routes: {}
32
- };
33
- }
34
-
35
- /**
36
- * Generates all bundles
37
- */
38
- async generate() {
39
- console.log('🔨 Generating bundles...');
40
-
41
- // 0. Create bundles directory
42
- await fs.ensureDir(this.bundlesPath);
43
-
44
- // 1. Determine optimal strategy
45
- this.determineStrategy();
46
-
47
- // 2. Identify critical components
48
- this.identifyCriticalComponents();
49
-
50
- // 3. Assign components to routes
51
- this.assignRouteComponents();
52
-
53
- // 4. Generate bundle files
54
- const files = await this.generateBundleFiles();
55
-
56
- // 5. Generate configuration
57
- const config = this.generateBundleConfig();
58
-
59
- console.log('✅ Bundles generated successfully');
60
-
61
- return {
62
- bundles: this.bundles,
63
- config,
64
- files
65
- };
66
- }
67
-
68
- /**
69
- * Determines the optimal bundling strategy
70
- */
71
- determineStrategy() {
72
- const { metrics } = this.analysisData;
73
- const { totalComponents, sharedPercentage } = metrics;
74
-
75
- // Strategy based on size and usage pattern
76
- if (totalComponents < 20 || sharedPercentage > 60) {
77
- this.config.strategy = 'global';
78
- console.log('📦 Strategy: Global Bundle (small project or highly shared)');
79
- } else if (totalComponents < 100) {
80
- this.config.strategy = 'hybrid';
81
- console.log('📦 Strategy: Hybrid (critical + grouped routes)');
82
- } else {
83
- this.config.strategy = 'per-route';
84
- console.log('📦 Strategy: Per Route (large project)');
85
- }
86
- }
87
-
88
- /**
89
- * Identifies critical components for the initial bundle
90
- */
91
- identifyCriticalComponents() {
92
- const { components } = this.analysisData;
93
-
94
- // Filter critical candidates
95
- const candidates = components
96
- .filter(comp => {
97
- // Shared components (used in 3+ routes)
98
- const isShared = comp.routes.size >= this.config.minSharedUsage;
99
-
100
- // Structural components (Navbar, Footer, etc.)
101
- const isStructural = comp.categoryType === 'Structural' ||
102
- ['Navbar', 'Footer', 'Layout'].includes(comp.name);
103
-
104
- // Small and highly used components (only if used in 3+ routes)
105
- const isSmallAndUseful = comp.size < 2000 && comp.routes.size >= 3;
106
-
107
- return isShared || isStructural || isSmallAndUseful;
108
- })
109
- .sort((a, b) => {
110
- // Prioritize by: (usage * 10) - size
111
- const priorityA = (a.routes.size * 10) - (a.size / 1000);
112
- const priorityB = (b.routes.size * 10) - (b.size / 1000);
113
- return priorityB - priorityA;
114
- });
115
-
116
- // Fill critical bundle up to limit
117
- for (const comp of candidates) {
118
- const dependencies = this.getComponentDependencies(comp);
119
- const totalSize = comp.size + dependencies.reduce((sum, dep) => sum + dep.size, 0);
120
- const totalCount = 1 + dependencies.length;
121
-
122
- const wouldExceedSize = this.bundles.critical.size + totalSize > this.config.maxCriticalSize;
123
- const wouldExceedCount = this.bundles.critical.components.length + totalCount > this.config.maxCriticalComponents;
124
-
125
- if (wouldExceedSize || wouldExceedCount) continue;
126
-
127
- // Add component and its dependencies
128
- if (!this.bundles.critical.components.find(c => c.name === comp.name)) {
129
- this.bundles.critical.components.push(comp);
130
- this.bundles.critical.size += comp.size;
131
- }
132
-
133
- for (const dep of dependencies) {
134
- if (!this.bundles.critical.components.find(c => c.name === dep.name)) {
135
- this.bundles.critical.components.push(dep);
136
- this.bundles.critical.size += dep.size;
137
- }
138
- }
139
- }
140
-
141
- console.log(`✓ Critical bundle: ${this.bundles.critical.components.length} components, ${(this.bundles.critical.size / 1024).toFixed(1)} KB`);
142
- }
143
-
144
- /**
145
- * Assigns remaining components to route bundles
146
- */
147
- assignRouteComponents() {
148
- const criticalNames = new Set(this.bundles.critical.components.map(c => c.name));
149
-
150
- if (this.config.strategy === 'hybrid') {
151
- this.assignHybridBundles(criticalNames);
152
- } else {
153
- this.assignPerRouteBundles(criticalNames);
154
- }
155
- }
156
-
157
- /**
158
- * Assigns components to per-route bundles
159
- */
160
- assignPerRouteBundles(criticalNames) {
161
- for (const route of this.analysisData.routes) {
162
- const routePath = route.path;
163
- // Get all route dependencies
164
- const routeComponents = this.getRouteComponents(route.component);
165
-
166
- // Include dependencies for all route components
167
- const allComponents = new Set();
168
- for (const comp of routeComponents) {
169
- allComponents.add(comp);
170
- const dependencies = this.getComponentDependencies(comp);
171
- for (const dep of dependencies) {
172
- allComponents.add(dep);
173
- }
174
- }
175
-
176
- // Filter those already in critical
177
- const uniqueComponents = Array.from(allComponents).filter(comp =>
178
- !criticalNames.has(comp.name)
179
- );
180
-
181
- if (uniqueComponents.length === 0) continue;
182
-
183
- const routeKey = this.routeToFileName(routePath);
184
- const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
185
-
186
- this.bundles.routes[routeKey] = {
187
- path: routePath,
188
- components: uniqueComponents,
189
- size: totalSize,
190
- file: `slice-bundle.${routeKey}.js`
191
- };
192
-
193
- console.log(`✓ Bundle ${routeKey}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB`);
194
- }
195
- }
196
-
197
- /**
198
- * Gets all component dependencies transitively
199
- */
200
- getComponentDependencies(component, visited = new Set()) {
201
- if (visited.has(component.name)) return [];
202
- visited.add(component.name);
203
-
204
- const dependencies = [];
205
-
206
- // Add direct dependencies
207
- for (const depName of component.dependencies) {
208
- const depComp = this.analysisData.components.find(c => c.name === depName);
209
- if (depComp && !visited.has(depName)) {
210
- dependencies.push(depComp);
211
- // Add transitive dependencies
212
- dependencies.push(...this.getComponentDependencies(depComp, visited));
213
- }
214
- }
215
-
216
- return dependencies;
217
- }
218
-
219
- /**
220
- * Assigns components to hybrid bundles (grouped by category)
221
- */
222
- assignHybridBundles(criticalNames) {
223
- const routeGroups = new Map();
224
-
225
- // First, handle MultiRoute groups
226
- if (this.analysisData.routeGroups) {
227
- for (const [groupKey, groupData] of this.analysisData.routeGroups) {
228
- if (groupData.type === 'multiroute') {
229
- // Create a bundle for this MultiRoute group
230
- const allComponents = new Set();
231
-
232
- // Add the main component (MultiRoute handler)
233
- const mainComponent = this.analysisData.components.find(c => c.name === groupData.component);
234
- if (mainComponent) {
235
- allComponents.add(mainComponent);
236
-
237
- // Add all components used by this MultiRoute
238
- const routeComponents = this.getRouteComponents(mainComponent.name);
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) {
255
- const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
256
-
257
- this.bundles.routes[groupKey] = {
258
- paths: groupData.routes,
259
- components: uniqueComponents,
260
- size: totalSize,
261
- file: `slice-bundle.${this.routeToFileName(groupKey)}.js`
262
- };
263
-
264
- console.log(`✓ Bundle ${groupKey}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB (${groupData.routes.length} routes)`);
265
- }
266
- }
267
- }
268
- }
269
-
270
- // Group remaining routes by category (skip those already handled by MultiRoute)
271
- for (const route of this.analysisData.routes) {
272
- // Check if this route is already handled by a MultiRoute group
273
- const isHandledByMultiRoute = this.analysisData.routeGroups &&
274
- Array.from(this.analysisData.routeGroups.values()).some(group =>
275
- group.type === 'multiroute' && group.routes.includes(route.path)
276
- );
277
-
278
- if (!isHandledByMultiRoute) {
279
- const category = this.categorizeRoute(route.path);
280
- if (!routeGroups.has(category)) {
281
- routeGroups.set(category, []);
282
- }
283
- routeGroups.get(category).push(route);
284
- }
285
- }
286
-
287
- // Create bundles for each group
288
- for (const [category, routes] of routeGroups) {
289
- const allComponents = new Set();
290
-
291
- // Collect all unique components for this category (including dependencies)
292
- for (const route of routes) {
293
- const routeComponents = this.getRouteComponents(route.component);
294
- for (const comp of routeComponents) {
295
- allComponents.add(comp);
296
- // Add transitive dependencies
297
- const dependencies = this.getComponentDependencies(comp);
298
- for (const dep of dependencies) {
299
- allComponents.add(dep);
300
- }
301
- }
302
- }
303
-
304
- // Filter those already in critical
305
- const uniqueComponents = Array.from(allComponents).filter(comp =>
306
- !criticalNames.has(comp.name)
307
- );
308
-
309
- if (uniqueComponents.length === 0) continue;
310
-
311
- const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
312
- const routePaths = routes.map(r => r.path);
313
-
314
- this.bundles.routes[category] = {
315
- paths: routePaths,
316
- components: uniqueComponents,
317
- size: totalSize,
318
- file: `slice-bundle.${this.routeToFileName(category)}.js`
319
- };
320
-
321
- console.log(`✓ Bundle ${category}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB (${routes.length} routes)`);
322
- }
323
- }
324
-
325
- /**
326
- * Categorizes a route path for grouping, considering MultiRoute context
327
- */
328
- categorizeRoute(routePath) {
329
- // Check if this route belongs to a MultiRoute handler
330
- if (this.analysisData.routeGroups) {
331
- for (const [groupKey, groupData] of this.analysisData.routeGroups) {
332
- if (groupData.type === 'multiroute' && groupData.routes.includes(routePath)) {
333
- return groupKey; // Return the MultiRoute group key
334
- }
335
- }
336
- }
337
-
338
- // Default categorization
339
- const path = routePath.toLowerCase();
340
-
341
- if (path === '/' || path === '/home') return 'home';
342
- if (path.includes('docum') || path.includes('documentation')) return 'documentation';
343
- if (path.includes('component') || path.includes('visual') || path.includes('card') ||
344
- path.includes('button') || path.includes('input') || path.includes('switch') ||
345
- path.includes('checkbox') || path.includes('select') || path.includes('details') ||
346
- path.includes('grid') || path.includes('loading') || path.includes('layout') ||
347
- path.includes('navbar') || path.includes('treeview') || path.includes('multiroute')) return 'components';
348
- if (path.includes('theme') || path.includes('slice') || path.includes('config')) return 'configuration';
349
- if (path.includes('routing') || path.includes('guard')) return 'routing';
350
- if (path.includes('service') || path.includes('command')) return 'services';
351
- if (path.includes('structural') || path.includes('lifecycle') || path.includes('static') ||
352
- path.includes('build')) return 'advanced';
353
- if (path.includes('playground') || path.includes('creator')) return 'tools';
354
- if (path.includes('about') || path.includes('404')) return 'misc';
355
-
356
- return 'general';
357
- }
358
-
359
- /**
360
- * Gets all components needed for a route
361
- */
362
- getRouteComponents(componentName) {
363
- const result = [];
364
- const visited = new Set();
365
-
366
- const traverse = (name) => {
367
- if (visited.has(name)) return;
368
- visited.add(name);
369
-
370
- const component = this.analysisData.components.find(c => c.name === name);
371
- if (!component) return;
372
-
373
- result.push(component);
374
-
375
- // Add dependencies recursively
376
- for (const dep of component.dependencies) {
377
- traverse(dep);
378
- }
379
- };
380
-
381
- traverse(componentName);
382
- return result;
383
- }
384
-
385
- /**
386
- * Generates the physical bundle files
387
- */
388
- async generateBundleFiles() {
389
- const files = [];
390
-
391
- // 1. Critical bundle
392
- if (this.bundles.critical.components.length > 0) {
393
- const criticalFile = await this.createBundleFile(
394
- this.bundles.critical.components,
395
- 'critical',
396
- null
397
- );
398
- files.push(criticalFile);
399
- }
400
-
401
- // 2. Route bundles
402
- for (const [routeKey, bundle] of Object.entries(this.bundles.routes)) {
403
- const routeIdentifier = Array.isArray(bundle.path || bundle.paths)
404
- ? routeKey
405
- : (bundle.path || bundle.paths || routeKey);
406
-
407
- const routeFile = await this.createBundleFile(
408
- bundle.components,
409
- 'route',
410
- routeIdentifier
411
- );
412
- files.push(routeFile);
413
- }
414
-
415
- return files;
416
- }
417
-
418
- /**
419
- * Creates a bundle file
420
- */
421
- async createBundleFile(components, type, routePath) {
422
- const routeKey = routePath ? this.routeToFileName(routePath) : 'critical';
423
- const fileName = `slice-bundle.${routeKey}.js`;
424
- const filePath = path.join(this.bundlesPath, fileName);
425
-
426
- const bundleContent = await this.generateBundleContent(
427
- components,
428
- type,
429
- routePath
430
- );
431
-
432
- await fs.writeFile(filePath, bundleContent, 'utf-8');
433
-
434
- const hash = crypto.createHash('md5').update(bundleContent).digest('hex').substring(0, 12);
435
-
436
- return {
437
- name: routeKey,
438
- file: fileName,
439
- path: filePath,
440
- size: Buffer.byteLength(bundleContent, 'utf-8'),
441
- hash,
442
- componentCount: components.length
443
- };
444
- }
445
-
446
- /**
447
- * Analyzes dependencies of a JavaScript file using simple regex
448
- */
449
- analyzeDependencies(jsContent, componentPath) {
450
- const dependencies = [];
451
-
452
- const resolveImportPath = (importPath) => {
453
- const resolvedPath = path.resolve(componentPath, importPath);
454
- let finalPath = resolvedPath;
455
- const ext = path.extname(resolvedPath);
456
- if (!ext) {
457
- const extensions = ['.js', '.json', '.mjs'];
458
- for (const extension of extensions) {
459
- if (fs.existsSync(resolvedPath + extension)) {
460
- finalPath = resolvedPath + extension;
461
- break;
462
- }
463
- }
464
- }
465
-
466
- return fs.existsSync(finalPath) ? finalPath : null;
467
- };
468
-
469
- try {
470
- const ast = parse(jsContent, {
471
- sourceType: 'module',
472
- plugins: ['jsx']
473
- });
474
-
475
- traverse.default(ast, {
476
- ImportDeclaration(pathNode) {
477
- const importPath = pathNode.node.source.value;
478
- if (!importPath.startsWith('./') && !importPath.startsWith('../')) {
479
- return;
480
- }
481
-
482
- const resolvedPath = resolveImportPath(importPath);
483
- if (!resolvedPath) {
484
- return;
485
- }
486
-
487
- const bindings = pathNode.node.specifiers.map(spec => {
488
- if (spec.type === 'ImportDefaultSpecifier') {
489
- return {
490
- type: 'default',
491
- importedName: 'default',
492
- localName: spec.local.name
493
- };
494
- }
495
-
496
- if (spec.type === 'ImportSpecifier') {
497
- return {
498
- type: 'named',
499
- importedName: spec.imported.name,
500
- localName: spec.local.name
501
- };
502
- }
503
-
504
- if (spec.type === 'ImportNamespaceSpecifier') {
505
- return {
506
- type: 'namespace',
507
- localName: spec.local.name
508
- };
509
- }
510
-
511
- return null;
512
- }).filter(Boolean);
513
-
514
- dependencies.push({
515
- path: resolvedPath,
516
- bindings
517
- });
518
- }
519
- });
520
- } catch (error) {
521
- console.warn(`Warning: Could not analyze dependencies for ${componentPath}:`, error.message);
522
- }
523
-
524
- return dependencies;
525
- }
526
-
527
- /**
528
- * Generates the content of a bundle
529
- */
530
- async generateBundleContent(components, type, routePath) {
531
- const componentsData = {};
532
-
533
- for (const comp of components) {
534
- const jsPath = path.join(comp.path, `${comp.name}.js`);
535
- const jsContent = await fs.readFile(jsPath, 'utf-8');
536
-
537
- // Analyze dependencies
538
- const dependencies = this.analyzeDependencies(jsContent, comp.path);
539
- const dependencyContents = {};
540
-
541
- // Read all dependency files
542
- for (const dep of dependencies) {
543
- const depPath = dep.path;
544
- try {
545
- const depContent = await fs.readFile(depPath, 'utf-8');
546
- const depName = path
547
- .relative(this.srcPath, depPath)
548
- .replace(/\\/g, '/');
549
- dependencyContents[depName] = {
550
- content: depContent,
551
- bindings: dep.bindings || []
552
- };
553
- } catch (error) {
554
- console.warn(`Warning: Could not read dependency ${depPath}:`, error.message);
555
- }
556
- }
557
-
558
- let htmlContent = null;
559
- let cssContent = null;
560
-
561
- const htmlPath = path.join(comp.path, `${comp.name}.html`);
562
- const cssPath = path.join(comp.path, `${comp.name}.css`);
563
-
564
- if (await fs.pathExists(htmlPath)) {
565
- htmlContent = await fs.readFile(htmlPath, 'utf-8');
566
- }
567
-
568
- if (await fs.pathExists(cssPath)) {
569
- cssContent = await fs.readFile(cssPath, 'utf-8');
570
- }
571
-
572
- componentsData[comp.name] = {
573
- name: comp.name,
574
- category: comp.category,
575
- categoryType: comp.categoryType,
576
- js: this.cleanJavaScript(jsContent, comp.name),
577
- externalDependencies: dependencyContents, // Files imported with import statements
578
- componentDependencies: Array.from(comp.dependencies), // Other components this one depends on
579
- html: htmlContent,
580
- css: cssContent,
581
- size: comp.size
582
- };
583
- }
584
-
585
- const metadata = {
586
- version: '2.0.0',
587
- type,
588
- route: routePath,
589
- generated: new Date().toISOString(),
590
- totalSize: components.reduce((sum, c) => sum + c.size, 0),
591
- componentCount: components.length,
592
- strategy: this.config.strategy
593
- };
594
-
595
- return this.formatBundleFile(componentsData, metadata);
596
- }
597
-
598
- /**
599
- * Cleans JavaScript code by removing imports/exports and ensuring class is available globally
600
- */
601
- cleanJavaScript(code, componentName) {
602
- // Remove export default
603
- code = code.replace(/export\s+default\s+/g, '');
604
-
605
- // Remove imports (components will already be available)
606
- code = code.replace(/import\s+.*?from\s+['"].*?['"];?\s*/g, '');
607
-
608
- // Make sure the class is available globally for bundle evaluation
609
- // Preserve original customElements.define if it exists
610
- if (code.includes('customElements.define')) {
611
- // Add global assignment before customElements.define
612
- code = code.replace(/customElements\.define\([^;]+\);?\s*$/, `window.${componentName} = ${componentName};\n$&`);
613
- } else {
614
- // If no customElements.define found, just assign to global
615
- code += `\nwindow.${componentName} = ${componentName};`;
616
- }
617
-
618
- // Add return statement for bundle evaluation compatibility
619
- code += `\nreturn ${componentName};`;
620
-
621
- return code;
622
- }
623
-
624
- /**
625
- * Formats the bundle file
626
- */
627
- formatBundleFile(componentsData, metadata) {
628
- return `/**
629
- * Slice.js Bundle
630
- * Type: ${metadata.type}
631
- * Generated: ${metadata.generated}
632
- * Strategy: ${metadata.strategy}
633
- * Components: ${metadata.componentCount}
634
- * Total Size: ${(metadata.totalSize / 1024).toFixed(1)} KB
635
- */
636
-
637
- export const SLICE_BUNDLE = {
638
- metadata: ${JSON.stringify(metadata, null, 2)},
639
-
640
- components: ${JSON.stringify(componentsData, null, 2)}
641
- };
642
-
643
- // Auto-registration of components
644
- if (window.slice && window.slice.controller) {
645
- slice.controller.registerBundle(SLICE_BUNDLE);
646
- }
647
- `;
648
- }
649
-
650
- /**
651
- * Generates the bundle configuration
652
- */
653
- generateBundleConfig() {
654
- const config = {
655
- version: '2.0.0',
656
- strategy: this.config.strategy,
657
- generated: new Date().toISOString(),
658
-
659
- stats: {
660
- totalComponents: this.analysisData.metrics.totalComponents,
661
- totalRoutes: this.analysisData.metrics.totalRoutes,
662
- sharedComponents: this.bundles.critical.components.length,
663
- sharedPercentage: this.analysisData.metrics.sharedPercentage,
664
- totalSize: this.analysisData.metrics.totalSize,
665
- criticalSize: this.bundles.critical.size
666
- },
667
-
668
- bundles: {
669
- critical: {
670
- file: this.bundles.critical.file,
671
- size: this.bundles.critical.size,
672
- components: this.bundles.critical.components.map(c => c.name)
673
- },
674
- routes: {}
675
- }
676
- };
677
-
678
- for (const [key, bundle] of Object.entries(this.bundles.routes)) {
679
- const routeIdentifier = Array.isArray(bundle.path || bundle.paths)
680
- ? key
681
- : (bundle.path || bundle.paths || key);
682
-
683
- config.bundles.routes[key] = {
684
- path: bundle.path || bundle.paths || key, // Support both single path and array of paths, fallback to key
685
- file: `slice-bundle.${this.routeToFileName(routeIdentifier)}.js`,
686
- size: bundle.size,
687
- components: bundle.components.map(c => c.name),
688
- dependencies: ['critical']
689
- };
690
- }
691
-
692
- return config;
693
- }
694
-
695
- /**
696
- * Converts a route to filename
697
- */
698
- routeToFileName(routePath) {
699
- if (routePath === '/') return 'home';
700
- return routePath
701
- .replace(/^\//, '')
702
- .replace(/\//g, '-')
703
- .replace(/[^a-zA-Z0-9-]/g, '')
704
- .toLowerCase();
705
- }
706
-
707
- /**
708
- * Saves the configuration to file
709
- */
710
- async saveBundleConfig(config) {
711
- // Ensure bundles directory exists
712
- await fs.ensureDir(this.bundlesPath);
713
-
714
- // Save JSON config
715
- const configPath = path.join(this.bundlesPath, 'bundle.config.json');
716
- await fs.writeJson(configPath, config, { spaces: 2 });
717
-
718
- // Generate JavaScript module for direct import
719
- const jsConfigPath = path.join(this.bundlesPath, 'bundle.config.js');
720
- const jsConfig = this.generateBundleConfigJS(config);
721
- await fs.writeFile(jsConfigPath, jsConfig, 'utf-8');
722
-
723
- console.log(`✓ Configuration saved to ${configPath}`);
724
- console.log(`✓ JavaScript config generated: ${jsConfigPath}`);
725
- }
726
-
727
- /**
728
- * Creates a default bundle config file if none exists
729
- */
730
- async createDefaultBundleConfig() {
731
- const defaultConfigPath = path.join(this.srcPath, 'bundles', 'bundle.config.js');
732
-
733
- // Only create if it doesn't exist
734
- if (await fs.pathExists(defaultConfigPath)) {
735
- return;
736
- }
737
-
738
- await fs.ensureDir(path.dirname(defaultConfigPath));
739
-
740
- const defaultConfig = `/**
741
- * Slice.js Bundle Configuration
742
- * Default empty configuration - no bundles available
743
- * Run 'slice bundle' to generate optimized bundles
744
- */
745
-
746
- // No bundles available - using individual component loading
747
- export const SLICE_BUNDLE_CONFIG = null;
748
-
749
- // No auto-initialization needed for default config
750
- `;
751
-
752
- await fs.writeFile(defaultConfigPath, defaultConfig, 'utf-8');
753
- console.log(`✓ Default bundle config created: ${defaultConfigPath}`);
754
- }
755
-
756
- /**
757
- * Generates JavaScript module for direct import
758
- */
759
- generateBundleConfigJS(config) {
760
- return `/**
761
- * Slice.js Bundle Configuration
762
- * Generated: ${new Date().toISOString()}
763
- * Strategy: ${config.strategy}
764
- */
765
-
766
- // Direct bundle configuration (no fetch required)
767
- export const SLICE_BUNDLE_CONFIG = ${JSON.stringify(config, null, 2)};
768
-
769
- // Auto-initialization if slice is available
770
- if (typeof window !== 'undefined' && window.slice && window.slice.controller) {
771
- window.slice.controller.bundleConfig = SLICE_BUNDLE_CONFIG;
772
-
773
- // Load critical bundle automatically
774
- if (SLICE_BUNDLE_CONFIG.bundles.critical && !window.slice.controller.criticalBundleLoaded) {
775
- import('./slice-bundle.critical.js').catch(err =>
776
- console.warn('Failed to load critical bundle:', err)
777
- );
778
- window.slice.controller.criticalBundleLoaded = true;
779
- }
780
- }
781
- `;
782
- }
783
- }
1
+ // cli/utils/bundling/BundleGenerator.js
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+ import { parse } from '@babel/parser';
6
+ import traverse from '@babel/traverse';
7
+ import { minify as terserMinify } from 'terser';
8
+ import { getSrcPath, getComponentsJsPath, getDistPath } from '../PathHelper.js';
9
+
10
+ export default class BundleGenerator {
11
+ constructor(moduleUrl, analysisData, options = {}) {
12
+ this.moduleUrl = moduleUrl;
13
+ this.analysisData = analysisData;
14
+ this.srcPath = getSrcPath(moduleUrl);
15
+ this.distPath = getDistPath(moduleUrl);
16
+ this.output = options.output || 'src';
17
+ this.bundlesPath = this.output === 'dist'
18
+ ? path.join(this.distPath, 'bundles')
19
+ : path.join(this.srcPath, 'bundles');
20
+ this.componentsPath = path.dirname(getComponentsJsPath(moduleUrl));
21
+ this.options = {
22
+ minify: !!options.minify,
23
+ obfuscate: !!options.obfuscate
24
+ };
25
+
26
+ // Configuration
27
+ this.config = {
28
+ maxCriticalSize: 50 * 1024, // 50KB
29
+ maxCriticalComponents: 15,
30
+ minSharedUsage: 3, // Minimum routes to be considered "shared"
31
+ strategy: 'hybrid' // 'global', 'hybrid', 'per-route'
32
+ };
33
+
34
+ this.bundles = {
35
+ critical: {
36
+ components: [],
37
+ size: 0,
38
+ file: 'slice-bundle.critical.js'
39
+ },
40
+ routes: {}
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Computes deterministic integrity hash for bundle metadata.
46
+ * @param {Array} components
47
+ * @param {string} type
48
+ * @param {string|null} routePath
49
+ * @param {string} bundleKey
50
+ * @param {string} fileName
51
+ * @returns {string}
52
+ */
53
+ computeBundleIntegrity(components, type, routePath, bundleKey, fileName) {
54
+ const metadata = {
55
+ version: '2.0.0',
56
+ type,
57
+ route: routePath,
58
+ bundleKey,
59
+ file: fileName,
60
+ generated: 'static',
61
+ totalSize: components.reduce((sum, c) => sum + c.size, 0),
62
+ componentCount: components.length,
63
+ strategy: this.config.strategy
64
+ };
65
+
66
+ const payload = {
67
+ metadata,
68
+ components: components.reduce((acc, comp) => {
69
+ acc[comp.name] = {
70
+ name: comp.name,
71
+ category: comp.category,
72
+ categoryType: comp.categoryType,
73
+ componentDependencies: Array.from(comp.dependencies)
74
+ };
75
+ return acc;
76
+ }, {})
77
+ };
78
+
79
+ return `sha256:${crypto.createHash('sha256')
80
+ .update(JSON.stringify(payload))
81
+ .digest('hex')}`;
82
+ }
83
+
84
+ /**
85
+ * Generates all bundles
86
+ */
87
+ async generate() {
88
+ console.log('🔨 Generating bundles...');
89
+
90
+ // 0. Create bundles directory
91
+ await fs.ensureDir(this.bundlesPath);
92
+ if (this.output === 'dist') {
93
+ await fs.ensureDir(this.distPath);
94
+ }
95
+
96
+ // 1. Determine optimal strategy
97
+ this.determineStrategy();
98
+
99
+ // 2. Identify critical components
100
+ this.identifyCriticalComponents();
101
+
102
+ // 3. Assign components to routes
103
+ this.assignRouteComponents();
104
+
105
+ // 4. Generate bundle files
106
+ const files = await this.generateBundleFiles();
107
+
108
+ // 5. Generate framework bundle (structural)
109
+ const frameworkComponents = this.collectFrameworkComponents();
110
+ let frameworkBundle = null;
111
+ if (frameworkComponents.length > 0) {
112
+ frameworkBundle = await this.createFrameworkBundle(frameworkComponents);
113
+ files.push(frameworkBundle);
114
+ }
115
+
116
+ // 6. Generate configuration
117
+ const config = this.generateBundleConfig(frameworkBundle);
118
+
119
+ console.log('✅ Bundles generated successfully');
120
+
121
+ return {
122
+ bundles: this.bundles,
123
+ config,
124
+ files
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Determines the optimal bundling strategy
130
+ */
131
+ determineStrategy() {
132
+ const { metrics } = this.analysisData;
133
+ const { totalComponents, sharedPercentage } = metrics;
134
+
135
+ // Strategy based on size and usage pattern
136
+ if (totalComponents < 20 || sharedPercentage > 60) {
137
+ this.config.strategy = 'global';
138
+ console.log('📦 Strategy: Global Bundle (small project or highly shared)');
139
+ } else if (totalComponents < 100) {
140
+ this.config.strategy = 'hybrid';
141
+ console.log('📦 Strategy: Hybrid (critical + grouped routes)');
142
+ } else {
143
+ this.config.strategy = 'per-route';
144
+ console.log('📦 Strategy: Per Route (large project)');
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Identifies critical components for the initial bundle
150
+ */
151
+ identifyCriticalComponents() {
152
+ const { components } = this.analysisData;
153
+
154
+ // Filter critical candidates
155
+ const candidates = components
156
+ .filter(comp => {
157
+ // Shared components (used in 3+ routes)
158
+ const isShared = comp.routes.size >= this.config.minSharedUsage;
159
+
160
+ // Structural components (Navbar, Footer, etc.)
161
+ const isStructural = comp.categoryType === 'Structural' ||
162
+ ['Navbar', 'Footer', 'Layout'].includes(comp.name);
163
+
164
+ // Small and highly used components (only if used in 3+ routes)
165
+ const isSmallAndUseful = comp.size < 2000 && comp.routes.size >= 3;
166
+
167
+ return isShared || isStructural || isSmallAndUseful;
168
+ })
169
+ .sort((a, b) => {
170
+ // Prioritize by: (usage * 10) - size
171
+ const priorityA = (a.routes.size * 10) - (a.size / 1000);
172
+ const priorityB = (b.routes.size * 10) - (b.size / 1000);
173
+ return priorityB - priorityA;
174
+ });
175
+
176
+ const loadingComponent = components.find((comp) => comp.name === 'Loading');
177
+ if (loadingComponent && !candidates.includes(loadingComponent)) {
178
+ candidates.unshift(loadingComponent);
179
+ }
180
+
181
+ // Fill critical bundle up to limit
182
+ for (const comp of candidates) {
183
+ const dependencies = this.getComponentDependencies(comp);
184
+ const totalSize = comp.size + dependencies.reduce((sum, dep) => sum + dep.size, 0);
185
+ const totalCount = 1 + dependencies.length;
186
+
187
+ const wouldExceedSize = this.bundles.critical.size + totalSize > this.config.maxCriticalSize;
188
+ const wouldExceedCount = this.bundles.critical.components.length + totalCount > this.config.maxCriticalComponents;
189
+
190
+ if ((wouldExceedSize || wouldExceedCount) && comp.name !== 'Loading') continue;
191
+
192
+ // Add component and its dependencies
193
+ if (!this.bundles.critical.components.find(c => c.name === comp.name)) {
194
+ this.bundles.critical.components.push(comp);
195
+ this.bundles.critical.size += comp.size;
196
+ }
197
+
198
+ for (const dep of dependencies) {
199
+ if (!this.bundles.critical.components.find(c => c.name === dep.name)) {
200
+ this.bundles.critical.components.push(dep);
201
+ this.bundles.critical.size += dep.size;
202
+ }
203
+ }
204
+ }
205
+
206
+ console.log(`✓ Critical bundle: ${this.bundles.critical.components.length} components, ${(this.bundles.critical.size / 1024).toFixed(1)} KB`);
207
+ }
208
+
209
+ /**
210
+ * Assigns remaining components to route bundles
211
+ */
212
+ assignRouteComponents() {
213
+ const criticalNames = new Set(this.bundles.critical.components.map(c => c.name));
214
+
215
+ if (this.config.strategy === 'hybrid') {
216
+ this.assignHybridBundles(criticalNames);
217
+ } else {
218
+ this.assignPerRouteBundles(criticalNames);
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Assigns components to per-route bundles
224
+ */
225
+ assignPerRouteBundles(criticalNames) {
226
+ for (const route of this.analysisData.routes) {
227
+ const routePath = route.path;
228
+ // Get all route dependencies
229
+ const routeComponents = this.getRouteComponents(route.component);
230
+
231
+ // Include dependencies for all route components
232
+ const allComponents = new Set();
233
+ for (const comp of routeComponents) {
234
+ allComponents.add(comp);
235
+ const dependencies = this.getComponentDependencies(comp);
236
+ for (const dep of dependencies) {
237
+ allComponents.add(dep);
238
+ }
239
+ }
240
+
241
+ // Filter those already in critical
242
+ const uniqueComponents = Array.from(allComponents).filter(comp =>
243
+ !criticalNames.has(comp.name)
244
+ );
245
+
246
+ if (uniqueComponents.length === 0) continue;
247
+
248
+ const routeKey = this.routeToFileName(routePath);
249
+ const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
250
+
251
+ this.bundles.routes[routeKey] = {
252
+ path: routePath,
253
+ components: uniqueComponents,
254
+ size: totalSize,
255
+ file: `slice-bundle.${routeKey}.js`
256
+ };
257
+
258
+ console.log(`✓ Bundle ${routeKey}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB`);
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Gets all component dependencies transitively
264
+ */
265
+ getComponentDependencies(component, visited = new Set()) {
266
+ if (visited.has(component.name)) return [];
267
+ visited.add(component.name);
268
+
269
+ const dependencies = [];
270
+
271
+ // Add direct dependencies
272
+ for (const depName of component.dependencies) {
273
+ const depComp = this.analysisData.components.find(c => c.name === depName);
274
+ if (depComp && !visited.has(depName)) {
275
+ dependencies.push(depComp);
276
+ // Add transitive dependencies
277
+ dependencies.push(...this.getComponentDependencies(depComp, visited));
278
+ }
279
+ }
280
+
281
+ return dependencies;
282
+ }
283
+
284
+ /**
285
+ * Assigns components to hybrid bundles (grouped by category)
286
+ */
287
+ assignHybridBundles(criticalNames) {
288
+ const routeGroups = new Map();
289
+
290
+ // First, handle MultiRoute groups
291
+ if (this.analysisData.routeGroups) {
292
+ for (const [groupKey, groupData] of this.analysisData.routeGroups) {
293
+ if (groupData.type === 'multiroute') {
294
+ // Create a bundle for this MultiRoute group
295
+ const allComponents = new Set();
296
+
297
+ // Add the main component (MultiRoute handler)
298
+ const mainComponent = this.analysisData.components.find(c => c.name === groupData.component);
299
+ if (mainComponent) {
300
+ allComponents.add(mainComponent);
301
+
302
+ // Add all components used by this MultiRoute
303
+ const routeComponents = this.getRouteComponents(mainComponent.name);
304
+ for (const comp of routeComponents) {
305
+ allComponents.add(comp);
306
+ // Add transitive dependencies
307
+ const dependencies = this.getComponentDependencies(comp);
308
+ for (const dep of dependencies) {
309
+ allComponents.add(dep);
310
+ }
311
+ }
312
+ }
313
+
314
+ // Filter those already in critical
315
+ const uniqueComponents = Array.from(allComponents).filter(comp =>
316
+ !criticalNames.has(comp.name)
317
+ );
318
+
319
+ if (uniqueComponents.length > 0) {
320
+ const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
321
+
322
+ this.bundles.routes[groupKey] = {
323
+ paths: groupData.routes,
324
+ components: uniqueComponents,
325
+ size: totalSize,
326
+ file: `slice-bundle.${this.routeToFileName(groupKey)}.js`
327
+ };
328
+
329
+ console.log(`✓ Bundle ${groupKey}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB (${groupData.routes.length} routes)`);
330
+ }
331
+ }
332
+ }
333
+ }
334
+
335
+ // Group remaining routes by category (skip those already handled by MultiRoute)
336
+ for (const route of this.analysisData.routes) {
337
+ // Check if this route is already handled by a MultiRoute group
338
+ const isHandledByMultiRoute = this.analysisData.routeGroups &&
339
+ Array.from(this.analysisData.routeGroups.values()).some(group =>
340
+ group.type === 'multiroute' && group.routes.includes(route.path)
341
+ );
342
+
343
+ if (!isHandledByMultiRoute) {
344
+ const category = this.categorizeRoute(route.path);
345
+ if (!routeGroups.has(category)) {
346
+ routeGroups.set(category, []);
347
+ }
348
+ routeGroups.get(category).push(route);
349
+ }
350
+ }
351
+
352
+ // Create bundles for each group
353
+ for (const [category, routes] of routeGroups) {
354
+ const allComponents = new Set();
355
+
356
+ // Collect all unique components for this category (including dependencies)
357
+ for (const route of routes) {
358
+ const routeComponents = this.getRouteComponents(route.component);
359
+ for (const comp of routeComponents) {
360
+ allComponents.add(comp);
361
+ // Add transitive dependencies
362
+ const dependencies = this.getComponentDependencies(comp);
363
+ for (const dep of dependencies) {
364
+ allComponents.add(dep);
365
+ }
366
+ }
367
+ }
368
+
369
+ // Filter those already in critical
370
+ const uniqueComponents = Array.from(allComponents).filter(comp =>
371
+ !criticalNames.has(comp.name)
372
+ );
373
+
374
+ if (uniqueComponents.length === 0) continue;
375
+
376
+ const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
377
+ const routePaths = routes.map(r => r.path);
378
+
379
+ this.bundles.routes[category] = {
380
+ paths: routePaths,
381
+ components: uniqueComponents,
382
+ size: totalSize,
383
+ file: `slice-bundle.${this.routeToFileName(category)}.js`
384
+ };
385
+
386
+ console.log(`✓ Bundle ${category}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB (${routes.length} routes)`);
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Categorizes a route path for grouping, considering MultiRoute context
392
+ */
393
+ categorizeRoute(routePath) {
394
+ // Check if this route belongs to a MultiRoute handler
395
+ if (this.analysisData.routeGroups) {
396
+ for (const [groupKey, groupData] of this.analysisData.routeGroups) {
397
+ if (groupData.type === 'multiroute' && groupData.routes.includes(routePath)) {
398
+ return groupKey; // Return the MultiRoute group key
399
+ }
400
+ }
401
+ }
402
+
403
+ // Default categorization
404
+ const path = routePath.toLowerCase();
405
+
406
+ if (path === '/' || path === '/home') return 'home';
407
+ if (path.includes('docum') || path.includes('documentation')) return 'documentation';
408
+ if (path.includes('component') || path.includes('visual') || path.includes('card') ||
409
+ path.includes('button') || path.includes('input') || path.includes('switch') ||
410
+ path.includes('checkbox') || path.includes('select') || path.includes('details') ||
411
+ path.includes('grid') || path.includes('loading') || path.includes('layout') ||
412
+ path.includes('navbar') || path.includes('treeview') || path.includes('multiroute')) return 'components';
413
+ if (path.includes('theme') || path.includes('slice') || path.includes('config')) return 'configuration';
414
+ if (path.includes('routing') || path.includes('guard')) return 'routing';
415
+ if (path.includes('service') || path.includes('command')) return 'services';
416
+ if (path.includes('structural') || path.includes('lifecycle') || path.includes('static') ||
417
+ path.includes('build')) return 'advanced';
418
+ if (path.includes('playground') || path.includes('creator')) return 'tools';
419
+ if (path.includes('about') || path.includes('404')) return 'misc';
420
+
421
+ return 'general';
422
+ }
423
+
424
+ /**
425
+ * Gets all components needed for a route
426
+ */
427
+ getRouteComponents(componentName) {
428
+ const result = [];
429
+ const visited = new Set();
430
+
431
+ const traverse = (name) => {
432
+ if (visited.has(name)) return;
433
+ visited.add(name);
434
+
435
+ const component = this.analysisData.components.find(c => c.name === name);
436
+ if (!component) return;
437
+
438
+ result.push(component);
439
+
440
+ // Add dependencies recursively
441
+ for (const dep of component.dependencies) {
442
+ traverse(dep);
443
+ }
444
+ };
445
+
446
+ traverse(componentName);
447
+ return result;
448
+ }
449
+
450
+ /**
451
+ * Generates the physical bundle files
452
+ */
453
+ async generateBundleFiles() {
454
+ const files = [];
455
+
456
+ // 1. Critical bundle
457
+ if (this.bundles.critical.components.length > 0) {
458
+ const criticalFile = await this.createBundleFile(
459
+ this.bundles.critical.components,
460
+ 'critical',
461
+ null
462
+ );
463
+ const criticalIntegrity = this.computeBundleIntegrity(
464
+ this.bundles.critical.components,
465
+ 'critical',
466
+ null,
467
+ 'critical',
468
+ criticalFile.file
469
+ );
470
+ this.bundles.critical.integrity = `sha256:${criticalFile.hash}`;
471
+ this.bundles.critical.hash = criticalFile.hash;
472
+ files.push(criticalFile);
473
+ }
474
+
475
+ // 2. Route bundles
476
+ for (const [routeKey, bundle] of Object.entries(this.bundles.routes)) {
477
+ const routeIdentifier = Array.isArray(bundle.path || bundle.paths)
478
+ ? routeKey
479
+ : (bundle.path || bundle.paths || routeKey);
480
+
481
+ const routeFile = await this.createBundleFile(
482
+ bundle.components,
483
+ 'route',
484
+ routeIdentifier
485
+ );
486
+ const routeIntegrity = `sha256:${routeFile.hash}`;
487
+ const matchingBundle = Object.values(this.bundles.routes)
488
+ .find((entry) => entry.file === routeFile.file);
489
+ if (matchingBundle) {
490
+ matchingBundle.hash = routeFile.hash;
491
+ matchingBundle.integrity = routeIntegrity;
492
+ }
493
+ files.push(routeFile);
494
+ }
495
+
496
+ return files;
497
+ }
498
+
499
+ /**
500
+ * Creates a bundle file
501
+ */
502
+ async createBundleFile(components, type, routePath) {
503
+ const routeKey = routePath ? this.routeToFileName(routePath) : 'critical';
504
+ const fileName = `slice-bundle.${routeKey}.js`;
505
+ const filePath = path.join(this.bundlesPath, fileName);
506
+
507
+ const bundleContent = await this.generateBundleContent(
508
+ components,
509
+ type,
510
+ routePath,
511
+ routeKey,
512
+ fileName
513
+ );
514
+
515
+ const finalContent = await this.applyBundleTransforms(bundleContent, fileName);
516
+
517
+ await fs.ensureDir(path.dirname(filePath));
518
+ await fs.writeFile(filePath, finalContent, 'utf-8');
519
+
520
+ const hash = crypto.createHash('sha256').update(finalContent).digest('hex');
521
+
522
+ return {
523
+ name: routeKey,
524
+ file: fileName,
525
+ path: filePath,
526
+ size: Buffer.byteLength(bundleContent, 'utf-8'),
527
+ hash,
528
+ componentCount: components.length
529
+ };
530
+ }
531
+
532
+ async applyBundleTransforms(bundleContent, fileName) {
533
+ if (!this.options.minify && !this.options.obfuscate) {
534
+ return bundleContent;
535
+ }
536
+
537
+ const options = {
538
+ parse: {
539
+ ecma: 2022
540
+ },
541
+ ecma: 2022,
542
+ compress: this.options.minify ? {
543
+ drop_console: false,
544
+ drop_debugger: true,
545
+ passes: 1
546
+ } : false,
547
+ mangle: this.options.obfuscate ? {
548
+ properties: false
549
+ } : false,
550
+ keep_fnames: true,
551
+ keep_classnames: true,
552
+ format: {
553
+ comments: false,
554
+ ecma: 2022
555
+ }
556
+ };
557
+
558
+ let result;
559
+ try {
560
+ result = await terserMinify(bundleContent, options);
561
+ } catch (error) {
562
+ const tmpDir = path.resolve(process.cwd(), '.tmp');
563
+ const safeName = fileName.replace(/[^a-zA-Z0-9_.-]/g, '_');
564
+ const tmpPath = path.join(tmpDir, `terser-fail-${safeName}`);
565
+ try {
566
+ await fs.ensureDir(tmpDir);
567
+ await fs.writeFile(tmpPath, bundleContent, 'utf-8');
568
+ } catch (writeError) {
569
+ console.warn(`Warning: Failed to write ${tmpPath}:`, writeError.message);
570
+ }
571
+ const message = error?.message ? `${error.message}.` : 'Unknown Terser error.';
572
+ throw new Error(`Terser failed for ${fileName}: ${message} Saved bundle to ${tmpPath}`);
573
+ }
574
+
575
+ if (result.error) {
576
+ const tmpDir = path.resolve(process.cwd(), '.tmp');
577
+ const safeName = fileName.replace(/[^a-zA-Z0-9_.-]/g, '_');
578
+ const tmpPath = path.join(tmpDir, `terser-fail-${safeName}`);
579
+ try {
580
+ await fs.ensureDir(tmpDir);
581
+ await fs.writeFile(tmpPath, bundleContent, 'utf-8');
582
+ } catch (writeError) {
583
+ console.warn(`Warning: Failed to write ${tmpPath}:`, writeError.message);
584
+ }
585
+ throw new Error(`Terser failed for ${fileName}: ${result.error.message}. Saved bundle to ${tmpPath}`);
586
+ }
587
+
588
+ return result.code || bundleContent;
589
+ }
590
+
591
+
592
+ /**
593
+ * Analyzes dependencies of a JavaScript file using simple regex
594
+ */
595
+ analyzeDependencies(jsContent, componentPath) {
596
+ const dependencies = [];
597
+
598
+ const resolveImportPath = (importPath) => {
599
+ const resolvedPath = path.resolve(componentPath, importPath);
600
+ let finalPath = resolvedPath;
601
+ const ext = path.extname(resolvedPath);
602
+ if (!ext) {
603
+ const extensions = ['.js', '.json', '.mjs'];
604
+ for (const extension of extensions) {
605
+ if (fs.existsSync(resolvedPath + extension)) {
606
+ finalPath = resolvedPath + extension;
607
+ break;
608
+ }
609
+ }
610
+ }
611
+
612
+ return fs.existsSync(finalPath) ? finalPath : null;
613
+ };
614
+
615
+ try {
616
+ const ast = parse(jsContent, {
617
+ sourceType: 'module',
618
+ plugins: ['jsx']
619
+ });
620
+
621
+ traverse.default(ast, {
622
+ ImportDeclaration(pathNode) {
623
+ const importPath = pathNode.node.source.value;
624
+ if (!importPath.startsWith('./') && !importPath.startsWith('../')) {
625
+ return;
626
+ }
627
+
628
+ const resolvedPath = resolveImportPath(importPath);
629
+ if (!resolvedPath) {
630
+ return;
631
+ }
632
+
633
+ const bindings = pathNode.node.specifiers.map(spec => {
634
+ if (spec.type === 'ImportDefaultSpecifier') {
635
+ return {
636
+ type: 'default',
637
+ importedName: 'default',
638
+ localName: spec.local.name
639
+ };
640
+ }
641
+
642
+ if (spec.type === 'ImportSpecifier') {
643
+ return {
644
+ type: 'named',
645
+ importedName: spec.imported.name,
646
+ localName: spec.local.name
647
+ };
648
+ }
649
+
650
+ if (spec.type === 'ImportNamespaceSpecifier') {
651
+ return {
652
+ type: 'namespace',
653
+ localName: spec.local.name
654
+ };
655
+ }
656
+
657
+ return null;
658
+ }).filter(Boolean);
659
+
660
+ dependencies.push({
661
+ path: resolvedPath,
662
+ bindings
663
+ });
664
+ }
665
+ });
666
+ } catch (error) {
667
+ console.warn(`Warning: Could not analyze dependencies for ${componentPath}:`, error.message);
668
+ }
669
+
670
+ return dependencies;
671
+ }
672
+
673
+ /**
674
+ * Generates the content of a bundle
675
+ */
676
+ async generateBundleContent(components, type, routePath, bundleKey, fileName) {
677
+ const componentsData = {};
678
+
679
+ for (const comp of components) {
680
+ const fileBaseName = comp.fileName || comp.name;
681
+ const jsPath = path.join(comp.path, `${fileBaseName}.js`);
682
+ const jsContent = await fs.readFile(jsPath, 'utf-8');
683
+
684
+ const dependencyContents = await this.buildDependencyContents(jsContent, comp.path);
685
+
686
+ let htmlContent = null;
687
+ let cssContent = null;
688
+
689
+ const htmlPath = path.join(comp.path, `${fileBaseName}.html`);
690
+ const cssPath = path.join(comp.path, `${fileBaseName}.css`);
691
+
692
+ if (await fs.pathExists(htmlPath)) {
693
+ htmlContent = await fs.readFile(htmlPath, 'utf-8');
694
+ }
695
+
696
+ if (await fs.pathExists(cssPath)) {
697
+ cssContent = await fs.readFile(cssPath, 'utf-8');
698
+ }
699
+
700
+ const componentKey = comp.isFramework ? `Framework/Structural/${comp.name}` : comp.name;
701
+ componentsData[componentKey] = {
702
+ name: comp.name,
703
+ category: comp.category,
704
+ categoryType: comp.categoryType,
705
+ isFramework: !!comp.isFramework,
706
+ 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
+ html: htmlContent,
710
+ css: cssContent,
711
+ size: comp.size
712
+ };
713
+ }
714
+
715
+ const metadata = {
716
+ version: '2.0.0',
717
+ type,
718
+ route: routePath,
719
+ 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
727
+ };
728
+
729
+ return this.formatBundleFile(componentsData, metadata);
730
+ }
731
+
732
+ async buildDependencyContents(jsContent, componentPath) {
733
+ const dependencies = this.analyzeDependencies(jsContent, componentPath);
734
+ const dependencyContents = {};
735
+
736
+ for (const dep of dependencies) {
737
+ const depPath = dep.path;
738
+ try {
739
+ const depContent = await fs.readFile(depPath, 'utf-8');
740
+ const depName = path
741
+ .relative(this.srcPath, depPath)
742
+ .replace(/\\/g, '/');
743
+ dependencyContents[depName] = {
744
+ content: depContent,
745
+ bindings: dep.bindings || []
746
+ };
747
+ } catch (error) {
748
+ console.warn(`Warning: Could not read dependency ${depPath}:`, error.message);
749
+ }
750
+ }
751
+
752
+ return dependencyContents;
753
+ }
754
+
755
+ /**
756
+ * Cleans JavaScript code by removing imports/exports and ensuring class is available globally
757
+ */
758
+ cleanJavaScript(code, componentName) {
759
+ // Remove export default
760
+ code = code.replace(/export\s+default\s+/g, '');
761
+
762
+ // Remove imports (components will already be available)
763
+ code = code.replace(/import\s+.*?from\s+['"].*?['"];?\s*/g, '');
764
+
765
+ // Guard customElements.define to avoid duplicate registrations
766
+ code = code.replace(
767
+ /customElements\.define\(([^)]+)\);?/g,
768
+ (match, args) => {
769
+ const firstArg = args.split(',')[0]?.trim() || '';
770
+ if (!/^['"][^'"]+['"]$/.test(firstArg)) {
771
+ return match;
772
+ }
773
+ return `if (!customElements.get(${firstArg})) { customElements.define(${args}); }`;
774
+ }
775
+ );
776
+
777
+ // Make sure the class is available globally for bundle evaluation
778
+ // Preserve original customElements.define if it exists
779
+ if (code.includes('customElements.define')) {
780
+ // Add global assignment before guarded or direct customElements.define
781
+ const globalAssignment = `window.${componentName} = ${componentName};\n`;
782
+ const guardedDefineRegex = /if\s*\(\s*!\s*customElements\.get\([^)]*\)\s*\)\s*\{\s*customElements\.define\([^;]+\);?\s*\}\s*$/;
783
+ const directDefineRegex = /customElements\.define\([^;]+\);?\s*$/;
784
+ if (guardedDefineRegex.test(code)) {
785
+ code = code.replace(guardedDefineRegex, `${globalAssignment}$&`);
786
+ } else {
787
+ code = code.replace(directDefineRegex, `${globalAssignment}$&`);
788
+ }
789
+ } else {
790
+ // If no customElements.define found, just assign to global
791
+ code += `\nwindow.${componentName} = ${componentName};`;
792
+ }
793
+
794
+ // Add return statement for bundle evaluation compatibility
795
+ code += `\nreturn ${componentName};`;
796
+
797
+ return code;
798
+ }
799
+
800
+ /**
801
+ * Formats the bundle file
802
+ */
803
+ formatBundleFile(componentsData, metadata) {
804
+ const integrityPayload = {
805
+ metadata: {
806
+ ...metadata,
807
+ generated: 'static'
808
+ },
809
+ components: Object.fromEntries(
810
+ Object.entries(componentsData).map(([name, data]) => [
811
+ name,
812
+ {
813
+ name: data.name,
814
+ category: data.category,
815
+ categoryType: data.categoryType,
816
+ componentDependencies: data.componentDependencies
817
+ }
818
+ ])
819
+ )
820
+ };
821
+ const integrity = `sha256:${crypto
822
+ .createHash('sha256')
823
+ .update(JSON.stringify(integrityPayload))
824
+ .digest('hex')}`;
825
+
826
+ const dependencyBlock = this.buildDependencyModuleBlock(componentsData);
827
+ const componentBlock = this.buildComponentBundleBlock(componentsData);
828
+
829
+ return `/**
830
+ * Slice.js Bundle
831
+ * Type: ${metadata.type}
832
+ * Generated: ${metadata.generated}
833
+ * Strategy: ${metadata.strategy}
834
+ * Components: ${metadata.componentCount}
835
+ * Total Size: ${(metadata.totalSize / 1024).toFixed(1)} KB
836
+ */
837
+
838
+ ${dependencyBlock}
839
+ ${componentBlock}
840
+
841
+ export const SLICE_BUNDLE = {
842
+ metadata: ${JSON.stringify({ ...metadata, integrity }, null, 2)},
843
+ components: SLICE_BUNDLE_COMPONENTS
844
+ };
845
+
846
+ // Auto-registration of components
847
+ if (window.slice && window.slice.controller) {
848
+ slice.controller.registerBundle(SLICE_BUNDLE);
849
+ }
850
+ `;
851
+ }
852
+
853
+ buildDependencyModuleBlock(componentsData) {
854
+ const dependencyModules = this.collectDependencyModules(componentsData);
855
+ if (dependencyModules.length === 0) {
856
+ return 'const SLICE_BUNDLE_DEPENDENCIES = {};';
857
+ }
858
+
859
+ const lines = ['const SLICE_BUNDLE_DEPENDENCIES = {};'];
860
+ dependencyModules.forEach((module, index) => {
861
+ const exportVar = `__sliceDepExports${index}`;
862
+ const content = this.transformDependencyContent(module.content, exportVar, module.name);
863
+ lines.push(`// Dependency: ${module.name}`);
864
+ lines.push(`const ${exportVar} = {};`);
865
+ lines.push(content.trim());
866
+ lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
867
+ });
868
+
869
+ return `${lines.join('\n')}`;
870
+ }
871
+
872
+ collectDependencyModules(componentsData) {
873
+ const modules = new Map();
874
+ Object.values(componentsData).forEach((component) => {
875
+ Object.entries(component.externalDependencies || {}).forEach(([name, entry]) => {
876
+ if (modules.has(name)) return;
877
+ const content = typeof entry === 'string' ? entry : entry.content;
878
+ modules.set(name, { name, content });
879
+ });
880
+ });
881
+ return Array.from(modules.values());
882
+ }
883
+
884
+ transformDependencyContent(content, exportVar, moduleName) {
885
+ const baseName = moduleName.split('/').pop().replace(/\.[^.]+$/, '');
886
+ const dataName = baseName ? `${baseName}Data` : null;
887
+ const exportPrefix = dataName ? `${exportVar}.${dataName} = ` : `${exportVar}.default = `;
888
+
889
+ return content
890
+ .replace(/export\s+const\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
891
+ .replace(/export\s+let\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
892
+ .replace(/export\s+var\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
893
+ .replace(/export\s+function\s+(\w+)/g, `${exportVar}.$1 = function`)
894
+ .replace(/export\s+default\s+/g, exportPrefix)
895
+ .replace(/export\s*{\s*([^}]+)\s*}/g, (match, exportsStr) => {
896
+ return exportsStr
897
+ .split(',')
898
+ .map((exp) => {
899
+ const cleanExp = exp.trim();
900
+ const varName = cleanExp.split(' as ')[0].trim();
901
+ return `${exportVar}.${varName} = ${varName};`;
902
+ })
903
+ .join('\n');
904
+ })
905
+ .replace(/^\s*export\s+/gm, '');
906
+ }
907
+
908
+ buildComponentBundleBlock(componentsData) {
909
+ const componentEntries = [];
910
+ const componentDefs = [];
911
+ const frameworkEntries = [];
912
+
913
+ Object.entries(componentsData).forEach(([name, data]) => {
914
+ const classVar = this.toSafeIdentifier(name);
915
+ const bindings = this.buildDependencyBindings(data.externalDependencies || {});
916
+
917
+ componentDefs.push(`const ${classVar} = (() => {\n${bindings}\n${data.js}\nreturn ${name};\n})();`);
918
+
919
+ if (data.isFramework) {
920
+ frameworkEntries.push(`${JSON.stringify(data.name)}: ${classVar}`);
921
+ }
922
+
923
+ componentEntries.push(
924
+ `${JSON.stringify(name)}: {\n` +
925
+ ` name: ${JSON.stringify(data.name)},\n` +
926
+ ` category: ${JSON.stringify(data.category)},\n` +
927
+ ` categoryType: ${JSON.stringify(data.categoryType)},\n` +
928
+ ` componentDependencies: ${JSON.stringify(data.componentDependencies)},\n` +
929
+ ` html: ${JSON.stringify(data.html)},\n` +
930
+ ` css: ${JSON.stringify(data.css)},\n` +
931
+ ` size: ${JSON.stringify(data.size)},\n` +
932
+ ` class: ${classVar}\n` +
933
+ `}`
934
+ );
935
+ });
936
+
937
+ const frameworkBlock = frameworkEntries.length > 0
938
+ ? `const SLICE_FRAMEWORK_CLASSES = {\n${frameworkEntries.join(',\n')}\n};\nwindow.SLICE_FRAMEWORK_CLASSES = SLICE_FRAMEWORK_CLASSES;`
939
+ : '';
940
+
941
+ return `${componentDefs.join('\n\n')}\n\nconst SLICE_BUNDLE_COMPONENTS = {\n${componentEntries.join(',\n')}\n};\n${frameworkBlock}`;
942
+ }
943
+
944
+ buildDependencyBindings(externalDependencies) {
945
+ const lines = [];
946
+ Object.entries(externalDependencies).forEach(([name, entry]) => {
947
+ const bindings = typeof entry === 'string' ? [] : entry.bindings || [];
948
+ const depVar = `SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(name)}]`;
949
+ const baseName = name.split('/').pop().replace(/\.[^.]+$/, '');
950
+ const dataName = baseName ? `${baseName}Data` : null;
951
+
952
+ bindings.forEach((binding) => {
953
+ if (!binding?.localName) return;
954
+ if (binding.type === 'default') {
955
+ const fallback = dataName ? `${depVar}.${dataName}` : `${depVar}.default`;
956
+ lines.push(`const ${binding.localName} = ${depVar}.default !== undefined ? ${depVar}.default : ${fallback};`);
957
+ }
958
+ if (binding.type === 'named') {
959
+ lines.push(`const ${binding.localName} = ${depVar}.${binding.importedName};`);
960
+ }
961
+ if (binding.type === 'namespace') {
962
+ lines.push(`const ${binding.localName} = ${depVar};`);
963
+ }
964
+ });
965
+ });
966
+
967
+ return lines.join('\n');
968
+ }
969
+
970
+ toSafeIdentifier(name) {
971
+ const cleaned = name.replace(/[^a-zA-Z0-9_]/g, '_');
972
+ if (/^\d/.test(cleaned)) {
973
+ return `SliceComponent_${cleaned}`;
974
+ }
975
+ return `SliceComponent_${cleaned}`;
976
+ }
977
+
978
+ /**
979
+ * Generates the bundle configuration
980
+ */
981
+ generateBundleConfig(frameworkBundle = null) {
982
+ const config = {
983
+ version: '2.0.0',
984
+ strategy: this.config.strategy,
985
+ minified: this.options.minify,
986
+ obfuscated: this.options.obfuscate,
987
+ production: true,
988
+ generated: new Date().toISOString(),
989
+
990
+ stats: {
991
+ totalComponents: this.analysisData.metrics.totalComponents,
992
+ totalRoutes: this.analysisData.metrics.totalRoutes,
993
+ sharedComponents: this.bundles.critical.components.length,
994
+ sharedPercentage: this.analysisData.metrics.sharedPercentage,
995
+ totalSize: this.analysisData.metrics.totalSize,
996
+ criticalSize: this.bundles.critical.size
997
+ },
998
+
999
+ bundles: {
1000
+ framework: {
1001
+ file: 'slice-bundle.framework.js',
1002
+ size: 0,
1003
+ hash: null,
1004
+ integrity: null,
1005
+ components: []
1006
+ },
1007
+ critical: {
1008
+ file: this.bundles.critical.file,
1009
+ size: this.bundles.critical.size,
1010
+ hash: this.bundles.critical.hash || null,
1011
+ integrity: this.bundles.critical.integrity || null,
1012
+ components: this.bundles.critical.components.map(c => c.name)
1013
+ },
1014
+ routes: {}
1015
+ },
1016
+ routeBundles: {}
1017
+ };
1018
+
1019
+ for (const [key, bundle] of Object.entries(this.bundles.routes)) {
1020
+ const routeIdentifier = Array.isArray(bundle.path || bundle.paths)
1021
+ ? key
1022
+ : (bundle.path || bundle.paths || key);
1023
+
1024
+ config.bundles.routes[key] = {
1025
+ path: bundle.path || bundle.paths || key, // Support both single path and array of paths, fallback to key
1026
+ file: `slice-bundle.${this.routeToFileName(routeIdentifier)}.js`,
1027
+ size: bundle.size,
1028
+ hash: bundle.hash || null,
1029
+ integrity: bundle.integrity || null,
1030
+ components: bundle.components.map(c => c.name),
1031
+ dependencies: ['critical']
1032
+ };
1033
+
1034
+ const paths = Array.isArray(config.bundles.routes[key].path)
1035
+ ? config.bundles.routes[key].path
1036
+ : [config.bundles.routes[key].path];
1037
+
1038
+ for (const routePath of paths) {
1039
+ if (!config.routeBundles[routePath]) {
1040
+ config.routeBundles[routePath] = ['critical'];
1041
+ }
1042
+ if (!config.routeBundles[routePath].includes(key)) {
1043
+ config.routeBundles[routePath].push(key);
1044
+ }
1045
+ }
1046
+ }
1047
+
1048
+ if (frameworkBundle) {
1049
+ config.bundles.framework = {
1050
+ file: frameworkBundle.file,
1051
+ size: frameworkBundle.size,
1052
+ hash: frameworkBundle.hash,
1053
+ integrity: frameworkBundle.integrity,
1054
+ components: frameworkBundle.components || []
1055
+ };
1056
+ }
1057
+
1058
+ return config;
1059
+ }
1060
+
1061
+ collectFrameworkComponents() {
1062
+ return this.analysisData.components.filter((comp) => comp.isFramework);
1063
+ }
1064
+
1065
+ async createFrameworkBundle(components) {
1066
+ const fileName = 'slice-bundle.framework.js';
1067
+ const filePath = path.join(this.bundlesPath, fileName);
1068
+ return this.generateFrameworkBundleFile(components, fileName, filePath);
1069
+ }
1070
+
1071
+ async generateFrameworkBundleFile(components, fileName, filePath) {
1072
+ const componentsData = {};
1073
+ const componentsMap = await this.loadComponentsMap();
1074
+ const metadata = {
1075
+ version: '2.0.0',
1076
+ type: 'framework',
1077
+ route: null,
1078
+ bundleKey: 'framework',
1079
+ file: fileName,
1080
+ generated: new Date().toISOString(),
1081
+ totalSize: components.reduce((sum, c) => sum + c.size, 0),
1082
+ componentCount: components.length,
1083
+ strategy: this.config.strategy,
1084
+ minified: this.options.minify,
1085
+ obfuscated: this.options.obfuscate
1086
+ };
1087
+
1088
+ components.forEach((comp) => {
1089
+ const componentKey = `Framework/Structural/${comp.name}`;
1090
+ const fileBaseName = comp.fileName || comp.name;
1091
+ const jsPath = path.join(comp.path, `${fileBaseName}.js`);
1092
+ const jsContent = fs.readFileSync(jsPath, 'utf-8');
1093
+ const dependencyContents = this.buildDependencyContentsSync(jsContent, comp.path);
1094
+ componentsData[componentKey] = {
1095
+ name: comp.name,
1096
+ category: comp.category,
1097
+ categoryType: comp.categoryType,
1098
+ isFramework: true,
1099
+ js: this.cleanJavaScript(jsContent, comp.name),
1100
+ externalDependencies: dependencyContents,
1101
+ componentDependencies: Array.from(comp.dependencies),
1102
+ html: fs.existsSync(path.join(comp.path, `${fileBaseName}.html`))
1103
+ ? fs.readFileSync(path.join(comp.path, `${fileBaseName}.html`), 'utf-8')
1104
+ : null,
1105
+ css: fs.existsSync(path.join(comp.path, `${fileBaseName}.css`))
1106
+ ? fs.readFileSync(path.join(comp.path, `${fileBaseName}.css`), 'utf-8')
1107
+ : null,
1108
+ size: comp.size
1109
+ };
1110
+ });
1111
+
1112
+ const prelude = `const components = ${JSON.stringify(componentsMap)};`;
1113
+ const bundleContent = `${prelude}\n${this.formatBundleFile(componentsData, metadata)}`;
1114
+ const finalContent = await this.applyBundleTransforms(bundleContent, fileName);
1115
+ await fs.ensureDir(path.dirname(filePath));
1116
+ await fs.writeFile(filePath, finalContent, 'utf-8');
1117
+
1118
+ const hash = crypto.createHash('sha256').update(finalContent).digest('hex');
1119
+ const integrity = `sha256:${hash}`;
1120
+
1121
+ return {
1122
+ name: 'framework',
1123
+ file: fileName,
1124
+ size: Buffer.byteLength(bundleContent, 'utf-8'),
1125
+ hash,
1126
+ integrity,
1127
+ componentCount: components.length,
1128
+ components: components.map((comp) => `Framework/Structural/${comp.name}`)
1129
+ };
1130
+ }
1131
+
1132
+ buildDependencyContentsSync(jsContent, componentPath) {
1133
+ const dependencies = this.analyzeDependencies(jsContent, componentPath);
1134
+ const dependencyContents = {};
1135
+
1136
+ for (const dep of dependencies) {
1137
+ const depPath = dep.path;
1138
+ try {
1139
+ const depContent = fs.readFileSync(depPath, 'utf-8');
1140
+ const depName = path
1141
+ .relative(this.srcPath, depPath)
1142
+ .replace(/\\/g, '/');
1143
+ dependencyContents[depName] = {
1144
+ content: depContent,
1145
+ bindings: dep.bindings || []
1146
+ };
1147
+ } catch (error) {
1148
+ console.warn(`Warning: Could not read dependency ${depPath}:`, error.message);
1149
+ }
1150
+ }
1151
+
1152
+ return dependencyContents;
1153
+ }
1154
+
1155
+ stripImports(code) {
1156
+ return code.replace(/import\s+.*?from\s+['"].*?['"];?\s*/g, '');
1157
+ }
1158
+
1159
+ async loadComponentsMap() {
1160
+ const componentsConfigPath = path.join(this.componentsPath, 'components.js');
1161
+ if (!await fs.pathExists(componentsConfigPath)) {
1162
+ return {};
1163
+ }
1164
+
1165
+ const content = await fs.readFile(componentsConfigPath, 'utf-8');
1166
+ return this.parseComponentsConfig(content);
1167
+ }
1168
+
1169
+ parseComponentsConfig(content) {
1170
+ try {
1171
+ const ast = parse(content, {
1172
+ sourceType: 'module',
1173
+ plugins: ['jsx']
1174
+ });
1175
+
1176
+ let componentsNode = null;
1177
+
1178
+ traverse.default(ast, {
1179
+ VariableDeclarator(path) {
1180
+ if (path.node.id?.type === 'Identifier' && path.node.id.name === 'components') {
1181
+ componentsNode = path.node.init;
1182
+ path.stop();
1183
+ }
1184
+ }
1185
+ });
1186
+
1187
+ if (!componentsNode || componentsNode.type !== 'ObjectExpression') {
1188
+ throw new Error('components object not found');
1189
+ }
1190
+
1191
+ const config = {};
1192
+ for (const prop of componentsNode.properties) {
1193
+ if (prop.type !== 'ObjectProperty') continue;
1194
+
1195
+ const key = this.extractStringValue(prop.key);
1196
+ const value = this.extractStringValue(prop.value);
1197
+
1198
+ if (!key || !value) {
1199
+ throw new Error('Invalid components entry');
1200
+ }
1201
+
1202
+ config[key] = value;
1203
+ }
1204
+
1205
+ return config;
1206
+ } catch (error) {
1207
+ console.warn(`Could not parse components.js: ${error.message}`);
1208
+ return {};
1209
+ }
1210
+ }
1211
+
1212
+ extractStringValue(node) {
1213
+ if (!node) return null;
1214
+
1215
+ if (node.type === 'StringLiteral') {
1216
+ return node.value;
1217
+ }
1218
+
1219
+ if (node.type === 'Identifier') {
1220
+ return node.name;
1221
+ }
1222
+
1223
+ if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
1224
+ return node.quasis.map((q) => q.value.cooked).join('');
1225
+ }
1226
+
1227
+ return null;
1228
+ }
1229
+
1230
+ /**
1231
+ * Converts a route to filename
1232
+ */
1233
+ routeToFileName(routePath) {
1234
+ if (routePath === '/') return 'home';
1235
+ return routePath
1236
+ .replace(/^\//, '')
1237
+ .replace(/\//g, '-')
1238
+ .replace(/[^a-zA-Z0-9-]/g, '')
1239
+ .toLowerCase();
1240
+ }
1241
+
1242
+ /**
1243
+ * Saves the configuration to file
1244
+ */
1245
+ async saveBundleConfig(config) {
1246
+ // Ensure bundles directory exists
1247
+ await fs.ensureDir(this.bundlesPath);
1248
+
1249
+ // Save JSON config
1250
+ const configPath = path.join(this.bundlesPath, 'bundle.config.json');
1251
+ await fs.writeJson(configPath, config, { spaces: 2 });
1252
+
1253
+ // Generate JavaScript module for direct import
1254
+ const jsConfigPath = path.join(this.bundlesPath, 'bundle.config.js');
1255
+ const jsConfig = this.generateBundleConfigJS(config);
1256
+ await fs.writeFile(jsConfigPath, jsConfig, 'utf-8');
1257
+
1258
+ console.log(`✓ Configuration saved to ${configPath}`);
1259
+ console.log(`✓ JavaScript config generated: ${jsConfigPath}`);
1260
+ }
1261
+
1262
+ /**
1263
+ * Creates a default bundle config file if none exists
1264
+ */
1265
+ async createDefaultBundleConfig() {
1266
+ const defaultConfigPath = path.join(this.srcPath, 'bundles', 'bundle.config.js');
1267
+
1268
+ // Only create if it doesn't exist
1269
+ if (await fs.pathExists(defaultConfigPath)) {
1270
+ return;
1271
+ }
1272
+
1273
+ await fs.ensureDir(path.dirname(defaultConfigPath));
1274
+
1275
+ const defaultConfig = `/**
1276
+ * Slice.js Bundle Configuration
1277
+ * Default empty configuration - no bundles available
1278
+ * Run 'slice build' to generate optimized bundles
1279
+ */
1280
+
1281
+ // No bundles available - using individual component loading
1282
+ export const SLICE_BUNDLE_CONFIG = null;
1283
+
1284
+ // No auto-initialization needed for default config
1285
+ `;
1286
+
1287
+ await fs.writeFile(defaultConfigPath, defaultConfig, 'utf-8');
1288
+ console.log(`✓ Default bundle config created: ${defaultConfigPath}`);
1289
+ }
1290
+
1291
+ /**
1292
+ * Generates JavaScript module for direct import
1293
+ */
1294
+ generateBundleConfigJS(config) {
1295
+ return `/**
1296
+ * Slice.js Bundle Configuration
1297
+ * Generated: ${new Date().toISOString()}
1298
+ * Strategy: ${config.strategy}
1299
+ */
1300
+
1301
+ // Direct bundle configuration (no fetch required)
1302
+ export const SLICE_BUNDLE_CONFIG = ${JSON.stringify(config, null, 2)};
1303
+
1304
+ // Auto-initialization if slice is available
1305
+ if (typeof window !== 'undefined' && window.slice && window.slice.controller) {
1306
+ window.slice.controller.bundleConfig = SLICE_BUNDLE_CONFIG;
1307
+
1308
+ // Load critical bundle automatically
1309
+ if (SLICE_BUNDLE_CONFIG.bundles.critical && !window.slice.controller.criticalBundleLoaded) {
1310
+ (async () => {
1311
+ const bundlePath = "/bundles/" + SLICE_BUNDLE_CONFIG.bundles.critical.file;
1312
+ const integrity = SLICE_BUNDLE_CONFIG.bundles.critical.integrity;
1313
+
1314
+ if (typeof window.slice.controller.verifyBundleIntegrity === 'function') {
1315
+ const ok = await window.slice.controller.verifyBundleIntegrity(bundlePath, integrity);
1316
+ if (!ok) {
1317
+ console.warn('Failed to load critical bundle: integrity check failed');
1318
+ return;
1319
+ }
1320
+ }
1321
+
1322
+ import('./slice-bundle.critical.js').catch(err =>
1323
+ console.warn('Failed to load critical bundle:', err)
1324
+ );
1325
+ window.slice.controller.criticalBundleLoaded = true;
1326
+ })();
1327
+ }
1328
+ }
1329
+ `;
1330
+ }
1331
+ }