slicejs-cli 3.0.4 → 3.2.0

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.
Files changed (35) hide show
  1. package/README.md +375 -375
  2. package/client.js +579 -555
  3. package/commands/Print.js +167 -167
  4. package/commands/Validations.js +103 -103
  5. package/commands/build/build.js +40 -40
  6. package/commands/buildProduction/buildProduction.js +579 -579
  7. package/commands/bundle/bundle.js +235 -235
  8. package/commands/createComponent/VisualComponentTemplate.js +55 -55
  9. package/commands/createComponent/createComponent.js +126 -126
  10. package/commands/deleteComponent/deleteComponent.js +77 -77
  11. package/commands/doctor/doctor.js +369 -369
  12. package/commands/getComponent/getComponent.js +747 -747
  13. package/commands/init/init.js +261 -261
  14. package/commands/listComponents/listComponents.js +175 -175
  15. package/commands/startServer/startServer.js +264 -264
  16. package/commands/startServer/watchServer.js +79 -79
  17. package/commands/types/types.js +538 -0
  18. package/commands/utils/LocalCliDelegation.js +53 -53
  19. package/commands/utils/PathHelper.js +68 -68
  20. package/commands/utils/VersionChecker.js +167 -167
  21. package/commands/utils/bundling/BundleGenerator.js +2292 -2031
  22. package/commands/utils/bundling/DependencyAnalyzer.js +933 -933
  23. package/commands/utils/updateManager.js +453 -453
  24. package/docs/superpowers/specs/2026-05-10-pwa-generate-design.md +182 -0
  25. package/package.json +46 -46
  26. package/post.js +25 -25
  27. package/refactor.md +271 -271
  28. package/tests/bundle-generator.test.js +708 -388
  29. package/tests/bundle-v2-register-output.test.js +470 -251
  30. package/tests/client-launcher-contract.test.js +211 -211
  31. package/tests/client-update-flow-contract.test.js +272 -272
  32. package/tests/dependency-analyzer.test.js +24 -24
  33. package/tests/local-cli-delegation.test.js +79 -79
  34. package/tests/types-generator.test.js +356 -0
  35. package/tests/update-manager-notifications.test.js +88 -88
@@ -1,2031 +1,2292 @@
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, getConfigPath } from '../PathHelper.js';
9
-
10
- export default class BundleGenerator {
11
- constructor(moduleUrl, analysisData, options = {}) {
12
- this.moduleUrl = moduleUrl;
13
- this.analysisData = analysisData || { components: [], routes: [], metrics: {} };
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
- this.format = 'v2';
26
- this.sliceConfig = this.resolveSliceConfig();
27
- this.loadingPolicy = this.resolveLoadingPolicy();
28
-
29
- // Configuration
30
- this.config = {
31
- maxCriticalSize: 50 * 1024, // 50KB
32
- maxCriticalComponents: 15,
33
- minSharedUsage: 3, // Minimum routes to be considered "shared"
34
- maxRouteBundleSize: 120 * 1024,
35
- maxRouteRequests: 12,
36
- strategy: 'hybrid' // 'global', 'hybrid', 'per-route'
37
- };
38
-
39
- this.bundles = {
40
- critical: {
41
- components: [],
42
- size: 0,
43
- file: 'slice-bundle.critical.js'
44
- },
45
- routes: {}
46
- };
47
- }
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
-
70
- /**
71
- * Computes deterministic integrity hash for bundle metadata.
72
- * @param {Array} components
73
- * @param {string} type
74
- * @param {string|null} routePath
75
- * @param {string} bundleKey
76
- * @param {string} fileName
77
- * @returns {string}
78
- */
79
- computeBundleIntegrity(components, type, routePath, bundleKey, fileName) {
80
- const metadata = {
81
- version: '2.0.0',
82
- type,
83
- route: routePath,
84
- bundleKey,
85
- file: fileName,
86
- generated: 'static',
87
- totalSize: components.reduce((sum, c) => sum + c.size, 0),
88
- componentCount: components.length,
89
- strategy: this.config.strategy
90
- };
91
-
92
- const payload = {
93
- metadata,
94
- components: components.reduce((acc, comp) => {
95
- acc[comp.name] = {
96
- name: comp.name,
97
- category: comp.category,
98
- categoryType: comp.categoryType,
99
- componentDependencies: Array.from(comp.dependencies)
100
- };
101
- return acc;
102
- }, {})
103
- };
104
-
105
- return `sha256:${crypto.createHash('sha256')
106
- .update(JSON.stringify(payload))
107
- .digest('hex')}`;
108
- }
109
-
110
- /**
111
- * Generates all bundles
112
- */
113
- async generate() {
114
- console.log('🔨 Generating bundles...');
115
-
116
- // 0. Create bundles directory
117
- await fs.ensureDir(this.bundlesPath);
118
- if (this.output === 'dist') {
119
- await fs.ensureDir(this.distPath);
120
- }
121
-
122
- // 1. Determine optimal strategy
123
- this.determineStrategy();
124
-
125
- // 2. Identify critical components
126
- this.identifyCriticalComponents();
127
-
128
- // 3. Assign components to routes
129
- this.assignRouteComponents();
130
-
131
- // 4. Generate bundle files
132
- const files = await this.generateBundleFiles();
133
-
134
- // 5. Generate framework bundle (structural)
135
- const frameworkComponents = this.collectFrameworkComponents();
136
- let frameworkBundle = null;
137
- if (frameworkComponents.length > 0) {
138
- frameworkBundle = await this.createFrameworkBundle(frameworkComponents);
139
- files.push(frameworkBundle);
140
- }
141
-
142
- // 6. Generate configuration
143
- const config = this.generateBundleConfig(frameworkBundle);
144
-
145
- console.log('✅ Bundles generated successfully');
146
-
147
- return {
148
- bundles: this.bundles,
149
- config,
150
- files
151
- };
152
- }
153
-
154
- /**
155
- * Determines the optimal bundling strategy
156
- */
157
- determineStrategy() {
158
- const { metrics } = this.analysisData;
159
- const { totalComponents, sharedPercentage } = metrics;
160
-
161
- // Strategy based on size and usage pattern
162
- if (totalComponents < 20 || sharedPercentage > 60) {
163
- this.config.strategy = 'global';
164
- console.log('📦 Strategy: Global Bundle (small project or highly shared)');
165
- } else if (totalComponents < 100) {
166
- this.config.strategy = 'hybrid';
167
- console.log('📦 Strategy: Hybrid (critical + grouped routes)');
168
- } else {
169
- this.config.strategy = 'per-route';
170
- console.log('📦 Strategy: Per Route (large project)');
171
- }
172
- }
173
-
174
- /**
175
- * Identifies critical components for the initial bundle
176
- */
177
- identifyCriticalComponents() {
178
- const { components } = this.analysisData;
179
-
180
- // Filter critical candidates
181
- const candidates = components
182
- .filter(comp => {
183
- if (!this.isComponentAllowedByLoadingPolicy(comp)) return false;
184
- // Shared components (used in 3+ routes)
185
- const isShared = comp.routes.size >= this.config.minSharedUsage;
186
-
187
- // Structural components (Navbar, Footer, etc.)
188
- const isStructural = comp.categoryType === 'Structural' ||
189
- ['Navbar', 'Footer', 'Layout'].includes(comp.name);
190
-
191
- // Small and highly used components (only if used in 3+ routes)
192
- const isSmallAndUseful = comp.size < 2000 && comp.routes.size >= 3;
193
-
194
- return isShared || isStructural || isSmallAndUseful;
195
- })
196
- .sort((a, b) => {
197
- // Prioritize by: (usage * 10) - size
198
- const priorityA = (a.routes.size * 10) - (a.size / 1000);
199
- const priorityB = (b.routes.size * 10) - (b.size / 1000);
200
- return priorityB - priorityA;
201
- });
202
-
203
- const loadingComponent = components.find((comp) => comp.name === 'Loading' && this.isComponentAllowedByLoadingPolicy(comp));
204
- if (this.loadingPolicy === 'enabled' && loadingComponent && !candidates.includes(loadingComponent)) {
205
- candidates.unshift(loadingComponent);
206
- }
207
-
208
- // Fill critical bundle up to limit
209
- for (const comp of candidates) {
210
- const dependencies = this.getComponentDependencies(comp);
211
- const totalSize = comp.size + dependencies.reduce((sum, dep) => sum + dep.size, 0);
212
- const totalCount = 1 + dependencies.length;
213
-
214
- const wouldExceedSize = this.bundles.critical.size + totalSize > this.config.maxCriticalSize;
215
- const wouldExceedCount = this.bundles.critical.components.length + totalCount > this.config.maxCriticalComponents;
216
-
217
- if ((wouldExceedSize || wouldExceedCount) && comp.name !== 'Loading') continue;
218
-
219
- // Add component and its dependencies
220
- if (!this.bundles.critical.components.find(c => c.name === comp.name)) {
221
- this.bundles.critical.components.push(comp);
222
- this.bundles.critical.size += comp.size;
223
- }
224
-
225
- for (const dep of dependencies) {
226
- if (!this.bundles.critical.components.find(c => c.name === dep.name)) {
227
- this.bundles.critical.components.push(dep);
228
- this.bundles.critical.size += dep.size;
229
- }
230
- }
231
- }
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
-
238
- console.log(`✓ Critical bundle: ${this.bundles.critical.components.length} components, ${(this.bundles.critical.size / 1024).toFixed(1)} KB`);
239
- }
240
-
241
- /**
242
- * Assigns remaining components to route bundles
243
- */
244
- assignRouteComponents() {
245
- const criticalNames = new Set(this.bundles.critical.components.map(c => c.name));
246
-
247
- if (this.config.strategy === 'hybrid') {
248
- this.assignHybridBundles(criticalNames);
249
- } else {
250
- this.assignPerRouteBundles(criticalNames);
251
- }
252
-
253
- this.extractSharedComponents(criticalNames);
254
- this.rebalanceBundlesByBudget(this.bundles.routes, {
255
- maxBundleSize: this.config.maxRouteBundleSize,
256
- maxRequests: this.config.maxRouteRequests
257
- });
258
- }
259
-
260
- /**
261
- * Assigns components to per-route bundles
262
- */
263
- assignPerRouteBundles(criticalNames) {
264
- for (const route of this.analysisData.routes) {
265
- const routePath = route.path;
266
- // Get all route dependencies
267
- const routeComponents = this.getRouteComponents(route.component);
268
-
269
- // Include dependencies for all route components
270
- const allComponents = new Set();
271
- for (const comp of routeComponents) {
272
- allComponents.add(comp);
273
- const dependencies = this.getComponentDependencies(comp);
274
- for (const dep of dependencies) {
275
- allComponents.add(dep);
276
- }
277
- }
278
-
279
- // Filter those already in critical
280
- const uniqueComponents = Array.from(allComponents).filter(comp =>
281
- !criticalNames.has(comp.name) && this.isComponentAllowedByLoadingPolicy(comp)
282
- );
283
-
284
- if (uniqueComponents.length === 0) continue;
285
-
286
- const routeKey = this.routeToFileName(routePath);
287
- const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
288
-
289
- this.bundles.routes[routeKey] = {
290
- path: routePath,
291
- components: this.sortComponentsByName(uniqueComponents),
292
- size: totalSize,
293
- file: `slice-bundle.${routeKey}.js`
294
- };
295
-
296
- console.log(`✓ Bundle ${routeKey}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB`);
297
- }
298
- }
299
-
300
- /**
301
- * Gets all component dependencies transitively
302
- */
303
- getComponentDependencies(component, visited = new Set()) {
304
- if (visited.has(component.name)) return [];
305
- visited.add(component.name);
306
-
307
- const dependencies = [];
308
-
309
- // Add direct dependencies
310
- for (const depName of component.dependencies) {
311
- const depComp = this.analysisData.components.find(c => c.name === depName);
312
- if (depComp && !visited.has(depName)) {
313
- dependencies.push(depComp);
314
- // Add transitive dependencies
315
- dependencies.push(...this.getComponentDependencies(depComp, visited));
316
- }
317
- }
318
-
319
- return dependencies;
320
- }
321
-
322
- /**
323
- * Assigns components to hybrid bundles (grouped by category)
324
- */
325
- assignHybridBundles(criticalNames) {
326
- const routeGroups = new Map();
327
-
328
- // First, handle MultiRoute groups
329
- if (this.analysisData.routeGroups) {
330
- for (const [groupKey, groupData] of this.analysisData.routeGroups) {
331
- if (groupData.type === 'multiroute') {
332
- // Create a bundle for this MultiRoute group
333
- const allComponents = new Set();
334
-
335
- // Add the main component (MultiRoute handler)
336
- const mainComponent = this.analysisData.components.find(c => c.name === groupData.component);
337
- if (mainComponent) {
338
- allComponents.add(mainComponent);
339
-
340
- // Add all components used by this MultiRoute
341
- const routeComponents = this.getRouteComponents(mainComponent.name);
342
- for (const comp of routeComponents) {
343
- allComponents.add(comp);
344
- // Add transitive dependencies
345
- const dependencies = this.getComponentDependencies(comp);
346
- for (const dep of dependencies) {
347
- allComponents.add(dep);
348
- }
349
- }
350
- }
351
-
352
- // Filter those already in critical
353
- const uniqueComponents = Array.from(allComponents).filter(comp =>
354
- !criticalNames.has(comp.name) && this.isComponentAllowedByLoadingPolicy(comp)
355
- );
356
-
357
- if (uniqueComponents.length > 0) {
358
- const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
359
-
360
- this.bundles.routes[groupKey] = {
361
- paths: groupData.routes,
362
- components: this.sortComponentsByName(uniqueComponents),
363
- size: totalSize,
364
- file: `slice-bundle.${this.routeToFileName(groupKey)}.js`
365
- };
366
-
367
- console.log(`✓ Bundle ${groupKey}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB (${groupData.routes.length} routes)`);
368
- }
369
- }
370
- }
371
- }
372
-
373
- // Group remaining routes by category (skip those already handled by MultiRoute)
374
- for (const route of this.analysisData.routes) {
375
- // Check if this route is already handled by a MultiRoute group
376
- const isHandledByMultiRoute = this.analysisData.routeGroups &&
377
- Array.from(this.analysisData.routeGroups.values()).some(group =>
378
- group.type === 'multiroute' && group.routes.includes(route.path)
379
- );
380
-
381
- if (!isHandledByMultiRoute) {
382
- const category = this.categorizeRoute(route.path);
383
- if (!routeGroups.has(category)) {
384
- routeGroups.set(category, []);
385
- }
386
- routeGroups.get(category).push(route);
387
- }
388
- }
389
-
390
- // Create bundles for each group
391
- for (const [category, routes] of routeGroups) {
392
- const allComponents = new Set();
393
-
394
- // Collect all unique components for this category (including dependencies)
395
- for (const route of routes) {
396
- const routeComponents = this.getRouteComponents(route.component);
397
- for (const comp of routeComponents) {
398
- allComponents.add(comp);
399
- // Add transitive dependencies
400
- const dependencies = this.getComponentDependencies(comp);
401
- for (const dep of dependencies) {
402
- allComponents.add(dep);
403
- }
404
- }
405
- }
406
-
407
- // Filter those already in critical
408
- const uniqueComponents = Array.from(allComponents).filter(comp =>
409
- !criticalNames.has(comp.name) && this.isComponentAllowedByLoadingPolicy(comp)
410
- );
411
-
412
- if (uniqueComponents.length === 0) continue;
413
-
414
- const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
415
- const routePaths = routes.map(r => r.path);
416
-
417
- this.bundles.routes[category] = {
418
- paths: routePaths,
419
- components: this.sortComponentsByName(uniqueComponents),
420
- size: totalSize,
421
- file: `slice-bundle.${this.routeToFileName(category)}.js`
422
- };
423
-
424
- console.log(`✓ Bundle ${category}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB (${routes.length} routes)`);
425
- }
426
- }
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
-
647
- /**
648
- * Categorizes a route path for grouping, considering MultiRoute context
649
- */
650
- categorizeRoute(routePath) {
651
- // Check if this route belongs to a MultiRoute handler
652
- if (this.analysisData.routeGroups) {
653
- for (const [groupKey, groupData] of this.analysisData.routeGroups) {
654
- if (groupData.type === 'multiroute' && groupData.routes.includes(routePath)) {
655
- return groupKey; // Return the MultiRoute group key
656
- }
657
- }
658
- }
659
-
660
- // Default categorization
661
- const path = routePath.toLowerCase();
662
-
663
- if (path === '/' || path === '/home') return 'home';
664
- if (path.includes('docum') || path.includes('documentation')) return 'documentation';
665
- if (path.includes('component') || path.includes('visual') || path.includes('card') ||
666
- path.includes('button') || path.includes('input') || path.includes('switch') ||
667
- path.includes('checkbox') || path.includes('select') || path.includes('details') ||
668
- path.includes('grid') || path.includes('loading') || path.includes('layout') ||
669
- path.includes('navbar') || path.includes('treeview') || path.includes('multiroute')) return 'components';
670
- if (path.includes('theme') || path.includes('slice') || path.includes('config')) return 'configuration';
671
- if (path.includes('routing') || path.includes('guard')) return 'routing';
672
- if (path.includes('service') || path.includes('command')) return 'services';
673
- if (path.includes('structural') || path.includes('lifecycle') || path.includes('static') ||
674
- path.includes('build')) return 'advanced';
675
- if (path.includes('playground') || path.includes('creator')) return 'tools';
676
- if (path.includes('about') || path.includes('404')) return 'misc';
677
-
678
- return 'general';
679
- }
680
-
681
- /**
682
- * Gets all components needed for a route
683
- */
684
- getRouteComponents(componentName) {
685
- const result = [];
686
- const visited = new Set();
687
-
688
- const traverse = (name) => {
689
- if (visited.has(name)) return;
690
- visited.add(name);
691
-
692
- const component = this.analysisData.components.find(c => c.name === name);
693
- if (!component) return;
694
-
695
- result.push(component);
696
-
697
- // Add dependencies recursively
698
- for (const dep of component.dependencies) {
699
- traverse(dep);
700
- }
701
- };
702
-
703
- traverse(componentName);
704
- return result;
705
- }
706
-
707
- /**
708
- * Generates the physical bundle files
709
- */
710
- async generateBundleFiles() {
711
- const files = [];
712
-
713
- // 1. Critical bundle
714
- if (this.bundles.critical.components.length > 0) {
715
- const criticalFile = await this.createBundleFile(
716
- this.bundles.critical.components,
717
- 'critical',
718
- null
719
- );
720
- const criticalIntegrity = this.computeBundleIntegrity(
721
- this.bundles.critical.components,
722
- 'critical',
723
- null,
724
- 'critical',
725
- criticalFile.file
726
- );
727
- this.bundles.critical.integrity = `sha256:${criticalFile.hash}`;
728
- this.bundles.critical.hash = criticalFile.hash;
729
- files.push(criticalFile);
730
- }
731
-
732
- // 2. Route bundles
733
- for (const [routeKey, bundle] of Object.entries(this.bundles.routes)) {
734
- const routeIdentifier = Array.isArray(bundle.path || bundle.paths)
735
- ? routeKey
736
- : (bundle.path || bundle.paths || routeKey);
737
-
738
- const routeFile = await this.createBundleFile(
739
- bundle.components,
740
- 'route',
741
- routeIdentifier
742
- );
743
- const routeIntegrity = `sha256:${routeFile.hash}`;
744
- const matchingBundle = Object.values(this.bundles.routes)
745
- .find((entry) => entry.file === routeFile.file);
746
- if (matchingBundle) {
747
- matchingBundle.hash = routeFile.hash;
748
- matchingBundle.integrity = routeIntegrity;
749
- }
750
- files.push(routeFile);
751
- }
752
-
753
- return files;
754
- }
755
-
756
- /**
757
- * Creates a bundle file
758
- */
759
- async createBundleFile(components, type, routePath) {
760
- const routeKey = routePath ? this.routeToFileName(routePath) : 'critical';
761
- const fileName = `slice-bundle.${routeKey}.js`;
762
- const filePath = path.join(this.bundlesPath, fileName);
763
-
764
- const bundleContent = await this.generateBundleContent(
765
- components,
766
- type,
767
- routePath,
768
- routeKey,
769
- fileName
770
- );
771
-
772
- const finalContent = await this.applyBundleTransforms(bundleContent, fileName);
773
-
774
- await fs.ensureDir(path.dirname(filePath));
775
- await fs.writeFile(filePath, finalContent, 'utf-8');
776
-
777
- const hash = crypto.createHash('sha256').update(finalContent).digest('hex');
778
-
779
- return {
780
- name: routeKey,
781
- file: fileName,
782
- path: filePath,
783
- size: Buffer.byteLength(bundleContent, 'utf-8'),
784
- hash,
785
- componentCount: components.length
786
- };
787
- }
788
-
789
- async applyBundleTransforms(bundleContent, fileName) {
790
- if (!this.options.minify && !this.options.obfuscate) {
791
- return bundleContent;
792
- }
793
-
794
- const options = {
795
- parse: {
796
- ecma: 2022
797
- },
798
- ecma: 2022,
799
- compress: this.options.minify ? {
800
- drop_console: false,
801
- drop_debugger: true,
802
- passes: 1
803
- } : false,
804
- mangle: this.options.obfuscate ? {
805
- properties: false
806
- } : false,
807
- keep_fnames: true,
808
- keep_classnames: true,
809
- format: {
810
- comments: false,
811
- ecma: 2022
812
- }
813
- };
814
-
815
- let result;
816
- try {
817
- result = await terserMinify(bundleContent, options);
818
- } catch (error) {
819
- const tmpDir = path.resolve(process.cwd(), '.tmp');
820
- const safeName = fileName.replace(/[^a-zA-Z0-9_.-]/g, '_');
821
- const tmpPath = path.join(tmpDir, `terser-fail-${safeName}`);
822
- try {
823
- await fs.ensureDir(tmpDir);
824
- await fs.writeFile(tmpPath, bundleContent, 'utf-8');
825
- } catch (writeError) {
826
- console.warn(`Warning: Failed to write ${tmpPath}:`, writeError.message);
827
- }
828
- const message = error?.message ? `${error.message}.` : 'Unknown Terser error.';
829
- throw new Error(`Terser failed for ${fileName}: ${message} Saved bundle to ${tmpPath}`);
830
- }
831
-
832
- if (result.error) {
833
- const tmpDir = path.resolve(process.cwd(), '.tmp');
834
- const safeName = fileName.replace(/[^a-zA-Z0-9_.-]/g, '_');
835
- const tmpPath = path.join(tmpDir, `terser-fail-${safeName}`);
836
- try {
837
- await fs.ensureDir(tmpDir);
838
- await fs.writeFile(tmpPath, bundleContent, 'utf-8');
839
- } catch (writeError) {
840
- console.warn(`Warning: Failed to write ${tmpPath}:`, writeError.message);
841
- }
842
- throw new Error(`Terser failed for ${fileName}: ${result.error.message}. Saved bundle to ${tmpPath}`);
843
- }
844
-
845
- return result.code || bundleContent;
846
- }
847
-
848
-
849
- /**
850
- * Analyzes dependencies of a JavaScript file using simple regex
851
- */
852
- analyzeDependencies(jsContent, componentPath) {
853
- const dependencies = [];
854
-
855
- const resolveImportPath = (importPath) => {
856
- const resolvedPath = path.resolve(componentPath, importPath);
857
- let finalPath = resolvedPath;
858
- const ext = path.extname(resolvedPath);
859
- if (!ext) {
860
- const extensions = ['.js', '.json', '.mjs'];
861
- for (const extension of extensions) {
862
- if (fs.existsSync(resolvedPath + extension)) {
863
- finalPath = resolvedPath + extension;
864
- break;
865
- }
866
- }
867
- }
868
-
869
- return fs.existsSync(finalPath) ? finalPath : null;
870
- };
871
-
872
- try {
873
- const ast = parse(jsContent, {
874
- sourceType: 'module',
875
- plugins: ['jsx']
876
- });
877
-
878
- traverse.default(ast, {
879
- ImportDeclaration(pathNode) {
880
- const importPath = pathNode.node.source.value;
881
- if (!importPath.startsWith('./') && !importPath.startsWith('../')) {
882
- return;
883
- }
884
-
885
- const resolvedPath = resolveImportPath(importPath);
886
- if (!resolvedPath) {
887
- return;
888
- }
889
-
890
- const bindings = pathNode.node.specifiers.map(spec => {
891
- if (spec.type === 'ImportDefaultSpecifier') {
892
- return {
893
- type: 'default',
894
- importedName: 'default',
895
- localName: spec.local.name
896
- };
897
- }
898
-
899
- if (spec.type === 'ImportSpecifier') {
900
- return {
901
- type: 'named',
902
- importedName: spec.imported.name,
903
- localName: spec.local.name
904
- };
905
- }
906
-
907
- if (spec.type === 'ImportNamespaceSpecifier') {
908
- return {
909
- type: 'namespace',
910
- localName: spec.local.name
911
- };
912
- }
913
-
914
- return null;
915
- }).filter(Boolean);
916
-
917
- dependencies.push({
918
- path: resolvedPath,
919
- bindings
920
- });
921
- }
922
- });
923
- } catch (error) {
924
- console.warn(`Warning: Could not analyze dependencies for ${componentPath}:`, error.message);
925
- }
926
-
927
- return dependencies;
928
- }
929
-
930
- /**
931
- * Generates the content of a bundle
932
- */
933
- async generateBundleContent(components, type, routePath, bundleKey, fileName) {
934
- const bundleComponents = [];
935
- const uniqueComponents = this.dedupeComponentsByName(components || []);
936
-
937
- for (const comp of uniqueComponents) {
938
- const fileBaseName = comp.fileName || comp.name;
939
- const jsPath = path.join(comp.path, `${fileBaseName}.js`);
940
- const jsContent = await fs.readFile(jsPath, 'utf-8');
941
-
942
- let htmlContent = null;
943
- let cssContent = null;
944
-
945
- const htmlPath = path.join(comp.path, `${fileBaseName}.html`);
946
- const cssPath = path.join(comp.path, `${fileBaseName}.css`);
947
-
948
- if (await fs.pathExists(htmlPath)) {
949
- htmlContent = await fs.readFile(htmlPath, 'utf-8');
950
- }
951
-
952
- if (await fs.pathExists(cssPath)) {
953
- cssContent = await fs.readFile(cssPath, 'utf-8');
954
- }
955
-
956
- const cleanedJavaScript = this.cleanJavaScript(jsContent, comp.name, jsPath);
957
-
958
- bundleComponents.push({
959
- name: comp.name,
960
- category: comp.category,
961
- categoryType: comp.categoryType,
962
- js: cleanedJavaScript.code,
963
- hoistedImports: cleanedJavaScript.hoistedImports,
964
- html: htmlContent,
965
- css: cssContent,
966
- externalDependencies: await this.buildDependencyContents(jsContent, comp.path),
967
- size: comp.size
968
- });
969
- }
970
-
971
- return this.generateBundleFileContent(fileName, type, this.sortComponentsByName(bundleComponents), routePath);
972
- }
973
-
974
- classFactoryName(componentName) {
975
- return `SLICE_CLASS_FACTORY_${this.toSafeIdentifier(componentName)}`;
976
- }
977
-
978
- indentCodeBlock(code, spaces = 2) {
979
- const indentation = ' '.repeat(spaces);
980
- return String(code)
981
- .split('\n')
982
- .map((line) => `${indentation}${line}`)
983
- .join('\n');
984
- }
985
-
986
- generateBundleFileContent(fileName, type, components, routePath = null) {
987
- const uniqueComponents = this.dedupeComponentsByName(components || []);
988
- const bundleKey = type === 'critical'
989
- ? 'critical'
990
- : type === 'framework'
991
- ? 'framework'
992
- : this.routeToFileName(routePath || fileName.replace('slice-bundle.', '').replace('.js', ''));
993
-
994
- const dependencyModules = this.collectDependencyModulesFromComponents(uniqueComponents);
995
- const dependencyModuleBlock = this.buildV2DependencyModuleBlock(uniqueComponents);
996
- const rawHoistedImports = uniqueComponents
997
- .flatMap((component) => component.hoistedImports || [])
998
- .map((statement) => String(statement).trim())
999
- .filter(Boolean);
1000
- const reservedIdentifiers = new Set([
1001
- 'SLICE_BUNDLE_META',
1002
- 'SLICE_BUNDLE_DEPENDENCIES',
1003
- ...uniqueComponents.map((component) => this.classFactoryName(component.name)),
1004
- ...uniqueComponents.map((component) => `__templateElement_${this.toSafeIdentifier(component.name)}`),
1005
- ...this.getDependencyExportVariableNames(dependencyModules)
1006
- ]);
1007
- this.validateHoistedImportCollisions(rawHoistedImports, reservedIdentifiers);
1008
- const hoistedImports = Array.from(new Set(rawHoistedImports));
1009
- const hoistedImportBlock = hoistedImports.join('\n');
1010
-
1011
- const classFactoryDefinitions = uniqueComponents
1012
- .map((component) => {
1013
- const factoryName = this.classFactoryName(component.name);
1014
- const dependencyBindings = this.buildDependencyBindings(component.externalDependencies || {});
1015
- const body = component.js && component.js.trim()
1016
- ? component.js
1017
- : `return window.${component.name};`;
1018
- const bodyWithBindings = dependencyBindings
1019
- ? `${dependencyBindings}\n${body}`
1020
- : body;
1021
- return `const ${factoryName} = () => {\n${this.indentCodeBlock(bodyWithBindings, 2)}\n};`;
1022
- })
1023
- .join('\n\n');
1024
-
1025
- const templateDeclarations = uniqueComponents
1026
- .map((component) => {
1027
- const templateVarName = `__templateElement_${this.toSafeIdentifier(component.name)}`;
1028
- return `const ${templateVarName} = document.createElement('template');\n${templateVarName}.innerHTML = ${JSON.stringify(component.html || '')};`;
1029
- })
1030
- .join('\n');
1031
-
1032
- const classRegistrations = uniqueComponents
1033
- .map((component) => {
1034
- const componentName = JSON.stringify(component.name);
1035
- return ` if (!controller.classes.has(${componentName})) {\n controller.classes.set(${componentName}, ${this.classFactoryName(component.name)}());\n }`;
1036
- })
1037
- .join('\n');
1038
-
1039
- const templateRegistrations = uniqueComponents
1040
- .map((component) => {
1041
- const componentName = JSON.stringify(component.name);
1042
- const templateVarName = `__templateElement_${this.toSafeIdentifier(component.name)}`;
1043
- return ` if (!controller.templates.has(${componentName})) {\n controller.templates.set(${componentName}, ${templateVarName});\n }`;
1044
- })
1045
- .join('\n');
1046
-
1047
- const cssRegistrationInit = uniqueComponents.length
1048
- ? ` if (!stylesManager.__sliceRegisteredComponentStyles) {\n stylesManager.__sliceRegisteredComponentStyles = new Set();\n }`
1049
- : '';
1050
-
1051
- const cssRegistrations = uniqueComponents
1052
- .map((component) => {
1053
- const componentName = JSON.stringify(component.name);
1054
- return ` if (!stylesManager.__sliceRegisteredComponentStyles.has(${componentName})) {\n stylesManager.registerComponentStyles(${componentName}, ${JSON.stringify(component.css || '')});\n stylesManager.__sliceRegisteredComponentStyles.add(${componentName});\n }`;
1055
- })
1056
- .join('\n');
1057
-
1058
- const categoryRegistrations = uniqueComponents
1059
- .map((component) => {
1060
- const componentName = JSON.stringify(component.name);
1061
- return ` if (!controller.componentCategories.has(${componentName})) {\n controller.componentCategories.set(${componentName}, ${JSON.stringify(component.category)});\n }`;
1062
- })
1063
- .join('\n');
1064
-
1065
- const metadata = {
1066
- version: '2',
1067
- bundleKey,
1068
- type,
1069
- routes: routePath ? [routePath] : [],
1070
- componentCount: uniqueComponents.length
1071
- };
1072
-
1073
- return `${hoistedImportBlock}${hoistedImportBlock ? '\n\n' : ''}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`;
1074
- }
1075
-
1076
- buildV2DependencyModuleBlock(components) {
1077
- const modules = this.collectDependencyModulesFromComponents(components);
1078
-
1079
- const lines = ['const SLICE_BUNDLE_DEPENDENCIES = {};'];
1080
- modules.forEach((module, index) => {
1081
- const exportVar = `__sliceDepExports${index}`;
1082
- const transformedContent = this.transformDependencyContent(module.content, exportVar, module.name);
1083
- lines.push(`const ${exportVar} = {};`);
1084
- lines.push(transformedContent.trim());
1085
- lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
1086
- });
1087
-
1088
- return lines.join('\n');
1089
- }
1090
-
1091
- async buildDependencyContents(jsContent, componentPath) {
1092
- const dependencies = this.analyzeDependencies(jsContent, componentPath);
1093
- const dependencyContents = {};
1094
-
1095
- for (const dep of dependencies) {
1096
- const depPath = dep.path;
1097
- try {
1098
- const depContent = await fs.readFile(depPath, 'utf-8');
1099
- const depName = path
1100
- .relative(this.srcPath, depPath)
1101
- .replace(/\\/g, '/');
1102
- dependencyContents[depName] = {
1103
- content: depContent,
1104
- bindings: dep.bindings || []
1105
- };
1106
- } catch (error) {
1107
- console.warn(`Warning: Could not read dependency ${depPath}:`, error.message);
1108
- }
1109
- }
1110
-
1111
- return dependencyContents;
1112
- }
1113
-
1114
- /**
1115
- * Cleans JavaScript code by removing imports/exports and ensuring class is available globally
1116
- */
1117
- cleanJavaScript(code, componentName, sourceContext = componentName) {
1118
- // Remove export default
1119
- code = code.replace(/export\s+default\s+/g, '');
1120
-
1121
- // Remove only unsupported imports (relative always removed, allowed absolute kept)
1122
- const stripped = this.stripImports(code, {
1123
- sourceContext,
1124
- collectHoistedImports: true
1125
- });
1126
- const hoistedImports = stripped.hoistedImports || [];
1127
- code = stripped.code;
1128
-
1129
- // Guard customElements.define to avoid duplicate registrations
1130
- code = code.replace(
1131
- /customElements\.define\(([^)]+)\);?/g,
1132
- (match, args) => {
1133
- const firstArg = args.split(',')[0]?.trim() || '';
1134
- if (!/^['"][^'"]+['"]$/.test(firstArg)) {
1135
- return match;
1136
- }
1137
- return `if (!customElements.get(${firstArg})) { customElements.define(${args}); }`;
1138
- }
1139
- );
1140
-
1141
- // Make sure the class is available globally for bundle evaluation
1142
- // Preserve original customElements.define if it exists
1143
- if (code.includes('customElements.define')) {
1144
- // Add global assignment before guarded or direct customElements.define
1145
- const globalAssignment = `window.${componentName} = ${componentName};\n`;
1146
- const guardedDefineRegex = /if\s*\(\s*!\s*customElements\.get\([^)]*\)\s*\)\s*\{\s*customElements\.define\([^;]+\);?\s*\}\s*$/;
1147
- const directDefineRegex = /customElements\.define\([^;]+\);?\s*$/;
1148
- if (guardedDefineRegex.test(code)) {
1149
- code = code.replace(guardedDefineRegex, `${globalAssignment}$&`);
1150
- } else {
1151
- code = code.replace(directDefineRegex, `${globalAssignment}$&`);
1152
- }
1153
- } else {
1154
- // If no customElements.define found, just assign to global
1155
- code += `\nwindow.${componentName} = ${componentName};`;
1156
- }
1157
-
1158
- // Add return statement for bundle evaluation compatibility
1159
- code += `\nreturn ${componentName};`;
1160
-
1161
- return {
1162
- code,
1163
- hoistedImports
1164
- };
1165
- }
1166
-
1167
- /**
1168
- * Formats the bundle file
1169
- */
1170
- formatBundleFile(componentsData, metadata) {
1171
- const integrityPayload = {
1172
- metadata: {
1173
- ...metadata,
1174
- generated: 'static'
1175
- },
1176
- components: Object.fromEntries(
1177
- Object.entries(componentsData).map(([name, data]) => [
1178
- name,
1179
- {
1180
- name: data.name,
1181
- category: data.category,
1182
- categoryType: data.categoryType,
1183
- componentDependencies: data.componentDependencies
1184
- }
1185
- ])
1186
- )
1187
- };
1188
- const integrity = `sha256:${crypto
1189
- .createHash('sha256')
1190
- .update(JSON.stringify(integrityPayload))
1191
- .digest('hex')}`;
1192
-
1193
- const dependencyModules = this.collectDependencyModules(componentsData);
1194
- const frameworkComponentKeys = Object.keys(componentsData || {});
1195
- const frameworkClassIdentifiers = frameworkComponentKeys.map((key) => this.toSafeIdentifier(key));
1196
- const frameworkReservedIdentifiers = new Set([
1197
- 'SLICE_BUNDLE',
1198
- 'SLICE_BUNDLE_COMPONENTS',
1199
- 'SLICE_BUNDLE_DEPENDENCIES',
1200
- 'SLICE_FRAMEWORK_CLASSES',
1201
- ...frameworkClassIdentifiers,
1202
- ...this.getDependencyExportVariableNames(dependencyModules)
1203
- ]);
1204
- const rawHoistedImports = Object.values(componentsData || {})
1205
- .flatMap((component) => component?.hoistedImports || [])
1206
- .map((statement) => String(statement).trim())
1207
- .filter(Boolean);
1208
- this.validateHoistedImportCollisions(rawHoistedImports, frameworkReservedIdentifiers);
1209
- const hoistedImportBlock = Array.from(new Set(rawHoistedImports)).join('\n');
1210
-
1211
- const dependencyBlock = this.buildDependencyModuleBlock(componentsData);
1212
- const componentBlock = this.buildComponentBundleBlock(componentsData);
1213
-
1214
- return `${hoistedImportBlock}${hoistedImportBlock ? '\n\n' : ''}/**
1215
- * Slice.js Bundle
1216
- * Type: ${metadata.type}
1217
- * Generated: ${metadata.generated}
1218
- * Strategy: ${metadata.strategy}
1219
- * Components: ${metadata.componentCount}
1220
- * Total Size: ${(metadata.totalSize / 1024).toFixed(1)} KB
1221
- */
1222
-
1223
- ${dependencyBlock}
1224
- ${componentBlock}
1225
-
1226
- export const SLICE_BUNDLE = {
1227
- metadata: ${JSON.stringify({ ...metadata, integrity }, null, 2)},
1228
- components: SLICE_BUNDLE_COMPONENTS
1229
- };
1230
-
1231
- // Auto-registration of components
1232
- if (window.slice && window.slice.controller) {
1233
- slice.controller.registerBundle(SLICE_BUNDLE);
1234
- }
1235
- `;
1236
- }
1237
-
1238
- buildDependencyModuleBlock(componentsData) {
1239
- const dependencyModules = this.collectDependencyModules(componentsData);
1240
- if (dependencyModules.length === 0) {
1241
- return 'const SLICE_BUNDLE_DEPENDENCIES = {};';
1242
- }
1243
-
1244
- const lines = ['const SLICE_BUNDLE_DEPENDENCIES = {};'];
1245
- dependencyModules.forEach((module, index) => {
1246
- const exportVar = `__sliceDepExports${index}`;
1247
- const content = this.transformDependencyContent(module.content, exportVar, module.name);
1248
- lines.push(`// Dependency: ${module.name}`);
1249
- lines.push(`const ${exportVar} = {};`);
1250
- lines.push(content.trim());
1251
- lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
1252
- });
1253
-
1254
- return `${lines.join('\n')}`;
1255
- }
1256
-
1257
- collectDependencyModules(componentsData) {
1258
- const modules = new Map();
1259
- Object.values(componentsData).forEach((component) => {
1260
- Object.entries(component.externalDependencies || {}).forEach(([name, entry]) => {
1261
- if (modules.has(name)) return;
1262
- const content = typeof entry === 'string' ? entry : entry.content;
1263
- modules.set(name, { name, content });
1264
- });
1265
- });
1266
- return Array.from(modules.values());
1267
- }
1268
-
1269
- collectDependencyModulesFromComponents(components = []) {
1270
- const modules = new Map();
1271
- for (const component of components || []) {
1272
- const externalDependencies = component.externalDependencies || {};
1273
- for (const [moduleName, entry] of Object.entries(externalDependencies)) {
1274
- if (modules.has(moduleName)) continue;
1275
- const content = typeof entry === 'string' ? entry : entry?.content;
1276
- if (!content) continue;
1277
- modules.set(moduleName, { name: moduleName, content });
1278
- }
1279
- }
1280
- return Array.from(modules.values());
1281
- }
1282
-
1283
- getDependencyExportVariableNames(dependencyModules = []) {
1284
- return (dependencyModules || []).map((_, index) => `__sliceDepExports${index}`);
1285
- }
1286
-
1287
- transformDependencyContent(content, exportVar, moduleName) {
1288
- const baseName = moduleName.split('/').pop().replace(/\.[^.]+$/, '');
1289
- const dataName = baseName ? `${baseName}Data` : null;
1290
- const exportPrefix = dataName ? `${exportVar}.${dataName} = ` : `${exportVar}.default = `;
1291
-
1292
- return content
1293
- .replace(/export\s+const\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
1294
- .replace(/export\s+let\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
1295
- .replace(/export\s+var\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
1296
- .replace(/export\s+function\s+(\w+)/g, `${exportVar}.$1 = function`)
1297
- .replace(/export\s+default\s+/g, exportPrefix)
1298
- .replace(/export\s*{\s*([^}]+)\s*}/g, (match, exportsStr) => {
1299
- return exportsStr
1300
- .split(',')
1301
- .map((exp) => {
1302
- const cleanExp = exp.trim();
1303
- const varName = cleanExp.split(' as ')[0].trim();
1304
- return `${exportVar}.${varName} = ${varName};`;
1305
- })
1306
- .join('\n');
1307
- })
1308
- .replace(/^\s*export\s+/gm, '');
1309
- }
1310
-
1311
- buildComponentBundleBlock(componentsData) {
1312
- const componentEntries = [];
1313
- const componentDefs = [];
1314
- const frameworkEntries = [];
1315
-
1316
- Object.entries(componentsData).forEach(([name, data]) => {
1317
- const classVar = this.toSafeIdentifier(name);
1318
- const bindings = this.buildDependencyBindings(data.externalDependencies || {});
1319
-
1320
- componentDefs.push(`const ${classVar} = (() => {\n${bindings}\n${data.js}\nreturn ${name};\n})();`);
1321
-
1322
- if (data.isFramework) {
1323
- frameworkEntries.push(`${JSON.stringify(data.name)}: ${classVar}`);
1324
- }
1325
-
1326
- componentEntries.push(
1327
- `${JSON.stringify(name)}: {\n` +
1328
- ` name: ${JSON.stringify(data.name)},\n` +
1329
- ` category: ${JSON.stringify(data.category)},\n` +
1330
- ` categoryType: ${JSON.stringify(data.categoryType)},\n` +
1331
- ` componentDependencies: ${JSON.stringify(data.componentDependencies)},\n` +
1332
- ` html: ${JSON.stringify(data.html)},\n` +
1333
- ` css: ${JSON.stringify(data.css)},\n` +
1334
- ` size: ${JSON.stringify(data.size)},\n` +
1335
- ` class: ${classVar}\n` +
1336
- `}`
1337
- );
1338
- });
1339
-
1340
- const frameworkBlock = frameworkEntries.length > 0
1341
- ? `const SLICE_FRAMEWORK_CLASSES = {\n${frameworkEntries.join(',\n')}\n};\nwindow.SLICE_FRAMEWORK_CLASSES = SLICE_FRAMEWORK_CLASSES;`
1342
- : '';
1343
-
1344
- return `${componentDefs.join('\n\n')}\n\nconst SLICE_BUNDLE_COMPONENTS = {\n${componentEntries.join(',\n')}\n};\n${frameworkBlock}`;
1345
- }
1346
-
1347
- buildDependencyBindings(externalDependencies) {
1348
- const lines = [];
1349
- Object.entries(externalDependencies).forEach(([name, entry]) => {
1350
- const bindings = typeof entry === 'string' ? [] : entry.bindings || [];
1351
- const depVar = `SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(name)}]`;
1352
- const baseName = name.split('/').pop().replace(/\.[^.]+$/, '');
1353
- const dataName = baseName ? `${baseName}Data` : null;
1354
-
1355
- bindings.forEach((binding) => {
1356
- if (!binding?.localName) return;
1357
- if (binding.type === 'default') {
1358
- const fallback = dataName ? `${depVar}.${dataName}` : `${depVar}.default`;
1359
- lines.push(`const ${binding.localName} = ${depVar}.default !== undefined ? ${depVar}.default : ${fallback};`);
1360
- }
1361
- if (binding.type === 'named') {
1362
- lines.push(`const ${binding.localName} = ${depVar}.${binding.importedName};`);
1363
- }
1364
- if (binding.type === 'namespace') {
1365
- lines.push(`const ${binding.localName} = ${depVar};`);
1366
- }
1367
- });
1368
- });
1369
-
1370
- return lines.join('\n');
1371
- }
1372
-
1373
- toSafeIdentifier(name) {
1374
- const cleaned = name.replace(/[^a-zA-Z0-9_]/g, '_');
1375
- if (/^\d/.test(cleaned)) {
1376
- return `SliceComponent_${cleaned}`;
1377
- }
1378
- return `SliceComponent_${cleaned}`;
1379
- }
1380
-
1381
- /**
1382
- * Generates the bundle configuration
1383
- */
1384
- generateBundleConfig(frameworkBundle = null) {
1385
- const metrics = this.analysisData.metrics || {};
1386
- const config = {
1387
- version: '2.0.0',
1388
- format: this.format,
1389
- loadingPolicy: this.loadingPolicy,
1390
- strategy: this.config.strategy,
1391
- minified: this.options.minify,
1392
- obfuscated: this.options.obfuscate,
1393
- production: true,
1394
- generated: new Date().toISOString(),
1395
-
1396
- stats: {
1397
- totalComponents: metrics.totalComponents || 0,
1398
- totalRoutes: metrics.totalRoutes || 0,
1399
- sharedComponents: this.bundles.critical.components.length,
1400
- sharedPercentage: metrics.sharedPercentage || 0,
1401
- totalSize: metrics.totalSize || 0,
1402
- criticalSize: this.bundles.critical.size
1403
- },
1404
-
1405
- bundles: {
1406
- framework: {
1407
- file: 'slice-bundle.framework.js',
1408
- size: 0,
1409
- hash: null,
1410
- integrity: null,
1411
- components: []
1412
- },
1413
- critical: {
1414
- file: this.bundles.critical.file,
1415
- size: this.bundles.critical.size,
1416
- hash: this.bundles.critical.hash || null,
1417
- integrity: this.bundles.critical.integrity || null,
1418
- components: this.bundles.critical.components.map(c => c.name)
1419
- },
1420
- routes: {}
1421
- },
1422
- routeBundles: {}
1423
- };
1424
-
1425
- for (const [key, bundle] of Object.entries(this.bundles.routes)) {
1426
- const routeIdentifier = Array.isArray(bundle.path || bundle.paths)
1427
- ? key
1428
- : (bundle.path || bundle.paths || key);
1429
- const dependencies = this.mergeBundleDependencies(bundle.dependencies || []);
1430
-
1431
- config.bundles.routes[key] = {
1432
- path: bundle.path || bundle.paths || key, // Support both single path and array of paths, fallback to key
1433
- file: `slice-bundle.${this.routeToFileName(routeIdentifier)}.js`,
1434
- size: bundle.size,
1435
- hash: bundle.hash || null,
1436
- integrity: bundle.integrity || null,
1437
- components: bundle.components.map(c => c.name),
1438
- dependencies
1439
- };
1440
-
1441
- const paths = Array.isArray(config.bundles.routes[key].path)
1442
- ? config.bundles.routes[key].path
1443
- : [config.bundles.routes[key].path];
1444
-
1445
- for (const routePath of paths) {
1446
- if (!config.routeBundles[routePath]) {
1447
- config.routeBundles[routePath] = ['critical'];
1448
- }
1449
- for (const dependency of dependencies.filter((dep) => dep !== 'critical')) {
1450
- if (!config.routeBundles[routePath].includes(dependency)) {
1451
- config.routeBundles[routePath].push(dependency);
1452
- }
1453
- }
1454
- if (!config.routeBundles[routePath].includes(key)) {
1455
- config.routeBundles[routePath].push(key);
1456
- }
1457
- }
1458
- }
1459
-
1460
- if (frameworkBundle) {
1461
- config.bundles.framework = {
1462
- file: frameworkBundle.file,
1463
- size: frameworkBundle.size,
1464
- hash: frameworkBundle.hash,
1465
- integrity: frameworkBundle.integrity,
1466
- components: frameworkBundle.components || []
1467
- };
1468
- }
1469
-
1470
- return config;
1471
- }
1472
-
1473
- collectFrameworkComponents() {
1474
- return this.analysisData.components.filter((comp) => comp.isFramework);
1475
- }
1476
-
1477
- async createFrameworkBundle(components) {
1478
- const fileName = 'slice-bundle.framework.js';
1479
- const filePath = path.join(this.bundlesPath, fileName);
1480
- return this.generateFrameworkBundleFile(components, fileName, filePath);
1481
- }
1482
-
1483
- async generateFrameworkBundleFile(components, fileName, filePath) {
1484
- const componentsData = {};
1485
- const componentsMap = await this.loadComponentsMap();
1486
- const metadata = {
1487
- version: '2.0.0',
1488
- type: 'framework',
1489
- route: null,
1490
- bundleKey: 'framework',
1491
- file: fileName,
1492
- generated: new Date().toISOString(),
1493
- totalSize: components.reduce((sum, c) => sum + c.size, 0),
1494
- componentCount: components.length,
1495
- strategy: this.config.strategy,
1496
- minified: this.options.minify,
1497
- obfuscated: this.options.obfuscate
1498
- };
1499
-
1500
- components.forEach((comp) => {
1501
- const componentKey = `Framework/Structural/${comp.name}`;
1502
- const fileBaseName = comp.fileName || comp.name;
1503
- const jsPath = path.join(comp.path, `${fileBaseName}.js`);
1504
- const jsContent = fs.readFileSync(jsPath, 'utf-8');
1505
- const dependencyContents = this.buildDependencyContentsSync(jsContent, comp.path);
1506
- const cleanedJavaScript = this.cleanJavaScript(jsContent, comp.name, jsPath);
1507
-
1508
- componentsData[componentKey] = {
1509
- name: comp.name,
1510
- category: comp.category,
1511
- categoryType: comp.categoryType,
1512
- isFramework: true,
1513
- js: cleanedJavaScript.code,
1514
- hoistedImports: cleanedJavaScript.hoistedImports,
1515
- externalDependencies: dependencyContents,
1516
- componentDependencies: Array.from(comp.dependencies),
1517
- html: fs.existsSync(path.join(comp.path, `${fileBaseName}.html`))
1518
- ? fs.readFileSync(path.join(comp.path, `${fileBaseName}.html`), 'utf-8')
1519
- : null,
1520
- css: fs.existsSync(path.join(comp.path, `${fileBaseName}.css`))
1521
- ? fs.readFileSync(path.join(comp.path, `${fileBaseName}.css`), 'utf-8')
1522
- : null,
1523
- size: comp.size
1524
- };
1525
- });
1526
-
1527
- const prelude = `const components = ${JSON.stringify(componentsMap)};`;
1528
- const bundleContent = `${prelude}\n${this.formatBundleFile(componentsData, metadata)}`;
1529
- const finalContent = await this.applyBundleTransforms(bundleContent, fileName);
1530
- await fs.ensureDir(path.dirname(filePath));
1531
- await fs.writeFile(filePath, finalContent, 'utf-8');
1532
-
1533
- const hash = crypto.createHash('sha256').update(finalContent).digest('hex');
1534
- const integrity = `sha256:${hash}`;
1535
-
1536
- return {
1537
- name: 'framework',
1538
- file: fileName,
1539
- size: Buffer.byteLength(bundleContent, 'utf-8'),
1540
- hash,
1541
- integrity,
1542
- componentCount: components.length,
1543
- components: components.map((comp) => `Framework/Structural/${comp.name}`)
1544
- };
1545
- }
1546
-
1547
- buildDependencyContentsSync(jsContent, componentPath) {
1548
- const dependencies = this.analyzeDependencies(jsContent, componentPath);
1549
- const dependencyContents = {};
1550
-
1551
- for (const dep of dependencies) {
1552
- const depPath = dep.path;
1553
- try {
1554
- const depContent = fs.readFileSync(depPath, 'utf-8');
1555
- const depName = path
1556
- .relative(this.srcPath, depPath)
1557
- .replace(/\\/g, '/');
1558
- dependencyContents[depName] = {
1559
- content: depContent,
1560
- bindings: dep.bindings || []
1561
- };
1562
- } catch (error) {
1563
- console.warn(`Warning: Could not read dependency ${depPath}:`, error.message);
1564
- }
1565
- }
1566
-
1567
- return dependencyContents;
1568
- }
1569
-
1570
- getConfiguredPublicFolders() {
1571
- const publicFolders = Array.isArray(this.sliceConfig?.publicFolders)
1572
- ? this.sliceConfig.publicFolders
1573
- : [];
1574
-
1575
- return publicFolders
1576
- .map((folder) => this.normalizePublicFolder(folder))
1577
- .filter(Boolean);
1578
- }
1579
-
1580
- normalizePublicFolder(folder) {
1581
- if (typeof folder !== 'string') return null;
1582
- let normalized = folder.trim();
1583
- if (!normalized) return null;
1584
-
1585
- if (!normalized.startsWith('/')) {
1586
- normalized = `/${normalized}`;
1587
- }
1588
-
1589
- normalized = normalized.replace(/\\+/g, '/').replace(/\/+/g, '/');
1590
- if (normalized.length > 1 && normalized.endsWith('/')) {
1591
- normalized = normalized.slice(0, -1);
1592
- }
1593
-
1594
- return normalized;
1595
- }
1596
-
1597
- normalizeImportPath(importPath) {
1598
- if (typeof importPath !== 'string') return '';
1599
- const cleanPath = importPath.split(/[?#]/)[0];
1600
- return cleanPath.replace(/\\+/g, '/').replace(/\/+/g, '/');
1601
- }
1602
-
1603
- isRelativeImport(importPath) {
1604
- return importPath.startsWith('./') || importPath.startsWith('../');
1605
- }
1606
-
1607
- isAbsoluteImport(importPath) {
1608
- return importPath.startsWith('/');
1609
- }
1610
-
1611
- isImportInPublicFolders(importPath, publicFolders) {
1612
- const normalizedImport = this.normalizeImportPath(importPath);
1613
- return publicFolders.some((folder) => normalizedImport === folder || normalizedImport.startsWith(`${folder}/`));
1614
- }
1615
-
1616
- classifyImport(importPath, publicFolders) {
1617
- if (typeof importPath !== 'string' || !importPath) {
1618
- return { keep: false, warning: 'Warning: Removing bare import: <unknown>' };
1619
- }
1620
-
1621
- if (this.isRelativeImport(importPath)) {
1622
- return { keep: false, warning: null };
1623
- }
1624
-
1625
- if (this.isAbsoluteImport(importPath)) {
1626
- if (this.isImportInPublicFolders(importPath, publicFolders)) {
1627
- return { keep: true, warning: null };
1628
- }
1629
-
1630
- return {
1631
- keep: false,
1632
- warning: `Warning: Removing absolute import outside publicFolders: ${importPath}`
1633
- };
1634
- }
1635
-
1636
- return {
1637
- keep: false,
1638
- warning: `Warning: Removing bare import: ${importPath}`
1639
- };
1640
- }
1641
-
1642
- buildImportWarningMessage(baseMessage, sourceContext) {
1643
- if (!sourceContext) return baseMessage;
1644
- return `${baseMessage} [${sourceContext}]`;
1645
- }
1646
-
1647
- extractLocalBindingsFromImportStatement(statement) {
1648
- const source = String(statement || '').trim();
1649
- if (!source.startsWith('import ')) return [];
1650
- if (/^import\s+['"][^'"]+['"]\s*;?$/.test(source)) return [];
1651
-
1652
- const bindings = [];
1653
-
1654
- const defaultMatch = source.match(/^import\s+([A-Za-z_$][\w$]*)\s*(,|\s+from\s+)/);
1655
- if (defaultMatch && defaultMatch[1] !== '*') {
1656
- bindings.push(defaultMatch[1]);
1657
- }
1658
-
1659
- const namespaceMatch = source.match(/,?\s*\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"]/);
1660
- if (namespaceMatch) {
1661
- bindings.push(namespaceMatch[1]);
1662
- }
1663
-
1664
- const namedMatch = source.match(/\{([\s\S]*?)\}\s*from\s*['"]/);
1665
- if (namedMatch) {
1666
- const namedSection = namedMatch[1];
1667
- for (const part of namedSection.split(',')) {
1668
- const cleanPart = part.trim();
1669
- if (!cleanPart) continue;
1670
- const aliasParts = cleanPart.split(/\s+as\s+/i).map((v) => v.trim()).filter(Boolean);
1671
- const localName = aliasParts.length > 1 ? aliasParts[1] : aliasParts[0];
1672
- if (/^[A-Za-z_$][\w$]*$/.test(localName)) {
1673
- bindings.push(localName);
1674
- }
1675
- }
1676
- }
1677
-
1678
- return Array.from(new Set(bindings));
1679
- }
1680
-
1681
- validateHoistedImportCollisions(importStatements, reservedIdentifiers = new Set()) {
1682
- const reserved = reservedIdentifiers instanceof Set
1683
- ? reservedIdentifiers
1684
- : new Set(reservedIdentifiers || []);
1685
- const bindingToStatement = new Map();
1686
-
1687
- for (const statement of importStatements || []) {
1688
- const normalizedStatement = String(statement || '').trim();
1689
- if (!normalizedStatement) continue;
1690
- const localBindings = this.extractLocalBindingsFromImportStatement(normalizedStatement);
1691
-
1692
- for (const localBinding of localBindings) {
1693
- if (reserved.has(localBinding)) {
1694
- throw new Error(`Hoisted import reserved identifier collision: ${localBinding}`);
1695
- }
1696
- const previousStatement = bindingToStatement.get(localBinding);
1697
- if (previousStatement && previousStatement !== normalizedStatement) {
1698
- throw new Error(`Hoisted import binding collision: ${localBinding}`);
1699
- }
1700
- bindingToStatement.set(localBinding, normalizedStatement);
1701
- }
1702
- }
1703
- }
1704
-
1705
- parseImportsFromCode(code) {
1706
- const ast = parse(code, {
1707
- sourceType: 'module',
1708
- plugins: ['jsx']
1709
- });
1710
-
1711
- const importNodes = [];
1712
- traverse.default(ast, {
1713
- ImportDeclaration(pathNode) {
1714
- importNodes.push(pathNode.node);
1715
- }
1716
- });
1717
-
1718
- return importNodes
1719
- .filter((node) => typeof node.start === 'number' && typeof node.end === 'number')
1720
- .sort((a, b) => a.start - b.start);
1721
- }
1722
-
1723
- parseImportsWithFallbackScanner(code) {
1724
- const entries = [];
1725
- const importRegex = /\bimport\b/g;
1726
- let match = null;
1727
-
1728
- while ((match = importRegex.exec(code)) !== null) {
1729
- const start = match.index;
1730
- const nextChar = code[start + 'import'.length];
1731
- if (nextChar === '(') {
1732
- continue;
1733
- }
1734
-
1735
- let index = start + 'import'.length;
1736
- let quote = null;
1737
- let escaped = false;
1738
-
1739
- while (index < code.length) {
1740
- const char = code[index];
1741
-
1742
- if (quote) {
1743
- if (escaped) {
1744
- escaped = false;
1745
- } else if (char === '\\') {
1746
- escaped = true;
1747
- } else if (char === quote) {
1748
- quote = null;
1749
- }
1750
- index += 1;
1751
- continue;
1752
- }
1753
-
1754
- if (char === '\'' || char === '"' || char === '`') {
1755
- quote = char;
1756
- index += 1;
1757
- continue;
1758
- }
1759
-
1760
- if (char === ';') {
1761
- index += 1;
1762
- break;
1763
- }
1764
-
1765
- index += 1;
1766
- }
1767
-
1768
- const end = index;
1769
- const statement = code.slice(start, end);
1770
- const fromMatch = statement.match(/\bfrom\s+['"]([^'"]+)['"]/);
1771
- const sideEffectMatch = statement.match(/\bimport\s+['"]([^'"]+)['"]/);
1772
- const importPath = fromMatch?.[1] || sideEffectMatch?.[1] || null;
1773
-
1774
- if (!importPath) {
1775
- continue;
1776
- }
1777
-
1778
- entries.push({ start, end, statement, importPath });
1779
- importRegex.lastIndex = end;
1780
- }
1781
-
1782
- return entries;
1783
- }
1784
-
1785
- stripImportsWithFallbackRegex(code, publicFolders, sourceContext, collectHoistedImports) {
1786
- const hoistedImports = [];
1787
- const importEntries = this.parseImportsWithFallbackScanner(code);
1788
- if (importEntries.length === 0) {
1789
- return { code, hoistedImports };
1790
- }
1791
-
1792
- let cleanedCode = '';
1793
- let cursor = 0;
1794
- for (const entry of importEntries) {
1795
- const { start, end, statement, importPath } = entry;
1796
- const classification = this.classifyImport(importPath, publicFolders);
1797
- cleanedCode += code.slice(cursor, start);
1798
- if (classification.keep) {
1799
- if (collectHoistedImports) {
1800
- hoistedImports.push(statement.trim());
1801
- } else {
1802
- cleanedCode += statement;
1803
- }
1804
- } else if (classification.warning) {
1805
- console.warn(this.buildImportWarningMessage(classification.warning, sourceContext));
1806
- }
1807
- cursor = end;
1808
- }
1809
-
1810
- cleanedCode += code.slice(cursor);
1811
-
1812
- return { code: cleanedCode, hoistedImports };
1813
- }
1814
-
1815
- stripImports(code, options = {}) {
1816
- const { sourceContext = null, collectHoistedImports = false } = options;
1817
- const publicFolders = this.getConfiguredPublicFolders();
1818
- const hoistedImports = [];
1819
-
1820
- try {
1821
- const importNodes = this.parseImportsFromCode(code);
1822
-
1823
- if (importNodes.length === 0) {
1824
- return collectHoistedImports ? { code, hoistedImports } : code;
1825
- }
1826
-
1827
- let cleaned = '';
1828
- let cursor = 0;
1829
-
1830
- for (const node of importNodes) {
1831
- const importPath = node.source?.value;
1832
- const classification = this.classifyImport(importPath, publicFolders);
1833
- const statement = code.slice(node.start, node.end);
1834
-
1835
- cleaned += code.slice(cursor, node.start);
1836
- if (classification.keep) {
1837
- if (collectHoistedImports) {
1838
- hoistedImports.push(statement.trim());
1839
- } else {
1840
- cleaned += statement;
1841
- }
1842
- } else if (classification.warning) {
1843
- console.warn(this.buildImportWarningMessage(classification.warning, sourceContext));
1844
- }
1845
-
1846
- cursor = node.end;
1847
- }
1848
-
1849
- cleaned += code.slice(cursor);
1850
- return collectHoistedImports
1851
- ? { code: cleaned, hoistedImports }
1852
- : cleaned;
1853
- } catch (error) {
1854
- const fallback = this.stripImportsWithFallbackRegex(code, publicFolders, sourceContext, collectHoistedImports);
1855
- return collectHoistedImports ? fallback : fallback.code;
1856
- }
1857
- }
1858
-
1859
- async loadComponentsMap() {
1860
- const componentsConfigPath = path.join(this.componentsPath, 'components.js');
1861
- if (!await fs.pathExists(componentsConfigPath)) {
1862
- return {};
1863
- }
1864
-
1865
- const content = await fs.readFile(componentsConfigPath, 'utf-8');
1866
- return this.parseComponentsConfig(content);
1867
- }
1868
-
1869
- parseComponentsConfig(content) {
1870
- try {
1871
- const ast = parse(content, {
1872
- sourceType: 'module',
1873
- plugins: ['jsx']
1874
- });
1875
-
1876
- let componentsNode = null;
1877
-
1878
- traverse.default(ast, {
1879
- VariableDeclarator(path) {
1880
- if (path.node.id?.type === 'Identifier' && path.node.id.name === 'components') {
1881
- componentsNode = path.node.init;
1882
- path.stop();
1883
- }
1884
- }
1885
- });
1886
-
1887
- if (!componentsNode || componentsNode.type !== 'ObjectExpression') {
1888
- throw new Error('components object not found');
1889
- }
1890
-
1891
- const config = {};
1892
- for (const prop of componentsNode.properties) {
1893
- if (prop.type !== 'ObjectProperty') continue;
1894
-
1895
- const key = this.extractStringValue(prop.key);
1896
- const value = this.extractStringValue(prop.value);
1897
-
1898
- if (!key || !value) {
1899
- throw new Error('Invalid components entry');
1900
- }
1901
-
1902
- config[key] = value;
1903
- }
1904
-
1905
- return config;
1906
- } catch (error) {
1907
- console.warn(`Could not parse components.js: ${error.message}`);
1908
- return {};
1909
- }
1910
- }
1911
-
1912
- extractStringValue(node) {
1913
- if (!node) return null;
1914
-
1915
- if (node.type === 'StringLiteral') {
1916
- return node.value;
1917
- }
1918
-
1919
- if (node.type === 'Identifier') {
1920
- return node.name;
1921
- }
1922
-
1923
- if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
1924
- return node.quasis.map((q) => q.value.cooked).join('');
1925
- }
1926
-
1927
- return null;
1928
- }
1929
-
1930
- /**
1931
- * Converts a route to filename
1932
- */
1933
- routeToFileName(routePath) {
1934
- if (routePath === '/') return 'home';
1935
- return routePath
1936
- .replace(/^\//, '')
1937
- .replace(/\//g, '-')
1938
- .replace(/[^a-zA-Z0-9-]/g, '')
1939
- .toLowerCase();
1940
- }
1941
-
1942
- /**
1943
- * Saves the configuration to file
1944
- */
1945
- async saveBundleConfig(config) {
1946
- // Ensure bundles directory exists
1947
- await fs.ensureDir(this.bundlesPath);
1948
-
1949
- // Save JSON config
1950
- const configPath = path.join(this.bundlesPath, 'bundle.config.json');
1951
- await fs.writeJson(configPath, config, { spaces: 2 });
1952
-
1953
- // Generate JavaScript module for direct import
1954
- const jsConfigPath = path.join(this.bundlesPath, 'bundle.config.js');
1955
- const jsConfig = this.generateBundleConfigJS(config);
1956
- await fs.writeFile(jsConfigPath, jsConfig, 'utf-8');
1957
-
1958
- console.log(`✓ Configuration saved to ${configPath}`);
1959
- console.log(`✓ JavaScript config generated: ${jsConfigPath}`);
1960
- }
1961
-
1962
- /**
1963
- * Creates a default bundle config file if none exists
1964
- */
1965
- async createDefaultBundleConfig() {
1966
- const defaultConfigPath = path.join(this.srcPath, 'bundles', 'bundle.config.js');
1967
-
1968
- // Only create if it doesn't exist
1969
- if (await fs.pathExists(defaultConfigPath)) {
1970
- return;
1971
- }
1972
-
1973
- await fs.ensureDir(path.dirname(defaultConfigPath));
1974
-
1975
- const defaultConfig = `/**
1976
- * Slice.js Bundle Configuration
1977
- * Default empty configuration - no bundles available
1978
- * Run 'slice build' to generate optimized bundles
1979
- */
1980
-
1981
- // No bundles available - using individual component loading
1982
- export const SLICE_BUNDLE_CONFIG = null;
1983
-
1984
- // No auto-initialization needed for default config
1985
- `;
1986
-
1987
- await fs.writeFile(defaultConfigPath, defaultConfig, 'utf-8');
1988
- console.log(`✓ Default bundle config created: ${defaultConfigPath}`);
1989
- }
1990
-
1991
- /**
1992
- * Generates JavaScript module for direct import
1993
- */
1994
- generateBundleConfigJS(config) {
1995
- return `/**
1996
- * Slice.js Bundle Configuration
1997
- * Generated: ${new Date().toISOString()}
1998
- * Strategy: ${config.strategy}
1999
- */
2000
-
2001
- // Direct bundle configuration (no fetch required)
2002
- export const SLICE_BUNDLE_CONFIG = ${JSON.stringify(config, null, 2)};
2003
-
2004
- // Auto-initialization if slice is available
2005
- if (typeof window !== 'undefined' && window.slice && window.slice.controller) {
2006
- window.slice.controller.bundleConfig = SLICE_BUNDLE_CONFIG;
2007
-
2008
- // Load critical bundle automatically
2009
- if (SLICE_BUNDLE_CONFIG.bundles.critical && !window.slice.controller.criticalBundleLoaded) {
2010
- (async () => {
2011
- const bundlePath = "/bundles/" + SLICE_BUNDLE_CONFIG.bundles.critical.file;
2012
- const integrity = SLICE_BUNDLE_CONFIG.bundles.critical.integrity;
2013
-
2014
- if (typeof window.slice.controller.verifyBundleIntegrity === 'function') {
2015
- const ok = await window.slice.controller.verifyBundleIntegrity(bundlePath, integrity);
2016
- if (!ok) {
2017
- console.warn('Failed to load critical bundle: integrity check failed');
2018
- return;
2019
- }
2020
- }
2021
-
2022
- import('./slice-bundle.critical.js').catch(err =>
2023
- console.warn('Failed to load critical bundle:', err)
2024
- );
2025
- window.slice.controller.criticalBundleLoaded = true;
2026
- })();
2027
- }
2028
- }
2029
- `;
2030
- }
2031
- }
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, getConfigPath } from '../PathHelper.js';
9
+
10
+ export default class BundleGenerator {
11
+ constructor(moduleUrl, analysisData, options = {}) {
12
+ this.moduleUrl = moduleUrl;
13
+ this.analysisData = analysisData || { components: [], routes: [], metrics: {} };
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
+ this.format = 'v2';
26
+ this.sliceConfig = this.resolveSliceConfig();
27
+ this.loadingPolicy = this.resolveLoadingPolicy();
28
+
29
+ // Configuration
30
+ this.config = {
31
+ maxCriticalSize: 50 * 1024, // 50KB
32
+ maxCriticalComponents: 15,
33
+ minSharedUsage: 3, // Minimum routes to be considered "shared"
34
+ minVendorSharedUsage: 2,
35
+ minVendorSharedTransformedSize: 2 * 1024,
36
+ maxRouteBundleSize: 120 * 1024,
37
+ maxRouteRequests: 12,
38
+ strategy: 'hybrid' // 'global', 'hybrid', 'per-route'
39
+ };
40
+
41
+ this.vendorShared = {
42
+ file: 'slice-bundle.vendor-shared.js',
43
+ dependencyModules: new Map(),
44
+ dependencyUsage: new Map(),
45
+ sharedDependencySet: new Set(),
46
+ bundleKeysUsingSharedDependencies: new Set(),
47
+ bundle: null
48
+ };
49
+
50
+ this.bundles = {
51
+ critical: {
52
+ components: [],
53
+ size: 0,
54
+ file: 'slice-bundle.critical.js'
55
+ },
56
+ routes: {}
57
+ };
58
+ }
59
+
60
+ resolveSliceConfig() {
61
+ if (this.analysisData?.sliceConfig && typeof this.analysisData.sliceConfig === 'object') {
62
+ return this.analysisData.sliceConfig;
63
+ }
64
+
65
+ try {
66
+ const configPath = getConfigPath(this.moduleUrl);
67
+ if (fs.existsSync(configPath)) {
68
+ return fs.readJsonSync(configPath);
69
+ }
70
+ } catch (error) {
71
+ console.warn('Warning: Could not read sliceConfig.json for loading policy:', error.message);
72
+ }
73
+
74
+ return {};
75
+ }
76
+
77
+ resolveLoadingPolicy() {
78
+ return this.sliceConfig?.loading?.enabled ? 'enabled' : 'disabled';
79
+ }
80
+
81
+ /**
82
+ * Computes deterministic integrity hash for bundle metadata.
83
+ * @param {Array} components
84
+ * @param {string} type
85
+ * @param {string|null} routePath
86
+ * @param {string} bundleKey
87
+ * @param {string} fileName
88
+ * @returns {string}
89
+ */
90
+ computeBundleIntegrity(components, type, routePath, bundleKey, fileName) {
91
+ const metadata = {
92
+ version: '2.0.0',
93
+ type,
94
+ route: routePath,
95
+ bundleKey,
96
+ file: fileName,
97
+ generated: 'static',
98
+ totalSize: components.reduce((sum, c) => sum + c.size, 0),
99
+ componentCount: components.length,
100
+ strategy: this.config.strategy
101
+ };
102
+
103
+ const payload = {
104
+ metadata,
105
+ components: components.reduce((acc, comp) => {
106
+ acc[comp.name] = {
107
+ name: comp.name,
108
+ category: comp.category,
109
+ categoryType: comp.categoryType,
110
+ componentDependencies: Array.from(comp.dependencies)
111
+ };
112
+ return acc;
113
+ }, {})
114
+ };
115
+
116
+ return `sha256:${crypto.createHash('sha256')
117
+ .update(JSON.stringify(payload))
118
+ .digest('hex')}`;
119
+ }
120
+
121
+ /**
122
+ * Generates all bundles
123
+ */
124
+ async generate() {
125
+ console.log('🔨 Generating bundles...');
126
+
127
+ // 0. Create bundles directory
128
+ await fs.ensureDir(this.bundlesPath);
129
+ if (this.output === 'dist') {
130
+ await fs.ensureDir(this.distPath);
131
+ }
132
+
133
+ // 1. Determine optimal strategy
134
+ this.determineStrategy();
135
+
136
+ // 2. Identify critical components
137
+ this.identifyCriticalComponents();
138
+
139
+ // 3. Assign components to routes
140
+ this.assignRouteComponents();
141
+
142
+ // 4. Generate bundle files
143
+ const files = await this.generateBundleFiles();
144
+
145
+ // 5. Generate framework bundle (structural)
146
+ const frameworkComponents = this.collectFrameworkComponents();
147
+ let frameworkBundle = null;
148
+ if (frameworkComponents.length > 0) {
149
+ frameworkBundle = await this.createFrameworkBundle(frameworkComponents);
150
+ files.push(frameworkBundle);
151
+ }
152
+
153
+ // 6. Generate configuration
154
+ const config = this.generateBundleConfig(frameworkBundle);
155
+
156
+ console.log('✅ Bundles generated successfully');
157
+
158
+ return {
159
+ bundles: this.bundles,
160
+ config,
161
+ files
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Determines the optimal bundling strategy
167
+ */
168
+ determineStrategy() {
169
+ const { metrics } = this.analysisData;
170
+ const { totalComponents, sharedPercentage } = metrics;
171
+
172
+ // Strategy based on size and usage pattern
173
+ if (totalComponents < 20 || sharedPercentage > 60) {
174
+ this.config.strategy = 'global';
175
+ console.log('📦 Strategy: Global Bundle (small project or highly shared)');
176
+ } else if (totalComponents < 100) {
177
+ this.config.strategy = 'hybrid';
178
+ console.log('📦 Strategy: Hybrid (critical + grouped routes)');
179
+ } else {
180
+ this.config.strategy = 'per-route';
181
+ console.log('📦 Strategy: Per Route (large project)');
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Identifies critical components for the initial bundle
187
+ */
188
+ identifyCriticalComponents() {
189
+ const { components } = this.analysisData;
190
+
191
+ // Filter critical candidates
192
+ const candidates = components
193
+ .filter(comp => {
194
+ if (!this.isComponentAllowedByLoadingPolicy(comp)) return false;
195
+ // Shared components (used in 3+ routes)
196
+ const isShared = comp.routes.size >= this.config.minSharedUsage;
197
+
198
+ // Structural components (Navbar, Footer, etc.)
199
+ const isStructural = comp.categoryType === 'Structural' ||
200
+ ['Navbar', 'Footer', 'Layout'].includes(comp.name);
201
+
202
+ // Small and highly used components (only if used in 3+ routes)
203
+ const isSmallAndUseful = comp.size < 2000 && comp.routes.size >= 3;
204
+
205
+ return isShared || isStructural || isSmallAndUseful;
206
+ })
207
+ .sort((a, b) => {
208
+ // Prioritize by: (usage * 10) - size
209
+ const priorityA = (a.routes.size * 10) - (a.size / 1000);
210
+ const priorityB = (b.routes.size * 10) - (b.size / 1000);
211
+ return priorityB - priorityA;
212
+ });
213
+
214
+ const loadingComponent = components.find((comp) => comp.name === 'Loading' && this.isComponentAllowedByLoadingPolicy(comp));
215
+ if (this.loadingPolicy === 'enabled' && loadingComponent && !candidates.includes(loadingComponent)) {
216
+ candidates.unshift(loadingComponent);
217
+ }
218
+
219
+ // Fill critical bundle up to limit
220
+ for (const comp of candidates) {
221
+ const dependencies = this.getComponentDependencies(comp);
222
+ const totalSize = comp.size + dependencies.reduce((sum, dep) => sum + dep.size, 0);
223
+ const totalCount = 1 + dependencies.length;
224
+
225
+ const wouldExceedSize = this.bundles.critical.size + totalSize > this.config.maxCriticalSize;
226
+ const wouldExceedCount = this.bundles.critical.components.length + totalCount > this.config.maxCriticalComponents;
227
+
228
+ if ((wouldExceedSize || wouldExceedCount) && comp.name !== 'Loading') continue;
229
+
230
+ // Add component and its dependencies
231
+ if (!this.bundles.critical.components.find(c => c.name === comp.name)) {
232
+ this.bundles.critical.components.push(comp);
233
+ this.bundles.critical.size += comp.size;
234
+ }
235
+
236
+ for (const dep of dependencies) {
237
+ if (!this.bundles.critical.components.find(c => c.name === dep.name)) {
238
+ this.bundles.critical.components.push(dep);
239
+ this.bundles.critical.size += dep.size;
240
+ }
241
+ }
242
+ }
243
+
244
+ if (this.loadingPolicy === 'disabled') {
245
+ this.bundles.critical.components = this.bundles.critical.components.filter((comp) => comp.name !== 'Loading');
246
+ this.bundles.critical.size = this.bundles.critical.components.reduce((sum, comp) => sum + comp.size, 0);
247
+ }
248
+
249
+ console.log(`✓ Critical bundle: ${this.bundles.critical.components.length} components, ${(this.bundles.critical.size / 1024).toFixed(1)} KB`);
250
+ }
251
+
252
+ /**
253
+ * Assigns remaining components to route bundles
254
+ */
255
+ assignRouteComponents() {
256
+ const criticalNames = new Set(this.bundles.critical.components.map(c => c.name));
257
+
258
+ if (this.config.strategy === 'hybrid') {
259
+ this.assignHybridBundles(criticalNames);
260
+ } else {
261
+ this.assignPerRouteBundles(criticalNames);
262
+ }
263
+
264
+ this.extractSharedComponents(criticalNames);
265
+ this.rebalanceBundlesByBudget(this.bundles.routes, {
266
+ maxBundleSize: this.config.maxRouteBundleSize,
267
+ maxRequests: this.config.maxRouteRequests
268
+ });
269
+ }
270
+
271
+ /**
272
+ * Assigns components to per-route bundles
273
+ */
274
+ assignPerRouteBundles(criticalNames) {
275
+ for (const route of this.analysisData.routes) {
276
+ const routePath = route.path;
277
+ // Get all route dependencies
278
+ const routeComponents = this.getRouteComponents(route.component);
279
+
280
+ // Include dependencies for all route components
281
+ const allComponents = new Set();
282
+ for (const comp of routeComponents) {
283
+ allComponents.add(comp);
284
+ const dependencies = this.getComponentDependencies(comp);
285
+ for (const dep of dependencies) {
286
+ allComponents.add(dep);
287
+ }
288
+ }
289
+
290
+ // Filter those already in critical
291
+ const uniqueComponents = Array.from(allComponents).filter(comp =>
292
+ !criticalNames.has(comp.name) && this.isComponentAllowedByLoadingPolicy(comp)
293
+ );
294
+
295
+ if (uniqueComponents.length === 0) continue;
296
+
297
+ const routeKey = this.routeToFileName(routePath);
298
+ const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
299
+
300
+ this.bundles.routes[routeKey] = {
301
+ path: routePath,
302
+ components: this.sortComponentsByName(uniqueComponents),
303
+ size: totalSize,
304
+ file: `slice-bundle.${routeKey}.js`
305
+ };
306
+
307
+ console.log(`✓ Bundle ${routeKey}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB`);
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Gets all component dependencies transitively
313
+ */
314
+ getComponentDependencies(component, visited = new Set()) {
315
+ if (visited.has(component.name)) return [];
316
+ visited.add(component.name);
317
+
318
+ const dependencies = [];
319
+
320
+ // Add direct dependencies
321
+ for (const depName of component.dependencies) {
322
+ const depComp = this.analysisData.components.find(c => c.name === depName);
323
+ if (depComp && !visited.has(depName)) {
324
+ dependencies.push(depComp);
325
+ // Add transitive dependencies
326
+ dependencies.push(...this.getComponentDependencies(depComp, visited));
327
+ }
328
+ }
329
+
330
+ return dependencies;
331
+ }
332
+
333
+ /**
334
+ * Assigns components to hybrid bundles (grouped by category)
335
+ */
336
+ assignHybridBundles(criticalNames) {
337
+ const routeGroups = new Map();
338
+
339
+ // First, handle MultiRoute groups
340
+ if (this.analysisData.routeGroups) {
341
+ for (const [groupKey, groupData] of this.analysisData.routeGroups) {
342
+ if (groupData.type === 'multiroute') {
343
+ // Create a bundle for this MultiRoute group
344
+ const allComponents = new Set();
345
+
346
+ // Add the main component (MultiRoute handler)
347
+ const mainComponent = this.analysisData.components.find(c => c.name === groupData.component);
348
+ if (mainComponent) {
349
+ allComponents.add(mainComponent);
350
+
351
+ // Add all components used by this MultiRoute
352
+ const routeComponents = this.getRouteComponents(mainComponent.name);
353
+ for (const comp of routeComponents) {
354
+ allComponents.add(comp);
355
+ // Add transitive dependencies
356
+ const dependencies = this.getComponentDependencies(comp);
357
+ for (const dep of dependencies) {
358
+ allComponents.add(dep);
359
+ }
360
+ }
361
+ }
362
+
363
+ // Filter those already in critical
364
+ const uniqueComponents = Array.from(allComponents).filter(comp =>
365
+ !criticalNames.has(comp.name) && this.isComponentAllowedByLoadingPolicy(comp)
366
+ );
367
+
368
+ if (uniqueComponents.length > 0) {
369
+ const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
370
+
371
+ this.bundles.routes[groupKey] = {
372
+ paths: groupData.routes,
373
+ components: this.sortComponentsByName(uniqueComponents),
374
+ size: totalSize,
375
+ file: `slice-bundle.${this.routeToFileName(groupKey)}.js`
376
+ };
377
+
378
+ console.log(`✓ Bundle ${groupKey}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB (${groupData.routes.length} routes)`);
379
+ }
380
+ }
381
+ }
382
+ }
383
+
384
+ // Group remaining routes by category (skip those already handled by MultiRoute)
385
+ for (const route of this.analysisData.routes) {
386
+ // Check if this route is already handled by a MultiRoute group
387
+ const isHandledByMultiRoute = this.analysisData.routeGroups &&
388
+ Array.from(this.analysisData.routeGroups.values()).some(group =>
389
+ group.type === 'multiroute' && group.routes.includes(route.path)
390
+ );
391
+
392
+ if (!isHandledByMultiRoute) {
393
+ const category = this.categorizeRoute(route.path);
394
+ if (!routeGroups.has(category)) {
395
+ routeGroups.set(category, []);
396
+ }
397
+ routeGroups.get(category).push(route);
398
+ }
399
+ }
400
+
401
+ // Create bundles for each group
402
+ for (const [category, routes] of routeGroups) {
403
+ const allComponents = new Set();
404
+
405
+ // Collect all unique components for this category (including dependencies)
406
+ for (const route of routes) {
407
+ const routeComponents = this.getRouteComponents(route.component);
408
+ for (const comp of routeComponents) {
409
+ allComponents.add(comp);
410
+ // Add transitive dependencies
411
+ const dependencies = this.getComponentDependencies(comp);
412
+ for (const dep of dependencies) {
413
+ allComponents.add(dep);
414
+ }
415
+ }
416
+ }
417
+
418
+ // Filter those already in critical
419
+ const uniqueComponents = Array.from(allComponents).filter(comp =>
420
+ !criticalNames.has(comp.name) && this.isComponentAllowedByLoadingPolicy(comp)
421
+ );
422
+
423
+ if (uniqueComponents.length === 0) continue;
424
+
425
+ const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
426
+ const routePaths = routes.map(r => r.path);
427
+
428
+ this.bundles.routes[category] = {
429
+ paths: routePaths,
430
+ components: this.sortComponentsByName(uniqueComponents),
431
+ size: totalSize,
432
+ file: `slice-bundle.${this.routeToFileName(category)}.js`
433
+ };
434
+
435
+ console.log(`✓ Bundle ${category}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB (${routes.length} routes)`);
436
+ }
437
+ }
438
+
439
+ isComponentAllowedByLoadingPolicy(component) {
440
+ if (!component) return false;
441
+ if (this.loadingPolicy === 'disabled' && component.name === 'Loading') {
442
+ return false;
443
+ }
444
+ return true;
445
+ }
446
+
447
+ sortComponentsByName(components) {
448
+ return [...components].sort((a, b) => a.name.localeCompare(b.name));
449
+ }
450
+
451
+ dedupeComponentsByName(components = []) {
452
+ const byName = new Map();
453
+ for (const component of components) {
454
+ if (!component?.name) continue;
455
+ if (!byName.has(component.name)) {
456
+ byName.set(component.name, component);
457
+ }
458
+ }
459
+ return this.sortComponentsByName(Array.from(byName.values()));
460
+ }
461
+
462
+ getBundlePaths(bundle = {}) {
463
+ const raw = Array.isArray(bundle.paths)
464
+ ? bundle.paths
465
+ : Array.isArray(bundle.path)
466
+ ? bundle.path
467
+ : bundle.path
468
+ ? [bundle.path]
469
+ : [];
470
+
471
+ return Array.from(new Set(raw.filter(Boolean))).sort((a, b) => a.localeCompare(b));
472
+ }
473
+
474
+ setBundlePaths(bundle, paths = []) {
475
+ const mergedPaths = Array.from(new Set((paths || []).filter(Boolean))).sort((a, b) => a.localeCompare(b));
476
+ if (mergedPaths.length === 0) {
477
+ delete bundle.path;
478
+ delete bundle.paths;
479
+ return;
480
+ }
481
+ if (mergedPaths.length === 1) {
482
+ bundle.path = mergedPaths[0];
483
+ delete bundle.paths;
484
+ return;
485
+ }
486
+ bundle.paths = mergedPaths;
487
+ delete bundle.path;
488
+ }
489
+
490
+ mergeBundleDependencies(...dependencyLists) {
491
+ const merged = [];
492
+ const append = (dep) => {
493
+ if (!dep || merged.includes(dep)) return;
494
+ if (dep === 'critical') {
495
+ merged.unshift(dep);
496
+ return;
497
+ }
498
+ merged.push(dep);
499
+ };
500
+
501
+ dependencyLists
502
+ .flat()
503
+ .forEach(append);
504
+
505
+ if (!merged.includes('critical')) {
506
+ merged.unshift('critical');
507
+ }
508
+
509
+ const rest = merged
510
+ .filter((dep) => dep !== 'critical')
511
+ .sort((a, b) => a.localeCompare(b));
512
+ return ['critical', ...rest];
513
+ }
514
+
515
+ extractSharedComponents(criticalNames) {
516
+ const usage = new Map();
517
+
518
+ for (const bundle of Object.values(this.bundles.routes)) {
519
+ const seenInBundle = new Set();
520
+ for (const component of this.dedupeComponentsByName(bundle.components || [])) {
521
+ if (criticalNames.has(component.name)) continue;
522
+ if (!this.isComponentAllowedByLoadingPolicy(component)) continue;
523
+ if (seenInBundle.has(component.name)) continue;
524
+ seenInBundle.add(component.name);
525
+ if (!usage.has(component.name)) {
526
+ usage.set(component.name, { component, count: 0 });
527
+ }
528
+ usage.get(component.name).count += 1;
529
+ }
530
+ }
531
+
532
+ const sharedComponents = Array.from(usage.values())
533
+ .filter((entry) => entry.count >= this.config.minSharedUsage)
534
+ .map((entry) => entry.component);
535
+
536
+ if (sharedComponents.length === 0) {
537
+ return;
538
+ }
539
+
540
+ const sharedSet = new Set(sharedComponents.map((component) => component.name));
541
+ const orderedShared = this.sortComponentsByName(sharedComponents);
542
+
543
+ for (const bundle of Object.values(this.bundles.routes)) {
544
+ const original = this.dedupeComponentsByName(bundle.components || []);
545
+ const filtered = original.filter((component) => !sharedSet.has(component.name));
546
+ const removedShared = original.length - filtered.length;
547
+ bundle.components = this.sortComponentsByName(filtered);
548
+ bundle.size = bundle.components.reduce((sum, component) => sum + component.size, 0);
549
+ if (removedShared > 0) {
550
+ bundle.dependencies = this.mergeBundleDependencies(bundle.dependencies || [], ['shared-core']);
551
+ }
552
+ }
553
+
554
+ this.bundles.routes['shared-core'] = {
555
+ paths: [],
556
+ components: orderedShared,
557
+ size: orderedShared.reduce((sum, component) => sum + component.size, 0),
558
+ file: `slice-bundle.${this.routeToFileName('shared-core')}.js`
559
+ };
560
+
561
+ for (const [key, bundle] of Object.entries(this.bundles.routes)) {
562
+ if (key === 'shared-core') continue;
563
+ if ((bundle.components || []).length === 0) {
564
+ delete this.bundles.routes[key];
565
+ }
566
+ }
567
+ }
568
+
569
+ rebalanceBundlesByBudget(bundles, limits = {}) {
570
+ const maxBundleSize = limits.maxBundleSize || this.config.maxRouteBundleSize;
571
+ const maxRequests = limits.maxRequests || this.config.maxRouteRequests;
572
+ const orderedEntries = Object.entries(bundles)
573
+ .sort(([a], [b]) => a.localeCompare(b));
574
+ const rebalanced = {};
575
+
576
+ for (const [key, bundle] of orderedEntries) {
577
+ const sortedComponents = this.dedupeComponentsByName(bundle.components || []);
578
+ const totalSize = sortedComponents.reduce((sum, component) => sum + component.size, 0);
579
+ if (totalSize <= maxBundleSize || sortedComponents.length <= 1) {
580
+ rebalanced[key] = {
581
+ ...bundle,
582
+ components: sortedComponents,
583
+ size: totalSize,
584
+ file: `slice-bundle.${this.routeToFileName(key)}.js`
585
+ };
586
+ continue;
587
+ }
588
+
589
+ let partIndex = 1;
590
+ let currentChunk = [];
591
+ let currentSize = 0;
592
+
593
+ for (const component of sortedComponents) {
594
+ const nextSize = currentSize + component.size;
595
+ const shouldFlush = currentChunk.length > 0 && nextSize > maxBundleSize;
596
+
597
+ if (shouldFlush) {
598
+ const partKey = `${key}--p${partIndex}`;
599
+ rebalanced[partKey] = {
600
+ ...bundle,
601
+ components: currentChunk,
602
+ size: currentSize,
603
+ file: `slice-bundle.${this.routeToFileName(partKey)}.js`
604
+ };
605
+ partIndex += 1;
606
+ currentChunk = [];
607
+ currentSize = 0;
608
+ }
609
+
610
+ currentChunk.push(component);
611
+ currentSize += component.size;
612
+ }
613
+
614
+ if (currentChunk.length > 0) {
615
+ const partKey = `${key}--p${partIndex}`;
616
+ rebalanced[partKey] = {
617
+ ...bundle,
618
+ components: currentChunk,
619
+ size: currentSize,
620
+ file: `slice-bundle.${this.routeToFileName(partKey)}.js`
621
+ };
622
+ }
623
+ }
624
+
625
+ const keys = Object.keys(rebalanced).sort((a, b) => a.localeCompare(b));
626
+ while (keys.length > maxRequests) {
627
+ const lastKey = keys.pop();
628
+ const targetKey = keys[keys.length - 1];
629
+ if (!lastKey || !targetKey) break;
630
+ const mergedComponents = this.dedupeComponentsByName([
631
+ ...(rebalanced[targetKey].components || []),
632
+ ...(rebalanced[lastKey].components || [])
633
+ ]);
634
+ const mergedPaths = [
635
+ ...this.getBundlePaths(rebalanced[targetKey]),
636
+ ...this.getBundlePaths(rebalanced[lastKey])
637
+ ];
638
+ const mergedDependencies = this.mergeBundleDependencies(
639
+ rebalanced[targetKey].dependencies || [],
640
+ rebalanced[lastKey].dependencies || []
641
+ );
642
+
643
+ rebalanced[targetKey].components = mergedComponents;
644
+ rebalanced[targetKey].size = mergedComponents.reduce((sum, component) => sum + component.size, 0);
645
+ rebalanced[targetKey].dependencies = mergedDependencies;
646
+ this.setBundlePaths(rebalanced[targetKey], mergedPaths);
647
+ delete rebalanced[lastKey];
648
+ }
649
+
650
+ Object.keys(bundles).forEach((key) => delete bundles[key]);
651
+ for (const [key, bundle] of Object.entries(rebalanced).sort(([a], [b]) => a.localeCompare(b))) {
652
+ bundles[key] = bundle;
653
+ }
654
+
655
+ return bundles;
656
+ }
657
+
658
+ /**
659
+ * Categorizes a route path for grouping, considering MultiRoute context
660
+ */
661
+ categorizeRoute(routePath) {
662
+ // Check if this route belongs to a MultiRoute handler
663
+ if (this.analysisData.routeGroups) {
664
+ for (const [groupKey, groupData] of this.analysisData.routeGroups) {
665
+ if (groupData.type === 'multiroute' && groupData.routes.includes(routePath)) {
666
+ return groupKey; // Return the MultiRoute group key
667
+ }
668
+ }
669
+ }
670
+
671
+ // Default categorization
672
+ const path = routePath.toLowerCase();
673
+
674
+ if (path === '/' || path === '/home') return 'home';
675
+ if (path.includes('docum') || path.includes('documentation')) return 'documentation';
676
+ if (path.includes('component') || path.includes('visual') || path.includes('card') ||
677
+ path.includes('button') || path.includes('input') || path.includes('switch') ||
678
+ path.includes('checkbox') || path.includes('select') || path.includes('details') ||
679
+ path.includes('grid') || path.includes('loading') || path.includes('layout') ||
680
+ path.includes('navbar') || path.includes('treeview') || path.includes('multiroute')) return 'components';
681
+ if (path.includes('theme') || path.includes('slice') || path.includes('config')) return 'configuration';
682
+ if (path.includes('routing') || path.includes('guard')) return 'routing';
683
+ if (path.includes('service') || path.includes('command')) return 'services';
684
+ if (path.includes('structural') || path.includes('lifecycle') || path.includes('static') ||
685
+ path.includes('build')) return 'advanced';
686
+ if (path.includes('playground') || path.includes('creator')) return 'tools';
687
+ if (path.includes('about') || path.includes('404')) return 'misc';
688
+
689
+ return 'general';
690
+ }
691
+
692
+ /**
693
+ * Gets all components needed for a route
694
+ */
695
+ getRouteComponents(componentName) {
696
+ const result = [];
697
+ const visited = new Set();
698
+
699
+ const traverse = (name) => {
700
+ if (visited.has(name)) return;
701
+ visited.add(name);
702
+
703
+ const component = this.analysisData.components.find(c => c.name === name);
704
+ if (!component) return;
705
+
706
+ result.push(component);
707
+
708
+ // Add dependencies recursively
709
+ for (const dep of component.dependencies) {
710
+ traverse(dep);
711
+ }
712
+ };
713
+
714
+ traverse(componentName);
715
+ return result;
716
+ }
717
+
718
+ /**
719
+ * Generates the physical bundle files
720
+ */
721
+ async generateBundleFiles() {
722
+ const files = [];
723
+
724
+ await this.prepareVendorSharedDependencies();
725
+
726
+ if (this.vendorShared.sharedDependencySet.size > 0) {
727
+ const vendorSharedFile = await this.createVendorSharedDependencyBundleFile(this.vendorShared.sharedDependencySet);
728
+ this.vendorShared.bundle = vendorSharedFile;
729
+ files.push(vendorSharedFile);
730
+ }
731
+
732
+ // 1. Critical bundle
733
+ if (this.bundles.critical.components.length > 0) {
734
+ const criticalFile = await this.createBundleFile(
735
+ this.bundles.critical.components,
736
+ 'critical',
737
+ null
738
+ );
739
+ const criticalIntegrity = this.computeBundleIntegrity(
740
+ this.bundles.critical.components,
741
+ 'critical',
742
+ null,
743
+ 'critical',
744
+ criticalFile.file
745
+ );
746
+ this.bundles.critical.integrity = `sha256:${criticalFile.hash}`;
747
+ this.bundles.critical.hash = criticalFile.hash;
748
+ files.push(criticalFile);
749
+ }
750
+
751
+ // 2. Route bundles
752
+ for (const [routeKey, bundle] of Object.entries(this.bundles.routes)) {
753
+ const routeIdentifier = Array.isArray(bundle.path || bundle.paths)
754
+ ? routeKey
755
+ : (bundle.path || bundle.paths || routeKey);
756
+
757
+ const routeFile = await this.createBundleFile(
758
+ bundle.components,
759
+ 'route',
760
+ routeIdentifier
761
+ );
762
+ const routeIntegrity = `sha256:${routeFile.hash}`;
763
+ const matchingBundle = Object.values(this.bundles.routes)
764
+ .find((entry) => entry.file === routeFile.file);
765
+ if (matchingBundle) {
766
+ matchingBundle.hash = routeFile.hash;
767
+ matchingBundle.integrity = routeIntegrity;
768
+ }
769
+ files.push(routeFile);
770
+ }
771
+
772
+ return files;
773
+ }
774
+
775
+ async prepareVendorSharedDependencies() {
776
+ const routeDependencyIndex = await this.collectRouteExternalDependencyIndex();
777
+ const usageIndex = this.indexExternalDependencyUsage(routeDependencyIndex);
778
+ const sharedDependencySet = this.computeSharedDependencySet(usageIndex);
779
+
780
+ this.vendorShared.dependencyUsage = usageIndex;
781
+ this.vendorShared.sharedDependencySet = sharedDependencySet;
782
+ this.vendorShared.bundleKeysUsingSharedDependencies = new Set();
783
+
784
+ if (sharedDependencySet.size === 0) {
785
+ return;
786
+ }
787
+
788
+ for (const dependencyName of sharedDependencySet) {
789
+ const usageEntry = usageIndex.get(dependencyName);
790
+ for (const bundleKey of usageEntry?.bundleKeys || []) {
791
+ this.vendorShared.bundleKeysUsingSharedDependencies.add(bundleKey);
792
+ }
793
+ }
794
+
795
+ for (const bundleKey of this.vendorShared.bundleKeysUsingSharedDependencies) {
796
+ const bundle = this.bundles.routes[bundleKey];
797
+ if (!bundle) continue;
798
+ bundle.dependencies = this.mergeBundleDependencies(bundle.dependencies || [], ['vendor-shared']);
799
+ }
800
+ }
801
+
802
+ async collectRouteExternalDependencyIndex() {
803
+ const routeDependencyIndex = {};
804
+
805
+ for (const [bundleKey, bundle] of Object.entries(this.bundles.routes)) {
806
+ routeDependencyIndex[bundleKey] = {};
807
+ const uniqueComponents = this.dedupeComponentsByName(bundle.components || []);
808
+
809
+ for (const comp of uniqueComponents) {
810
+ const fileBaseName = comp.fileName || comp.name;
811
+ const jsPath = path.join(comp.path, `${fileBaseName}.js`);
812
+ if (!await fs.pathExists(jsPath)) continue;
813
+ const jsContent = await fs.readFile(jsPath, 'utf-8');
814
+ const dependencies = await this.buildDependencyContents(jsContent, comp.path);
815
+ for (const [depName, depEntry] of Object.entries(dependencies || {})) {
816
+ if (!routeDependencyIndex[bundleKey][depName]) {
817
+ routeDependencyIndex[bundleKey][depName] = depEntry;
818
+ }
819
+ if (!this.vendorShared.dependencyModules.has(depName)) {
820
+ this.vendorShared.dependencyModules.set(depName, {
821
+ name: depName,
822
+ content: depEntry?.content || ''
823
+ });
824
+ }
825
+ }
826
+ }
827
+ }
828
+
829
+ return routeDependencyIndex;
830
+ }
831
+
832
+ indexExternalDependencyUsage(routeDependencyIndex = {}) {
833
+ const usage = new Map();
834
+
835
+ for (const [bundleKey, dependencies] of Object.entries(routeDependencyIndex || {})) {
836
+ for (const [dependencyName, dependencyEntry] of Object.entries(dependencies || {})) {
837
+ if (!usage.has(dependencyName)) {
838
+ usage.set(dependencyName, {
839
+ name: dependencyName,
840
+ bundleKeys: new Set(),
841
+ bundleCount: 0,
842
+ content: dependencyEntry?.content || ''
843
+ });
844
+ }
845
+ const entry = usage.get(dependencyName);
846
+ entry.bundleKeys.add(bundleKey);
847
+ entry.bundleCount = entry.bundleKeys.size;
848
+ }
849
+ }
850
+
851
+ return usage;
852
+ }
853
+
854
+ computeSharedDependencySet(usageIndex = new Map()) {
855
+ const shared = new Set();
856
+
857
+ for (const [dependencyName, entry] of usageIndex.entries()) {
858
+ if ((entry?.bundleCount || 0) < this.config.minVendorSharedUsage) continue;
859
+ const transformedContent = this.transformDependencyContent(
860
+ entry?.content || '',
861
+ '__sliceVendorSharedProbe',
862
+ dependencyName
863
+ );
864
+ const transformedSize = Buffer.byteLength(transformedContent, 'utf-8');
865
+ if (transformedSize < this.config.minVendorSharedTransformedSize) continue;
866
+ shared.add(dependencyName);
867
+ }
868
+
869
+ return shared;
870
+ }
871
+
872
+ generateVendorSharedDependencyBundleContent(sharedDependencySet = new Set()) {
873
+ const selectedModules = Array.from(sharedDependencySet)
874
+ .sort((a, b) => a.localeCompare(b))
875
+ .map((dependencyName) => {
876
+ const fromUsage = this.vendorShared.dependencyUsage.get(dependencyName)?.content;
877
+ const fromCollected = this.vendorShared.dependencyModules.get(dependencyName)?.content;
878
+ const content = fromUsage || fromCollected || '';
879
+ return { name: dependencyName, content };
880
+ })
881
+ .filter((entry) => !!entry.content);
882
+
883
+ const dependencyModuleBlock = this.buildV2DependencyModuleBlockFromModules(selectedModules);
884
+ const metadata = {
885
+ version: '2',
886
+ bundleKey: 'vendor-shared',
887
+ type: 'vendor-shared',
888
+ routes: [],
889
+ componentCount: 0,
890
+ dependencyCount: selectedModules.length
891
+ };
892
+
893
+ return `export const SLICE_BUNDLE_META = ${JSON.stringify(metadata, null, 2)};\n\n${dependencyModuleBlock}\n\nexport async function registerAll() {\n return SLICE_BUNDLE_DEPENDENCIES;\n}\n`;
894
+ }
895
+
896
+ async createVendorSharedDependencyBundleFile(sharedDependencySet) {
897
+ const fileName = this.vendorShared.file;
898
+ const filePath = path.join(this.bundlesPath, fileName);
899
+ const bundleContent = this.generateVendorSharedDependencyBundleContent(sharedDependencySet);
900
+ const finalContent = await this.applyBundleTransforms(bundleContent, fileName);
901
+ await fs.ensureDir(path.dirname(filePath));
902
+ await fs.writeFile(filePath, finalContent, 'utf-8');
903
+
904
+ const hash = crypto.createHash('sha256').update(finalContent).digest('hex');
905
+
906
+ return {
907
+ name: 'vendor-shared',
908
+ file: fileName,
909
+ path: filePath,
910
+ size: Buffer.byteLength(bundleContent, 'utf-8'),
911
+ hash,
912
+ integrity: `sha256:${hash}`,
913
+ componentCount: 0
914
+ };
915
+ }
916
+
917
+ /**
918
+ * Creates a bundle file
919
+ */
920
+ async createBundleFile(components, type, routePath) {
921
+ const routeKey = routePath ? this.routeToFileName(routePath) : 'critical';
922
+ const fileName = `slice-bundle.${routeKey}.js`;
923
+ const filePath = path.join(this.bundlesPath, fileName);
924
+
925
+ const bundleContent = await this.generateBundleContent(
926
+ components,
927
+ type,
928
+ routePath,
929
+ routeKey,
930
+ fileName
931
+ );
932
+
933
+ const finalContent = await this.applyBundleTransforms(bundleContent, fileName);
934
+
935
+ await fs.ensureDir(path.dirname(filePath));
936
+ await fs.writeFile(filePath, finalContent, 'utf-8');
937
+
938
+ const hash = crypto.createHash('sha256').update(finalContent).digest('hex');
939
+
940
+ return {
941
+ name: routeKey,
942
+ file: fileName,
943
+ path: filePath,
944
+ size: Buffer.byteLength(bundleContent, 'utf-8'),
945
+ hash,
946
+ componentCount: components.length
947
+ };
948
+ }
949
+
950
+ async applyBundleTransforms(bundleContent, fileName) {
951
+ if (!this.options.minify && !this.options.obfuscate) {
952
+ return bundleContent;
953
+ }
954
+
955
+ const options = {
956
+ parse: {
957
+ ecma: 2022
958
+ },
959
+ ecma: 2022,
960
+ compress: this.options.minify ? {
961
+ drop_console: false,
962
+ drop_debugger: true,
963
+ passes: 1
964
+ } : false,
965
+ mangle: this.options.obfuscate ? {
966
+ properties: false
967
+ } : false,
968
+ keep_fnames: true,
969
+ keep_classnames: true,
970
+ format: {
971
+ comments: false,
972
+ ecma: 2022
973
+ }
974
+ };
975
+
976
+ let result;
977
+ try {
978
+ result = await terserMinify(bundleContent, options);
979
+ } catch (error) {
980
+ const tmpDir = path.resolve(process.cwd(), '.tmp');
981
+ const safeName = fileName.replace(/[^a-zA-Z0-9_.-]/g, '_');
982
+ const tmpPath = path.join(tmpDir, `terser-fail-${safeName}`);
983
+ try {
984
+ await fs.ensureDir(tmpDir);
985
+ await fs.writeFile(tmpPath, bundleContent, 'utf-8');
986
+ } catch (writeError) {
987
+ console.warn(`Warning: Failed to write ${tmpPath}:`, writeError.message);
988
+ }
989
+ const message = error?.message ? `${error.message}.` : 'Unknown Terser error.';
990
+ throw new Error(`Terser failed for ${fileName}: ${message} Saved bundle to ${tmpPath}`);
991
+ }
992
+
993
+ if (result.error) {
994
+ const tmpDir = path.resolve(process.cwd(), '.tmp');
995
+ const safeName = fileName.replace(/[^a-zA-Z0-9_.-]/g, '_');
996
+ const tmpPath = path.join(tmpDir, `terser-fail-${safeName}`);
997
+ try {
998
+ await fs.ensureDir(tmpDir);
999
+ await fs.writeFile(tmpPath, bundleContent, 'utf-8');
1000
+ } catch (writeError) {
1001
+ console.warn(`Warning: Failed to write ${tmpPath}:`, writeError.message);
1002
+ }
1003
+ throw new Error(`Terser failed for ${fileName}: ${result.error.message}. Saved bundle to ${tmpPath}`);
1004
+ }
1005
+
1006
+ return result.code || bundleContent;
1007
+ }
1008
+
1009
+
1010
+ /**
1011
+ * Analyzes dependencies of a JavaScript file using simple regex
1012
+ */
1013
+ analyzeDependencies(jsContent, componentPath) {
1014
+ const dependencies = [];
1015
+
1016
+ const resolveImportPath = (importPath) => {
1017
+ const resolvedPath = path.resolve(componentPath, importPath);
1018
+ let finalPath = resolvedPath;
1019
+ const ext = path.extname(resolvedPath);
1020
+ if (!ext) {
1021
+ const extensions = ['.js', '.json', '.mjs'];
1022
+ for (const extension of extensions) {
1023
+ if (fs.existsSync(resolvedPath + extension)) {
1024
+ finalPath = resolvedPath + extension;
1025
+ break;
1026
+ }
1027
+ }
1028
+ }
1029
+
1030
+ return fs.existsSync(finalPath) ? finalPath : null;
1031
+ };
1032
+
1033
+ try {
1034
+ const ast = parse(jsContent, {
1035
+ sourceType: 'module',
1036
+ plugins: ['jsx']
1037
+ });
1038
+
1039
+ traverse.default(ast, {
1040
+ ImportDeclaration(pathNode) {
1041
+ const importPath = pathNode.node.source.value;
1042
+ if (!importPath.startsWith('./') && !importPath.startsWith('../')) {
1043
+ return;
1044
+ }
1045
+
1046
+ const resolvedPath = resolveImportPath(importPath);
1047
+ if (!resolvedPath) {
1048
+ return;
1049
+ }
1050
+
1051
+ const bindings = pathNode.node.specifiers.map(spec => {
1052
+ if (spec.type === 'ImportDefaultSpecifier') {
1053
+ return {
1054
+ type: 'default',
1055
+ importedName: 'default',
1056
+ localName: spec.local.name
1057
+ };
1058
+ }
1059
+
1060
+ if (spec.type === 'ImportSpecifier') {
1061
+ return {
1062
+ type: 'named',
1063
+ importedName: spec.imported.name,
1064
+ localName: spec.local.name
1065
+ };
1066
+ }
1067
+
1068
+ if (spec.type === 'ImportNamespaceSpecifier') {
1069
+ return {
1070
+ type: 'namespace',
1071
+ localName: spec.local.name
1072
+ };
1073
+ }
1074
+
1075
+ return null;
1076
+ }).filter(Boolean);
1077
+
1078
+ dependencies.push({
1079
+ path: resolvedPath,
1080
+ bindings
1081
+ });
1082
+ }
1083
+ });
1084
+ } catch (error) {
1085
+ console.warn(`Warning: Could not analyze dependencies for ${componentPath}:`, error.message);
1086
+ }
1087
+
1088
+ return dependencies;
1089
+ }
1090
+
1091
+ /**
1092
+ * Generates the content of a bundle
1093
+ */
1094
+ async generateBundleContent(components, type, routePath, bundleKey, fileName) {
1095
+ const bundleComponents = [];
1096
+ const uniqueComponents = this.dedupeComponentsByName(components || []);
1097
+
1098
+ for (const comp of uniqueComponents) {
1099
+ const fileBaseName = comp.fileName || comp.name;
1100
+ const jsPath = path.join(comp.path, `${fileBaseName}.js`);
1101
+ const jsContent = await fs.readFile(jsPath, 'utf-8');
1102
+
1103
+ let htmlContent = null;
1104
+ let cssContent = null;
1105
+
1106
+ const htmlPath = path.join(comp.path, `${fileBaseName}.html`);
1107
+ const cssPath = path.join(comp.path, `${fileBaseName}.css`);
1108
+
1109
+ if (await fs.pathExists(htmlPath)) {
1110
+ htmlContent = await fs.readFile(htmlPath, 'utf-8');
1111
+ }
1112
+
1113
+ if (await fs.pathExists(cssPath)) {
1114
+ cssContent = await fs.readFile(cssPath, 'utf-8');
1115
+ }
1116
+
1117
+ const cleanedJavaScript = this.cleanJavaScript(jsContent, comp.name, jsPath);
1118
+
1119
+ bundleComponents.push({
1120
+ name: comp.name,
1121
+ category: comp.category,
1122
+ categoryType: comp.categoryType,
1123
+ js: cleanedJavaScript.code,
1124
+ hoistedImports: cleanedJavaScript.hoistedImports,
1125
+ html: htmlContent,
1126
+ css: cssContent,
1127
+ externalDependencies: await this.buildDependencyContents(jsContent, comp.path),
1128
+ size: comp.size
1129
+ });
1130
+ }
1131
+
1132
+ return this.generateBundleFileContent(fileName, type, this.sortComponentsByName(bundleComponents), routePath);
1133
+ }
1134
+
1135
+ classFactoryName(componentName) {
1136
+ return `SLICE_CLASS_FACTORY_${this.toSafeIdentifier(componentName)}`;
1137
+ }
1138
+
1139
+ indentCodeBlock(code, spaces = 2) {
1140
+ const indentation = ' '.repeat(spaces);
1141
+ return String(code)
1142
+ .split('\n')
1143
+ .map((line) => `${indentation}${line}`)
1144
+ .join('\n');
1145
+ }
1146
+
1147
+ generateBundleFileContent(fileName, type, components, routePath = null) {
1148
+ const uniqueComponents = this.dedupeComponentsByName(components || []);
1149
+ const bundleKey = type === 'critical'
1150
+ ? 'critical'
1151
+ : type === 'framework'
1152
+ ? 'framework'
1153
+ : this.routeToFileName(routePath || fileName.replace('slice-bundle.', '').replace('.js', ''));
1154
+
1155
+ const dependencyModules = this.collectDependencyModulesFromComponents(uniqueComponents);
1156
+ const isRouteBundle = type === 'route';
1157
+ const dependencyModuleBlock = this.buildV2DependencyModuleBlock(uniqueComponents, {
1158
+ includeSharedResolver: isRouteBundle,
1159
+ omittedDependencies: isRouteBundle ? this.vendorShared.sharedDependencySet : null
1160
+ });
1161
+ const rawHoistedImports = uniqueComponents
1162
+ .flatMap((component) => component.hoistedImports || [])
1163
+ .map((statement) => String(statement).trim())
1164
+ .filter(Boolean);
1165
+ const reservedIdentifiers = new Set([
1166
+ 'SLICE_BUNDLE_META',
1167
+ 'SLICE_BUNDLE_DEPENDENCIES',
1168
+ ...uniqueComponents.map((component) => this.classFactoryName(component.name)),
1169
+ ...uniqueComponents.map((component) => `__templateElement_${this.toSafeIdentifier(component.name)}`),
1170
+ ...this.getDependencyExportVariableNames(dependencyModules)
1171
+ ]);
1172
+ this.validateHoistedImportCollisions(rawHoistedImports, reservedIdentifiers);
1173
+ const hoistedImports = Array.from(new Set(rawHoistedImports));
1174
+ const hoistedImportBlock = hoistedImports.join('\n');
1175
+
1176
+ const classFactoryDefinitions = uniqueComponents
1177
+ .map((component) => {
1178
+ const factoryName = this.classFactoryName(component.name);
1179
+ const dependencyBindings = this.buildDependencyBindings(component.externalDependencies || {}, {
1180
+ preferShared: isRouteBundle
1181
+ });
1182
+ const body = component.js && component.js.trim()
1183
+ ? component.js
1184
+ : `return window.${component.name};`;
1185
+ const bodyWithBindings = dependencyBindings
1186
+ ? `${dependencyBindings}\n${body}`
1187
+ : body;
1188
+ return `const ${factoryName} = () => {\n${this.indentCodeBlock(bodyWithBindings, 2)}\n};`;
1189
+ })
1190
+ .join('\n\n');
1191
+
1192
+ const templateDeclarations = uniqueComponents
1193
+ .map((component) => {
1194
+ const templateVarName = `__templateElement_${this.toSafeIdentifier(component.name)}`;
1195
+ return `const ${templateVarName} = document.createElement('template');\n${templateVarName}.innerHTML = ${JSON.stringify(component.html || '')};`;
1196
+ })
1197
+ .join('\n');
1198
+
1199
+ const classRegistrations = uniqueComponents
1200
+ .map((component) => {
1201
+ const componentName = JSON.stringify(component.name);
1202
+ return ` if (!controller.classes.has(${componentName})) {\n controller.classes.set(${componentName}, ${this.classFactoryName(component.name)}());\n }`;
1203
+ })
1204
+ .join('\n');
1205
+
1206
+ const templateRegistrations = uniqueComponents
1207
+ .map((component) => {
1208
+ const componentName = JSON.stringify(component.name);
1209
+ const templateVarName = `__templateElement_${this.toSafeIdentifier(component.name)}`;
1210
+ return ` if (!controller.templates.has(${componentName})) {\n controller.templates.set(${componentName}, ${templateVarName});\n }`;
1211
+ })
1212
+ .join('\n');
1213
+
1214
+ const cssRegistrationInit = uniqueComponents.length
1215
+ ? ` if (!stylesManager.__sliceRegisteredComponentStyles) {\n stylesManager.__sliceRegisteredComponentStyles = new Set();\n }`
1216
+ : '';
1217
+
1218
+ const cssRegistrations = uniqueComponents
1219
+ .map((component) => {
1220
+ const componentName = JSON.stringify(component.name);
1221
+ return ` if (!stylesManager.__sliceRegisteredComponentStyles.has(${componentName})) {\n stylesManager.registerComponentStyles(${componentName}, ${JSON.stringify(component.css || '')});\n stylesManager.__sliceRegisteredComponentStyles.add(${componentName});\n }`;
1222
+ })
1223
+ .join('\n');
1224
+
1225
+ const categoryRegistrations = uniqueComponents
1226
+ .map((component) => {
1227
+ const componentName = JSON.stringify(component.name);
1228
+ return ` if (!controller.componentCategories.has(${componentName})) {\n controller.componentCategories.set(${componentName}, ${JSON.stringify(component.category)});\n }`;
1229
+ })
1230
+ .join('\n');
1231
+
1232
+ const metadata = {
1233
+ version: '2',
1234
+ bundleKey,
1235
+ type,
1236
+ routes: routePath ? [routePath] : [],
1237
+ componentCount: uniqueComponents.length
1238
+ };
1239
+
1240
+ return `${hoistedImportBlock}${hoistedImportBlock ? '\n\n' : ''}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`;
1241
+ }
1242
+
1243
+ buildV2DependencyModuleBlock(components, options = {}) {
1244
+ const modules = this.collectDependencyModulesFromComponents(components);
1245
+ return this.buildV2DependencyModuleBlockFromModules(modules, options);
1246
+ }
1247
+
1248
+ buildV2DependencyModuleBlockFromModules(modules = [], options = {}) {
1249
+ const omittedDependencies = options.omittedDependencies instanceof Set
1250
+ ? options.omittedDependencies
1251
+ : new Set(options.omittedDependencies || []);
1252
+ const filteredModules = modules.filter((module) => !omittedDependencies.has(module.name));
1253
+
1254
+ const lines = [
1255
+ 'const SLICE_BUNDLE_DEPENDENCIES = {};',
1256
+ ...this.getDefaultExportResolverLines()
1257
+ ];
1258
+ if (options.includeSharedResolver) {
1259
+ lines.push(...this.getBundleDependencyResolverLines());
1260
+ }
1261
+ filteredModules.forEach((module, index) => {
1262
+ const exportVar = `__sliceDepExports${index}`;
1263
+ const transformedContent = this.transformDependencyContent(module.content, exportVar, module.name);
1264
+ lines.push(`const ${exportVar} = {};`);
1265
+ lines.push(transformedContent.trim());
1266
+ lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
1267
+ });
1268
+
1269
+ return lines.join('\n');
1270
+ }
1271
+
1272
+ async buildDependencyContents(jsContent, componentPath) {
1273
+ const dependencies = this.analyzeDependencies(jsContent, componentPath);
1274
+ const dependencyContents = {};
1275
+
1276
+ for (const dep of dependencies) {
1277
+ const depPath = dep.path;
1278
+ try {
1279
+ const depContent = await fs.readFile(depPath, 'utf-8');
1280
+ const depName = path
1281
+ .relative(this.srcPath, depPath)
1282
+ .replace(/\\/g, '/');
1283
+ dependencyContents[depName] = {
1284
+ content: depContent,
1285
+ bindings: dep.bindings || []
1286
+ };
1287
+ } catch (error) {
1288
+ console.warn(`Warning: Could not read dependency ${depPath}:`, error.message);
1289
+ }
1290
+ }
1291
+
1292
+ return dependencyContents;
1293
+ }
1294
+
1295
+ /**
1296
+ * Cleans JavaScript code by removing imports/exports and ensuring class is available globally
1297
+ */
1298
+ cleanJavaScript(code, componentName, sourceContext = componentName) {
1299
+ // Remove export default
1300
+ code = code.replace(/export\s+default\s+/g, '');
1301
+
1302
+ // Remove only unsupported imports (relative always removed, allowed absolute kept)
1303
+ const stripped = this.stripImports(code, {
1304
+ sourceContext,
1305
+ collectHoistedImports: true
1306
+ });
1307
+ const hoistedImports = stripped.hoistedImports || [];
1308
+ code = stripped.code;
1309
+
1310
+ // Guard customElements.define to avoid duplicate registrations
1311
+ code = code.replace(
1312
+ /customElements\.define\(([^)]+)\);?/g,
1313
+ (match, args) => {
1314
+ const firstArg = args.split(',')[0]?.trim() || '';
1315
+ if (!/^['"][^'"]+['"]$/.test(firstArg)) {
1316
+ return match;
1317
+ }
1318
+ return `if (!customElements.get(${firstArg})) { customElements.define(${args}); }`;
1319
+ }
1320
+ );
1321
+
1322
+ // Make sure the class is available globally for bundle evaluation
1323
+ // Preserve original customElements.define if it exists
1324
+ if (code.includes('customElements.define')) {
1325
+ // Add global assignment before guarded or direct customElements.define
1326
+ const globalAssignment = `window.${componentName} = ${componentName};\n`;
1327
+ const guardedDefineRegex = /if\s*\(\s*!\s*customElements\.get\([^)]*\)\s*\)\s*\{\s*customElements\.define\([^;]+\);?\s*\}\s*$/;
1328
+ const directDefineRegex = /customElements\.define\([^;]+\);?\s*$/;
1329
+ if (guardedDefineRegex.test(code)) {
1330
+ code = code.replace(guardedDefineRegex, `${globalAssignment}$&`);
1331
+ } else {
1332
+ code = code.replace(directDefineRegex, `${globalAssignment}$&`);
1333
+ }
1334
+ } else {
1335
+ // If no customElements.define found, just assign to global
1336
+ code += `\nwindow.${componentName} = ${componentName};`;
1337
+ }
1338
+
1339
+ // Add return statement for bundle evaluation compatibility
1340
+ code += `\nreturn ${componentName};`;
1341
+
1342
+ return {
1343
+ code,
1344
+ hoistedImports
1345
+ };
1346
+ }
1347
+
1348
+ /**
1349
+ * Formats the bundle file
1350
+ */
1351
+ formatBundleFile(componentsData, metadata) {
1352
+ const integrityPayload = {
1353
+ metadata: {
1354
+ ...metadata,
1355
+ generated: 'static'
1356
+ },
1357
+ components: Object.fromEntries(
1358
+ Object.entries(componentsData).map(([name, data]) => [
1359
+ name,
1360
+ {
1361
+ name: data.name,
1362
+ category: data.category,
1363
+ categoryType: data.categoryType,
1364
+ componentDependencies: data.componentDependencies
1365
+ }
1366
+ ])
1367
+ )
1368
+ };
1369
+ const integrity = `sha256:${crypto
1370
+ .createHash('sha256')
1371
+ .update(JSON.stringify(integrityPayload))
1372
+ .digest('hex')}`;
1373
+
1374
+ const dependencyModules = this.collectDependencyModules(componentsData);
1375
+ const frameworkComponentKeys = Object.keys(componentsData || {});
1376
+ const frameworkClassIdentifiers = frameworkComponentKeys.map((key) => this.toSafeIdentifier(key));
1377
+ const frameworkReservedIdentifiers = new Set([
1378
+ 'SLICE_BUNDLE',
1379
+ 'SLICE_BUNDLE_COMPONENTS',
1380
+ 'SLICE_BUNDLE_DEPENDENCIES',
1381
+ 'SLICE_FRAMEWORK_CLASSES',
1382
+ ...frameworkClassIdentifiers,
1383
+ ...this.getDependencyExportVariableNames(dependencyModules)
1384
+ ]);
1385
+ const rawHoistedImports = Object.values(componentsData || {})
1386
+ .flatMap((component) => component?.hoistedImports || [])
1387
+ .map((statement) => String(statement).trim())
1388
+ .filter(Boolean);
1389
+ this.validateHoistedImportCollisions(rawHoistedImports, frameworkReservedIdentifiers);
1390
+ const hoistedImportBlock = Array.from(new Set(rawHoistedImports)).join('\n');
1391
+
1392
+ const dependencyBlock = this.buildDependencyModuleBlock(componentsData);
1393
+ const componentBlock = this.buildComponentBundleBlock(componentsData);
1394
+
1395
+ return `${hoistedImportBlock}${hoistedImportBlock ? '\n\n' : ''}/**
1396
+ * Slice.js Bundle
1397
+ * Type: ${metadata.type}
1398
+ * Generated: ${metadata.generated}
1399
+ * Strategy: ${metadata.strategy}
1400
+ * Components: ${metadata.componentCount}
1401
+ * Total Size: ${(metadata.totalSize / 1024).toFixed(1)} KB
1402
+ */
1403
+
1404
+ ${dependencyBlock}
1405
+ ${componentBlock}
1406
+
1407
+ export const SLICE_BUNDLE = {
1408
+ metadata: ${JSON.stringify({ ...metadata, integrity }, null, 2)},
1409
+ components: SLICE_BUNDLE_COMPONENTS
1410
+ };
1411
+
1412
+ // Auto-registration of components
1413
+ if (window.slice && window.slice.controller) {
1414
+ slice.controller.registerBundle(SLICE_BUNDLE);
1415
+ }
1416
+ `;
1417
+ }
1418
+
1419
+ buildDependencyModuleBlock(componentsData) {
1420
+ const dependencyModules = this.collectDependencyModules(componentsData);
1421
+ const lines = [
1422
+ 'const SLICE_BUNDLE_DEPENDENCIES = {};',
1423
+ ...this.getDefaultExportResolverLines()
1424
+ ];
1425
+ if (dependencyModules.length === 0) {
1426
+ return `${lines.join('\n')}`;
1427
+ }
1428
+
1429
+ dependencyModules.forEach((module, index) => {
1430
+ const exportVar = `__sliceDepExports${index}`;
1431
+ const content = this.transformDependencyContent(module.content, exportVar, module.name);
1432
+ lines.push(`// Dependency: ${module.name}`);
1433
+ lines.push(`const ${exportVar} = {};`);
1434
+ lines.push(content.trim());
1435
+ lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
1436
+ });
1437
+
1438
+ return `${lines.join('\n')}`;
1439
+ }
1440
+
1441
+ collectDependencyModules(componentsData) {
1442
+ const modules = new Map();
1443
+ Object.values(componentsData).forEach((component) => {
1444
+ Object.entries(component.externalDependencies || {}).forEach(([name, entry]) => {
1445
+ if (modules.has(name)) return;
1446
+ const content = typeof entry === 'string' ? entry : entry.content;
1447
+ modules.set(name, { name, content });
1448
+ });
1449
+ });
1450
+ return Array.from(modules.values());
1451
+ }
1452
+
1453
+ collectDependencyModulesFromComponents(components = []) {
1454
+ const modules = new Map();
1455
+ for (const component of components || []) {
1456
+ const externalDependencies = component.externalDependencies || {};
1457
+ for (const [moduleName, entry] of Object.entries(externalDependencies)) {
1458
+ if (modules.has(moduleName)) continue;
1459
+ const content = typeof entry === 'string' ? entry : entry?.content;
1460
+ if (!content) continue;
1461
+ modules.set(moduleName, { name: moduleName, content });
1462
+ }
1463
+ }
1464
+ return Array.from(modules.values());
1465
+ }
1466
+
1467
+ getDependencyExportVariableNames(dependencyModules = []) {
1468
+ return (dependencyModules || []).map((_, index) => `__sliceDepExports${index}`);
1469
+ }
1470
+
1471
+ transformDependencyContent(content, exportVar, moduleName) {
1472
+ const dataName = this.getDependencyDefaultFallbackKey(moduleName);
1473
+ const exportPrefix = dataName ? `${exportVar}.${dataName} = ` : `${exportVar}.default = `;
1474
+
1475
+ return content
1476
+ .replace(/export\s+const\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
1477
+ .replace(/export\s+let\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
1478
+ .replace(/export\s+var\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
1479
+ .replace(/export\s+function\s+(\w+)/g, `${exportVar}.$1 = function`)
1480
+ .replace(/export\s+default\s+/g, exportPrefix)
1481
+ .replace(/export\s*{\s*([^}]+)\s*}/g, (match, exportsStr) => {
1482
+ return exportsStr
1483
+ .split(',')
1484
+ .map((exp) => {
1485
+ const cleanExp = exp.trim();
1486
+ const varName = cleanExp.split(' as ')[0].trim();
1487
+ return `${exportVar}.${varName} = ${varName};`;
1488
+ })
1489
+ .join('\n');
1490
+ })
1491
+ .replace(/^\s*export\s+/gm, '');
1492
+ }
1493
+
1494
+ getDependencyDefaultFallbackKey(moduleName) {
1495
+ const baseName = moduleName?.split('/').pop()?.replace(/\.[^.]+$/, '');
1496
+ return baseName ? `${baseName}Data` : null;
1497
+ }
1498
+
1499
+ buildComponentBundleBlock(componentsData) {
1500
+ const componentEntries = [];
1501
+ const componentDefs = [];
1502
+ const frameworkEntries = [];
1503
+
1504
+ Object.entries(componentsData).forEach(([name, data]) => {
1505
+ const classVar = this.toSafeIdentifier(name);
1506
+ const bindings = this.buildDependencyBindings(data.externalDependencies || {});
1507
+
1508
+ componentDefs.push(`const ${classVar} = (() => {\n${bindings}\n${data.js}\nreturn ${name};\n})();`);
1509
+
1510
+ if (data.isFramework) {
1511
+ frameworkEntries.push(`${JSON.stringify(data.name)}: ${classVar}`);
1512
+ }
1513
+
1514
+ componentEntries.push(
1515
+ `${JSON.stringify(name)}: {\n` +
1516
+ ` name: ${JSON.stringify(data.name)},\n` +
1517
+ ` category: ${JSON.stringify(data.category)},\n` +
1518
+ ` categoryType: ${JSON.stringify(data.categoryType)},\n` +
1519
+ ` componentDependencies: ${JSON.stringify(data.componentDependencies)},\n` +
1520
+ ` html: ${JSON.stringify(data.html)},\n` +
1521
+ ` css: ${JSON.stringify(data.css)},\n` +
1522
+ ` size: ${JSON.stringify(data.size)},\n` +
1523
+ ` class: ${classVar}\n` +
1524
+ `}`
1525
+ );
1526
+ });
1527
+
1528
+ const frameworkBlock = frameworkEntries.length > 0
1529
+ ? `const SLICE_FRAMEWORK_CLASSES = {\n${frameworkEntries.join(',\n')}\n};\nwindow.SLICE_FRAMEWORK_CLASSES = SLICE_FRAMEWORK_CLASSES;`
1530
+ : '';
1531
+
1532
+ return `${componentDefs.join('\n\n')}\n\nconst SLICE_BUNDLE_COMPONENTS = {\n${componentEntries.join(',\n')}\n};\n${frameworkBlock}`;
1533
+ }
1534
+
1535
+ buildDependencyBindings(externalDependencies, options = {}) {
1536
+ const lines = [];
1537
+ Object.entries(externalDependencies).forEach(([name, entry]) => {
1538
+ const bindings = typeof entry === 'string' ? [] : entry.bindings || [];
1539
+ const depVar = options.preferShared
1540
+ ? `__sliceResolveBundleDependency(${JSON.stringify(name)})`
1541
+ : `SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(name)}]`;
1542
+
1543
+ bindings.forEach((binding) => {
1544
+ if (!binding?.localName) return;
1545
+ if (binding.type === 'default') {
1546
+ const preferredKey = this.getDependencyDefaultFallbackKey(name);
1547
+ lines.push(`const ${binding.localName} = __sliceResolveDefaultExport(${depVar}, ${JSON.stringify(name)}, ${JSON.stringify(preferredKey)});`);
1548
+ }
1549
+ if (binding.type === 'named') {
1550
+ lines.push(`const ${binding.localName} = ${depVar}.${binding.importedName};`);
1551
+ }
1552
+ if (binding.type === 'namespace') {
1553
+ lines.push(`const ${binding.localName} = ${depVar};`);
1554
+ }
1555
+ });
1556
+ });
1557
+
1558
+ return lines.join('\n');
1559
+ }
1560
+
1561
+ getBundleDependencyResolverLines() {
1562
+ return [
1563
+ "const __sliceSharedDeps = typeof window !== 'undefined' ? (window.__SLICE_SHARED_DEPS__ || {}) : {};",
1564
+ 'const __sliceResolveBundleDependency = (depName) => Object.prototype.hasOwnProperty.call(__sliceSharedDeps, depName) ? __sliceSharedDeps[depName] : SLICE_BUNDLE_DEPENDENCIES[depName];'
1565
+ ];
1566
+ }
1567
+
1568
+ getDefaultExportResolverLines() {
1569
+ return [
1570
+ 'const __sliceDefaultExportWarningDeps = new Set();',
1571
+ "const __sliceDefaultExportPreferredKeys = ['module', 'exports', 'purify'];",
1572
+ 'const __sliceDeterministicKeyCompare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);',
1573
+ 'function __sliceResolveDefaultExport(dep, depName, preferredKey) {',
1574
+ ' if (dep?.default !== undefined) return dep.default;',
1575
+ " if (dep === null || (typeof dep !== 'object' && typeof dep !== 'function')) return dep;",
1576
+ " if (preferredKey && preferredKey !== 'default' && preferredKey !== '__esModule' && Object.prototype.hasOwnProperty.call(dep, preferredKey)) return dep[preferredKey];",
1577
+ " const keys = Object.keys(dep).filter((key) => key !== 'default' && key !== '__esModule');",
1578
+ ' if (keys.length === 1) return dep[keys[0]];',
1579
+ ' if (keys.length > 1) {',
1580
+ ' const preferredMatches = __sliceDefaultExportPreferredKeys.filter((key) => keys.includes(key));',
1581
+ ' if (preferredMatches.length === 1) return dep[preferredMatches[0]];',
1582
+ ' const sortedKeys = [...keys].sort(__sliceDeterministicKeyCompare);',
1583
+ ' const fallbackKey = sortedKeys[0];',
1584
+ ' const warningDepName = depName || "<unknown dependency>";',
1585
+ ' if (!__sliceDefaultExportWarningDeps.has(warningDepName)) {',
1586
+ ' __sliceDefaultExportWarningDeps.add(warningDepName);',
1587
+ ' console.warn(`[Slice.js bundler] Ambiguous default export resolution for "${warningDepName}". Falling back to "${fallbackKey}". Keys: ${sortedKeys.join(\', \')}`);',
1588
+ ' }',
1589
+ ' return dep[fallbackKey];',
1590
+ ' }',
1591
+ ' return dep;',
1592
+ '}'
1593
+ ];
1594
+ }
1595
+
1596
+ toSafeIdentifier(name) {
1597
+ const cleaned = name.replace(/[^a-zA-Z0-9_]/g, '_');
1598
+ if (/^\d/.test(cleaned)) {
1599
+ return `SliceComponent_${cleaned}`;
1600
+ }
1601
+ return `SliceComponent_${cleaned}`;
1602
+ }
1603
+
1604
+ /**
1605
+ * Generates the bundle configuration
1606
+ */
1607
+ generateBundleConfig(frameworkBundle = null) {
1608
+ const metrics = this.analysisData.metrics || {};
1609
+ const config = {
1610
+ version: '2.0.0',
1611
+ format: this.format,
1612
+ loadingPolicy: this.loadingPolicy,
1613
+ strategy: this.config.strategy,
1614
+ minified: this.options.minify,
1615
+ obfuscated: this.options.obfuscate,
1616
+ production: true,
1617
+ generated: new Date().toISOString(),
1618
+
1619
+ stats: {
1620
+ totalComponents: metrics.totalComponents || 0,
1621
+ totalRoutes: metrics.totalRoutes || 0,
1622
+ sharedComponents: this.bundles.critical.components.length,
1623
+ sharedPercentage: metrics.sharedPercentage || 0,
1624
+ totalSize: metrics.totalSize || 0,
1625
+ criticalSize: this.bundles.critical.size
1626
+ },
1627
+
1628
+ bundles: {
1629
+ framework: {
1630
+ file: 'slice-bundle.framework.js',
1631
+ size: 0,
1632
+ hash: null,
1633
+ integrity: null,
1634
+ components: []
1635
+ },
1636
+ vendorShared: {
1637
+ bundleKey: 'vendor-shared',
1638
+ type: 'vendor-shared',
1639
+ file: this.vendorShared.file,
1640
+ size: this.vendorShared.bundle?.size || 0,
1641
+ hash: this.vendorShared.bundle?.hash || null,
1642
+ integrity: this.vendorShared.bundle?.integrity || null,
1643
+ dependencies: Array.from(this.vendorShared.sharedDependencySet).sort((a, b) => a.localeCompare(b)),
1644
+ dependencyCount: this.vendorShared.sharedDependencySet.size,
1645
+ routes: Array.from(this.vendorShared.bundleKeysUsingSharedDependencies).sort((a, b) => a.localeCompare(b))
1646
+ },
1647
+ critical: {
1648
+ file: this.bundles.critical.file,
1649
+ size: this.bundles.critical.size,
1650
+ hash: this.bundles.critical.hash || null,
1651
+ integrity: this.bundles.critical.integrity || null,
1652
+ components: this.bundles.critical.components.map(c => c.name)
1653
+ },
1654
+ routes: {}
1655
+ },
1656
+ routeBundles: {},
1657
+ routeDependencyGraph: {}
1658
+ };
1659
+
1660
+ for (const [key, bundle] of Object.entries(this.bundles.routes)) {
1661
+ const routeIdentifier = Array.isArray(bundle.path || bundle.paths)
1662
+ ? key
1663
+ : (bundle.path || bundle.paths || key);
1664
+ const usesVendorShared = this.vendorShared.bundleKeysUsingSharedDependencies.has(key)
1665
+ || (bundle.dependencies || []).includes('vendor-shared');
1666
+ const dependencies = this.mergeBundleDependencies(
1667
+ bundle.dependencies || [],
1668
+ usesVendorShared ? ['vendor-shared'] : []
1669
+ );
1670
+
1671
+ config.bundles.routes[key] = {
1672
+ path: bundle.path || bundle.paths || key, // Support both single path and array of paths, fallback to key
1673
+ file: `slice-bundle.${this.routeToFileName(routeIdentifier)}.js`,
1674
+ size: bundle.size,
1675
+ hash: bundle.hash || null,
1676
+ integrity: bundle.integrity || null,
1677
+ components: bundle.components.map(c => c.name),
1678
+ dependencies
1679
+ };
1680
+
1681
+ const paths = Array.isArray(config.bundles.routes[key].path)
1682
+ ? config.bundles.routes[key].path
1683
+ : [config.bundles.routes[key].path];
1684
+
1685
+ for (const routePath of paths) {
1686
+ if (!config.routeBundles[routePath]) {
1687
+ config.routeBundles[routePath] = ['critical'];
1688
+ }
1689
+ for (const dependency of dependencies.filter((dep) => dep !== 'critical')) {
1690
+ if (!config.routeBundles[routePath].includes(dependency)) {
1691
+ config.routeBundles[routePath].push(dependency);
1692
+ }
1693
+ }
1694
+ if (!config.routeBundles[routePath].includes(key)) {
1695
+ config.routeBundles[routePath].push(key);
1696
+ }
1697
+
1698
+ const graphEntry = config.routeDependencyGraph[routePath] || {
1699
+ bundles: [],
1700
+ edges: []
1701
+ };
1702
+ if (!graphEntry.bundles.includes(key)) {
1703
+ graphEntry.bundles.push(key);
1704
+ graphEntry.bundles.sort((a, b) => a.localeCompare(b));
1705
+ }
1706
+
1707
+ const edgeKeys = new Set(graphEntry.edges.map((edge) => `${edge.from}->${edge.to}`));
1708
+ const orderedEdgeSources = ['critical', ...dependencies.filter((dependency) => dependency !== 'critical')];
1709
+ for (const source of orderedEdgeSources) {
1710
+ const edgeKey = `${source}->${key}`;
1711
+ if (!edgeKeys.has(edgeKey)) {
1712
+ graphEntry.edges.push({ from: source, to: key });
1713
+ edgeKeys.add(edgeKey);
1714
+ }
1715
+ }
1716
+
1717
+ config.routeDependencyGraph[routePath] = graphEntry;
1718
+ }
1719
+ }
1720
+
1721
+ if (frameworkBundle) {
1722
+ config.bundles.framework = {
1723
+ file: frameworkBundle.file,
1724
+ size: frameworkBundle.size,
1725
+ hash: frameworkBundle.hash,
1726
+ integrity: frameworkBundle.integrity,
1727
+ components: frameworkBundle.components || []
1728
+ };
1729
+ }
1730
+
1731
+ return config;
1732
+ }
1733
+
1734
+ collectFrameworkComponents() {
1735
+ return this.analysisData.components.filter((comp) => comp.isFramework);
1736
+ }
1737
+
1738
+ async createFrameworkBundle(components) {
1739
+ const fileName = 'slice-bundle.framework.js';
1740
+ const filePath = path.join(this.bundlesPath, fileName);
1741
+ return this.generateFrameworkBundleFile(components, fileName, filePath);
1742
+ }
1743
+
1744
+ async generateFrameworkBundleFile(components, fileName, filePath) {
1745
+ const componentsData = {};
1746
+ const componentsMap = await this.loadComponentsMap();
1747
+ const metadata = {
1748
+ version: '2.0.0',
1749
+ type: 'framework',
1750
+ route: null,
1751
+ bundleKey: 'framework',
1752
+ file: fileName,
1753
+ generated: new Date().toISOString(),
1754
+ totalSize: components.reduce((sum, c) => sum + c.size, 0),
1755
+ componentCount: components.length,
1756
+ strategy: this.config.strategy,
1757
+ minified: this.options.minify,
1758
+ obfuscated: this.options.obfuscate
1759
+ };
1760
+
1761
+ components.forEach((comp) => {
1762
+ const componentKey = `Framework/Structural/${comp.name}`;
1763
+ const fileBaseName = comp.fileName || comp.name;
1764
+ const jsPath = path.join(comp.path, `${fileBaseName}.js`);
1765
+ const jsContent = fs.readFileSync(jsPath, 'utf-8');
1766
+ const dependencyContents = this.buildDependencyContentsSync(jsContent, comp.path);
1767
+ const cleanedJavaScript = this.cleanJavaScript(jsContent, comp.name, jsPath);
1768
+
1769
+ componentsData[componentKey] = {
1770
+ name: comp.name,
1771
+ category: comp.category,
1772
+ categoryType: comp.categoryType,
1773
+ isFramework: true,
1774
+ js: cleanedJavaScript.code,
1775
+ hoistedImports: cleanedJavaScript.hoistedImports,
1776
+ externalDependencies: dependencyContents,
1777
+ componentDependencies: Array.from(comp.dependencies),
1778
+ html: fs.existsSync(path.join(comp.path, `${fileBaseName}.html`))
1779
+ ? fs.readFileSync(path.join(comp.path, `${fileBaseName}.html`), 'utf-8')
1780
+ : null,
1781
+ css: fs.existsSync(path.join(comp.path, `${fileBaseName}.css`))
1782
+ ? fs.readFileSync(path.join(comp.path, `${fileBaseName}.css`), 'utf-8')
1783
+ : null,
1784
+ size: comp.size
1785
+ };
1786
+ });
1787
+
1788
+ const prelude = `const components = ${JSON.stringify(componentsMap)};`;
1789
+ const bundleContent = `${prelude}\n${this.formatBundleFile(componentsData, metadata)}`;
1790
+ const finalContent = await this.applyBundleTransforms(bundleContent, fileName);
1791
+ await fs.ensureDir(path.dirname(filePath));
1792
+ await fs.writeFile(filePath, finalContent, 'utf-8');
1793
+
1794
+ const hash = crypto.createHash('sha256').update(finalContent).digest('hex');
1795
+ const integrity = `sha256:${hash}`;
1796
+
1797
+ return {
1798
+ name: 'framework',
1799
+ file: fileName,
1800
+ size: Buffer.byteLength(bundleContent, 'utf-8'),
1801
+ hash,
1802
+ integrity,
1803
+ componentCount: components.length,
1804
+ components: components.map((comp) => `Framework/Structural/${comp.name}`)
1805
+ };
1806
+ }
1807
+
1808
+ buildDependencyContentsSync(jsContent, componentPath) {
1809
+ const dependencies = this.analyzeDependencies(jsContent, componentPath);
1810
+ const dependencyContents = {};
1811
+
1812
+ for (const dep of dependencies) {
1813
+ const depPath = dep.path;
1814
+ try {
1815
+ const depContent = fs.readFileSync(depPath, 'utf-8');
1816
+ const depName = path
1817
+ .relative(this.srcPath, depPath)
1818
+ .replace(/\\/g, '/');
1819
+ dependencyContents[depName] = {
1820
+ content: depContent,
1821
+ bindings: dep.bindings || []
1822
+ };
1823
+ } catch (error) {
1824
+ console.warn(`Warning: Could not read dependency ${depPath}:`, error.message);
1825
+ }
1826
+ }
1827
+
1828
+ return dependencyContents;
1829
+ }
1830
+
1831
+ getConfiguredPublicFolders() {
1832
+ const publicFolders = Array.isArray(this.sliceConfig?.publicFolders)
1833
+ ? this.sliceConfig.publicFolders
1834
+ : [];
1835
+
1836
+ return publicFolders
1837
+ .map((folder) => this.normalizePublicFolder(folder))
1838
+ .filter(Boolean);
1839
+ }
1840
+
1841
+ normalizePublicFolder(folder) {
1842
+ if (typeof folder !== 'string') return null;
1843
+ let normalized = folder.trim();
1844
+ if (!normalized) return null;
1845
+
1846
+ if (!normalized.startsWith('/')) {
1847
+ normalized = `/${normalized}`;
1848
+ }
1849
+
1850
+ normalized = normalized.replace(/\\+/g, '/').replace(/\/+/g, '/');
1851
+ if (normalized.length > 1 && normalized.endsWith('/')) {
1852
+ normalized = normalized.slice(0, -1);
1853
+ }
1854
+
1855
+ return normalized;
1856
+ }
1857
+
1858
+ normalizeImportPath(importPath) {
1859
+ if (typeof importPath !== 'string') return '';
1860
+ const cleanPath = importPath.split(/[?#]/)[0];
1861
+ return cleanPath.replace(/\\+/g, '/').replace(/\/+/g, '/');
1862
+ }
1863
+
1864
+ isRelativeImport(importPath) {
1865
+ return importPath.startsWith('./') || importPath.startsWith('../');
1866
+ }
1867
+
1868
+ isAbsoluteImport(importPath) {
1869
+ return importPath.startsWith('/');
1870
+ }
1871
+
1872
+ isImportInPublicFolders(importPath, publicFolders) {
1873
+ const normalizedImport = this.normalizeImportPath(importPath);
1874
+ return publicFolders.some((folder) => normalizedImport === folder || normalizedImport.startsWith(`${folder}/`));
1875
+ }
1876
+
1877
+ classifyImport(importPath, publicFolders) {
1878
+ if (typeof importPath !== 'string' || !importPath) {
1879
+ return { keep: false, warning: 'Warning: Removing bare import: <unknown>' };
1880
+ }
1881
+
1882
+ if (this.isRelativeImport(importPath)) {
1883
+ return { keep: false, warning: null };
1884
+ }
1885
+
1886
+ if (this.isAbsoluteImport(importPath)) {
1887
+ if (this.isImportInPublicFolders(importPath, publicFolders)) {
1888
+ return { keep: true, warning: null };
1889
+ }
1890
+
1891
+ return {
1892
+ keep: false,
1893
+ warning: `Warning: Removing absolute import outside publicFolders: ${importPath}`
1894
+ };
1895
+ }
1896
+
1897
+ return {
1898
+ keep: false,
1899
+ warning: `Warning: Removing bare import: ${importPath}`
1900
+ };
1901
+ }
1902
+
1903
+ buildImportWarningMessage(baseMessage, sourceContext) {
1904
+ if (!sourceContext) return baseMessage;
1905
+ return `${baseMessage} [${sourceContext}]`;
1906
+ }
1907
+
1908
+ extractLocalBindingsFromImportStatement(statement) {
1909
+ const source = String(statement || '').trim();
1910
+ if (!source.startsWith('import ')) return [];
1911
+ if (/^import\s+['"][^'"]+['"]\s*;?$/.test(source)) return [];
1912
+
1913
+ const bindings = [];
1914
+
1915
+ const defaultMatch = source.match(/^import\s+([A-Za-z_$][\w$]*)\s*(,|\s+from\s+)/);
1916
+ if (defaultMatch && defaultMatch[1] !== '*') {
1917
+ bindings.push(defaultMatch[1]);
1918
+ }
1919
+
1920
+ const namespaceMatch = source.match(/,?\s*\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"]/);
1921
+ if (namespaceMatch) {
1922
+ bindings.push(namespaceMatch[1]);
1923
+ }
1924
+
1925
+ const namedMatch = source.match(/\{([\s\S]*?)\}\s*from\s*['"]/);
1926
+ if (namedMatch) {
1927
+ const namedSection = namedMatch[1];
1928
+ for (const part of namedSection.split(',')) {
1929
+ const cleanPart = part.trim();
1930
+ if (!cleanPart) continue;
1931
+ const aliasParts = cleanPart.split(/\s+as\s+/i).map((v) => v.trim()).filter(Boolean);
1932
+ const localName = aliasParts.length > 1 ? aliasParts[1] : aliasParts[0];
1933
+ if (/^[A-Za-z_$][\w$]*$/.test(localName)) {
1934
+ bindings.push(localName);
1935
+ }
1936
+ }
1937
+ }
1938
+
1939
+ return Array.from(new Set(bindings));
1940
+ }
1941
+
1942
+ validateHoistedImportCollisions(importStatements, reservedIdentifiers = new Set()) {
1943
+ const reserved = reservedIdentifiers instanceof Set
1944
+ ? reservedIdentifiers
1945
+ : new Set(reservedIdentifiers || []);
1946
+ const bindingToStatement = new Map();
1947
+
1948
+ for (const statement of importStatements || []) {
1949
+ const normalizedStatement = String(statement || '').trim();
1950
+ if (!normalizedStatement) continue;
1951
+ const localBindings = this.extractLocalBindingsFromImportStatement(normalizedStatement);
1952
+
1953
+ for (const localBinding of localBindings) {
1954
+ if (reserved.has(localBinding)) {
1955
+ throw new Error(`Hoisted import reserved identifier collision: ${localBinding}`);
1956
+ }
1957
+ const previousStatement = bindingToStatement.get(localBinding);
1958
+ if (previousStatement && previousStatement !== normalizedStatement) {
1959
+ throw new Error(`Hoisted import binding collision: ${localBinding}`);
1960
+ }
1961
+ bindingToStatement.set(localBinding, normalizedStatement);
1962
+ }
1963
+ }
1964
+ }
1965
+
1966
+ parseImportsFromCode(code) {
1967
+ const ast = parse(code, {
1968
+ sourceType: 'module',
1969
+ plugins: ['jsx']
1970
+ });
1971
+
1972
+ const importNodes = [];
1973
+ traverse.default(ast, {
1974
+ ImportDeclaration(pathNode) {
1975
+ importNodes.push(pathNode.node);
1976
+ }
1977
+ });
1978
+
1979
+ return importNodes
1980
+ .filter((node) => typeof node.start === 'number' && typeof node.end === 'number')
1981
+ .sort((a, b) => a.start - b.start);
1982
+ }
1983
+
1984
+ parseImportsWithFallbackScanner(code) {
1985
+ const entries = [];
1986
+ const importRegex = /\bimport\b/g;
1987
+ let match = null;
1988
+
1989
+ while ((match = importRegex.exec(code)) !== null) {
1990
+ const start = match.index;
1991
+ const nextChar = code[start + 'import'.length];
1992
+ if (nextChar === '(') {
1993
+ continue;
1994
+ }
1995
+
1996
+ let index = start + 'import'.length;
1997
+ let quote = null;
1998
+ let escaped = false;
1999
+
2000
+ while (index < code.length) {
2001
+ const char = code[index];
2002
+
2003
+ if (quote) {
2004
+ if (escaped) {
2005
+ escaped = false;
2006
+ } else if (char === '\\') {
2007
+ escaped = true;
2008
+ } else if (char === quote) {
2009
+ quote = null;
2010
+ }
2011
+ index += 1;
2012
+ continue;
2013
+ }
2014
+
2015
+ if (char === '\'' || char === '"' || char === '`') {
2016
+ quote = char;
2017
+ index += 1;
2018
+ continue;
2019
+ }
2020
+
2021
+ if (char === ';') {
2022
+ index += 1;
2023
+ break;
2024
+ }
2025
+
2026
+ index += 1;
2027
+ }
2028
+
2029
+ const end = index;
2030
+ const statement = code.slice(start, end);
2031
+ const fromMatch = statement.match(/\bfrom\s+['"]([^'"]+)['"]/);
2032
+ const sideEffectMatch = statement.match(/\bimport\s+['"]([^'"]+)['"]/);
2033
+ const importPath = fromMatch?.[1] || sideEffectMatch?.[1] || null;
2034
+
2035
+ if (!importPath) {
2036
+ continue;
2037
+ }
2038
+
2039
+ entries.push({ start, end, statement, importPath });
2040
+ importRegex.lastIndex = end;
2041
+ }
2042
+
2043
+ return entries;
2044
+ }
2045
+
2046
+ stripImportsWithFallbackRegex(code, publicFolders, sourceContext, collectHoistedImports) {
2047
+ const hoistedImports = [];
2048
+ const importEntries = this.parseImportsWithFallbackScanner(code);
2049
+ if (importEntries.length === 0) {
2050
+ return { code, hoistedImports };
2051
+ }
2052
+
2053
+ let cleanedCode = '';
2054
+ let cursor = 0;
2055
+ for (const entry of importEntries) {
2056
+ const { start, end, statement, importPath } = entry;
2057
+ const classification = this.classifyImport(importPath, publicFolders);
2058
+ cleanedCode += code.slice(cursor, start);
2059
+ if (classification.keep) {
2060
+ if (collectHoistedImports) {
2061
+ hoistedImports.push(statement.trim());
2062
+ } else {
2063
+ cleanedCode += statement;
2064
+ }
2065
+ } else if (classification.warning) {
2066
+ console.warn(this.buildImportWarningMessage(classification.warning, sourceContext));
2067
+ }
2068
+ cursor = end;
2069
+ }
2070
+
2071
+ cleanedCode += code.slice(cursor);
2072
+
2073
+ return { code: cleanedCode, hoistedImports };
2074
+ }
2075
+
2076
+ stripImports(code, options = {}) {
2077
+ const { sourceContext = null, collectHoistedImports = false } = options;
2078
+ const publicFolders = this.getConfiguredPublicFolders();
2079
+ const hoistedImports = [];
2080
+
2081
+ try {
2082
+ const importNodes = this.parseImportsFromCode(code);
2083
+
2084
+ if (importNodes.length === 0) {
2085
+ return collectHoistedImports ? { code, hoistedImports } : code;
2086
+ }
2087
+
2088
+ let cleaned = '';
2089
+ let cursor = 0;
2090
+
2091
+ for (const node of importNodes) {
2092
+ const importPath = node.source?.value;
2093
+ const classification = this.classifyImport(importPath, publicFolders);
2094
+ const statement = code.slice(node.start, node.end);
2095
+
2096
+ cleaned += code.slice(cursor, node.start);
2097
+ if (classification.keep) {
2098
+ if (collectHoistedImports) {
2099
+ hoistedImports.push(statement.trim());
2100
+ } else {
2101
+ cleaned += statement;
2102
+ }
2103
+ } else if (classification.warning) {
2104
+ console.warn(this.buildImportWarningMessage(classification.warning, sourceContext));
2105
+ }
2106
+
2107
+ cursor = node.end;
2108
+ }
2109
+
2110
+ cleaned += code.slice(cursor);
2111
+ return collectHoistedImports
2112
+ ? { code: cleaned, hoistedImports }
2113
+ : cleaned;
2114
+ } catch (error) {
2115
+ const fallback = this.stripImportsWithFallbackRegex(code, publicFolders, sourceContext, collectHoistedImports);
2116
+ return collectHoistedImports ? fallback : fallback.code;
2117
+ }
2118
+ }
2119
+
2120
+ async loadComponentsMap() {
2121
+ const componentsConfigPath = path.join(this.componentsPath, 'components.js');
2122
+ if (!await fs.pathExists(componentsConfigPath)) {
2123
+ return {};
2124
+ }
2125
+
2126
+ const content = await fs.readFile(componentsConfigPath, 'utf-8');
2127
+ return this.parseComponentsConfig(content);
2128
+ }
2129
+
2130
+ parseComponentsConfig(content) {
2131
+ try {
2132
+ const ast = parse(content, {
2133
+ sourceType: 'module',
2134
+ plugins: ['jsx']
2135
+ });
2136
+
2137
+ let componentsNode = null;
2138
+
2139
+ traverse.default(ast, {
2140
+ VariableDeclarator(path) {
2141
+ if (path.node.id?.type === 'Identifier' && path.node.id.name === 'components') {
2142
+ componentsNode = path.node.init;
2143
+ path.stop();
2144
+ }
2145
+ }
2146
+ });
2147
+
2148
+ if (!componentsNode || componentsNode.type !== 'ObjectExpression') {
2149
+ throw new Error('components object not found');
2150
+ }
2151
+
2152
+ const config = {};
2153
+ for (const prop of componentsNode.properties) {
2154
+ if (prop.type !== 'ObjectProperty') continue;
2155
+
2156
+ const key = this.extractStringValue(prop.key);
2157
+ const value = this.extractStringValue(prop.value);
2158
+
2159
+ if (!key || !value) {
2160
+ throw new Error('Invalid components entry');
2161
+ }
2162
+
2163
+ config[key] = value;
2164
+ }
2165
+
2166
+ return config;
2167
+ } catch (error) {
2168
+ console.warn(`Could not parse components.js: ${error.message}`);
2169
+ return {};
2170
+ }
2171
+ }
2172
+
2173
+ extractStringValue(node) {
2174
+ if (!node) return null;
2175
+
2176
+ if (node.type === 'StringLiteral') {
2177
+ return node.value;
2178
+ }
2179
+
2180
+ if (node.type === 'Identifier') {
2181
+ return node.name;
2182
+ }
2183
+
2184
+ if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
2185
+ return node.quasis.map((q) => q.value.cooked).join('');
2186
+ }
2187
+
2188
+ return null;
2189
+ }
2190
+
2191
+ /**
2192
+ * Converts a route to filename
2193
+ */
2194
+ routeToFileName(routePath) {
2195
+ if (routePath === '/') return 'home';
2196
+ return routePath
2197
+ .replace(/^\//, '')
2198
+ .replace(/\//g, '-')
2199
+ .replace(/[^a-zA-Z0-9-]/g, '')
2200
+ .toLowerCase();
2201
+ }
2202
+
2203
+ /**
2204
+ * Saves the configuration to file
2205
+ */
2206
+ async saveBundleConfig(config) {
2207
+ // Ensure bundles directory exists
2208
+ await fs.ensureDir(this.bundlesPath);
2209
+
2210
+ // Save JSON config
2211
+ const configPath = path.join(this.bundlesPath, 'bundle.config.json');
2212
+ await fs.writeJson(configPath, config, { spaces: 2 });
2213
+
2214
+ // Generate JavaScript module for direct import
2215
+ const jsConfigPath = path.join(this.bundlesPath, 'bundle.config.js');
2216
+ const jsConfig = this.generateBundleConfigJS(config);
2217
+ await fs.writeFile(jsConfigPath, jsConfig, 'utf-8');
2218
+
2219
+ console.log(`✓ Configuration saved to ${configPath}`);
2220
+ console.log(`✓ JavaScript config generated: ${jsConfigPath}`);
2221
+ }
2222
+
2223
+ /**
2224
+ * Creates a default bundle config file if none exists
2225
+ */
2226
+ async createDefaultBundleConfig() {
2227
+ const defaultConfigPath = path.join(this.srcPath, 'bundles', 'bundle.config.js');
2228
+
2229
+ // Only create if it doesn't exist
2230
+ if (await fs.pathExists(defaultConfigPath)) {
2231
+ return;
2232
+ }
2233
+
2234
+ await fs.ensureDir(path.dirname(defaultConfigPath));
2235
+
2236
+ const defaultConfig = `/**
2237
+ * Slice.js Bundle Configuration
2238
+ * Default empty configuration - no bundles available
2239
+ * Run 'slice build' to generate optimized bundles
2240
+ */
2241
+
2242
+ // No bundles available - using individual component loading
2243
+ export const SLICE_BUNDLE_CONFIG = null;
2244
+
2245
+ // No auto-initialization needed for default config
2246
+ `;
2247
+
2248
+ await fs.writeFile(defaultConfigPath, defaultConfig, 'utf-8');
2249
+ console.log(`✓ Default bundle config created: ${defaultConfigPath}`);
2250
+ }
2251
+
2252
+ /**
2253
+ * Generates JavaScript module for direct import
2254
+ */
2255
+ generateBundleConfigJS(config) {
2256
+ return `/**
2257
+ * Slice.js Bundle Configuration
2258
+ * Generated: ${new Date().toISOString()}
2259
+ * Strategy: ${config.strategy}
2260
+ */
2261
+
2262
+ // Direct bundle configuration (no fetch required)
2263
+ export const SLICE_BUNDLE_CONFIG = ${JSON.stringify(config, null, 2)};
2264
+
2265
+ // Auto-initialization if slice is available
2266
+ if (typeof window !== 'undefined' && window.slice && window.slice.controller) {
2267
+ window.slice.controller.bundleConfig = SLICE_BUNDLE_CONFIG;
2268
+
2269
+ // Load critical bundle automatically
2270
+ if (SLICE_BUNDLE_CONFIG.bundles.critical && !window.slice.controller.criticalBundleLoaded) {
2271
+ (async () => {
2272
+ const bundlePath = "/bundles/" + SLICE_BUNDLE_CONFIG.bundles.critical.file;
2273
+ const integrity = SLICE_BUNDLE_CONFIG.bundles.critical.integrity;
2274
+
2275
+ if (typeof window.slice.controller.verifyBundleIntegrity === 'function') {
2276
+ const ok = await window.slice.controller.verifyBundleIntegrity(bundlePath, integrity);
2277
+ if (!ok) {
2278
+ console.warn('Failed to load critical bundle: integrity check failed');
2279
+ return;
2280
+ }
2281
+ }
2282
+
2283
+ import('./slice-bundle.critical.js').catch(err =>
2284
+ console.warn('Failed to load critical bundle:', err)
2285
+ );
2286
+ window.slice.controller.criticalBundleLoaded = true;
2287
+ })();
2288
+ }
2289
+ }
2290
+ `;
2291
+ }
2292
+ }